Battle AI support + improve Battle mechanics support (#3107)

* - First draft of (very sketchy) Battle AI code.

* - Imports fix.

* - Slightly cleaner refreshCombatants.

* - Update Combat to allow the protecting player to participate in declaring blocks to defend a battle.

* - Update AiBlockController in order to allow the AI to participate in defending battles it's protecting.

* Clean up

* - Minor cleanup.

* Fix checking backside

* Add TODO

* Fix missing combat removal

* - Suggested minor cleanup.

* - Fix imports.

* - Improve support for battles in getAllPossibleDefenders.

* - AI: prefer own Battles before choosing allied Battles.

* Fix ClassCastException

---------

Co-authored-by: TRT <>
This commit is contained in:
Agetian
2023-05-15 19:56:31 +03:00
committed by GitHub
parent 15a2ccb9f3
commit 38990aff32
6 changed files with 73 additions and 68 deletions

View File

@@ -119,7 +119,11 @@ public class AiAttackController {
} // overloaded constructor to evaluate single specified attacker } // overloaded constructor to evaluate single specified attacker
private void refreshCombatants(GameEntity defender) { private void refreshCombatants(GameEntity defender) {
this.oppList = getOpponentCreatures(defendingOpponent); if (defender instanceof Card && ((Card) defender).isBattle()) {
this.oppList = getOpponentCreatures(((Card) defender).getProtectingPlayer());
} else {
this.oppList = getOpponentCreatures(defendingOpponent);
}
this.attackers = new ArrayList<>(); this.attackers = new ArrayList<>();
for (Card c : myList) { for (Card c : myList) {
if (canAttackWrapper(c, defender)) { if (canAttackWrapper(c, defender)) {
@@ -722,9 +726,14 @@ public class AiAttackController {
return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending); return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending);
} }
List<Card> battleDefending = c.getDefendingBattles(); // Get the preferred battle (prefer own battles, then ally battles)
if (!battleDefending.isEmpty()) { final CardCollection defBattles = c.getDefendingBattles();
// TODO filter for team ones List<Card> ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai));
List<Card> allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies()));
List<Card> prefBattleList = ownBattleDefending.isEmpty() ? allyBattleDefending : ownBattleDefending;
if (!prefBattleList.isEmpty()) {
// TODO try to be less predictable here, should really check if something would make the back uncastable
return Collections.min(prefBattleList, CardPredicates.compareByCounterType(CounterEnumType.DEFENSE));
} }
return prefDefender; return prefDefender;
@@ -756,7 +765,17 @@ public class AiAttackController {
// decided to attack another defender so related lists need to be updated // decided to attack another defender so related lists need to be updated
// (though usually rather try to avoid this situation for performance reasons) // (though usually rather try to avoid this situation for performance reasons)
if (defender != defendingOpponent) { if (defender != defendingOpponent) {
defendingOpponent = defender instanceof Player ? (Player) defender : ((Card)defender).getController(); if (defender instanceof Player) {
defendingOpponent = (Player) defender;
} else if (defender instanceof Card) {
Card defCard = (Card) defender;
if (defCard.isBattle()) {
defendingOpponent = defCard.getProtectingPlayer();
} else {
// TODO: assume Planeswalker for now, may need to be updated later if more unique mechanics appear like Battle
defendingOpponent = defCard.getController();
}
}
refreshCombatants(defender); refreshCombatants(defender);
} }
if (this.attackers.isEmpty()) { if (this.attackers.isEmpty()) {

View File

@@ -165,10 +165,12 @@ public class AiBlockController {
} }
// TODO Add creatures attacking Planeswalkers in order of which we want to protect // TODO Add creatures attacking Planeswalkers in order of which we want to protect
// defend planeswalkers with more loyalty before planeswalkers with less loyalty // defend planeswalkers with more loyalty before planeswalkers with less loyalty,
// if planeswalker will be too difficult to defend don't even bother // defend battles with fewer defense counters before battles with more defense counters,
// if planeswalker/battle will be too difficult to defend don't even bother
for (GameEntity defender : defenders) { for (GameEntity defender : defenders) {
if (defender instanceof Card && ((Card) defender).getController().equals(ai)) { if ((defender instanceof Card && ((Card) defender).getController().equals(ai))
|| (defender instanceof Card && ((Card) defender).isBattle() && ((Card) defender).getProtectingPlayer().equals(ai))) {
final CardCollection attackers = combat.getAttackersOf(defender); final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat // Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers); CardLists.sortByPowerDesc(attackers);

View File

@@ -139,17 +139,25 @@ public class PlayAi extends SpellAbilityAi {
@Override @Override
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
final boolean isOptional, Player targetedPlayer, Map<String, Object> params) { final boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
final CardStateName state;
if (sa.hasParam("CastTransformed")) {
state = CardStateName.Transformed;
options.forEach(c -> c.changeToState(CardStateName.Transformed));
} else {
state = CardStateName.Original;
}
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() { List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
// TODO needs to be aligned for MDFC along with getAbilityToPlay so the knowledge // TODO needs to be aligned for MDFC along with getAbilityToPlay so the knowledge
// of which spell was the reason for the choice can be used there // of which spell was the reason for the choice can be used there
for (SpellAbility s : c.getBasicSpells(c.getState(CardStateName.Original))) { for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai, state)) {
if (!(s instanceof Spell)) {
continue;
}
Spell spell = (Spell) s; Spell spell = (Spell) s;
s.setActivatingPlayer(ai, true); s.setActivatingPlayer(ai, true);
// timing restrictions still apply
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
if (params != null && params.containsKey("CMCLimit")) { if (params != null && params.containsKey("CMCLimit")) {
Integer cmcLimit = (Integer) params.get("CMCLimit"); Integer cmcLimit = (Integer) params.get("CMCLimit");
if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit) if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit)
@@ -188,6 +196,11 @@ public class PlayAi extends SpellAbilityAi {
return false; return false;
} }
}); });
if (sa.hasParam("CastTransformed")) {
options.forEach(c -> c.changeToState(CardStateName.Original));
}
final Card best = ComputerUtilCard.getBestAI(tgtCards); final Card best = ComputerUtilCard.getBestAI(tgtCards);
if (sa.usesTargeting() && !sa.isTargetNumberValid()) { if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
sa.getTargets().add(best); sa.getTargets().add(best);

View File

@@ -452,7 +452,7 @@ public class GameAction {
} }
if (zoneFrom != null) { if (zoneFrom != null) {
if (fromBattlefield && c.isCreature() && game.getCombat() != null) { if (fromBattlefield && game.getCombat() != null) {
if (!toBattlefield) { if (!toBattlefield) {
game.getCombat().saveLKI(lastKnownInfo); game.getCombat().saveLKI(lastKnownInfo);
} }

View File

@@ -17,50 +17,28 @@
*/ */
package forge.game.combat; package forge.game.combat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.*;
import com.google.common.collect.Iterables; import forge.game.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Table;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameEntityCounterTable;
import forge.game.GameLogEntryType;
import forge.game.GameObjectMap;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardDamageMap;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementType; import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance; import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.util.CardTranslation; import forge.util.CardTranslation;
import forge.util.Localizer; import forge.util.Localizer;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
/** /**
* <p> * <p>
@@ -652,9 +630,7 @@ public class Combat {
// iterate all attackers and remove illegal declarations // iterate all attackers and remove illegal declarations
CardCollection missingCombatants = new CardCollection(); CardCollection missingCombatants = new CardCollection();
for (Entry<GameEntity, AttackingBand> ee : attackedByBands.entries()) { for (Entry<GameEntity, AttackingBand> ee : attackedByBands.entries()) {
CardCollectionView atk = ee.getValue().getAttackers(); for (Card c : ee.getValue().getAttackers()) {
for (int i = atk.size() - 1; i >= 0; i--) { // might remove items from collection, so no iterators
Card c = atk.get(i);
if (!c.isInPlay() || !c.isCreature()) { if (!c.isInPlay() || !c.isCreature()) {
missingCombatants.add(c); missingCombatants.add(c);
} }
@@ -967,7 +943,7 @@ public class Combat {
public boolean isPlayerAttacked(Player who) { public boolean isPlayerAttacked(Player who) {
for (GameEntity defender : attackedByBands.keySet()) { for (GameEntity defender : attackedByBands.keySet()) {
Card defenderAsCard = defender instanceof Card ? (Card)defender : null; Card defenderAsCard = defender instanceof Card ? (Card)defender : null;
if ((null != defenderAsCard && defenderAsCard.getController() != who) || if ((null != defenderAsCard && (defenderAsCard.getController() != who && defenderAsCard.getProtectingPlayer() != who)) ||
(null == defenderAsCard && defender != who)) { (null == defenderAsCard && defender != who)) {
continue; // defender is not related to player 'who' continue; // defender is not related to player 'who'
} }

View File

@@ -17,18 +17,10 @@
*/ */
package forge.game.combat; package forge.game.combat;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.CardType; import forge.card.CardType;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
@@ -36,12 +28,7 @@ import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.keyword.Keyword; import forge.game.keyword.Keyword;
@@ -59,6 +46,12 @@ import forge.util.TextUtil;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import forge.util.maps.MapToAmount; import forge.util.maps.MapToAmount;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@@ -75,15 +68,17 @@ public class CombatUtil {
final FCollection<GameEntity> defenders = new FCollection<>(); final FCollection<GameEntity> defenders = new FCollection<>();
for (final Player defender : playerWhoAttacks.getOpponents()) { for (final Player defender : playerWhoAttacks.getOpponents()) {
defenders.add(defender); defenders.add(defender);
final CardCollection planeswalkers = defender.getPlaneswalkersInPlay(); defenders.addAll(defender.getPlaneswalkersInPlay());
defenders.addAll(planeswalkers); }
for (Card battle : defender.getBattlesInPlay()) {
if (!playerWhoAttacks.equals(battle.getProtectingPlayer()) && battle.getType().hasSubtype("Siege")) { // Relevant battles (protected by the attacking player's opponents)
defenders.add(battle); final Game game = playerWhoAttacks.getGame();
} final CardCollection battles = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.BATTLES);
for (Card battle : battles) {
if (battle.getType().hasSubtype("Siege") && battle.getProtectingPlayer().isOpponentOf(playerWhoAttacks)) {
defenders.add(battle);
} }
} }
defenders.addAll(playerWhoAttacks.getBattlesInPlay());
return defenders; return defenders;
} }