diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 3ec3f81ffb5..af2adaa30f7 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -2076,6 +2076,32 @@ public class ComputerUtil { return ComputerUtilCard.getBestCreatureAI(killables); } + public static int predictDamageFromSpell(final SpellAbility sa, final Player targetPlayer) { + int damage = -1; // returns -1 if the spell does not deal damage + final Card card = sa.getHostCard(); + + SpellAbility ab = sa; + while (ab != null) { + if (ab.getApi() == ApiType.DealDamage) { + if (damage == -1) { damage = 0; } // found a damage-dealing spell + if (!ab.hasParam("NumDmg")) { + continue; + } + damage += ComputerUtilCombat.predictDamageTo(targetPlayer, + AbilityUtils.calculateAmount(card, ab.getParam("NumDmg"), ab), card, false); + } else if (ab.getApi() == ApiType.LoseLife) { + if (damage == -1) { damage = 0; } // found a damage-dealing spell + if (!ab.hasParam("LifeAmount")) { + continue; + } + damage += AbilityUtils.calculateAmount(card, ab.getParam("LifeAmount"), ab); + } + ab = ab.getSubAbility(); + } + + return damage; + } + public static int getDamageForPlaying(final Player player, final SpellAbility sa) { // check for bad spell cast triggers diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java index cd86a6d2406..5e1541000fb 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java @@ -15,7 +15,9 @@ import forge.game.card.CardLists; import forge.game.card.CardPredicates.Presets; import forge.game.player.Player; import forge.game.spellability.SpellAbility; +import forge.game.spellability.SpellAbilityStackInstance; import forge.game.zone.ZoneType; +import java.util.Iterator; public class ComputerUtilAbility { public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) { @@ -106,4 +108,22 @@ public class ComputerUtilAbility { } return result; } + + public static SpellAbility getTopSpellAbilityOnStack(Game game, SpellAbility sa) { + Iterator it = game.getStack().iterator(); + + if (!it.hasNext()) { + return null; + } + + SpellAbility tgtSA = it.next().getSpellAbility(true); + // Grab the topmost spellability that isn't this SA and use that for comparisons + if (sa.equals(tgtSA) && game.getStack().size() > 1) { + if (!it.hasNext()) { + return null; + } + tgtSA = it.next().getSpellAbility(true); + } + return tgtSA; + } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 37563560197..5091f64313e 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -314,6 +314,7 @@ public class ComputerUtilMana { private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, boolean checkPlayable) { adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai); List manaSpentToPay = test ? new ArrayList() : sa.getPayingMana(); + boolean purePhyrexian = cost.containsOnlyPhyrexianMana(); List paymentList = Lists.newArrayList(); @@ -324,7 +325,7 @@ public class ComputerUtilMana { boolean hasConverge = sa.getHostCard().hasConverge(); ListMultimap sourcesForShards = getSourcesForShards(cost, sa, ai, test, checkPlayable, manaSpentToPay, hasConverge); - if (sourcesForShards == null) { + if (sourcesForShards == null && !purePhyrexian) { return false; // no mana abilities to use for paying } @@ -353,7 +354,11 @@ public class ComputerUtilMana { hasConverge = false; } } else { - saList = sourcesForShards.get(toPay); + if (!(sourcesForShards == null && purePhyrexian)) { + saList = sourcesForShards.get(toPay); + } else { + saList = Lists.newArrayList(); // Phyrexian mana only: no valid mana sources, but can still pay life + } } if (saList == null) { break; @@ -361,7 +366,7 @@ public class ComputerUtilMana { saList.removeAll(saExcludeList); - SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, saList, checkPlayable || !test); + SpellAbility saPayment = saList.isEmpty() ? null : chooseManaAbility(cost, sa, ai, toPay, saList, checkPlayable || !test); if (saPayment != null && saPayment.hasParam("AILogic")) { boolean consider = false; @@ -375,7 +380,7 @@ public class ComputerUtilMana { } if (saPayment == null) { - if (!toPay.isPhyrexian() || !ai.canPayLife(2)) { + if (!toPay.isPhyrexian() || !ai.canPayLife(2) || (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) { break; // cannot pay } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java index a57590442de..0049061daec 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java @@ -1,6 +1,13 @@ package forge.ai.ability; +import forge.ai.ComputerUtil; +import forge.ai.ComputerUtilAbility; +import forge.ai.ComputerUtilMana; import forge.ai.SpellAbilityAi; +import forge.card.mana.ManaCost; +import forge.card.mana.ManaCostParser; +import forge.game.Game; +import forge.game.mana.ManaCostBeingPaid; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -16,7 +23,47 @@ public class ChangeTargetsAi extends SpellAbilityAi { */ @Override protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return false; + final Game game = sa.getHostCard().getGame(); + final SpellAbility topSa = game.getStack().isEmpty() ? null : ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa); + + if (sa.hasParam("AILogic")) { + if ("SpellMagnet".equals(sa.getParam("AILogic"))) { + // Cards like Spellskite that retarget spells to itself + + if (topSa == null) { + // nothing on stack, so nothing to target + return false; + } + if (!topSa.usesTargeting() || topSa.getTargets().getTargetCards().contains(sa.getHostCard())) { + // if this does not target at all or already targets host, no need to redirect it again + return false; + } + if (topSa.getHostCard() != null && !topSa.getHostCard().getController().isOpponentOf(aiPlayer)) { + // make sure not to redirect our own abilities + return false; + } + if (!topSa.canTarget(sa.getHostCard())) { + // don't try targeting it if we can't legally target Spellskite with it in the first place + return false; + } + + if ("Spellskite".equals(sa.getHostCard().getName())) { + int potentialDmg = ComputerUtil.predictDamageFromSpell(topSa, aiPlayer); + boolean canPayBlue = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(new ManaCost(new ManaCostParser("U"))), sa, aiPlayer); + if (potentialDmg != -1 && potentialDmg <= 2 && !canPayBlue && topSa.getTargets().getTargets().contains(aiPlayer)) { + // do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life + return false; + } + } + + sa.resetTargets(); + sa.getTargets().add(topSa); + return true; + } + } + + // The AI can't otherwise play this ability, but should at least not miss mandatory activations (e.g. triggers). + return sa.isMandatory(); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index fce3f320024..7980a4ddf45 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import forge.ai.AiProps; +import forge.ai.ComputerUtilAbility; import java.util.Iterator; import forge.ai.ComputerUtilCost; @@ -54,7 +55,7 @@ public class CounterAi extends SpellAbilityAi { final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { - final SpellAbility topSA = findTopSpellAbility(game, sa); + final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa); if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai || ai.getAllies().contains(topSA.getActivatingPlayer())) { // might as well check for player's friendliness @@ -257,17 +258,4 @@ public class CounterAi extends SpellAbilityAi { return new ImmutablePair<>(bestOption != null ? bestOption : leastBadOption, bestOption != null); } - - public SpellAbility findTopSpellAbility(Game game, SpellAbility sa) { - Iterator it = game.getStack().iterator(); - SpellAbility tgtSA = it.next().getSpellAbility(true); - // Grab the topmost spellability that isn't this SA and use that for comparisons - if (sa.equals(tgtSA) && game.getStack().size() > 1) { - if (!it.hasNext()) { - return null; - } - tgtSA = it.next().getSpellAbility(true); - } - return tgtSA; - } } diff --git a/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java b/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java index 52cd50ea33c..5b66276b147 100644 --- a/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java +++ b/forge-game/src/main/java/forge/game/mana/ManaCostBeingPaid.java @@ -170,6 +170,15 @@ public class ManaCostBeingPaid { return false; } + public final boolean containsOnlyPhyrexianMana() { + for (ManaCostShard shard : unpaidShards.keySet()) { + if (!shard.isPhyrexian()) { + return false; + } + } + return true; + } + public final boolean payPhyrexian() { ManaCostShard phy = null; for (ManaCostShard mcs : unpaidShards.keySet()) { diff --git a/forge-gui/res/cardsfolder/s/spellskite.txt b/forge-gui/res/cardsfolder/s/spellskite.txt index 1738e528d97..9abb587325a 100644 --- a/forge-gui/res/cardsfolder/s/spellskite.txt +++ b/forge-gui/res/cardsfolder/s/spellskite.txt @@ -2,6 +2,6 @@ Name:Spellskite ManaCost:2 Types:Artifact Creature Horror PT:0/4 -A:AB$ ChangeTargets | Cost$ PU | TargetType$ Spell,Activated,Triggered | ValidTgts$ Card | DefinedMagnet$ Self | ChangeSingleTarget$ True | SpellDescription$ Change a target of target spell or ability to CARDNAME. +A:AB$ ChangeTargets | Cost$ PU | TargetType$ Spell,Activated,Triggered | ValidTgts$ Card | DefinedMagnet$ Self | ChangeSingleTarget$ True | AILogic$ SpellMagnet | SpellDescription$ Change a target of target spell or ability to CARDNAME. SVar:Picture:http://www.wizards.com/global/images/magic/general/spellskite.jpg Oracle:{P/U}: Change a target of target spell or ability to Spellskite. ({P/U} can be paid with either {U} or 2 life.)