From bd0e4fb2bc77c7396212e36855534673748d3cf1 Mon Sep 17 00:00:00 2001 From: Agetian Date: Sat, 7 Jan 2017 14:11:57 +0000 Subject: [PATCH] - Refactored the card-specific AIs implemented so far to its own class SpecialCardAi, will consider moving other card-specific hardcoded logic there later. - Implemented rudimentary AI logic for Necropotence and Yawgmoth's Bargain, could use improvement but should be fine in typical situations. - Promoted Necropotence and Yawgmoth's Bargain from RemAIDeck to RemRandomDeck. - Propagated the SA parameter to checkLifeCost (and closed the previous TODO associated with that), was also necessary to support the Necro AI logic. --- .gitattributes | 1 + .../src/main/java/forge/ai/ComputerUtil.java | 2 +- .../main/java/forge/ai/ComputerUtilCost.java | 8 +- .../main/java/forge/ai/ComputerUtilMana.java | 2 +- .../src/main/java/forge/ai/SpecialCardAi.java | 256 ++++++++++++++++++ .../main/java/forge/ai/SpellAbilityAi.java | 2 +- .../main/java/forge/ai/ability/AttachAi.java | 2 +- .../java/forge/ai/ability/ChangeZoneAi.java | 7 +- .../forge/ai/ability/ChangeZoneAllAi.java | 2 +- .../java/forge/ai/ability/ChooseColorAi.java | 49 +--- .../java/forge/ai/ability/ChooseSourceAi.java | 2 +- .../main/java/forge/ai/ability/CounterAi.java | 2 +- .../java/forge/ai/ability/CountersPutAi.java | 2 +- .../forge/ai/ability/CountersPutAllAi.java | 2 +- .../forge/ai/ability/CountersRemoveAi.java | 2 +- .../java/forge/ai/ability/DamageAllAi.java | 2 +- .../java/forge/ai/ability/DamageDealAi.java | 2 +- .../forge/ai/ability/DamagePreventAi.java | 2 +- .../forge/ai/ability/DamagePreventAllAi.java | 2 +- .../main/java/forge/ai/ability/DebuffAi.java | 2 +- .../main/java/forge/ai/ability/DestroyAi.java | 2 +- .../java/forge/ai/ability/DestroyAllAi.java | 2 +- .../main/java/forge/ai/ability/DiscardAi.java | 2 +- .../main/java/forge/ai/ability/DrawAi.java | 17 +- .../java/forge/ai/ability/LifeGainAi.java | 2 +- .../java/forge/ai/ability/LifeLoseAi.java | 2 +- .../main/java/forge/ai/ability/PoisonAi.java | 2 +- .../java/forge/ai/ability/ProtectAllAi.java | 2 +- .../main/java/forge/ai/ability/PumpAi.java | 43 +-- .../java/forge/ai/ability/SacrificeAllAi.java | 2 +- forge-gui/res/cardsfolder/n/necropotence.txt | 4 +- .../res/cardsfolder/y/yawgmoths_bargain.txt | 4 +- 32 files changed, 316 insertions(+), 119 deletions(-) create mode 100644 forge-ai/src/main/java/forge/ai/SpecialCardAi.java diff --git a/.gitattributes b/.gitattributes index accb4e41d86..cba180f38d3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -32,6 +32,7 @@ forge-ai/src/main/java/forge/ai/CreatureEvaluator.java -text forge-ai/src/main/java/forge/ai/GameState.java -text forge-ai/src/main/java/forge/ai/LobbyPlayerAi.java -text forge-ai/src/main/java/forge/ai/PlayerControllerAi.java -text +forge-ai/src/main/java/forge/ai/SpecialCardAi.java -text forge-ai/src/main/java/forge/ai/SpellAbilityAi.java -text forge-ai/src/main/java/forge/ai/SpellApiToAi.java -text forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java -text diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 5ccf64d1916..965ad5da3be 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -751,7 +751,7 @@ public class ComputerUtil { if (controller == ai) { final Cost abCost = sa.getPayCosts(); if (abCost != null) { - if (!ComputerUtilCost.checkLifeCost(controller, abCost, c, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(controller, abCost, c, 4, sa)) { continue; // Won't play ability } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java index 4a61579781e..2efc763ad2d 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java @@ -195,8 +195,7 @@ public class ComputerUtilCost { * @param sourceAbility TODO * @return true, if successful */ - public static boolean checkLifeCost(final Player ai, final Cost cost, final Card source, final int remainingLife, SpellAbility sourceAbility) { - // TODO - Pass in SA for everything else that calls this function + public static boolean checkLifeCost(final Player ai, final Cost cost, final Card source, int remainingLife, SpellAbility sourceAbility) { if (cost == null) { return true; } @@ -209,6 +208,11 @@ public class ComputerUtilCost { amount = AbilityUtils.calculateAmount(source, payLife.getAmount(), sourceAbility); } + // check if there's override for the remainingLife threshold + if (sourceAbility != null && sourceAbility.hasParam("AIMinLifeThreshold")) { + remainingLife = Integer.parseInt(sourceAbility.getParam("AIMinLifeThreshold")); + } + if (ai.getLife() - amount < remainingLife && !ai.cantLoseForZeroOrLessLife()) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index 79e48ddd99f..b5b90127211 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1166,7 +1166,7 @@ public class ComputerUtilMana { // don't kill yourself final Cost abCost = m.getPayCosts(); - if (!ComputerUtilCost.checkLifeCost(ai, abCost, sourceCard, 1, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, sourceCard, 1, m)) { continue; } diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java new file mode 100644 index 00000000000..4e25b6e2f07 --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -0,0 +1,256 @@ +/* + * Forge: Play Magic: the Gathering. + * Copyright (C) 2011 Forge Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package forge.ai; + +import com.google.common.collect.Iterables; +import com.google.common.collect.Lists; +import forge.card.MagicColor; +import forge.card.mana.ManaCost; +import forge.game.Game; +import forge.game.card.Card; +import forge.game.card.CardCollectionView; +import forge.game.card.CardFactoryUtil; +import forge.game.card.CardLists; +import forge.game.card.CardPredicates; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.player.PlayerPredicates; +import forge.game.spellability.SpellAbility; +import forge.game.zone.ZoneType; +import java.util.Collections; +import java.util.List; + +/** + * Special logic for individual cards + * + * Specific methods for each card that requires special handling are stored in inner classes + * Each class should have a name based on the name of the card and ideally preceded with a + * single-line comment with the full English card name to make searching for them easier. + * + * Class methods should return "true" if they are successful and have completed their task in full, + * otherwise should return "false" to signal that the AI should not use the card under current + * circumstances. A good convention to follow is to call the method "consider" if it's the only + * method necessary, or considerXXXX if several methods do different tasks, and use at least two + * mandatory parameters (Player ai, SpellAbility sa, in this order) and, if necessary, additional + * parameters later. + * + * If this class ends up being busy, consider splitting it into individual classes, each in its + * own file, inside its own package, for example, forge.ai.cards. + */ +public class SpecialCardAi { + + // Donate + public static class Donate { + public static boolean considerTargetingOpponent(Player ai, SpellAbility sa) { + final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter( + ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); + if (donateTarget != null) { + // first filter for opponents which can be targeted by SA + final Iterable oppList = Iterables.filter(ai.getOpponents(), + PlayerPredicates.isTargetableBy(sa)); + + // filter for player who does not have donate target already + Iterable oppTarget = Iterables.filter(oppList, + PlayerPredicates.isNotCardInPlay(donateTarget.getName())); + // fall back to previous list + if (Iterables.isEmpty(oppTarget)) { + oppTarget = oppList; + } + + // select player with less lands on the field (helpful for Illusions of Grandeur and probably Pacts too) + Player opp = Collections.min(Lists.newArrayList(oppTarget), + PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.LANDS)); + + if (opp != null) { + sa.resetTargets(); + sa.getTargets().add(opp); + return true; + } + return true; + } + // No targets found to donate, so do nothing. + return false; + } + + public static boolean considerDonatingPermanent(Player ai, SpellAbility sa) { + Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); + if (donateTarget != null) { + sa.resetTargets(); + sa.getTargets().add(donateTarget); + return true; + } + + // Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail + System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first."); + return false; + } + } + + // Necropotence + public static class Necropotence { + public static boolean consider(Player ai, SpellAbility sa) { + Game game = ai.getGame(); + int computerHandSize = ai.getZone(ZoneType.Hand).size(); + int maxHandSize = ai.getMaxHandSize(); + + if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).size() > 0) { + // Prefer Yawgmoth's Bargain because AI is generally better with it + + // TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead? + // (not sure how to detect the presence of such effects yet) + return false; + } + + PhaseHandler ph = game.getPhaseHandler(); + + int exiledWithNecro = 1; // start with 1 because if this succeeds, one extra card will be exiled with Necro + for (Card c : ai.getCardsIn(ZoneType.Exile)) { + if (c.getExiledWith() != null && "Necropotence".equals(c.getExiledWith().getName()) && c.isFaceDown()) { + exiledWithNecro++; + } + } + + // TODO: Any other bad effects like that? + boolean blackViseOTB = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).size() > 0; + + if (!ph.isPlayerTurn(ai) && ph.is(PhaseType.MAIN2) + && ai.getSpellsCastLastTurn() == 0 + && ai.getSpellsCastThisTurn() == 0 + && ai.getLandsPlayedLastTurn() == 0) { + // We're in a situation when we have nothing castable in hand, something needs to be done + if (!blackViseOTB) { + // exile-loot +1 card when a max hand size, hoping to get a workable spell or land + return computerHandSize + exiledWithNecro - 1 == maxHandSize; + } else { + // Loot to 7 in presence of Black Vise, hoping to find what to do + // NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead? + return computerHandSize + exiledWithNecro <= maxHandSize; + } + } else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) { + // try not to overdraw in presence of Black Vise + return false; + } else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) { + // Only draw until we reach max hand size + return false; + } else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) { + // Only activate in AI's own turn (sans the exception above) + return false; + } + + return true; + } + } + + // Nykthos, Shrine to Nyx + public static class NykthosShrineToNyx { + public static boolean consider(Player ai, SpellAbility sa) { + Game game = ai.getGame(); + PhaseHandler ph = game.getPhaseHandler(); + if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) { + // TODO: currently limited to Main 2, somehow improve to let the AI use this SA at other time? + return false; + } + String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield)); + int devotion = CardFactoryUtil.xCount(sa.getHostCard(), "Count$Devotion." + prominentColor); + int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0); + + // do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage) + if (devotion < activationCost + 1) { + return false; + } + + final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command}); + List all = ComputerUtilAbility.getSpellAbilities(cards, ai); + + int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableMana(ai, true), CardPredicates.Presets.UNTAPPED).size(); + + for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) { + ManaCost cost = testSa.getPayCosts().getTotalMana(); + byte colorProfile = cost.getColorProfile(); + + if ((cost.getCMC() == 0)) { + // no mana cost, no need to activate this SA then (additional mana not needed) + continue; + } else if (colorProfile != 0 && (cost.getColorProfile() & MagicColor.fromName(prominentColor)) == 0) { + // does not feature prominent color, won't be able to pay for it with SA activated for this color + continue; + } else if ((testSa.getPayCosts().getTotalMana().getCMC() > devotion + numManaSrcs - activationCost)) { + // the cost may be too high even if we activate this SA + continue; + } + + if (testSa.getHostCard().getName().equals(sa.getHostCard().getName())) { + // prevent infinitely recursing own ability when testing AI play decision + continue; + } + + testSa.setActivatingPlayer(ai); + if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSa) == AiPlayDecision.WillPlay) { + // the AI is willing to play the spell + return true; + } + } + + return false; // haven't found anything to play with the excess generated mana + } + } + // Yawgmoth's Bargain + public static class YawgmothsBargain { + public static boolean consider(Player ai, SpellAbility sa) { + Game game = ai.getGame(); + PhaseHandler ph = game.getPhaseHandler(); + + int computerHandSize = ai.getZone(ZoneType.Hand).size(); + int maxHandSize = ai.getMaxHandSize(); + + // TODO: Any other bad effects like that? + boolean blackViseOTB = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Black Vise")).size() > 0; + + // TODO: Consider effects like "whenever a player draws a card, he loses N life" (e.g. Nekusar, the Mindraiser), + // and effects that draw an additional card whenever a card is drawn. + + if (!ph.isPlayerTurn(ai) && ph.is(PhaseType.END_OF_TURN) + && ai.getSpellsCastLastTurn() == 0 + && ai.getSpellsCastThisTurn() == 0 + && ai.getLandsPlayedLastTurn() == 0) { + // We're in a situation when we have nothing castable in hand, something needs to be done + if (!blackViseOTB) { + // draw +1 card when a max hand size, hoping to draw a workable spell or land + return computerHandSize == maxHandSize; + } else { + // draw cards hoping to draw answers even in presence of Black Vise if there's no valid play + // TODO: maybe limit to 1 or 2 cards at a time? + return computerHandSize + 1 <= maxHandSize; // currently draws to 7 cards + } + } else if (blackViseOTB && computerHandSize + 1 > 4) { + // try not to overdraw in presence of Black Vise + return false; + } else if (computerHandSize + 1 > maxHandSize) { + // Only draw until we reach max hand size + return false; + } else if (!ph.isPlayerTurn(ai)) { + // Only activate in AI's own turn (sans the exception above) + return false; + } + + return true; + } + } + +} diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index c86cef8f94a..4eaffe00857 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -86,7 +86,7 @@ public abstract class SpellAbilityAi { * Evaluated costs are: life, discard, sacrifice and counter-removal */ protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) { - if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) { return false; } if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) { diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index 116ee89d9c3..8d6cd3d6299 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -41,7 +41,7 @@ public class AttachAi extends SpellAbilityAi { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { 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 ce8a4a4c5dd..8fa38ae4298 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -137,6 +137,9 @@ public class ChangeZoneAi extends SpellAbilityAi { return true; } } + if (aiLogic.equals("Necropotence")) { + return SpecialCardAi.Necropotence.consider(aiPlayer, sa); + } } String origin = null; if (sa.hasParam("Origin")) { @@ -245,7 +248,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } @@ -654,7 +657,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java index 8293900445f..e5cf5bbed61 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java @@ -41,7 +41,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java index b9b46c71a2d..43100a86338 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java @@ -7,6 +7,7 @@ import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilMana; import forge.ai.PlayerControllerAi; +import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.card.MagicColor; import forge.card.mana.ManaCost; @@ -41,53 +42,7 @@ public class ChooseColorAi extends SpellAbilityAi { } if ("Nykthos, Shrine to Nyx".equals(source.getName())) { - PhaseHandler ph = game.getPhaseHandler(); - if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) { - // TODO: currently limited to Main 2, somehow improve to let the AI use this SA at other time? - return false; - } - String prominentColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield)); - int devotion = CardFactoryUtil.xCount(source, "Count$Devotion." + prominentColor); - int activationCost = sa.getPayCosts().getTotalMana().getCMC() + (sa.getPayCosts().hasTapCost() ? 1 : 0); - - // do not use this SA if devotion to most prominent color is less than its own activation cost + 1 (to actually get advantage) - if (devotion < activationCost + 1) { - return false; - } - - final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command}); - List all = ComputerUtilAbility.getSpellAbilities(cards, ai); - - int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableMana(ai, true), CardPredicates.Presets.UNTAPPED).size(); - - for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) { - ManaCost cost = testSa.getPayCosts().getTotalMana(); - byte colorProfile = cost.getColorProfile(); - - if ((cost.getCMC() == 0)) { - // no mana cost, no need to activate this SA then (additional mana not needed) - continue; - } else if (colorProfile != 0 && (cost.getColorProfile() & MagicColor.fromName(prominentColor)) == 0) { - // does not feature prominent color, won't be able to pay for it with SA activated for this color - continue; - } else if ((testSa.getPayCosts().getTotalMana().getCMC() > devotion + numManaSrcs - activationCost)) { - // the cost may be too high even if we activate this SA - continue; - } - - if (testSa.getHostCard().getName().equals(source.getName())) { - // prevent infinitely recursing own ability when testing AI play decision - continue; - } - - testSa.setActivatingPlayer(ai); - if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSa) == AiPlayDecision.WillPlay) { - // the AI is willing to play the spell - return true; - } - } - - return false; // haven't found anything to play with the excess generated mana + return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa); } if ("Oona, Queen of the Fae".equals(source.getName())) { 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 d3b119a7676..c4759a27005 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java @@ -43,7 +43,7 @@ public class ChooseSourceAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } 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 8a73f2e69d6..8ddf5c872df 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -35,7 +35,7 @@ public class CounterAi extends SpellAbilityAi { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index 14a83cab2b4..382eebf85d9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -66,7 +66,7 @@ public class CountersPutAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java index 16434add2c2..17d5120330d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java @@ -43,7 +43,7 @@ public class CountersPutAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java index 5e722902d54..4a6da8ec867 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -34,7 +34,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index f6b3e4a5e52..d4293390630 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -33,7 +33,7 @@ public class DamageAllAi extends SpellAbilityAi { final Cost abCost = sa.getPayCosts(); if (abCost != null) { // AI currently disabled for some costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index 9cd93f26370..9a7df5dbcec 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -112,7 +112,7 @@ public class DamageDealAi extends DamageAiBase { } // temporarily disabled until better AI - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java index 7bc8c6e204e..5ce58e80d49 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java @@ -34,7 +34,7 @@ public class DamagePreventAi extends SpellAbilityAi { final Cost cost = sa.getPayCosts(); // temporarily disabled until better AI - if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java index 1b5674c7217..ef47d6f50ff 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAllAi.java @@ -22,7 +22,7 @@ public class DamagePreventAllAi extends SpellAbilityAi { final Cost cost = sa.getPayCosts(); // temporarily disabled until better AI - if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) { return false; } 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 36adb9f60c0..8ca45a9d293 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java @@ -46,7 +46,7 @@ public class DebuffAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 6cd5adc7286..1de5cbe6627 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -43,7 +43,7 @@ public class DestroyAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } 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 fbcdb40549c..3e9e38a98d3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -112,7 +112,7 @@ public class DestroyAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for some costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } } 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 7b7d990a562..a7cf3517d3e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -32,7 +32,7 @@ public class DiscardAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java index 72faea1e3da..4855af13602 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -29,6 +29,7 @@ import forge.game.cost.Cost; import forge.game.cost.CostDiscard; import forge.game.cost.CostPart; import forge.game.cost.PaymentDecision; +import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; @@ -60,7 +61,7 @@ public class DrawAi extends SpellAbilityAi { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } @@ -119,7 +120,8 @@ public class DrawAi extends SpellAbilityAi { || game.getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN)) && !sa.hasParam("PlayerTurn") && !SpellAbilityAi.isSorcerySpeed(sa) && ai.getCardsIn(ZoneType.Hand).size() > 1 - && !ComputerUtil.activateForCost(sa, ai)) { + && !ComputerUtil.activateForCost(sa, ai) + && !"YawgmothsBargain".equals(sa.getParam("AILogic"))) { return false; } @@ -221,9 +223,18 @@ public class DrawAi extends SpellAbilityAi { } //if (n) + + // Logic for cards that require special handling + if (sa.hasParam("AILogic")) { + if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) { + return SpecialCardAi.YawgmothsBargain.consider(ai, sa); + } + } + + // Generic logic for all cards that do not need any special handling + // TODO: if xPaid and one of the below reasons would fail, instead of // bailing reduce toPay amount to acceptable level - if (tgt != null) { // ability is targeted sa.resetTargets(); diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java index 3cd0ebe58b1..91bb818a2cf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -68,7 +68,7 @@ public class LifeGainAi extends SpellAbilityAi { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, false)) { return false; } - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java index 1e3a96422b8..72abec77dcc 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java @@ -69,7 +69,7 @@ public class LifeLoseAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, amount, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, amount, sa)) { 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 d80562c77c9..a7fb6f0bd74 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -39,7 +39,7 @@ public class PoisonAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 1, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 1, sa)) { return false; } 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 eaf19d12853..32f48c04718 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java @@ -21,7 +21,7 @@ public class ProtectAllAi extends SpellAbilityAi { final Cost cost = sa.getPayCosts(); // temporarily disabled until better AI - if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, cost, hostCard, 4, sa)) { return false; } diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java index 320ef586863..11377dfa538 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -17,6 +17,7 @@ import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilMana; import forge.ai.PlayerControllerAi; +import forge.ai.SpecialCardAi; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -277,38 +278,8 @@ public class PumpAi extends PumpAiBase { return true; } } else if (sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("Donate")) { - final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter( - ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); - if (donateTarget != null) { - // Donate, step 1 - target the opponent. - if (sa.getParam("AILogic").equals("DonateTargetPlayer")) { - // first filter for opponents which can be targeted by SA - final Iterable oppList = Iterables.filter(ai.getOpponents(), - PlayerPredicates.isTargetableBy(sa)); - - // filter for player with does not have donate target - // already - Iterable oppTarget = Iterables.filter(oppList, - PlayerPredicates.isNotCardInPlay(donateTarget.getName())); - // fall back to previous list - if (Iterables.isEmpty(oppTarget)) { - oppTarget = oppList; - } - - // select player with less lands on the field - Player opp = Collections.min(Lists.newArrayList(oppTarget), - PlayerPredicates.compareByZoneSize(ZoneType.Battlefield, CardPredicates.Presets.LANDS)); - - if (opp != null) { - sa.resetTargets(); - sa.getTargets().add(opp); - return true; - } - return true; - } - } - // No targets found to donate, so do nothing. - return false; + // Donate step 1 - try to target an opponent, preferably one who does not have a donate target yet + return SpecialCardAi.Donate.considerTargetingOpponent(ai, sa); } if (ComputerUtil.preventRunAwayActivations(sa)) { @@ -449,12 +420,8 @@ public class PumpAi extends PumpAiBase { return false; } } else if (sa.getParam("AILogic").equals("DonateTargetPerm")) { - // Donate, step 2 - target a donatable permanent. - Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); - if (donateTarget != null) { - sa.getTargets().add(donateTarget); - return true; - } + // Donate step 2 - target a donatable permanent. + return SpecialCardAi.Donate.considerDonatingPermanent(ai, sa); } if (isFight) { return FightAi.canFightAi(ai, sa, attack, defense); 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 856637b1640..cb9a61fac7c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java @@ -44,7 +44,7 @@ public class SacrificeAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for some costs - if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, null)) { + if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { return false; } } diff --git a/forge-gui/res/cardsfolder/n/necropotence.txt b/forge-gui/res/cardsfolder/n/necropotence.txt index 4baecfab59e..1249ef8bca5 100644 --- a/forge-gui/res/cardsfolder/n/necropotence.txt +++ b/forge-gui/res/cardsfolder/n/necropotence.txt @@ -4,10 +4,10 @@ Types:Enchantment S:Mode$ Continuous | Affected$ You | AddKeyword$ Skip your draw step. | Description$ Skip your draw step. T:Mode$ Discarded | ValidCard$ Card.YouCtrl | TriggerZones$ Battlefield | Execute$ TrigChange | TriggerDescription$ Whenever you discard a card, exile that card from your graveyard. SVar:TrigChange:AB$ChangeZone | Cost$ 0 | Defined$ TriggeredCard | Origin$ Graveyard | Destination$ Exile -A:AB$ ChangeZone | Cost$ PayLife<1> | Defined$ TopOfLibrary | Origin$ Library | Destination$ Exile | ExileFaceDown$ True | RememberChanged$ True | SubAbility$ DelayedReturn | SpellDescription$ Exile the top card of your library face down. Put that card into your hand at the beginning of your next end step. +A:AB$ ChangeZone | Cost$ PayLife<1> | Defined$ TopOfLibrary | Origin$ Library | Destination$ Exile | ExileFaceDown$ True | RememberChanged$ True | SubAbility$ DelayedReturn | AILogic$ Necropotence | AIMinLifeThreshold$ 1 | SpellDescription$ Exile the top card of your library face down. Put that card into your hand at the beginning of your next end step. SVar:DelayedReturn:DB$ DelayedTrigger | Mode$ Phase | Phase$ End of Turn | ValidPlayer$ You | Execute$ TrigReturn | RememberObjects$ Remembered | TriggerDescription$ Put the exiled card into your hand. | SubAbility$ DBCleanup SVar:TrigReturn:DB$ ChangeZone | Origin$ Exile | Destination$ Hand | Defined$ DelayTriggerRemembered SVar:DBCleanup:DB$ Cleanup | ClearRemembered$ True -SVar:RemAIDeck:True +SVar:RemRandomDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/necropotence.jpg Oracle:Skip your draw step.\nWhenever you discard a card, exile that card from your graveyard.\nPay 1 life: Exile the top card of your library face down. Put that card into your hand at the beginning of your next end step. diff --git a/forge-gui/res/cardsfolder/y/yawgmoths_bargain.txt b/forge-gui/res/cardsfolder/y/yawgmoths_bargain.txt index 3bd28fd9e57..82a882fce85 100644 --- a/forge-gui/res/cardsfolder/y/yawgmoths_bargain.txt +++ b/forge-gui/res/cardsfolder/y/yawgmoths_bargain.txt @@ -2,7 +2,7 @@ Name:Yawgmoth's Bargain ManaCost:4 B B Types:Enchantment S:Mode$ Continuous | Affected$ You | AddKeyword$ Skip your draw step. | Description$ Skip your draw step. -A:AB$ Draw | Cost$ PayLife<1> | NumCards$ 1 | SpellDescription$ Draw a card. -SVar:RemAIDeck:True +A:AB$ Draw | Cost$ PayLife<1> | NumCards$ 1 | AILogic$ YawgmothsBargain | AIMinLifeThreshold$ 1 | SpellDescription$ Draw a card. +SVar:RemRandomDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/yawgmoths_bargain.jpg Oracle:Skip your draw step.\nPay 1 life: Draw a card.