From 38990aff326e8aedd46523c297d38a366c1fef0e Mon Sep 17 00:00:00 2001 From: Agetian Date: Mon, 15 May 2023 19:56:31 +0300 Subject: [PATCH] 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 <> --- .../java/forge/ai/AiAttackController.java | 29 +++++++++--- .../main/java/forge/ai/AiBlockController.java | 8 ++-- .../main/java/forge/ai/ability/PlayAi.java | 21 +++++++-- .../src/main/java/forge/game/GameAction.java | 2 +- .../main/java/forge/game/combat/Combat.java | 44 +++++-------------- .../java/forge/game/combat/CombatUtil.java | 37 +++++++--------- 6 files changed, 73 insertions(+), 68 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index cfd651b39e2..c776932e798 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -119,7 +119,11 @@ public class AiAttackController { } // overloaded constructor to evaluate single specified attacker 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<>(); for (Card c : myList) { if (canAttackWrapper(c, defender)) { @@ -722,9 +726,14 @@ public class AiAttackController { return pwNearUlti != null ? pwNearUlti : ComputerUtilCard.getBestPlaneswalkerAI(pwDefending); } - List battleDefending = c.getDefendingBattles(); - if (!battleDefending.isEmpty()) { - // TODO filter for team ones + // Get the preferred battle (prefer own battles, then ally battles) + final CardCollection defBattles = c.getDefendingBattles(); + List ownBattleDefending = CardLists.filter(defBattles, CardPredicates.isController(ai)); + List allyBattleDefending = CardLists.filter(defBattles, CardPredicates.isControlledByAnyOf(ai.getAllies())); + List 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; @@ -756,7 +765,17 @@ public class AiAttackController { // decided to attack another defender so related lists need to be updated // (though usually rather try to avoid this situation for performance reasons) 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); } if (this.attackers.isEmpty()) { diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index 5a7a7722c6c..37114291098 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -165,10 +165,12 @@ public class AiBlockController { } // TODO Add creatures attacking Planeswalkers in order of which we want to protect - // defend planeswalkers with more loyalty before planeswalkers with less loyalty - // if planeswalker will be too difficult to defend don't even bother + // defend planeswalkers with more loyalty before planeswalkers with less loyalty, + // 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) { - 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); // Begin with the attackers that pose the biggest threat CardLists.sortByPowerDesc(attackers); diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java index 6d9f1305da9..4dd32581358 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -139,17 +139,25 @@ public class PlayAi extends SpellAbilityAi { @Override public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, final boolean isOptional, Player targetedPlayer, Map params) { + final CardStateName state; + if (sa.hasParam("CastTransformed")) { + state = CardStateName.Transformed; + options.forEach(c -> c.changeToState(CardStateName.Transformed)); + } else { + state = CardStateName.Original; + } + List tgtCards = CardLists.filter(options, new Predicate() { @Override public boolean apply(final Card c) { // 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 - 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; s.setActivatingPlayer(ai, true); - // timing restrictions still apply - if (!s.getRestrictions().checkTimingRestrictions(c, s)) - continue; if (params != null && params.containsKey("CMCLimit")) { Integer cmcLimit = (Integer) params.get("CMCLimit"); if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit) @@ -188,6 +196,11 @@ public class PlayAi extends SpellAbilityAi { return false; } }); + + if (sa.hasParam("CastTransformed")) { + options.forEach(c -> c.changeToState(CardStateName.Original)); + } + final Card best = ComputerUtilCard.getBestAI(tgtCards); if (sa.usesTargeting() && !sa.isTargetNumberValid()) { sa.getTargets().add(best); diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index a8fc7fa7162..4ecb72a7584 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -452,7 +452,7 @@ public class GameAction { } if (zoneFrom != null) { - if (fromBattlefield && c.isCreature() && game.getCombat() != null) { + if (fromBattlefield && game.getCombat() != null) { if (!toBattlefield) { game.getCombat().saveLKI(lastKnownInfo); } diff --git a/forge-game/src/main/java/forge/game/combat/Combat.java b/forge-game/src/main/java/forge/game/combat/Combat.java index 851303f937d..46b32a95b84 100644 --- a/forge-game/src/main/java/forge/game/combat/Combat.java +++ b/forge-game/src/main/java/forge/game/combat/Combat.java @@ -17,50 +17,28 @@ */ 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.collect.ArrayListMultimap; -import com.google.common.collect.Iterables; -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 com.google.common.collect.*; +import forge.game.*; import forge.game.ability.AbilityKey; import forge.game.ability.ApiType; -import forge.game.card.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.card.*; import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; import forge.game.replacement.ReplacementType; import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbilityStackInstance; +import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked; import forge.game.trigger.TriggerType; import forge.util.CardTranslation; import forge.util.Localizer; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.*; +import java.util.Map.Entry; /** *

@@ -652,9 +630,7 @@ public class Combat { // iterate all attackers and remove illegal declarations CardCollection missingCombatants = new CardCollection(); for (Entry ee : attackedByBands.entries()) { - CardCollectionView atk = 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); + for (Card c : ee.getValue().getAttackers()) { if (!c.isInPlay() || !c.isCreature()) { missingCombatants.add(c); } @@ -967,7 +943,7 @@ public class Combat { public boolean isPlayerAttacked(Player who) { for (GameEntity defender : attackedByBands.keySet()) { 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)) { continue; // defender is not related to player 'who' } diff --git a/forge-game/src/main/java/forge/game/combat/CombatUtil.java b/forge-game/src/main/java/forge/game/combat/CombatUtil.java index 389e968c69f..a84b3b5bcb1 100644 --- a/forge-game/src/main/java/forge/game/combat/CombatUtil.java +++ b/forge-game/src/main/java/forge/game/combat/CombatUtil.java @@ -17,18 +17,10 @@ */ 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.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; - import forge.card.CardType; import forge.card.MagicColor; import forge.card.mana.ManaCost; @@ -36,12 +28,7 @@ import forge.game.Game; import forge.game.GameEntity; import forge.game.GlobalRuleChange; import forge.game.ability.AbilityKey; -import forge.game.card.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.card.*; import forge.game.cost.Cost; import forge.game.cost.CostPart; import forge.game.keyword.Keyword; @@ -59,6 +46,12 @@ import forge.util.TextUtil; import forge.util.collect.FCollection; import forge.util.collect.FCollectionView; 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; /** *

@@ -75,15 +68,17 @@ public class CombatUtil { final FCollection defenders = new FCollection<>(); for (final Player defender : playerWhoAttacks.getOpponents()) { defenders.add(defender); - final CardCollection planeswalkers = defender.getPlaneswalkersInPlay(); - defenders.addAll(planeswalkers); - for (Card battle : defender.getBattlesInPlay()) { - if (!playerWhoAttacks.equals(battle.getProtectingPlayer()) && battle.getType().hasSubtype("Siege")) { - defenders.add(battle); - } + defenders.addAll(defender.getPlaneswalkersInPlay()); + } + + // Relevant battles (protected by the attacking player's opponents) + 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; }