diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java new file mode 100644 index 00000000000..6a378ea6d0f --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -0,0 +1,341 @@ +package forge.ai; + +import com.google.common.base.Predicate; +import forge.ai.ability.TokenAi; +import forge.game.Game; +import forge.game.ability.AbilityUtils; +import forge.game.ability.ApiType; +import forge.game.card.*; +import forge.game.combat.Combat; +import forge.game.keyword.Keyword; +import forge.game.phase.PhaseHandler; +import forge.game.phase.PhaseType; +import forge.game.player.Player; +import forge.game.spellability.SpellAbility; +import forge.game.spellability.TargetRestrictions; +import forge.game.zone.ZoneType; +import forge.util.Aggregates; + +/* + * This class contains logic which is shared by several cards with different ability types (e.g. AF ChangeZone / AF Destroy) + * Ideally, the naming scheme for methods in this class should be doXXXLogic, where XXX is the name of the logic, + * and the signature of the method should be "public static boolean doXXXLogic(final Player ai, final SpellAbility sa), + * possibly followed with any additional necessary parameters. These AI logic routines generally do all the work, so returning + * true from them should indicate that the AI has made a decision and configured the spell ability (targeting, etc.) as it + * deemed necessary. + */ + +public class SpecialAiLogic { + // A logic for cards like Pongify, Crib Swap, Angelic Ascension + public static boolean doPongifyLogic(final Player ai, final SpellAbility sa) { + Card source = sa.getHostCard(); + Game game = source.getGame(); + PhaseHandler ph = game.getPhaseHandler(); + TargetRestrictions tgt = sa.getTargetRestrictions(); + + CardCollection listOpp = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, source, sa); + listOpp = CardLists.getTargetableCards(listOpp, sa); + + Card choice = ComputerUtilCard.getMostExpensivePermanentAI(listOpp); + + final Card token = choice != null ? TokenAi.spawnToken(choice.getController(), sa.getSubAbility()) : null; + if (token == null || !token.isCreature() || token.getNetToughness() < 1) { + return true; // becomes Terminate + } else if (choice != null && choice.isPlaneswalker()) { + if (choice.getCurrentLoyalty() * 35 > ComputerUtilCard.evaluateCreature(token)) { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } else { + return false; + } + } else { + boolean hasOppTarget = true; + if (choice != null + && ((!choice.isCreature() || choice.isTapped()) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.getPlayerTurn() == ai) // prevent surprise combatant + || ComputerUtilCard.evaluateCreature(choice) < 1.5 * ComputerUtilCard.evaluateCreature(token)) { + + hasOppTarget = false; + } + + // See if we have anything we can upgrade + if (!hasOppTarget) { + CardCollection listOwn = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, source, sa); + listOwn = CardLists.getTargetableCards(listOwn, sa); + + Card bestOwnCardToUpgrade = ComputerUtilCard.getWorstCreatureAI(CardLists.filter(listOwn, new Predicate() { + @Override + public boolean apply(Card card) { + return card.isCreature() && (ComputerUtilCard.isUselessCreature(ai, card) + || ComputerUtilCard.evaluateCreature(token) > 2 * ComputerUtilCard.evaluateCreature(card)); + } + })); + if (bestOwnCardToUpgrade != null) { + if (ComputerUtilCard.isUselessCreature(ai, bestOwnCardToUpgrade) || (ph.getPhase().isAfter(PhaseType.COMBAT_END) || ph.getPlayerTurn() != ai)) { + sa.resetTargets(); + sa.getTargets().add(bestOwnCardToUpgrade); + return true; + } + } + } else { + sa.resetTargets(); + sa.getTargets().add(choice); + return true; + } + + return hasOppTarget; + } + } + + // A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT" + public static boolean doAristocratLogic(final Player ai, final SpellAbility sa) { + final Game game = ai.getGame(); + final Combat combat = game.getCombat(); + 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 boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible"); + final int selfEval = ComputerUtilCard.evaluateCreature(source); + final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); + + 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 || indestructible)) { + 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 = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus)); + + if (numCreatsToSac > 1) { // probably not worth sacrificing too much + return false; + } + + if (indestructible || (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? + } + } + ); + return sacFodder.size() >= numCreatsToSac; + } + } + + return false; + } + + if (combat == null) { + return false; + } + + if (combat.isAttacking(source)) { + if (combat.getBlockers(source).isEmpty()) { + // Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if + // the opponent is tapped out or if we're willing to risk it (will currently risk it + // in case it sacs less than half its creatures to deal lethal damage) + + // TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully + // for potential damage prevention, various effects like reducing damage to 0, etc.) + + final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); + final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; + + final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp + int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); + + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent + } + + final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / (powerBonus != 0 ? powerBonus : 1); + + if (defTappedOut || numCreatsToSac < numOtherCreats / 2) { + return source.getNetCombatDamage() < lethalDmg + && source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg; + } else { + return false; + } + } else { + // We have already attacked. Thus, see if we have a creature to sac that is worse to lose + // than the card we attacked with. + final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(), + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || ComputerUtilCard.evaluateCreature(card) < selfEval; + } + } + ); + + if (sacTgts.isEmpty()) { + return false; + } + + final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); + final int DefP = indestructible ? 0 : 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() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; + } + } else { + // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances + 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? + } + } + ); + + return !sacFodder.isEmpty(); + } + } + + // A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat) + public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) { + final Card source = sa.getHostCard(); + final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied + final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS); + + final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); + if (numOtherCreats == 0) { + // Cut short if there's nothing to sac at all + return false; + } + + // Check if the standard Aristocrats logic applies first (if in the right conditions for it) + final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); + if (isDeclareBlockers || isThreatened) { + if (doAristocratLogic(ai, sa)) { + return true; + } + } + + // Check if anything is to be gained from the PutCounter subability + SpellAbility countersSa = null; + if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) { + if (sa.getApi() == ApiType.PutCounter) { + // called directly from CountersPutAi + countersSa = sa; + } + } else { + countersSa = sa.getSubAbility(); + } + + if (countersSa == null) { + // Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?) + System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!"); + return false; + } + + final Game game = ai.getGame(); + final Combat combat = game.getCombat(); + final int selfEval = ComputerUtilCard.evaluateCreature(source); + + String typeToGainCtr = ""; + if (logic.contains(".")) { + typeToGainCtr = logic.substring(logic.indexOf(".") + 1); + } + CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay() + : CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr)); + relevantCreats.remove(source); + if (relevantCreats.isEmpty()) { + // No relevant creatures to sac + return false; + } + + int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa); + + if (combat != null && combat.isAttacking(source) && isDeclareBlockers) { + if (combat.getBlockers(source).isEmpty()) { + // Unblocked. Check if we can deal lethal after receiving counters. + final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); + final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; + + final boolean isInfect = source.hasKeyword(Keyword.INFECT); + int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); + + if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { + lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent + } + + // Check if there's anything that will die anyway that can be eaten to gain a perma-bonus + final CardCollection forcedSacTgts = CardLists.filter(relevantCreats, + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card) + || (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat)); + } + } + ); + if (!forcedSacTgts.isEmpty()) { + return true; + } + + final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs); + + if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) { + return source.getNetCombatDamage() < lethalDmg + && source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg; + } else { + return false; + } + } else { + // We have already attacked. Thus, see if we have a creature to sac that is worse to lose + // than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing + // things that are also threatened to be destroyed anyway. + final CardCollection sacTgts = CardLists.filter(relevantCreats, + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || ComputerUtilCard.evaluateCreature(card) < selfEval + || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); + } + } + ); + + if (sacTgts.isEmpty()) { + return false; + } + + final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source); + final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); + final int DefP = sourceCantDie ? 0 : 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() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; + } + } else { + // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances + final boolean isBlocking = combat != null && combat.isBlocking(source); + final CardCollection sacFodder = CardLists.filter(relevantCreats, + new Predicate() { + @Override + public boolean apply(Card card) { + return ComputerUtilCard.isUselessCreature(ai, card) + || card.hasSVar("SacMe") + || (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval) + || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); + } + } + ); + + return !sacFodder.isEmpty(); + } + } +} 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 469cba9b53d..31d7fed88c8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -97,18 +97,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return true; } else if (aiLogic.equals("Pongify")) { - PhaseHandler ph = sa.getHostCard().getGame().getPhaseHandler(); - Card choice = ComputerUtilCard.getBestCreatureAI(ai.getOpponents().getCreaturesInPlay()); // TODO: improve this for cases where the AI would prefer a planeswalker - final Card token = TokenAi.spawnToken(choice.getController(), sa.getSubAbility()); - if (token == null || !token.isCreature() || token.getNetToughness() < 1) { - return true; // becomes Terminate - } else { - if ((ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.getPlayerTurn() == ai) || // prevent surprise combatant - ComputerUtilCard.evaluateCreature(choice) < 1.5 - * ComputerUtilCard.evaluateCreature(token)) { - return false; - } - } + return SpecialAiLogic.doPongifyLogic(ai, sa); } return super.checkAiLogic(ai, sa, aiLogic); @@ -143,6 +132,8 @@ public class ChangeZoneAi extends SpellAbilityAi { multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa); } else if (aiLogic.equals("MazesEnd")) { return SpecialCardAi.MazesEnd.consider(aiPlayer, sa); + } else if (aiLogic.equals("Pongify")) { + return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic } } if (isHidden(sa)) { 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 c3448d4af15..5826a4ca764 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -223,7 +223,7 @@ public class CountersPutAi extends SpellAbilityAi { } else if ("AlwaysWithNoTgt".equals(logic)) { return true; } else if ("AristocratCounters".equals(logic)) { - return PumpAi.doAristocratWithCountersLogic(sa, ai); + return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); } else if ("PayEnergy".equals(logic)) { return true; } else if ("PayEnergyConservatively".equals(logic)) { 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 8c3855385e6..26191723180 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -222,6 +222,10 @@ public class DestroyAi extends SpellAbilityAi { Card choice = null; // If the targets are only of one type, take the best if (CardLists.getNotType(list, "Creature").isEmpty()) { + if ("Pongify".equals(logic)) { + return SpecialAiLogic.doPongifyLogic(ai, sa); + } + choice = ComputerUtilCard.getBestCreatureAI(list); if ("OppDestroyYours".equals(logic)) { Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay()); @@ -229,19 +233,6 @@ public class DestroyAi extends SpellAbilityAi { return false; } } - if ("Pongify".equals(logic)) { - final Card token = TokenAi.spawnToken(choice.getController(), sa.getSubAbility()); - if (token == null || !token.isCreature() || token.getNetToughness() < 1) { - return true; // becomes Terminate - } else { - if (source.getGame().getPhaseHandler().getPhase() - .isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) || // prevent surprise combatant - ComputerUtilCard.evaluateCreature(choice) < 1.5 - * ComputerUtilCard.evaluateCreature(token)) { - return false; - } - } - } } else if (CardLists.getNotType(list, "Land").isEmpty()) { choice = ComputerUtilCard.getBestLandAI(list); 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 75ecdc36a27..232baeff90a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -71,9 +71,9 @@ public class PumpAi extends PumpAiBase { return false; } } else if ("Aristocrat".equals(aiLogic)) { - return doAristocratLogic(sa, ai); + return SpecialAiLogic.doAristocratLogic(ai, sa); } else if (aiLogic.startsWith("AristocratCounters")) { - return doAristocratWithCountersLogic(sa, ai); + return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); } else if ("RiskFactor".equals(aiLogic)) { if (ai.getCardsIn(ZoneType.Hand).size() + 3 >= ai.getMaxHandSize()) { return false; @@ -794,256 +794,4 @@ public class PumpAi extends PumpAiBase { //and the pump isn't mandatory return true; } - - public static boolean doAristocratLogic(final SpellAbility sa, final Player ai) { - // A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT" - final Game game = ai.getGame(); - final Combat combat = game.getCombat(); - 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 boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible"); - final int selfEval = ComputerUtilCard.evaluateCreature(source); - final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); - - 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 || indestructible)) { - 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 = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus)); - - if (numCreatsToSac > 1) { // probably not worth sacrificing too much - return false; - } - - if (indestructible || (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? - } - } - ); - return sacFodder.size() >= numCreatsToSac; - } - } - - return false; - } - - if (combat == null) { - return false; - } - - if (combat.isAttacking(source)) { - if (combat.getBlockers(source).isEmpty()) { - // Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if - // the opponent is tapped out or if we're willing to risk it (will currently risk it - // in case it sacs less than half its creatures to deal lethal damage) - - // TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully - // for potential damage prevention, various effects like reducing damage to 0, etc.) - - final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); - final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; - - final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp - int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { - lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent - } - - final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / (powerBonus != 0 ? powerBonus : 1); - - if (defTappedOut || numCreatsToSac < numOtherCreats / 2) { - return source.getNetCombatDamage() < lethalDmg - && source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg; - } else { - return false; - } - } else { - // We have already attacked. Thus, see if we have a creature to sac that is worse to lose - // than the card we attacked with. - final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(), - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || ComputerUtilCard.evaluateCreature(card) < selfEval; - } - } - ); - - if (sacTgts.isEmpty()) { - return false; - } - - final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); - final int DefP = indestructible ? 0 : 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() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; - } - } else { - // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances - 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? - } - } - ); - - return !sacFodder.isEmpty(); - } - } - - public static boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) { - // A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat) - final Card source = sa.getHostCard(); - final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied - final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS); - - final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); - if (numOtherCreats == 0) { - // Cut short if there's nothing to sac at all - return false; - } - - // Check if the standard Aristocrats logic applies first (if in the right conditions for it) - final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); - if (isDeclareBlockers || isThreatened) { - if (doAristocratLogic(sa, ai)) { - return true; - } - } - - // Check if anything is to be gained from the PutCounter subability - SpellAbility countersSa = null; - if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) { - if (sa.getApi() == ApiType.PutCounter) { - // called directly from CountersPutAi - countersSa = sa; - } - } else { - countersSa = sa.getSubAbility(); - } - - if (countersSa == null) { - // Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?) - System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!"); - return false; - } - - final Game game = ai.getGame(); - final Combat combat = game.getCombat(); - final int selfEval = ComputerUtilCard.evaluateCreature(source); - - String typeToGainCtr = ""; - if (logic.contains(".")) { - typeToGainCtr = logic.substring(logic.indexOf(".") + 1); - } - CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay() - : CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr)); - relevantCreats.remove(source); - if (relevantCreats.isEmpty()) { - // No relevant creatures to sac - return false; - } - - int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa); - - if (combat != null && combat.isAttacking(source) && isDeclareBlockers) { - if (combat.getBlockers(source).isEmpty()) { - // Unblocked. Check if we can deal lethal after receiving counters. - final Player defPlayer = combat.getDefendingPlayerRelatedTo(source); - final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0; - - final boolean isInfect = source.hasKeyword(Keyword.INFECT); - int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife(); - - if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { - lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent - } - - // Check if there's anything that will die anyway that can be eaten to gain a perma-bonus - final CardCollection forcedSacTgts = CardLists.filter(relevantCreats, - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card) - || (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat)); - } - } - ); - if (!forcedSacTgts.isEmpty()) { - return true; - } - - final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs); - - if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) { - return source.getNetCombatDamage() < lethalDmg - && source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg; - } else { - return false; - } - } else { - // We have already attacked. Thus, see if we have a creature to sac that is worse to lose - // than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing - // things that are also threatened to be destroyed anyway. - final CardCollection sacTgts = CardLists.filter(relevantCreats, - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || ComputerUtilCard.evaluateCreature(card) < selfEval - || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); - } - } - ); - - if (sacTgts.isEmpty()) { - return false; - } - - final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source); - final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness); - final int DefP = sourceCantDie ? 0 : 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() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; - } - } else { - // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances - final boolean isBlocking = combat != null && combat.isBlocking(source); - final CardCollection sacFodder = CardLists.filter(relevantCreats, - new Predicate() { - @Override - public boolean apply(Card card) { - return ComputerUtilCard.isUselessCreature(ai, card) - || card.hasSVar("SacMe") - || (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval) - || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card); - } - } - ); - - return !sacFodder.isEmpty(); - } - } }