From 5d7e06d3ad7ca8a0a578ceed8ff4568116fd1619 Mon Sep 17 00:00:00 2001 From: tool4EvEr Date: Sun, 21 Nov 2021 15:05:34 +0100 Subject: [PATCH] Fix targeting of some effects (e.g. Axis of Mortality) --- .../java/forge/ai/ComputerUtilAbility.java | 2 +- .../main/java/forge/ai/ability/AddTurnAi.java | 2 +- .../java/forge/ai/ability/DamageEachAi.java | 7 ++-- .../src/main/java/forge/ai/ability/DigAi.java | 2 +- .../main/java/forge/ai/ability/EncodeAi.java | 1 - .../java/forge/ai/ability/LifeExchangeAi.java | 32 ++++++++------- .../ai/ability/LifeExchangeVariantAi.java | 3 +- .../main/java/forge/ai/ability/LifeSetAi.java | 41 +++++++++++-------- .../main/java/forge/ai/ability/PoisonAi.java | 3 +- .../forge/ai/ability/PowerExchangeAi.java | 3 +- .../java/forge/ai/ability/ProtectAllAi.java | 1 - .../ai/ability/RearrangeTopOfLibraryAi.java | 2 +- .../java/forge/ai/ability/RegenerateAi.java | 9 ++-- .../java/forge/ai/ability/SacrificeAi.java | 26 +++++++----- .../main/java/forge/ai/ability/TapAllAi.java | 12 ++++-- 15 files changed, 81 insertions(+), 65 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index 75a09008778..b590b3e23a3 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -218,7 +218,7 @@ public class ComputerUtilAbility { public static boolean isFullyTargetable(SpellAbility sa) { SpellAbility sub = sa; while (sub != null) { - if (sub.usesTargeting() && !sub.getTargetRestrictions().hasCandidates(sub)) { + if (sub.usesTargeting() && sub.getTargetRestrictions().getNumCandidates(sub, true) < sub.getMinTargets()) { return false; } sub = sub.getSubAbility(); diff --git a/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java b/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java index 222092308aa..e67ddaee73e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java @@ -54,7 +54,7 @@ public class AddTurnAi extends SpellAbilityAi { break; } } - if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && sa.canTarget(opp)) { + if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) { sa.getTargets().add(opp); } else { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java index 2471f3c52b0..59209d27f67 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java @@ -21,13 +21,12 @@ public class DamageEachAi extends DamageAiBase { Player weakestOpp = targetableOpps.min(PlayerPredicates.compareByLife()); if (sa.usesTargeting() && weakestOpp != null) { + if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) { + return false; + } sa.resetTargets(); sa.getTargets().add(weakestOpp); } - - if ("MadSarkhanUltimate".equals(logic)) { - return SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp); - } final String damage = sa.getParam("NumDmg"); final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); 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 e16d8ce48df..0bbb527bbb3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -47,7 +47,7 @@ public class DigAi extends SpellAbilityAi { if (sa.usesTargeting()) { sa.resetTargets(); - if (!opp.canBeTargetedBy(sa)) { + if (!sa.canTarget(opp)) { return false; } sa.getTargets().add(opp); diff --git a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java index 3f68bd73ed8..6627b3847c0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java @@ -61,7 +61,6 @@ public final class EncodeAi extends SpellAbilityAi { public boolean chkAIDrawback(SpellAbility sa, Player ai) { return true; } - /* * (non-Javadoc) 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 6ad44da50f6..71cdad4a7be 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java @@ -1,8 +1,9 @@ package forge.ai.ability; -import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; +import forge.game.player.PlayerCollection; +import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; import forge.util.MyRandom; @@ -18,14 +19,15 @@ public class LifeExchangeAi extends SpellAbilityAi { */ @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - final int myLife = aiPlayer.getLife(); - Player opponent = AiAttackController.choosePreferredDefenderPlayer(aiPlayer); - final int hLife = opponent.getLife(); - if (!aiPlayer.canGainLife()) { return false; } + final int myLife = aiPlayer.getLife(); + final PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); + final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife()); + final int hLife = opponent == null ? 0 : opponent.getLife(); + // prevent run-away activations - first time will always return true boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); @@ -36,24 +38,23 @@ public class LifeExchangeAi extends SpellAbilityAi { */ if (sa.usesTargeting()) { sa.resetTargets(); - if (opponent.canBeTargetedBy(sa)) { + if (opponent != null && opponent.canLoseLife()) { // never target self, that would be silly for exchange sa.getTargets().add(opponent); - if (!opponent.canLoseLife()) { - return false; - } + } else { + return false; } } // if life is in danger, always activate - if ((myLife < 5) && (hLife > myLife)) { + if (myLife < 5 && hLife > myLife) { return true; } // cost includes sacrifice probably, so make sure it's worth it chance &= (hLife > (myLife + 8)); - return ((MyRandom.getRandom().nextFloat() < .6667) && chance); + return MyRandom.getRandom().nextFloat() < .6667 && chance; } /** @@ -70,13 +71,16 @@ public class LifeExchangeAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, - final boolean mandatory) { - Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); + protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); + Player opp = targetableOpps.max(PlayerPredicates.compareByLife()); if (sa.usesTargeting()) { sa.resetTargets(); if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { sa.getTargets().add(opp); + if (sa.canAddMoreTarget()) { + sa.getTargets().add(ai); + } } else { return false; } 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 f10c29d4ccd..f5f57169981 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java @@ -149,8 +149,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, - final boolean mandatory) { + protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); 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 ef07915ded9..834abec5fd0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java @@ -1,15 +1,21 @@ package forge.ai.ability; +import com.google.common.collect.Iterables; + import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; +import forge.game.card.CardPredicates; import forge.game.card.CounterEnumType; import forge.game.phase.PhaseType; import forge.game.player.Player; +import forge.game.player.PlayerCollection; +import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; import forge.game.spellability.TargetRestrictions; +import forge.game.zone.ZoneType; import forge.util.MyRandom; public class LifeSetAi extends SpellAbilityAi { @@ -17,14 +23,11 @@ public class LifeSetAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { final int myLife = ai.getLife(); - final Player opponent = ai.getStrongestOpponent(); - final int hlife = opponent.getLife(); + final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); + final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife()); + final int hlife = opponent == null ? 0 : opponent.getLife(); final String amountStr = sa.getParam("LifeAmount"); - if (!ai.canGainLife()) { - return false; - } - // Don't use setLife before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")) { @@ -55,20 +58,20 @@ public class LifeSetAi extends SpellAbilityAi { if (tgt != null) { sa.resetTargets(); 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. // possibly add a combo here for Magister Sphinx and // Higedetsu's (sp?) Second Rite - if ((amount > hlife) || !opponent.canLoseLife()) { + if (opponent == null || amount > hlife || !opponent.canLoseLife()) { return false; } + sa.getTargets().add(opponent); } else { - if ((amount > myLife) && (myLife <= 10)) { + if (amount > myLife && myLife <= 10 && ai.canGainLife()) { sa.getTargets().add(ai); } else if (hlife > amount) { sa.getTargets().add(opponent); - } else if (amount > myLife) { + } else if (amount > myLife && ai.canGainLife()) { sa.getTargets().add(ai); } else { return false; @@ -90,18 +93,19 @@ public class LifeSetAi extends SpellAbilityAi { } // if life is in danger, always activate - if ((myLife < 3) && (amount > myLife)) { + if (myLife < 3 && amount > myLife && ai.canGainLife()) { return true; } - return ((MyRandom.getRandom().nextFloat() < .6667) && chance); + return MyRandom.getRandom().nextFloat() < .6667 && chance; } @Override protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final int myLife = ai.getLife(); - final Player opponent = ai.getStrongestOpponent(); - final int hlife = opponent.getLife(); + final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); + final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife()); + final int hlife = opponent == null ? 0 : opponent.getLife(); final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); @@ -118,13 +122,13 @@ public class LifeSetAi extends SpellAbilityAi { } // special cases when amount can't be calculated without targeting first - if (amount == 0 && "TargetedPlayer$StartingLife/HalfDown".equals(source.getSVar(amountStr))) { + if (amount == 0 && opponent != null && "TargetedPlayer$StartingLife/HalfDown".equals(source.getSVar(amountStr))) { // e.g. Torgaar, Famine Incarnate return doHalfStartingLifeLogic(ai, opponent, sa); } if (sourceName.equals("Eternity Vessel") - && (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterEnumType.CHARGE) == 0))) { + && (Iterables.any(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Vampire Hexmage")) || (source.getCounters(CounterEnumType.CHARGE) == 0))) { return false; } @@ -134,13 +138,16 @@ public class LifeSetAi extends SpellAbilityAi { if (tgt != null) { sa.resetTargets(); if (tgt.canOnlyTgtOpponent()) { + if (opponent == null) { + return false; + } sa.getTargets().add(opponent); } else { if (amount > myLife && myLife <= 10) { sa.getTargets().add(ai); } else if (hlife > amount) { sa.getTargets().add(opponent); - } else if (amount > myLife) { + } else if (amount > myLife || mandatory) { sa.getTargets().add(ai); } else { return false; diff --git a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java index b9547f2e875..d6a65513f4e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -25,8 +25,7 @@ public class PoisonAi extends SpellAbilityAi { */ @Override protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { - return !ph.getPhase().isBefore(PhaseType.MAIN2) - || sa.hasParam("ActivationPhases"); + return !ph.getPhase().isBefore(PhaseType.MAIN2) || sa.hasParam("ActivationPhases"); } /* 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 fa362f97a0d..04f3e23379a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java @@ -31,11 +31,10 @@ public class PowerExchangeAi extends SpellAbilityAi { List list = 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) && c.getController() != ai; + return c.canBeTargetedBy(sa) && c.getController() != ai; } }); CardLists.sortByPowerAsc(list); diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java index 69765ce7717..6352447e814 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java @@ -26,7 +26,6 @@ public class ProtectAllAi extends SpellAbilityAi { return false; } // protectAllCanPlayAI() - @Override protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { return true; diff --git a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java index 408d912f76b..b446d81c98c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java @@ -53,7 +53,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { PlayerCollection targetableOpps = aiPlayer.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); Player opp = targetableOpps.min(PlayerPredicates.compareByLife()); final boolean canTgtAI = sa.canTarget(aiPlayer); - final boolean canTgtHuman = opp != null && sa.canTarget(opp); + final boolean canTgtHuman = sa.canTarget(opp); if (canTgtHuman && canTgtAI) { // TODO: maybe some other consideration rather than random? diff --git a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java index 5a1f1132f58..cfb749274fe 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java @@ -138,12 +138,11 @@ public class RegenerateAi extends SpellAbilityAi { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { boolean chance = false; - final TargetRestrictions tgt = sa.getTargetRestrictions(); - if (tgt == null) { + if (sa.usesTargeting()) { + chance = regenMandatoryTarget(ai, sa, mandatory); + } else { // If there's no target on the trigger, just say yes. chance = true; - } else { - chance = regenMandatoryTarget(ai, sa, mandatory); } return chance; @@ -164,7 +163,7 @@ public class RegenerateAi extends SpellAbilityAi { return false; } - if (!mandatory && (compTargetables.size() == 0)) { + if (!mandatory && compTargetables.size() == 0) { return false; } 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 5a94167b7bf..f4aed85c165 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import java.util.Collections; import java.util.List; import forge.ai.ComputerUtilCard; @@ -15,6 +16,8 @@ import forge.game.card.CardPredicates; import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; +import forge.game.player.PlayerCollection; +import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; @@ -22,14 +25,14 @@ public class SacrificeAi extends SpellAbilityAi { @Override protected boolean canPlayAI(Player ai, SpellAbility sa) { - return sacrificeTgtAI(ai, sa); + return sacrificeTgtAI(ai, sa, false); } @Override public boolean chkAIDrawback(SpellAbility sa, Player ai) { // AI should only activate this during Human's turn - return sacrificeTgtAI(ai, sa); + return sacrificeTgtAI(ai, sa, false); } @Override @@ -48,21 +51,24 @@ public class SacrificeAi extends SpellAbilityAi { // Eventually, we can call the trigger of ETB abilities with not // mandatory as part of the checks to cast something - return sacrificeTgtAI(ai, sa) || mandatory; + return sacrificeTgtAI(ai, sa, mandatory) || mandatory; } - private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) { + private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); final boolean destroy = sa.hasParam("Destroy"); - - Player opp = ai.getStrongestOpponent(); + final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); + final Player opp = Collections.max(targetableOpps, PlayerPredicates.compareByLife()); if (sa.usesTargeting()) { - sa.resetTargets(); - if (!opp.canBeTargetedBy(sa)) { + if (opp == null) { return false; } + sa.resetTargets(); sa.getTargets().add(opp); + if (mandatory) { + return true; + } final String valid = sa.getParam("SacValid"); String num = sa.getParamOrDefault("Amount" , "1"); final int amount = AbilityUtils.calculateAmount(source, num, sa); @@ -79,7 +85,7 @@ public class SacrificeAi extends SpellAbilityAi { for (Card c : list) { if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) { - return false; + return false; } } if (!destroy) { @@ -131,7 +137,7 @@ public class SacrificeAi extends SpellAbilityAi { List humanList = null; try { - humanList = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa); + humanList = CardLists.getValidCards(ai.getStrongestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), source, sa); } catch (NullPointerException e) { return false; } finally { diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java index dd89e45ab75..831c5c702d1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java @@ -16,6 +16,8 @@ import forge.game.card.CardPredicates; import forge.game.combat.CombatUtil; import forge.game.phase.PhaseType; import forge.game.player.Player; +import forge.game.player.PlayerCollection; +import forge.game.player.PlayerPredicates; import forge.game.spellability.SpellAbility; import forge.game.zone.ZoneType; import forge.util.MyRandom; @@ -101,10 +103,14 @@ public class TapAllAi extends SpellAbilityAi { CardCollectionView validTappables = getTapAllTargets(valid, source, sa); if (sa.usesTargeting()) { + final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); + Player target = targetableOpps.max(PlayerPredicates.compareByLife()); + if (target == null && mandatory) { + target = ai; + } sa.resetTargets(); - Player opp = ai.getStrongestOpponent(); - sa.getTargets().add(opp); - validTappables = opp.getCardsIn(ZoneType.Battlefield); + sa.getTargets().add(target); + validTappables = target.getCardsIn(ZoneType.Battlefield); } if (mandatory) {