diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index ce66ad54284..e2aa82831ce 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -70,7 +70,7 @@ public class SpecialCardAi { // Black Lotus and Lotus Bloom public static class BlackLotus { - public static boolean consider(Player ai, SpellAbility sa, ManaCostBeingPaid cost) { + public static boolean consider(final Player ai, final SpellAbility sa, final ManaCostBeingPaid cost) { CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true); int numManaSrcs = manaSources.size(); @@ -99,7 +99,7 @@ public class SpecialCardAi { // Bonds of Faith public static class BondsOfFaith { - public static Card getBestAttachTarget(final Player ai, SpellAbility sa, List list) { + public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List list) { Card chosen = null; List aiHumans = CardLists.filter(list, new Predicate() { @@ -139,7 +139,7 @@ public class SpecialCardAi { // Chain of Acid public static class ChainOfAcid { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { List AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS); List OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), @@ -155,7 +155,7 @@ public class SpecialCardAi { // Chain of Smog public static class ChainOfSmog { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { if (ai.getCardsIn(ZoneType.Hand).isEmpty()) { // to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point) // TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a @@ -180,7 +180,7 @@ public class SpecialCardAi { // Cursed Scroll public static class CursedScroll { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { CardCollectionView hand = ai.getCardsIn(ZoneType.Hand); if (hand.isEmpty()) { return false; @@ -190,7 +190,7 @@ public class SpecialCardAi { return CardLists.filter(hand, CardPredicates.nameEquals(hand.getFirst().getName())).size() == hand.size(); } - public static String chooseCard(Player ai, SpellAbility sa) { + public static String chooseCard(final Player ai, final SpellAbility sa) { int maxCount = 0; Card best = null; CardCollectionView hand = ai.getCardsIn(ZoneType.Hand); @@ -211,7 +211,7 @@ public class SpecialCardAi { public static class DesecrationDemon { private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer - public static boolean considerSacrificingCreature(Player ai, SpellAbility sa) { + public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) { CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach")))); boolean hasUsefulBlocker = false; @@ -235,7 +235,7 @@ public class SpecialCardAi { // Donate public static class Donate { - public static boolean considerTargetingOpponent(Player ai, SpellAbility sa) { + public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) { final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter( ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); if (donateTarget != null) { @@ -271,7 +271,7 @@ public class SpecialCardAi { return false; } - public static boolean considerDonatingPermanent(Player ai, SpellAbility sa) { + public static boolean considerDonatingPermanent(final Player ai, final 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(); @@ -287,7 +287,7 @@ public class SpecialCardAi { // Electrostatic Pummeler public static class ElectrostaticPummeler { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); Game game = ai.getGame(); Combat combat = game.getCombat(); @@ -299,7 +299,7 @@ public class SpecialCardAi { if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) { int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop); - if (source.getNetToughness() <= dmg && predictedPT.getRight() > dmg) + if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg) return true; } } @@ -369,15 +369,15 @@ public class SpecialCardAi { } } - if (predictedPT.getRight() <= oppP && oppHasFirstStrike && !cantDie) { + if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) { // Can't survive first strike or double strike, don't pump return false; } - if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() <= oppP)) { + if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) { // Can't pump enough to kill the blockers and survive, don't pump return false; } - if (source.getNetPower() > oppT && source.getNetToughness() > oppP) { + if (source.getNetPower() > oppT && source.getNetToughness() - source.getDamage() > oppP) { // Already enough to kill the blockers and survive, don't overpump return false; } @@ -392,7 +392,7 @@ public class SpecialCardAi { return true; } - public static boolean predictOverwhelmingDamage(Player ai, SpellAbility sa) { + public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); int oppLife = ai.getWeakestOpponent().getLife(); CardCollection oppInPlay = ai.getWeakestOpponent().getCreaturesInPlay(); @@ -429,7 +429,7 @@ public class SpecialCardAi { } // Force of Will public static class ForceOfWill { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { CardCollection blueCards = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.isColor(MagicColor.BLUE)); boolean isExileMode = false; @@ -458,7 +458,7 @@ public class SpecialCardAi { // Guilty Conscience public static class GuiltyConscience { - public static Card getBestAttachTarget(final Player ai, SpellAbility sa, List list) { + public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List list) { Card chosen = null; List aiStuffies = CardLists.filter(list, new Predicate() { @@ -495,7 +495,7 @@ public class SpecialCardAi { // Living Death (and possibly other similar cards using AILogic LivingDeath) public static class LivingDeath { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { int aiBattlefieldPower = 0, aiGraveyardPower = 0; int threshold = 320; // approximately a 4/4 Flying creature worth of extra value @@ -543,7 +543,7 @@ public class SpecialCardAi { public static class MairsilThePretender { // Scan the fetch list for a card with at least one activated ability. // TODO: can be improved to a full consider(sa, ai) logic which would scan the graveyard first and hand last - public static Card considerCardFromList(CardCollection fetchList) { + public static Card considerCardFromList(final CardCollection fetchList) { for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) { for (SpellAbility ab : c.getSpellAbilities()) { if (ab.isAbility() && !ab.isTrigger()) { @@ -569,7 +569,7 @@ public class SpecialCardAi { // Momir Vig, Simic Visionary Avatar public static class MomirVigAvatar { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { Card source = sa.getHostCard(); if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) { @@ -595,7 +595,7 @@ public class SpecialCardAi { // Necropotence public static class Necropotence { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { Game game = ai.getGame(); int computerHandSize = ai.getZone(ZoneType.Hand).size(); int maxHandSize = ai.getMaxHandSize(); @@ -650,7 +650,7 @@ public class SpecialCardAi { // Null Brooch public static class NullBrooch { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { // TODO: improve the detection of Ensnaring Bridge type effects ("GTX", "X" need generalization) boolean hasEnsnaringBridgeEffect = false; for (Card otb : ai.getCardsIn(ZoneType.Battlefield)) { @@ -675,7 +675,7 @@ public class SpecialCardAi { // Nykthos, Shrine to Nyx public static class NykthosShrineToNyx { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { Game game = ai.getGame(); PhaseHandler ph = game.getPhaseHandler(); if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) { @@ -734,7 +734,7 @@ public class SpecialCardAi { // Phyrexian Dreadnought public static class PhyrexianDreadnought { - public static CardCollection reviseCreatureSacList(Player ai, SpellAbility sa, CardCollection choices) { + public static CardCollection reviseCreatureSacList(final Player ai, final SpellAbility sa, final CardCollection choices) { choices.sort(Collections.reverseOrder(ComputerUtilCard.EvaluateCreatureComparator)); int power = 0; List toKeep = Lists.newArrayList(); @@ -758,11 +758,11 @@ public class SpecialCardAi { // Sarkhan the Mad public static class SarkhanTheMad { - public static boolean considerDig(Player ai, SpellAbility sa) { + public static boolean considerDig(final Player ai, final SpellAbility sa) { return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1; } - public static boolean considerMakeDragon(Player ai, SpellAbility sa) { + public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) { // TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier? CardCollection creatures = ai.getCreaturesInPlay(); boolean hasValidTgt = !CardLists.filter(creatures, new Predicate() { @@ -779,7 +779,7 @@ public class SpecialCardAi { return false; } - public static boolean considerUltimate(Player ai, SpellAbility sa, Player weakestOpp) { + public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) { int minLife = weakestOpp.getLife(); int dragonPower = 0; @@ -794,7 +794,7 @@ public class SpecialCardAi { // Survival of the Fittest public static class SurvivalOfTheFittest { - public static Card considerDiscardTarget(Player ai) { + public static Card considerDiscardTarget(final Player ai) { // The AI here only checks the number of available creatures of various CMC, which is equivalent to knowing // your deck composition and checking (and counting) the cards in other zones so you know what you have left // in the library. As such, this does not cause unfair advantage, at least unless there are cards that are @@ -867,7 +867,7 @@ public class SpecialCardAi { return null; } - public static Card considerCardToGet(Player ai, SpellAbility sa) { + public static Card considerCardToGet(final Player ai, final SpellAbility sa) { CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES); if (creatsInLib.isEmpty()) { return null; } @@ -894,7 +894,7 @@ public class SpecialCardAi { // Timetwister public static class Timetwister { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size(); int maxOppHandSize = 0; @@ -918,7 +918,7 @@ public class SpecialCardAi { // Volrath's Shapeshifter public static class VolrathsShapeshifter { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard); Card topGY = null; Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand)); @@ -937,7 +937,7 @@ public class SpecialCardAi { return false; } - public static CardCollection targetBestCreature(Player ai, SpellAbility sa) { + public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) { Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand)); if (creatHand != null) { CardCollection cc = new CardCollection(); @@ -953,7 +953,7 @@ public class SpecialCardAi { // Ugin, the Spirit Dragon public static class UginTheSpiritDragon { - public static boolean considerPWAbilityPriority(Player ai, SpellAbility sa, ZoneType origin, CardCollectionView oppType, CardCollectionView computerType) { + public static boolean considerPWAbilityPriority(final Player ai, final SpellAbility sa, final ZoneType origin, CardCollectionView oppType, CardCollectionView computerType) { Card source = sa.getHostCard(); Game game = source.getGame(); @@ -1012,7 +1012,7 @@ public class SpecialCardAi { // Yawgmoth's Bargain public static class YawgmothsBargain { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { Game game = ai.getGame(); PhaseHandler ph = game.getPhaseHandler(); @@ -1055,7 +1055,7 @@ public class SpecialCardAi { // Yawgmoth's Will (can potentially be expanded for other broadly similar effects too) public static class YawgmothsWill { - public static boolean consider(Player ai, SpellAbility sa) { + public static boolean consider(final Player ai, final SpellAbility sa) { CardCollectionView cardsInGY = ai.getCardsIn(ZoneType.Graveyard); if (cardsInGY.size() == 0) { 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 b64750b9104..cac07a9d7eb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -95,7 +95,8 @@ public class PumpAi extends PumpAiBase { } return true; } else if (logic.equals("Aristocrat")) { - if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { + final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(sa.getHostCard()); + if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) { return false; } } @@ -743,9 +744,47 @@ public class PumpAi extends PumpAiBase { final Card source = sa.getHostCard(); final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0; + final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0; final int selfEval = ComputerUtilCard.evaluateCreature(source); + final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); - if (combat == null || numOtherCreats == 0) { + if (numOtherCreats == 0) { + return false; + } + + // Try to save the card from death by pumping it if it's threatened with a damage spell + if (isThreatened && toughnessBonus > 0) { + SpellAbility saTop = game.getStack().peekAbility(); + + if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) { + int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage(); + final int numCreatsToSac = Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus)); + + if (numCreatsToSac > 1) { // probably not worth sacrificing too much + return false; + } + + if (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg) { + final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(), + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || card.hasSVar("SacMe") + || ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK? + } + } + ); + if (sacFodder.size() >= numCreatsToSac) { + return true; + } + } + } + + return false; + } + + if (combat == null) { return false; } @@ -793,7 +832,7 @@ public class PumpAi extends PumpAiBase { final int DefP = Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower); // Make sure we don't over-sacrifice, only sac until we can survive and kill a creature - return source.getNetToughness() <= DefP || source.getNetPower() < minDefT; + return source.getNetToughness() - source.getDamage() <= DefP || source.getNetPower() < minDefT; } } else { // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances