From fd01dc1bb1d36d4e274112205da509f14159f1c0 Mon Sep 17 00:00:00 2001 From: Bug Hunter Date: Fri, 26 Mar 2021 10:58:22 +0000 Subject: [PATCH] AI Multiplayer improvements --- .../java/forge/ai/AiAttackController.java | 19 +- .../src/main/java/forge/ai/ComputerUtil.java | 132 +++++-------- .../main/java/forge/ai/ComputerUtilCard.java | 2 +- .../java/forge/ai/ComputerUtilCombat.java | 21 ++- .../forge/ai/ability/ActivateAbilityAi.java | 5 +- .../main/java/forge/ai/ability/BalanceAi.java | 15 +- .../main/java/forge/ai/ability/BidLifeAi.java | 3 +- .../java/forge/ai/ability/ChangeZoneAi.java | 23 +-- .../java/forge/ai/ability/ChooseCardAi.java | 3 +- .../forge/ai/ability/ChooseCardNameAi.java | 3 +- .../forge/ai/ability/ChooseEvenOddAi.java | 3 +- .../java/forge/ai/ability/ChooseNumberAi.java | 3 +- .../java/forge/ai/ability/ChooseSourceAi.java | 3 +- .../forge/ai/ability/ControlExchangeAi.java | 3 +- .../main/java/forge/ai/ability/DebuffAi.java | 17 +- .../java/forge/ai/ability/DestroyAllAi.java | 178 +++++++++--------- .../src/main/java/forge/ai/ability/DigAi.java | 5 +- .../java/forge/ai/ability/DigMultipleAi.java | 5 +- .../java/forge/ai/ability/DigUntilAi.java | 7 +- .../main/java/forge/ai/ability/DiscardAi.java | 9 +- .../java/forge/ai/ability/DrainManaAi.java | 3 +- .../java/forge/ai/ability/GameLossAi.java | 3 +- .../java/forge/ai/ability/LifeExchangeAi.java | 5 +- .../ai/ability/LifeExchangeVariantAi.java | 3 +- .../main/java/forge/ai/ability/LifeSetAi.java | 19 +- .../forge/ai/ability/PowerExchangeAi.java | 7 +- .../java/forge/ai/ability/PumpAiBase.java | 3 +- .../main/java/forge/ai/ability/PumpAllAi.java | 33 ++-- .../main/java/forge/ai/ability/RepeatAi.java | 6 +- .../java/forge/ai/ability/SacrificeAi.java | 15 +- .../java/forge/ai/ability/SacrificeAllAi.java | 52 ++--- .../src/main/java/forge/ai/ability/TapAi.java | 1 - .../main/java/forge/ai/ability/TapAiBase.java | 3 +- .../java/forge/ai/ability/TwoPilesAi.java | 3 +- .../java/forge/ai/ability/UnattachAllAi.java | 5 +- .../main/java/forge/ai/ability/UntapAi.java | 7 +- .../src/main/java/forge/game/card/Card.java | 11 -- .../main/java/forge/game/player/Player.java | 3 + .../res/cardsfolder/g/goblin_artisans.txt | 1 + .../res/cardsfolder/h/hellion_eruption.txt | 3 +- 40 files changed, 287 insertions(+), 358 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 5f2e126f872..c3ef8885a03 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -91,7 +91,7 @@ public class AiAttackController { public AiAttackController(final Player ai, boolean nextTurn) { this.ai = ai; - this.defendingOpponent = choosePreferredDefenderPlayer(); + this.defendingOpponent = choosePreferredDefenderPlayer(ai); this.oppList = getOpponentCreatures(this.defendingOpponent); this.myList = ai.getCreaturesInPlay(); this.attackers = new ArrayList<>(); @@ -107,7 +107,7 @@ public class AiAttackController { public AiAttackController(final Player ai, Card attacker) { this.ai = ai; - this.defendingOpponent = choosePreferredDefenderPlayer(); + this.defendingOpponent = choosePreferredDefenderPlayer(ai); this.oppList = getOpponentCreatures(this.defendingOpponent); this.myList = ai.getCreaturesInPlay(); this.attackers = new ArrayList<>(); @@ -156,13 +156,12 @@ public class AiAttackController { } /** Choose opponent for AI to attack here. Expand as necessary. */ - private Player choosePreferredDefenderPlayer() { - Player defender = ai.getWeakestOpponent(); //Gets opponent with the least life + public static Player choosePreferredDefenderPlayer(Player ai) { + Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range - if (defender.getLife() < 8) { //Concentrate on opponent within easy kill range - return defender; - } else { //Otherwise choose a random opponent to ensure no ganging up on players - defender = ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size())); + if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players + // TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked + return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size())); } return defender; } @@ -624,7 +623,7 @@ public class AiAttackController { int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage; int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp); - if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai) >= opp.getLife() + if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife() && !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) { return true; } @@ -919,7 +918,7 @@ public class AiAttackController { // find the potential damage ratio the AI can cause double humanLifeToDamageRatio = 1000000; if (candidateUnblockedDamage > 0) { - humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai)) / candidateUnblockedDamage; + humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai, opp)) / candidateUnblockedDamage; } // determine if the ai outnumbers the player diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 03dceacc08e..17fb2007fab 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -188,7 +188,6 @@ public class ComputerUtil { final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); - // Play higher costing spells first? final Cost cost = sa.getPayCosts(); @@ -213,7 +212,8 @@ public class ComputerUtil { if (unless != null && !unless.endsWith(">")) { final int amount = AbilityUtils.calculateAmount(source, unless, sa); - final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size(); + // this is enough as long as the AI is only smart enough to target top of stack + final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtilAbility.getTopSpellAbilityOnStack(ai.getGame(), sa).getActivatingPlayer(), true).size(); // If the Unless isn't enough, this should be less likely to be used if (amount > usableManaSources) { @@ -1068,9 +1068,6 @@ public class ComputerUtil { return true; } } - if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) { - return true; - } if (card.isCreature()) { if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) { return true; @@ -1093,8 +1090,8 @@ public class ComputerUtil { } // BuffedBy - // get all cards the human controls with AntiBuffedBy - final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield); + // there's a good chance AI will attack weak target + final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield); for (Card buffedcard : antibuffed) { if (buffedcard.hasSVar("AntiBuffedBy")) { final String buffedby = buffedcard.getSVar("AntiBuffedBy"); @@ -1142,27 +1139,16 @@ public class ComputerUtil { * @return true if it's OK to cast this Card for less than the max targets */ public static boolean shouldCastLessThanMax(final Player ai, final Card source) { - boolean ret = true; - if (source.getManaCost().countX() > 0) { - // If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by mana available. - return ret; - } else { - // Otherwise, if life is possibly in danger, then this is fine. - Combat combat = new Combat(ComputerUtil.getOpponentFor(ai)); - CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay(); - for (Card att : attackers) { - if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { - combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController())); - } - } - AiBlockController aiBlock = new AiBlockController(ai); - aiBlock.assignBlockersForCombat(combat); - if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { - // Otherwise, return false. Do not play now. - ret = false; - } + if (source.getXManaCostPaid() > 0) { + // If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available. + return true; } - return ret; + if (aiLifeInDanger(ai, false, 0)) { + // Otherwise, if life is possibly in danger, then this is fine. + return true; + } + // do not play now. + return false; } /** @@ -1266,8 +1252,8 @@ public class ComputerUtil { } } - // get all cards the human controls with AntiBuffedBy - final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield); + // there's a good chance AI will attack weak target + final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield); for (Card buffedcard : antibuffed) { if (buffedcard.hasSVar("AntiBuffedBy")) { final String buffedby = buffedcard.getSVar("AntiBuffedBy"); @@ -1463,7 +1449,7 @@ public class ComputerUtil { return false; } - public static int possibleNonCombatDamage(Player ai) { + public static int possibleNonCombatDamage(Player ai, Player enemy) { int damage = 0; final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); all.addAll(ai.getCardsActivableInExternalZones(true)); @@ -1483,7 +1469,6 @@ public class ComputerUtil { if (tgt == null) { continue; } - final Player enemy = ComputerUtil.getOpponentFor(ai); if (!sa.canTarget(enemy)) { continue; } @@ -2346,7 +2331,7 @@ public class ComputerUtil { } } else if (logic.equals("ChosenLandwalk")) { - for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) { + for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) { for (String t : c.getType()) { if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) { chosen = t; @@ -2364,7 +2349,7 @@ public class ComputerUtil { else if (kindOfType.equals("Land")) { if (logic != null) { if (logic.equals("ChosenLandwalk")) { - for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) { + for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) { for (String t : c.getType().getLandTypes()) { if (!invalidTypes.contains(t)) { chosen = t; @@ -2399,23 +2384,26 @@ public class ComputerUtil { case "Torture": return "Torture"; case "GraceOrCondemnation": - return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace" - : "Condemnation"; + List graceZones = new ArrayList(); + graceZones.add(ZoneType.Battlefield); + graceZones.add(ZoneType.Graveyard); + CardCollection graceCreatures = CardLists.getType(sa.getHostCard().getGame().getCardsIn(graceZones), "Creature"); + int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size(); + int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size(); + return aiGrace > humanGrace ? "Grace" : "Condemnation"; case "CarnageOrHomage": - CardCollection cardsInPlay = CardLists - .getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land"); + CardCollection cardsInPlay = CardLists.getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land"); CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents()); - CardCollection computerlist = CardLists.filterControlledBy(cardsInPlay, ai); - return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard - .evaluatePermanentList(humanlist) ? "Carnage" : "Homage"; + CardCollection computerlist = ai.getCreaturesInPlay(); + return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage"; case "Judgment": if (votes.isEmpty()) { CardCollection list = new CardCollection(); for (Object o : options) { if (o instanceof Card) { list.add((Card) o); - } } + } return ComputerUtilCard.getBestAI(list); } else { return Iterables.getFirst(votes.keySet(), null); @@ -2934,23 +2922,6 @@ public class ComputerUtil { return true; } - - @Deprecated - public static final Player getOpponentFor(final Player player) { - // This method is deprecated and currently functions as a synonym for player.getWeakestOpponent - // until it can be replaced everywhere in the code. - - // Consider replacing calls to this method either with a multiplayer-friendly determination of - // opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent - // where that is applicable and makes sense from the point of view of multiplayer AI logic. - Player opponent = player.getWeakestOpponent(); - if (opponent != null) { - return opponent; - } - - throw new IllegalStateException("No opponents left ingame for " + player); - } - public static int countUsefulCreatures(Player p) { CardCollection creats = p.getCreaturesInPlay(); int count = 0; @@ -3033,31 +3004,32 @@ public class ComputerUtil { // call this to determine if it's safe to use a life payment spell // or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell. public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) { - Player opponent = ComputerUtil.getOpponentFor(ai); - // test whether the human can kill the ai next turn - Combat combat = new Combat(opponent); - boolean containsAttacker = false; - for (Card att : opponent.getCreaturesInPlay()) { - if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { - combat.addAttacker(att, ai); - containsAttacker = true; + for (Player opponent: ai.getOpponents()) { + // test whether the human can kill the ai next turn + Combat combat = new Combat(opponent); + boolean containsAttacker = false; + for (Card att : opponent.getCreaturesInPlay()) { + if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { + combat.addAttacker(att, ai); + containsAttacker = true; + } } - } - if (!containsAttacker) { - return false; - } - AiBlockController block = new AiBlockController(ai); - block.assignBlockersForCombat(combat); + if (!containsAttacker) { + return false; + } + AiBlockController block = new AiBlockController(ai); + block.assignBlockersForCombat(combat); - // TODO predict other, noncombat sources of damage and add them to the "payment" variable. - // examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc - // If added, might need a parameter to define whether we want to check all threats or combat threats. + // TODO predict other, noncombat sources of damage and add them to the "payment" variable. + // examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc + // If added, might need a parameter to define whether we want to check all threats or combat threats. - if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) { - return true; - } - if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) { - return true; + if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) { + return true; + } + if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) { + return true; + } } return false; diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 00433b71a17..19172312c04 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -550,7 +550,7 @@ public class ComputerUtilCard { */ public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) { AiBlockController aiBlk = new AiBlockController(ai); - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); Combat combat = new Combat(opp); //Use actual attackers if available, else consider all possible attackers Combat currentCombat = ai.getGame().getCombat(); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index d4b7d549f74..ebcde781c0e 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -97,34 +97,39 @@ public class ComputerUtilCombat { * canAttackNextTurn. *

* - * @param atacker + * @param attacker * a {@link forge.game.card.Card} object. * @param defender * the defending {@link GameEntity}. * @return a boolean. */ - public static boolean canAttackNextTurn(final Card atacker, final GameEntity defender) { - if (!atacker.isCreature()) { + public static boolean canAttackNextTurn(final Card attacker, final GameEntity defender) { + if (!attacker.isCreature()) { return false; } - if (!CombatUtil.canAttackNextTurn(atacker, defender)) { + if (!CombatUtil.canAttackNextTurn(attacker, defender)) { return false; } - for (final KeywordInterface inst : atacker.getKeywords()) { + for (final KeywordInterface inst : attacker.getKeywords()) { final String keyword = inst.getOriginal(); if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) { final String defined = keyword.split(":")[1]; - final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0); + final Player player = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0); if (!defender.equals(player)) { return false; } } } + // TODO this should be a factor but needs some alignment with AttachAi + //boolean leavesPlay = !ComputerUtilCard.hasActiveUndyingOrPersist(attacker) + // && ((attacker.hasKeyword(Keyword.VANISHING) && attacker.getCounters(CounterEnumType.TIME) == 1) + // || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0) + // || attacker.hasSVar("EndOfTurnLeavePlay")); // The creature won't untap next turn - return !atacker.isTapped() || Untap.canUntap(atacker); - } // canAttackNextTurn(Card, GameEntity) + return !attacker.isTapped() || Untap.canUntap(attacker); + } /** *

diff --git a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java index e261dab37db..3340761aff4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java @@ -22,7 +22,6 @@ public class ActivateAbilityAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); final Player opp = ai.getWeakestOpponent(); - boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); List list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card")); if (list.isEmpty()) { @@ -40,6 +39,7 @@ public class ActivateAbilityAi extends SpellAbilityAi { sa.getTargets().add(opp); } + boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); return randomReturn; } @@ -56,7 +56,8 @@ public class ActivateAbilityAi extends SpellAbilityAi { } else { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); - return defined.contains(opp); + // if at least two players are returned we can affect another opponent + return defined.size() > 1 || defined.contains(opp); } } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java b/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java index 9aba6e53fe9..902741c6da3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java @@ -13,18 +13,21 @@ public class BalanceAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { String logic = sa.getParam("AILogic"); - int diff = 0; - // TODO Add support for multiplayer logic - final Player opp = aiPlayer.getWeakestOpponent(); - final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield); + Player opp = aiPlayer.getWeakestOpponent(); final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield); + for (Player min : aiPlayer.getOpponents()) { + if (min.getCardsIn(ZoneType.Battlefield).size() < opp.getCardsIn(ZoneType.Battlefield).size()) { + opp = min; + } + } + final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield); if ("BalanceCreaturesAndLands".equals(logic)) { - // Copied over from hardcoded Balance. We should be checking value of the lands/creatures not just counting + // TODO Copied over from hardcoded Balance. We should be checking value of the lands/creatures for each opponent, not just counting diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() - CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size(); - diff += 1.5 * ( CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() - + diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() - CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size()); } else if ("BalancePermanents".equals(logic)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java index 634fabc1254..6f7b6e943d6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi { if (tgt != null) { sa.resetTargets(); if (tgt.canTgtCreature()) { - List list = CardLists.getTargetableCards(aiPlayer.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), sa); + List list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa); list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); if (list.isEmpty()) { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java index 58ac2740907..51c19811578 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -15,6 +15,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import forge.ai.AiAttackController; import forge.ai.AiBlockController; import forge.ai.AiCardMemory; import forge.ai.AiController; @@ -263,7 +264,7 @@ public class ChangeZoneAi extends SpellAbilityAi { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); ZoneType origin = null; - final Player opponent = ai.getWeakestOpponent(); + final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai); boolean activateForCost = ComputerUtil.activateForCost(sa, ai); if (sa.hasParam("Origin")) { @@ -471,7 +472,7 @@ public class ChangeZoneAi extends SpellAbilityAi { // if putting cards from hand to library and parent is drawing cards // make sure this will actually do something: final TargetRestrictions tgt = sa.getTargetRestrictions(); - final Player opp = aiPlayer.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); if (tgt != null && tgt.canTgtPlayer()) { boolean isCurse = sa.isCurse(); if (isCurse && sa.canTarget(opp)) { @@ -530,7 +531,7 @@ public class ChangeZoneAi extends SpellAbilityAi { Iterable pDefined; final TargetRestrictions tgt = sa.getTargetRestrictions(); if ((tgt != null) && tgt.canTgtPlayer()) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.isCurse()) { if (sa.canTarget(opp)) { sa.getTargets().add(opp); @@ -892,7 +893,6 @@ public class ChangeZoneAi extends SpellAbilityAi { // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? sa.setXManaCostPaid(xPay); - // TODO since change of PayX. the shouldCastLessThanMax logic might be faulty } CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa); @@ -913,9 +913,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (sa.isSpell()) { list.remove(source); // spells can't target their own source, because it's actually in the stack zone } - //System.out.println("isPreferredTarget " + list); if (sa.hasParam("AttachedTo")) { - //System.out.println("isPreferredTarget att " + list); list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { @@ -927,7 +925,6 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } }); - //System.out.println("isPreferredTarget ok " + list); } if (list.size() < sa.getMinTargets()) { @@ -1482,9 +1479,11 @@ public class ChangeZoneAi extends SpellAbilityAi { if ("DeathgorgeScavenger".equals(logic)) { return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa); - } else if ("ExtraplanarLens".equals(logic)) { + } + if ("ExtraplanarLens".equals(logic)) { return SpecialCardAi.ExtraplanarLens.consider(ai, sa); - } else if ("ExileCombatThreat".equals(logic)) { + } + if ("ExileCombatThreat".equals(logic)) { return doExileCombatThreatLogic(ai, sa); } @@ -1984,11 +1983,7 @@ public class ChangeZoneAi extends SpellAbilityAi { toPay = AbilityUtils.calculateAmount(source, unlessCost, sa); } - if (toPay == 0) { - canBeSaved.add(potentialTgt); - } - - if (toPay <= usableManaSources) { + if (toPay == 0 || toPay <= usableManaSources) { canBeSaved.add(potentialTgt); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index a50ee8c6def..3dd7825cad3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -9,6 +9,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; @@ -114,7 +115,7 @@ public class ChooseCardAi extends SpellAbilityAi { return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty(); } else if (aiLogic.equals("Duneblast")) { CardCollection aiCreatures = ai.getCreaturesInPlay(); - CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay(); + CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay(); aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE); oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE); diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java index 469a095fa4e..b12f37aecca 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java @@ -7,6 +7,7 @@ import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import forge.StaticData; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; import forge.ai.SpecialCardAi; @@ -44,7 +45,7 @@ public class ChooseCardNameAi extends SpellAbilityAi { if (tgt != null) { sa.resetTargets(); if (tgt.canOnlyTgtOpponent()) { - sa.getTargets().add(ai.getWeakestOpponent()); + sa.getTargets().add(AiAttackController.choosePreferredDefenderPlayer(ai)); } else { sa.getTargets().add(ai); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java index e5ac8a99d6d..2a6e465865a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -16,7 +17,7 @@ public class ChooseEvenOddAi extends SpellAbilityAi { TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - Player opp = aiPlayer.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java index 46276414778..639baa92c40 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -16,7 +17,7 @@ public class ChooseNumberAi extends SpellAbilityAi { TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - Player opp = aiPlayer.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java index 1396a1bd6dd..8a71e557d4d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java @@ -8,6 +8,7 @@ import com.google.common.base.Predicates; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; import forge.ai.SpellAbilityAi; @@ -54,7 +55,7 @@ public class ChooseSourceAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java index 57d56de1759..65f11c69b35 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java @@ -3,6 +3,7 @@ package forge.ai.ability; import com.google.common.base.Predicate; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi { sa.resetTargets(); CardCollection list = - CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); + CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); // AI won't try to grab cards that are filtered out of AI decks on // purpose list = CardLists.filter(list, new Predicate() { diff --git a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java index 4a28f540a1e..76263212db2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java @@ -8,6 +8,7 @@ import com.google.common.base.Predicate; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; @@ -26,9 +27,6 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; public class DebuffAi extends SpellAbilityAi { - // ************************************************************************* - // ***************************** Debuff ************************************ - // ************************************************************************* @Override protected boolean canPlayAI(final Player ai, final SpellAbility sa) { @@ -140,7 +138,6 @@ public class DebuffAi extends SpellAbilityAi { while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) { Card t = null; - // boolean goodt = false; if (list.isEmpty()) { if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) { @@ -176,19 +173,18 @@ public class DebuffAi extends SpellAbilityAi { * @return a CardCollection. */ private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List kws) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa); if (!list.isEmpty()) { list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return c.hasAnyKeyword(kws); // don't add duplicate negative - // keywords + return c.hasAnyKeyword(kws); // don't add duplicate negative keywords } }); } return list; - } // getCurseCreatures() + } /** *

@@ -216,7 +212,7 @@ public class DebuffAi extends SpellAbilityAi { list.remove(c); } - final CardCollection pref = CardLists.filterControlledBy(list, ai.getWeakestOpponent()); + final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponents()); final CardCollection forced = CardLists.filterControlledBy(list, ai); final Card source = sa.getHostCard(); @@ -242,8 +238,7 @@ public class DebuffAi extends SpellAbilityAi { break; } - // TODO - if forced targeting, just pick something without the given - // keyword + // TODO - if forced targeting, just pick something without the given keyword Card c; if (CardLists.getNotType(forced, "Creature").size() == 0) { c = ComputerUtilCard.getWorstCreatureAI(forced); diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java index 13f1a975b53..79f88577204 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -70,12 +70,12 @@ public class DestroyAllAi extends SpellAbilityAi { return doMassRemovalLogic(ai, sa); } - public boolean doMassRemovalLogic(Player ai, SpellAbility sa) { + public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String logic = sa.getParamOrDefault("AILogic", ""); - Player opponent = ai.getWeakestOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()? - final int CREATURE_EVAL_THRESHOLD = 200; + // if we hit the whole board, the other opponents who are not the reason to cast this probably still suffer a bit too + final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1); if (logic.equals("Always")) { return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing @@ -93,99 +93,101 @@ public class DestroyAllAi extends SpellAbilityAi { valid = valid.replace("X", Integer.toString(xPay)); } - CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), - valid.split(","), source.getController(), source, sa); - CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), - source.getController(), source, sa); - - opplist = CardLists.filter(opplist, predicate); - ailist = CardLists.filter(ailist, predicate); - if (opplist.isEmpty()) { - return false; - } + // TODO should probably sort results when targeted to use on biggest threat instead of first match + for (Player opponent: ai.getOpponents()) { + CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); + CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); - if (sa.usesTargeting()) { - sa.resetTargets(); - if (sa.canTarget(opponent)) { - sa.getTargets().add(opponent); - ailist.clear(); - } else { + opplist = CardLists.filter(opplist, predicate); + ailist = CardLists.filter(ailist, predicate); + if (opplist.isEmpty()) { return false; } - } - // Special handling for Raiding Party - if (logic.equals("RaidingParty")) { - int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size()); - int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size()); - - return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); - } - - // If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what! - if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) - && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { - return true; - } - - // If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much - if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) - && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat())) - && ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) { - return true; - } - - // if only creatures are affected evaluate both lists and pass only if - // human creatures are more valuable - if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) { - if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) { - return true; - } - - if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { - return false; - } - - // test whether the human can kill the ai next turn - Combat combat = new Combat(opponent); - boolean containsAttacker = false; - for (Card att : opponent.getCreaturesInPlay()) { - if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { - combat.addAttacker(att, ai); - containsAttacker = containsAttacker | opplist.contains(att); - } - } - if (!containsAttacker) { - return false; - } - AiBlockController block = new AiBlockController(ai); - block.assignBlockersForCombat(combat); - - if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { - return true; - } - return false; - } // only lands involved - else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) { - if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds") && !opplist.isEmpty()) { - return true; - } - // evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes - CardCollection aiCreatures = ai.getCreaturesInPlay(); - CardCollection oppCreatures = opponent.getCreaturesInPlay(); - if (!oppCreatures.isEmpty()) { - if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) { + if (sa.usesTargeting()) { + sa.resetTargets(); + if (sa.canTarget(opponent)) { + sa.getTargets().add(opponent); + ailist.clear(); + } else { return false; } } - // check if the AI would lose more lands than the opponent would - if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) { + + // Special handling for Raiding Party + if (logic.equals("RaidingParty")) { + int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size()); + int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size()); + + return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); + } + + // If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what! + if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) + && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { + return true; + } + + // If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much + if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) + && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat())) + && ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) { + return true; + } + + // if only creatures are affected evaluate both lists and pass only if human creatures are more valuable + if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) { + if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) { + return true; + } + + if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { + return false; + } + + // test whether the human can kill the ai next turn + Combat combat = new Combat(opponent); + boolean containsAttacker = false; + for (Card att : opponent.getCreaturesInPlay()) { + if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { + combat.addAttacker(att, ai); + containsAttacker = containsAttacker | opplist.contains(att); + } + } + if (!containsAttacker) { + return false; + } + AiBlockController block = new AiBlockController(ai); + block.assignBlockersForCombat(combat); + + if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { + return true; + } + return false; + } // only lands involved + else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) { + if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) { + return true; + } + // evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes + CardCollection aiCreatures = ai.getCreaturesInPlay(); + CardCollection oppCreatures = opponent.getCreaturesInPlay(); + if (!oppCreatures.isEmpty()) { + if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) { + return false; + } + } + // check if the AI would lose more lands than the opponent would + if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) { + return false; + } + } // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable + else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) { return false; } - } // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable - else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) { - return false; + return true; } - return true; + return false; } -} + +} \ No newline at end of file diff --git a/forge-ai/src/main/java/forge/ai/ability/DigAi.java b/forge-ai/src/main/java/forge/ai/ability/DigAi.java index 4d7ffc1f458..33e3c315dc8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -5,6 +5,7 @@ import java.util.Map; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; @@ -32,7 +33,7 @@ public class DigAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Card host = sa.getHostCard(); Player libraryOwner = ai; @@ -120,7 +121,7 @@ public class DigAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); if (mandatory && sa.canTarget(opp)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java index 07796e6fba9..1e3b0350408 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -19,7 +20,7 @@ public class DigMultipleAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Card host = sa.getHostCard(); Player libraryOwner = ai; @@ -77,7 +78,7 @@ public class DigMultipleAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); if (mandatory && sa.canTarget(opp)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java index 449561f46bf..d421a67f54d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -31,10 +32,8 @@ public class DigUntilAi extends SpellAbilityAi { chance = 1; } - final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); - Player libraryOwner = ai; - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if ("DontMillSelf".equals(logic)) { // A card that digs for specific things and puts everything revealed before it into graveyard @@ -92,12 +91,12 @@ public class DigUntilAi extends SpellAbilityAi { return false; } + final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); return randomReturn; } @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - if (sa.usesTargeting()) { sa.resetTargets(); if (sa.isCurse()) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index dd1e0fc35d6..17d69dcdbc0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCost; @@ -60,11 +61,9 @@ public class DiscardAi extends SpellAbilityAi { if (players.get(0) == ai) { // the ai should only be using something like this if he has // few cards in hand, - // cards like this better have a good drawback to be in the - // AIs deck + // cards like this better have a good drawback to be in the AIs deck } else { - // defined to the human, so that's fine as long the human - // has cards + // defined to the human, so that's fine as long the human has cards if (!humanHasHand) { return false; } @@ -170,7 +169,7 @@ public class DiscardAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (!discardTargetAI(ai, sa)) { if (mandatory && sa.canTarget(opp)) { sa.getTargets().add(opp); diff --git a/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java b/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java index 96310431fc3..870ae38bffd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java @@ -23,8 +23,7 @@ public class DrainManaAi extends SpellAbilityAi { if (tgt == null) { // assume we are looking to tap human's stuff - // TODO - check for things with untap abilities, and don't tap - // those. + // TODO - check for things with untap abilities, and don't tap those. final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); if (!defined.contains(opp)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java index 03b77143d99..8bdd71b1394 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java @@ -29,11 +29,10 @@ public class GameLossAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - // Phage the Untouchable // (Final Fortune would need to attach it's delayed trigger to a // specific turn, which can't be done yet) - Player opp = ai.getWeakestOpponent(); + Player opp = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard()); if (!mandatory && opp.cantLose()) { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java index 8b387b7791a..2aba049fa22 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -19,7 +20,7 @@ public class LifeExchangeAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { final int myLife = aiPlayer.getLife(); - Player opponent = aiPlayer.getWeakestOpponent(); + Player opponent = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); final int hLife = opponent.getLife(); if (!aiPlayer.canGainLife()) { @@ -75,7 +76,7 @@ public class LifeExchangeAi extends SpellAbilityAi { final boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (tgt != null) { sa.resetTargets(); if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java index 0fd3e1e0702..7c3147c3dfd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.AiProps; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; @@ -154,7 +155,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { final boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); - Player opp = ai.getWeakestOpponent(); + Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (tgt != null) { sa.resetTargets(); if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java index 91e17c2da2f..cbf94b08d48 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java @@ -17,7 +17,7 @@ public class LifeSetAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final int myLife = ai.getLife(); - final Player opponent = ai.getWeakestOpponent(); + final Player opponent = ai.getStrongestOpponent(); final int hlife = opponent.getLife(); final String amountStr = sa.getParam("LifeAmount"); @@ -36,8 +36,7 @@ public class LifeSetAi extends SpellAbilityAi { return false; } - // TODO handle proper calculation of X values based on Cost and what - // would be paid + // TODO handle proper calculation of X values based on Cost and what would be paid int amount; // we shouldn't have to worry too much about PayX for SetLife if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) { @@ -58,11 +57,9 @@ public class LifeSetAi extends SpellAbilityAi { if (tgt.canOnlyTgtOpponent()) { sa.getTargets().add(opponent); // if we can only target the human, and the Human's life - // would - // go up, don't play it. + // would go up, don't play it. // possibly add a combo here for Magister Sphinx and - // Higedetsu's - // (sp?) Second Rite + // Higedetsu's (sp?) Second Rite if ((amount > hlife) || !opponent.canLoseLife()) { return false; } @@ -81,8 +78,7 @@ public class LifeSetAi extends SpellAbilityAi { if (sa.getParam("Defined").equals("Player")) { if (amount == 0) { return false; - } else if (myLife > amount) { // will decrease computer's - // life + } else if (myLife > amount) { // will decrease computer's life if ((myLife < 5) || ((myLife - amount) > (hlife - amount))) { return false; } @@ -104,7 +100,7 @@ public class LifeSetAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final int myLife = ai.getLife(); - final Player opponent = ai.getWeakestOpponent(); + final Player opponent = ai.getStrongestOpponent(); final int hlife = opponent.getLife(); final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); @@ -133,8 +129,7 @@ public class LifeSetAi extends SpellAbilityAi { } // If the Target is gaining life, target self. - // if the Target is modifying how much life is gained, this needs to - // be handled better + // if the Target is modifying how much life is gained, this needs to be handled better final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); diff --git a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java index 9626b8f6f43..33d421d5626 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java @@ -30,13 +30,12 @@ public class PowerExchangeAi extends SpellAbilityAi { sa.resetTargets(); List list = - CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); - // AI won't try to grab cards that are filtered out of AI decks on - // purpose + CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); + // AI won't try to grab cards that are filtered out of AI decks on purpose list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa); + return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa) && c.getController() != ai; } }); CardLists.sortByPowerAsc(list); diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java index aba87269e83..0fc65c64997 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -6,6 +6,7 @@ import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCombat; @@ -201,7 +202,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { final Game game = ai.getGame(); final Combat combat = game.getCombat(); final PhaseHandler ph = game.getPhaseHandler(); - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final int newPower = card.getNetCombatDamage() + attack; //int defense = getNumDefense(sa); if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java index 381147880bd..386d043770d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java @@ -48,12 +48,6 @@ public class PumpAllAi extends PumpAiBase { } } - final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa); - final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa); - final List keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>(); - - final PhaseType phase = game.getPhaseHandler().getPhase(); - if (ComputerUtil.preventRunAwayActivations(sa)) { return false; } @@ -63,16 +57,10 @@ public class PumpAllAi extends PumpAiBase { return false; } } - - if (sa.hasParam("ValidCards")) { - valid = sa.getParam("ValidCards"); - } - - final Player opp = ai.getWeakestOpponent(); - CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); - CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); - + final TargetRestrictions tgt = sa.getTargetRestrictions(); + final Player opp = ai.getStrongestOpponent(); + if (tgt != null && sa.canTarget(opp) && sa.hasParam("IsCurse")) { sa.resetTargets(); sa.getTargets().add(opp); @@ -85,6 +73,18 @@ public class PumpAllAi extends PumpAiBase { return true; } + final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa); + final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa); + final List keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>(); + final PhaseType phase = game.getPhaseHandler().getPhase(); + + if (sa.hasParam("ValidCards")) { + valid = sa.getParam("ValidCards"); + } + + CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); + CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa); + if (!game.getStack().isEmpty() && !sa.isCurse()) { return pumpAgainstRemoval(ai, sa, comp); } @@ -139,8 +139,7 @@ public class PumpAllAi extends PumpAiBase { return true; } - // evaluate both lists and pass only if human creatures are more - // valuable + // evaluate both lists and pass only if human creatures are more valuable return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human); } // end Curse diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java index 0f9c2a20ffe..310ea94efdf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAttackController; import forge.ai.AiController; import forge.ai.ComputerUtilCost; import forge.ai.PlayerControllerAi; @@ -14,7 +15,7 @@ public class RepeatAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { if (!opp.canBeTargetedBy(sa)) { @@ -44,9 +45,8 @@ public class RepeatAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - if (sa.usesTargeting()) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.canTarget(opp)) { sa.resetTargets(); sa.getTargets().add(opp); diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java index dfd420dab29..36d0fa484c6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -20,9 +20,6 @@ import forge.game.spellability.TargetRestrictions; import forge.game.zone.ZoneType; public class SacrificeAi extends SpellAbilityAi { - // ************************************************************** - // *************************** Sacrifice *********************** - // ************************************************************** @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { @@ -48,8 +45,7 @@ public class SacrificeAi extends SpellAbilityAi { } // Improve AI for triggers. If source is a creature with: - // When ETB, sacrifice a creature. Check to see if the AI has something - // to sacrifice + // When ETB, sacrifice a creature. Check to see if the AI has something to sacrifice // Eventually, we can call the trigger of ETB abilities with not // mandatory as part of the checks to cast something @@ -58,12 +54,11 @@ public class SacrificeAi extends SpellAbilityAi { } private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) { - final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); final boolean destroy = sa.hasParam("Destroy"); - Player opp = ai.getWeakestOpponent(); + Player opp = ai.getStrongestOpponent(); if (tgt != null) { sa.resetTargets(); @@ -109,8 +104,7 @@ public class SacrificeAi extends SpellAbilityAi { sa.setXManaCostPaid(Math.min(ComputerUtilCost.getMaxXValue(sa, ai), amount)); } - final int half = (amount / 2) + (amount % 2); // Half of amount - // rounded up + final int half = (amount / 2) + (amount % 2); // Half of amount rounded up // If the Human has at least half rounded up of the amount to be // sacrificed, cast the spell @@ -130,8 +124,7 @@ public class SacrificeAi extends SpellAbilityAi { // If Sacrifice hits both players: // Only cast it if Human has the full amount of valid // Only cast it if AI doesn't have the full amount of Valid - // TODO: Cast if the type is favorable: my "worst" valid is - // worse than his "worst" valid + // TODO: Cast if the type is favorable: my "worst" valid is worse than his "worst" valid final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1"; int amount = AbilityUtils.calculateAmount(source, num, sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java index 84f496ef24e..11ebac1c75c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java @@ -2,17 +2,12 @@ package forge.ai.ability; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; -import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; import forge.game.card.Card; -import forge.game.card.CardCollection; -import forge.game.card.CardLists; import forge.game.cost.Cost; import forge.game.player.Player; import forge.game.spellability.SpellAbility; -import forge.game.zone.ZoneType; import forge.util.MyRandom; -import forge.util.TextUtil; public class SacrificeAllAi extends SpellAbilityAi { @@ -22,22 +17,7 @@ public class SacrificeAllAi extends SpellAbilityAi { // based on what the expected targets could be final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); - String valid = ""; - - if (sa.hasParam("ValidCards")) { - valid = sa.getParam("ValidCards"); - } - - if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) { - // Set PayX here to maximum value. - final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); - valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay)); - } - - CardCollection humanlist = - CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); - CardCollection computerlist = - CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); + final String logic = sa.getParamOrDefault("AILogic", ""); if (abCost != null) { // AI currently disabled for some costs @@ -45,30 +25,20 @@ public class SacrificeAllAi extends SpellAbilityAi { return false; } } + + if (logic.equals("HellionEruption")) { + if (ai.getCreaturesInPlay().size() < 5 || ai.getCreaturesInPlay().size() * 150 < ComputerUtilCard.evaluateCreatureList(ai.getCreaturesInPlay())) { + return false; + } + } + + if (!DestroyAllAi.doMassRemovalLogic(ai, sa)) { + return false; + } // prevent run-away activations - first time will always return true boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - // if only creatures are affected evaluate both lists and pass only if - // human creatures are more valuable - if ((CardLists.getNotType(humanlist, "Creature").size() == 0) && (CardLists.getNotType(computerlist, "Creature").size() == 0)) { - if ((ComputerUtilCard.evaluateCreatureList(computerlist) + 200) >= ComputerUtilCard - .evaluateCreatureList(humanlist)) { - return false; - } - } // only lands involved - else if ((CardLists.getNotType(humanlist, "Land").size() == 0) && (CardLists.getNotType(computerlist, "Land").size() == 0)) { - if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 1) >= ComputerUtilCard - .evaluatePermanentList(humanlist)) { - return false; - } - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are more valuable - else if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 3) >= ComputerUtilCard - .evaluatePermanentList(humanlist)) { - return false; - } - return ((MyRandom.getRandom().nextFloat() < .9667) && chance); } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java index cb6428ee193..7216345b1f8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java @@ -66,7 +66,6 @@ public class TapAi extends TapAiBase { // Set PayX here to maximum value. // TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore? sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai)); - // TODO since change of PayX. the shouldCastLessThanMax logic might be faulty } sa.resetTargets(); diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java index fb27b86a4d6..a8f46f2a180 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -5,6 +5,7 @@ import java.util.List; import com.google.common.base.Predicate; import com.google.common.collect.Iterables; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; @@ -109,7 +110,7 @@ public abstract class TapAiBase extends SpellAbilityAi { * @return a boolean. */ protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) { - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Game game = ai.getGame(); CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); tapList = CardLists.getTargetableCards(tapList, sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java index b871171de7b..d5b687f0774 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java @@ -2,6 +2,7 @@ package forge.ai.ability; import java.util.List; +import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi { valid = sa.getParam("ValidCards"); } - final Player opp = ai.getWeakestOpponent(); + final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { diff --git a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java index 15b2c12a6d0..1e017a982d8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java @@ -56,7 +56,6 @@ public class UnattachAllAi extends SpellAbilityAi { @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card card = sa.getHostCard(); - final Player opp = ai.getWeakestOpponent(); // Check if there are any valid targets List targets = new ArrayList<>(); final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -66,8 +65,8 @@ public class UnattachAllAi extends SpellAbilityAi { if (!mandatory && card.isEquipment() && !targets.isEmpty()) { Card newTarget = (Card) targets.get(0); - //don't equip human creatures - if (newTarget.getController().equals(opp)) { + //don't equip opponent creatures + if (!newTarget.getController().equals(ai)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 00c4837c82b..47c443f59b8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -6,6 +6,7 @@ import java.util.Map; import com.google.common.base.Predicate; import com.google.common.base.Predicates; +import forge.ai.AiAttackController; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; @@ -130,7 +131,8 @@ public class UntapAi extends SpellAbilityAi { Player targetController = ai; if (sa.isCurse()) { - targetController = ai.getWeakestOpponent(); + // TODO search through all opponents, may need to check if different controllers allowed + targetController = AiAttackController.choosePreferredDefenderPlayer(ai); } CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa); @@ -149,8 +151,7 @@ public class UntapAi extends SpellAbilityAi { } CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED); - // filter out enchantments and planeswalkers, their tapped state doesn't - // matter. + // filter out enchantments and planeswalkers, their tapped state doesn't matter. final String[] tappablePermanents = {"Creature", "Land", "Artifact"}; untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa); diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 8a35eee03e2..9d4320867b1 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -1247,17 +1247,6 @@ public class Card extends GameEntity implements Comparable, IHasSVars { currentState.addTrigger(t); return t; } - @Deprecated - public final void removeTrigger(final Trigger t) { - currentState.removeTrigger(t); - } - @Deprecated - public final void removeTrigger(final Trigger t, final CardStateName state) { - getState(state).removeTrigger(t); - } - public final void clearTriggersNew() { - currentState.clearTriggers(); - } public final boolean hasTrigger(final Trigger t) { return currentState.hasTrigger(t); diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index c83ce6d79db..7954026a69c 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -421,6 +421,9 @@ public class Player extends GameEntity implements Comparable { public final Player getWeakestOpponent() { return getOpponents().min(PlayerPredicates.compareByLife()); } + public final Player getStrongestOpponent() { + return getOpponents().max(PlayerPredicates.compareByLife()); + } public boolean isOpponentOf(Player other) { return other != this && other != null && (other.teamNumber < 0 || other.teamNumber != teamNumber); diff --git a/forge-gui/res/cardsfolder/g/goblin_artisans.txt b/forge-gui/res/cardsfolder/g/goblin_artisans.txt index 342618a2a0c..e0f85b739a0 100644 --- a/forge-gui/res/cardsfolder/g/goblin_artisans.txt +++ b/forge-gui/res/cardsfolder/g/goblin_artisans.txt @@ -10,4 +10,5 @@ SVar:DBCleanup:DB$ Cleanup | ForgetDefined$ TriggeredCard A:AB$ FlipACoin | Cost$ T | TgtZone$ Stack | TargetType$ Spell | ValidTgts$ Artifact.YouCtrl+IsRemembered | WinSubAbility$ DBDraw | LoseSubAbility$ DBCounter | TgtPrompt$ Select target Artifact spell | SpellDescription$ Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans. SVar:DBDraw:DB$ Draw | NumCards$ 1 | Defined$ You SVar:DBCounter:DB$ Counter | Defined$ Targeted +AI:RemoveDeck:All Oracle:{T}: Flip a coin. If you win the flip, draw a card. If you lose the flip, counter target artifact spell you control that isn't the target of an ability from another creature named Goblin Artisans. diff --git a/forge-gui/res/cardsfolder/h/hellion_eruption.txt b/forge-gui/res/cardsfolder/h/hellion_eruption.txt index 1d81e61d60b..b055e17e158 100644 --- a/forge-gui/res/cardsfolder/h/hellion_eruption.txt +++ b/forge-gui/res/cardsfolder/h/hellion_eruption.txt @@ -1,9 +1,8 @@ Name:Hellion Eruption ManaCost:5 R Types:Sorcery -A:SP$ SacrificeAll | Cost$ 5 R | ValidCards$ Creature.YouCtrl | RememberSacrificed$ True | SubAbility$ DBToken | SpellDescription$ Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. +A:SP$ SacrificeAll | Cost$ 5 R | ValidCards$ Creature.YouCtrl | RememberSacrificed$ True | SubAbility$ DBToken | AILogic$ HellionEruption | SpellDescription$ Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens. SVar:DBToken:DB$Token | TokenAmount$ X | TokenScript$ r_4_4_hellion | TokenOwner$ You | LegacyImage$ r 4 4 hellion roe SVar:X:Remembered$Amount -AI:RemoveDeck:All SVar:Picture:http://www.wizards.com/global/images/magic/general/hellion_eruption.jpg Oracle:Sacrifice all creatures you control, then create that many 4/4 red Hellion creature tokens.