diff --git a/forge-ai/src/main/java/forge/ai/AiPlayDecision.java b/forge-ai/src/main/java/forge/ai/AiPlayDecision.java index 5c518d720f3..3be4ca62dd2 100644 --- a/forge-ai/src/main/java/forge/ai/AiPlayDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiPlayDecision.java @@ -5,6 +5,8 @@ public enum AiPlayDecision { WillPlay, MandatoryPlay, PlayToEmptyHand, + ImpactCombat, + ResponseToStackResolve, AddBoardPresence, Removal, Tempo, @@ -22,14 +24,18 @@ public enum AiPlayDecision { CantPlayAi, CantAfford, CantAffordX, + DoesntImpactCombat, + DoesntImpactGame, MissingLogic, MissingNeededCards, TimingRestrictions, MissingPhaseRestrictions, + ConditionsNotMet, NeedsToPlayCriteriaNotMet, StopRunawayActivations, TargetingFailed, CostNotAcceptable, + LifeInDanger, WouldDestroyLegend, WouldDestroyOtherPlaneswalker, WouldBecomeZeroToughnessCreature, @@ -39,7 +45,7 @@ public enum AiPlayDecision { public boolean willingToPlay() { return switch (this) { - case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, Removal, Tempo, CardAdvantage -> true; + case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true; default -> false; }; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 7c5bc8c729f..d5a79253b64 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -1819,18 +1819,18 @@ public class ComputerUtilCard { * @param sa Pump* or CounterPut* * @return */ - public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) { + public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) { final List objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true); if (!sa.usesTargeting()) { final List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); for (final Card card : cards) { if (objects.contains(card)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve); } } // For pumps without targeting restrictions, just return immediately until this is fleshed out. - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); @@ -1849,11 +1849,11 @@ public class ComputerUtilCard { } if (!sa.isTargetNumberValid()) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } public static boolean isUselessCreature(Player ai, Card c) { diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 38681aa8d60..3106885d88e 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -214,7 +214,7 @@ public class SpecialAiLogic { } // 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) { + public static AiAbilityDecision 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); @@ -222,14 +222,14 @@ public class SpecialAiLogic { 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; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } // 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; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -247,7 +247,7 @@ public class SpecialAiLogic { 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; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } final Game game = ai.getGame(); @@ -263,7 +263,7 @@ public class SpecialAiLogic { relevantCreats.remove(source); if (relevantCreats.isEmpty()) { // No relevant creatures to sac - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa); @@ -287,16 +287,20 @@ public class SpecialAiLogic { || (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat)) ); if (!forcedSacTgts.isEmpty()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } 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; + if (source.getNetCombatDamage() < lethalDmg + && source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg) { + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); + } + + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { // We have already attacked. Thus, see if we have a creature to sac that is worse to lose @@ -309,7 +313,7 @@ public class SpecialAiLogic { ); if (sacTgts.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source); @@ -317,7 +321,10 @@ public class SpecialAiLogic { final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower); // 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; + if (source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances @@ -329,7 +336,11 @@ public class SpecialAiLogic { || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card) ); - return !sacFodder.isEmpty(); + if (sacFodder.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } + + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 1a9ccb90e37..6caeb28aa86 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -400,7 +400,7 @@ public class SpecialCardAi { // Donate public static class Donate { - public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision 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) { @@ -410,7 +410,7 @@ public class SpecialCardAi { // All opponents have hexproof or something like that if (Iterables.isEmpty(oppList)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // filter for player who does not have donate target already @@ -428,12 +428,11 @@ public class SpecialCardAi { if (opp != null) { sa.resetTargets(); sa.getTargets().add(opp); - return true; } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // No targets found to donate, so do nothing. - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) { @@ -452,7 +451,7 @@ public class SpecialCardAi { // Electrostatic Pummeler public static class ElectrostaticPummeler { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); Game game = ai.getGame(); Combat combat = game.getCombat(); @@ -465,13 +464,13 @@ 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() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg) - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } // Do not activate if damage will be prevented if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame); } // Activate Electrostatic Pummeler's pump only as a combat trick @@ -480,14 +479,14 @@ public class SpecialCardAi { // We'll try to deal lethal trample/unblocked damage, so remember the card for attack // and wait until declare blockers step. AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS); - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } boolean isBlocking = combat.isBlocking(source); @@ -512,11 +511,11 @@ public class SpecialCardAi { } if (totalDamageToPW >= oppT + loyalty) { // Already enough damage to take care of the planeswalker - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) { // Can pump to kill the planeswalker, go for it - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } } @@ -537,31 +536,31 @@ public class SpecialCardAi { // We can deal a lot of damage (either a lot of damage directly to the opponent, // or kill the blocker(s) and damage the opponent at the same time, so go for it AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS); - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } } if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) { // Can't survive first strike or double strike, don't pump - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } 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; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) { // Already enough to kill the blockers and survive, don't overpump - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage() && predictedPT.getLeft() <= oppT) { // Can't kill or cripple anyone, as well as can't Trample over, so don't pump - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } // If we got here, it should be a favorable combat pump, resulting in at least one // opposing creature dying, and hopefully with the Pummeler surviving combat. - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) { @@ -693,13 +692,13 @@ public class SpecialCardAi { // Gideon Blackblade public static class GideonBlackblade { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { sa.resetTargets(); CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa)); if (!otb.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestAI(otb)); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -930,12 +929,12 @@ public class SpecialCardAi { // Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll) public static class LivingDeath { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { // if there's another reanimator card currently suspended, don't cast a new one until the previous // one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End) for (Card ex : ai.getCardsIn(ZoneType.Exile)) { if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -946,7 +945,7 @@ public class SpecialCardAi { if (aiCreaturesInGY.isEmpty()) { // nothing in graveyard, so cut short - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } for (Card c : ai.getCreaturesInPlay()) { @@ -978,17 +977,30 @@ public class SpecialCardAi { } // if we get more value out of this than our opponent does (hopefully), go for it - return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold); + if ((aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } // Maze's End public static class MazesEnd { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { PhaseHandler ph = ai.getGame().getPhaseHandler(); CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate")); - return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty(); + if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (availableGates.isEmpty()) { + // No gates available, so don't activate Maze's End + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } + + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } public static Card considerCardToGet(final Player ai, final SpellAbility sa) @@ -1146,13 +1158,13 @@ public class SpecialCardAi { // Necropotence public static class Necropotence { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { Game game = ai.getGame(); int computerHandSize = ai.getZone(ZoneType.Hand).size(); int maxHandSize = ai.getMaxHandSize(); if (ai.getCardsIn(ZoneType.Library).isEmpty()) { - return false; // nothing to draw from the library + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) { @@ -1160,7 +1172,7 @@ public class SpecialCardAi { // 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; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } PhaseHandler ph = game.getPhaseHandler(); @@ -1182,23 +1194,33 @@ public class SpecialCardAi { // We're in a situation when we have nothing castable in hand, something needs to be done if (!blackViseOTB) { // exile-loot +1 card when at max hand size, hoping to get a workable spell or land - return computerHandSize + exiledWithNecro - 1 == maxHandSize; + if (computerHandSize + exiledWithNecro - 1 == maxHandSize) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } 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; + if (computerHandSize + exiledWithNecro <= maxHandSize) { + // Loot to 7, hoping to find something playable + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // Loot to 8, hoping to find something playable + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) { // try not to overdraw in presence of Black Vise - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) { // Only draw until we reach max hand size - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) { // Only activate in AI's own turn (sans the exception above) - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -1480,7 +1502,7 @@ public class SpecialCardAi { // Sorin, Vengeful Bloodlord public static class SorinVengefulBloodlord { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY); CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES @@ -1494,7 +1516,7 @@ public class SpecialCardAi { CardLists.sortByCmcDesc(creaturesToGet); if (creaturesToGet.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // pick the best creature that will stay on the battlefield @@ -1510,10 +1532,10 @@ public class SpecialCardAi { sa.resetTargets(); sa.getTargets().add(best); sa.setXManaCostPaid(best.getCMC()); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -1627,23 +1649,27 @@ public class SpecialCardAi { // The One Ring public static class TheOneRing { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD); int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN); - return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger - && ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1; + if (ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger + && ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.LifeInDanger); } } // The Scarab God public static class TheScarabGod { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES)); Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES)); @@ -1654,13 +1680,19 @@ public class SpecialCardAi { sa.getTargets().add(worstOwnCreat); } - return sa.getTargets().size() > 0; + if (!sa.getTargets().isEmpty()) { + // If we have a target, we can play this ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // No valid targets, can't play this ability + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } } // Timetwister public static class Timetwister { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size(); int maxOppHandSize = 0; @@ -1674,7 +1706,14 @@ public class SpecialCardAi { } // use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand - return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD; + if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) { + // if the AI has less than 3 cards in hand or the opponent has more than 3 cards in hand than the AI + // then the AI is willing to play this ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // otherwise, don't play this ability + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index 444a103b716..853a9717088 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -95,14 +95,15 @@ public abstract class SpellAbilityAi { return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions); } - if (!checkApiLogic(ai, sa)) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + AiAbilityDecision decision = checkApiLogic(ai, sa); + if (!decision.willingToPlay()) { + return decision; } // needs to be after API logic because needs to check possible X Cost? if (cost != null && !willPayCosts(ai, sa, cost, source)) { return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return decision; } protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) { @@ -169,12 +170,18 @@ public abstract class SpellAbilityAi { /** * The rest of the logic not covered by the canPlayAI template is defined here */ - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } - return MyRandom.getRandom().nextFloat() < .8f; + if (MyRandom.getRandom().nextFloat() < .8f) { + // 80% chance to play the ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + // 20% chance to not play the ability + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { diff --git a/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java b/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java index 51fdc74be0d..ed728a8f64a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AlterAttributeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -16,7 +18,7 @@ import java.util.Map; public class AlterAttributeAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) { final Card source = sa.getHostCard(); boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true")); String[] attributes = sa.getParam("Attributes").split(","); @@ -24,7 +26,7 @@ public class AlterAttributeAi extends SpellAbilityAi { if (sa.usesTargeting()) { // TODO add targeting logic // needed for Suspected - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } final List defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); @@ -36,7 +38,7 @@ public class AlterAttributeAi extends SpellAbilityAi { case "Solved": // there is currently no effect that would un-solve something if (!c.isSolved() && activate) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } break; case "Suspect": @@ -44,21 +46,21 @@ public class AlterAttributeAi extends SpellAbilityAi { // is Suspected good or bad? // currently Suspected is better if (!activate) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); case "Saddle": case "Saddled": // AI should not try to Saddle again? if (c.isSaddled()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java index 12d2a140da9..8ba969a78d7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java @@ -21,13 +21,19 @@ import java.util.Map; public class AmassAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) { CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army"); Card host = sa.getHostCard(); final Game game = ai.getGame(); if (!aiArmies.isEmpty()) { - return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1)); + if (aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1))) { + // If AI has an Army that can receive counters, play the ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // AI has Armies but none can receive counters, so don't play + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame); + } } final String type = sa.getParam("Type"); final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army"; @@ -36,7 +42,7 @@ public class AmassAi extends SpellAbilityAi { Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false); if (token == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } token.setController(ai, 0); @@ -63,7 +69,11 @@ public class AmassAi extends SpellAbilityAi { //reset static abilities game.getAction().checkStaticAbilities(false); - return result; + if (result) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override @@ -83,8 +93,11 @@ public class AmassAi extends SpellAbilityAi { @Override protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - boolean result = mandatory || checkApiLogic(ai, sa); - return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + + return checkApiLogic(ai, sa); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java index e5b77a7af8c..82637daf1ee 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java @@ -142,21 +142,22 @@ public class AnimateAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) { final Card source = sa.getHostCard(); final Game game = aiPlayer.getGame(); final PhaseHandler ph = game.getPhaseHandler(); if (!sa.metConditions() && sa.getSubAbility() == null) { - return false; // what is this for? + return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); // what is this for? } if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) { + // Should I animate a card before i have to sacrifice something better? if (!isAnimatedThisTurn(aiPlayer, source)) { rememberAnimatedThisTurn(aiPlayer, source); - return true; // interrupt sacrifice + return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve); } } if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) { - return false; // prevent crewing with equal or better creatures + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); // prevent crewing with equal or better creatures } if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) { @@ -202,16 +203,16 @@ public class AnimateAi extends SpellAbilityAi { if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) { if (sa.isCrew() && c.isCreature()) { // Do not try to crew a vehicle which is already a creature - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } Card animatedCopy = becomeAnimated(c, sa); if (ph.isPlayerTurn(aiPlayer) && !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } if (ph.getPlayerTurn().isOpponentOf(aiPlayer) && !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } // also check if maybe there are static effects applied to the animated copy that would matter // (e.g. Myth Realized) @@ -227,11 +228,12 @@ public class AnimateAi extends SpellAbilityAi { } if (bFlag) { rememberAnimatedThisTurn(aiPlayer, sa.getHostCard()); + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return bFlag; // All of the defined stuff is animated, not very useful + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else { sa.resetTargets(); - return animateTgtAI(sa).willingToPlay(); + return animateTgtAI(sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/AssembleContraptionAi.java b/forge-ai/src/main/java/forge/ai/ability/AssembleContraptionAi.java index 4f329dd153f..702532e098f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AssembleContraptionAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AssembleContraptionAi.java @@ -52,11 +52,11 @@ public class AssembleContraptionAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) { int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (xPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getRootAbility().setXManaCostPaid(xPay); } @@ -66,7 +66,7 @@ public class AssembleContraptionAi extends SpellAbilityAi { if(target != null) sa.getTargets().add(target); else - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } return super.checkApiLogic(ai, sa); 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 4dea256169d..25ef2f53ee8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeTargetsAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilMana; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.card.mana.ManaCost; import forge.game.Game; import forge.game.card.Card; @@ -21,7 +18,7 @@ public class ChangeTargetsAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final Game game = sa.getHostCard().getGame(); final SpellAbility topSa = game.getStack().isEmpty() ? null : ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa); @@ -32,47 +29,50 @@ public class ChangeTargetsAi extends SpellAbilityAi { // The AI can't otherwise play this ability, but should at least not // miss mandatory activations (e.g. triggers). - return sa.isMandatory(); + if (sa.isMandatory()) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - private boolean doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) { + private AiAbilityDecision doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) { // For cards like Spellskite that retarget spells to itself if (topSa == null) { // nothing on stack, so nothing to target - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final TargetChoices topTargets = topSa.getTargets(); final Card topHost = topSa.getHostCard(); - if (sa.getTargets().size() != 0 && sa.isTrigger()) { + if (!sa.getTargets().isEmpty() && sa.isTrigger()) { // something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) { // if this does not target at all or already targets host, no need to redirect it again - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } for (Card tgt : topTargets.getTargetCards()) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) { // We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites), // no need to retarget again to another one - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) { // make sure not to redirect our own abilities - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!topSa.canTarget(sa.getHostCard())) { // don't try targeting it if we can't legally target the host card with it in the first place - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!sa.canTarget(topSa)) { // don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) { @@ -85,22 +85,22 @@ public class ChangeTargetsAi extends SpellAbilityAi { if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay && topTargets.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; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } Card firstCard = topTargets.getFirstTargetedCard(); // if we're not the target don't intervene unless we can steal a buff if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } Player firstPlayer = topTargets.getFirstTargetedPlayer(); if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); sa.getTargets().add(topSa); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } 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 193e723191b..7d9de167cf1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -129,14 +129,14 @@ public class ChangeZoneAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) { // Checks for "return true" unlike checkAiLogic() multipleCardsToChoose.clear(); String aiLogic = sa.getParam("AILogic"); if (aiLogic != null) { if (aiLogic.equals("Always")) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc. return doSacAndUpgradeLogic(aiPlayer, sa); } else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc. @@ -156,10 +156,18 @@ public class ChangeZoneAi extends SpellAbilityAi { } else if (aiLogic.equals("MazesEnd")) { return SpecialCardAi.MazesEnd.consider(aiPlayer, sa); } else if (aiLogic.equals("Pongify")) { - return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic + if (sa.isTargetNumberValid()) { + // Pre-targeted in checkAiLogic + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if (aiLogic.equals("ReturnCastable")) { - return !sa.getHostCard().getExiledCards().isEmpty() - && ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false); + if (!sa.getHostCard().getExiledCards().isEmpty() + && ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (sa.isHidden()) { @@ -258,7 +266,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. */ - private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) { + private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) { // Fetching should occur fairly often as it helps cast more spells, and // have access to more mana final Cost abCost = sa.getPayCosts(); @@ -275,7 +283,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } catch (IllegalArgumentException ex) { // This happens when Origin is something like // "Graveyard,Library" (Doomsday) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } final String destination = sa.getParam("Destination"); @@ -284,11 +292,11 @@ public class ChangeZoneAi extends SpellAbilityAi { // AI currently disabled for these costs if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa) && !(destination.equals("Battlefield") && !source.isLand())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { @@ -297,7 +305,7 @@ public class ChangeZoneAi extends SpellAbilityAi { CostDiscard cd = (CostDiscard) part; // this is mainly for typecycling if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } } @@ -305,14 +313,14 @@ public class ChangeZoneAi extends SpellAbilityAi { if (sa.isNinjutsu()) { if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } if (ai.getGame().getCombat() == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } List attackers = ai.getGame().getCombat().getUnblockedAttackers(); boolean lowerCMC = false; @@ -323,7 +331,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } } if (!lowerCMC) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -333,15 +341,15 @@ public class ChangeZoneAi extends SpellAbilityAi { final AbilitySub abSub = sa.getSubAbility(); if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) { if (!abSub.metConditions()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); } } // prevent run-away activations - first time will always return true if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } Iterable pDefined = Lists.newArrayList(source.getController()); @@ -355,7 +363,7 @@ public class ChangeZoneAi extends SpellAbilityAi { sa.getTargets().add(ai); } if (!sa.isTargetNumberValid()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } pDefined = sa.getTargets().getTargetPlayers(); } else { @@ -399,12 +407,12 @@ public class ChangeZoneAi extends SpellAbilityAi { } if (!activateForCost && list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Atarka's Command".equals(sourceName) && (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) { // be strict on playing lands off charms - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } String num = sa.getParamOrDefault("ChangeNum", "1"); @@ -412,55 +420,65 @@ public class ChangeZoneAi extends SpellAbilityAi { if (sa.getSVar("X").equals("Count$xPaid")) { // Set PayX here to maximum value. int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); - if (xPay == 0) return false; + if (xPay == 0) { + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); + } xPay = Math.min(xPay, list.size()); sa.setXManaCostPaid(xPay); } else { // Figure out the X amount, bail if it's zero (nothing will change zone). int xValue = AbilityUtils.calculateAmount(source, "X", sa); if (xValue == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } } } if (sourceName.equals("Temur Sabertooth")) { // activated bounce + pump - if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) || - ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility())) { + boolean pumpDecision = ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")); + AiAbilityDecision saveDecision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility()); + if (pumpDecision || saveDecision.willingToPlay()) { for (Card c : list) { if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve); } } } - return canBouncePermanent(ai, sa, list) != null; + if (canBouncePermanent(ai, sa, list) != null) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (ComputerUtil.playImmediately(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // don't use fetching to top of library/graveyard before main2 if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")) { if (!destination.equals("Battlefield") && !destination.equals("Hand")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Only tutor something in main1 if hand is almost empty if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand") && !aiLogic.equals("AnyMainPhase")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (ComputerUtil.waitForBlocking(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb).willingToPlay(); + if (subAb == null) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb); } /** @@ -681,7 +699,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. */ - private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility sa) { + private static AiAbilityDecision knownOriginCanPlayAI(final Player ai, final SpellAbility sa) { // Retrieve either this card, or target Cards in Graveyard final List origin = Lists.newArrayList(); @@ -694,19 +712,19 @@ public class ChangeZoneAi extends SpellAbilityAi { final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (sa.usesTargeting()) { if (!isPreferredTarget(ai, sa, false, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { // non-targeted retrieval final List retrieval = sa.knownDetermineDefined(sa.getParam("Defined")); if (retrieval == null || retrieval.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // return this card from graveyard: cards like Hammer of Bogardan @@ -717,7 +735,7 @@ public class ChangeZoneAi extends SpellAbilityAi { // (dying or losing control of) if (origin.contains(ZoneType.Battlefield)) { if (ai.getGame().getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final AbilitySub abSub = sa.getSubAbility(); @@ -730,7 +748,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (!(destination.equals(ZoneType.Exile) && (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic")))) && !destination.equals(ZoneType.Hand)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final List objects = ComputerUtil.predictThreatenedObjects(ai, sa); @@ -742,13 +760,13 @@ public class ChangeZoneAi extends SpellAbilityAi { } } if (!contains) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (destination == ZoneType.Battlefield) { if (ComputerUtil.isETBprevented(retrieval.get(0))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // predict whether something may put a ETBing creature below zero toughness @@ -758,7 +776,7 @@ public class ChangeZoneAi extends SpellAbilityAi { final Card copy = CardCopyService.getLKICopy(c); ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null); if (copy.getNetToughness() <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -772,13 +790,17 @@ public class ChangeZoneAi extends SpellAbilityAi { } } if (nothingWillReturn) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb).willingToPlay(); + if (subAb == null) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb); } /* @@ -1816,7 +1838,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return super.chooseSingleAttackableEntity(ai, sa, options, params); } - private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) { + private AiAbilityDecision doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) { Card source = sa.getHostCard(); String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1]; @@ -1835,14 +1857,14 @@ public class ChangeZoneAi extends SpellAbilityAi { sa.resetTargets(); sa.getTargets().add(bestRet); source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC()); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) { + private AiAbilityDecision doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) { Card source = sa.getHostCard(); PhaseHandler ph = ai.getGame().getPhaseHandler(); String logic = sa.getParam("AILogic"); @@ -1850,7 +1872,7 @@ public class ChangeZoneAi extends SpellAbilityAi { if (!ph.is(PhaseType.MAIN2)) { // Should be given a chance to cast other spells as well as to use a previously upgraded creature - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1]; @@ -1889,12 +1911,11 @@ public class ChangeZoneAi extends SpellAbilityAi { if (!listGoal.isEmpty()) { // make sure we're upgrading sacCMC->goalCMC source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - // no candidates to upgrade - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) { 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 33d03af40b9..ba2251ff7a4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java @@ -69,11 +69,9 @@ public class ChangeZoneAllAi extends SpellAbilityAi { computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa); if ("LivingDeath".equals(aiLogic)) { - boolean result = SpecialCardAi.LivingDeath.consider(ai, sa); - return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return SpecialCardAi.LivingDeath.consider(ai, sa); } else if ("Timetwister".equals(aiLogic)) { - boolean result = SpecialCardAi.Timetwister.consider(ai, sa); - return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return SpecialCardAi.Timetwister.consider(ai, sa); } else if ("RetDiscardedThisTurn".equals(aiLogic)) { boolean result = !ai.getDiscardedThisTurn().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN); return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java index de299dad1cd..f067bc3dc0d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -18,7 +18,7 @@ import java.util.Map; public class CharmAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); List choices = CharmEffect.makePossibleOptions(sa); @@ -70,10 +70,10 @@ public class CharmAi extends SpellAbilityAi { // Set minimum choices for triggers where chooseMultipleOptionsAi() returns null chosenList = chooseOptionsAi(sa, choices, ai, true, num, min); if (chosenList.isEmpty() && min != 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -81,7 +81,7 @@ public class CharmAi extends SpellAbilityAi { sa.setChosenList(chosenList); if (choiceForOpp) { - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.isSpell()) { @@ -90,7 +90,11 @@ public class CharmAi extends SpellAbilityAi { } // prevent run-away activations - first time will always return true - return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private List chooseOptionsAi(SpellAbility sa, List choices, final Player ai, boolean isTrigger, int num, int min) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java index 1f957f42dd8..f5eb7f74efe 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -26,19 +26,19 @@ public class ChooseCardAi extends SpellAbilityAi { * The rest of the logic not covered by the canPlayAI template is defined here */ @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { if (sa.usesTargeting()) { sa.resetTargets(); // search targetable Opponents final List oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); if (oppList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(Iterables.getFirst(oppList, null)); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -140,11 +140,7 @@ public class ChooseCardAi extends SpellAbilityAi { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - if (checkApiLogic(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } + return checkApiLogic(ai, sa); } protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java index fb650cafe77..24256d9ddc9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java @@ -43,8 +43,13 @@ public class ChooseGenericAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { - return sa.hasParam("AILogic"); + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { + if (sa.hasParam("AILogic")) { + // This is equivilant to what was here before but feels bad + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) @@ -52,10 +57,14 @@ public class ChooseGenericAi extends SpellAbilityAi { */ @Override public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { - boolean result = sa.isTrigger() - ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()).willingToPlay() - : checkApiLogic(aiPlayer, sa); - return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + AiAbilityDecision decision; + if (sa.isTrigger()) { + decision = doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()); + } else { + decision = checkApiLogic(aiPlayer, sa); + } + + return decision; } @Override @@ -73,8 +82,7 @@ public class ChooseGenericAi extends SpellAbilityAi { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - AiAbilityDecision superDecision = super.doTriggerAINoCost(aiPlayer, sa, mandatory); - return superDecision; + return super.doTriggerAINoCost(aiPlayer, sa, mandatory); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java index 64e66ea380b..9916f3a3b2f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java @@ -42,14 +42,17 @@ public class ClashAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { boolean legalAction = true; if (sa.usesTargeting()) { legalAction = selectTarget(ai, sa); + if (!legalAction) { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return legalAction; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java index a5acefb25b0..b7784e862ec 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java @@ -19,20 +19,25 @@ import java.util.Map; public class CountersMoveAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { + AiAbilityDecision decision = new AiAbilityDecision(100, AiPlayDecision.WillPlay); if (sa.usesTargeting()) { sa.resetTargets(); - AiAbilityDecision decision = moveTgtAI(ai, sa); + decision = moveTgtAI(ai, sa); if (!decision.willingToPlay()) { - return false; + return decision; } } if (!playReusable(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return MyRandom.getRandom().nextFloat() < .8f; // random success + if (MyRandom.getRandom().nextFloat() < .8f) { + return decision; + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java index 259a18271d8..82e9b079537 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java @@ -19,7 +19,7 @@ import java.util.Map; public class CountersMultiplyAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final CounterType counterType = getCounterType(sa); if (!sa.usesTargeting()) { @@ -51,7 +51,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { }); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } else { return setTargets(ai, sa); @@ -87,8 +87,10 @@ public class CountersMultiplyAi extends SpellAbilityAi { if (!sa.usesTargeting()) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - if (setTargets(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + + AiAbilityDecision decision = setTargets(ai, sa); + if (decision.willingToPlay()) { + return decision; } else if (mandatory) { CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) { @@ -98,7 +100,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { .filter(CardPredicates.hasCounters().negate()) .findFirst().orElse(null); sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); @@ -116,7 +118,7 @@ public class CountersMultiplyAi extends SpellAbilityAi { return null; } - private boolean setTargets(Player ai, SpellAbility sa) { + private AiAbilityDecision setTargets(Player ai, SpellAbility sa) { final CounterType counterType = getCounterType(sa); final Game game = ai.getGame(); @@ -172,10 +174,10 @@ public class CountersMultiplyAi extends SpellAbilityAi { // targeting does failed if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list, diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java index f806b820dad..2081ca606b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java @@ -16,7 +16,7 @@ import java.util.Map; public class CountersProliferateAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final List cperms = Lists.newArrayList(); boolean allyExpOrEnergy = false; @@ -68,7 +68,13 @@ public class CountersProliferateAi extends SpellAbilityAi { })); } - return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy; + if (!cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy) { + // AI will play it if there are any counters to proliferate + // or if there are no counters, but AI has experience or energy counters + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override @@ -92,11 +98,7 @@ public class CountersProliferateAi extends SpellAbilityAi { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - if (checkApiLogic(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } - - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return checkApiLogic(ai, 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 7d87be6e651..1526689ae3d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -118,7 +118,7 @@ public class CountersPutAi extends CountersAi { } @Override - protected boolean checkApiLogic(Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) { // AI needs to be expanded, since this function can be pretty complex // based on what the expected targets could be final Cost abCost = sa.getPayCosts(); @@ -159,7 +159,7 @@ public class CountersPutAi extends CountersAi { PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9)); if (!poisonList.isEmpty()) { sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife())); - return true; + return new AiAbilityDecision(1000, AiPlayDecision.WillPlay); } } @@ -175,7 +175,7 @@ public class CountersPutAi extends CountersAi { Card best = ComputerUtilCard.getBestAI(oppCreatM1); if (best != null) { sa.getTargets().add(best); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa); @@ -195,7 +195,7 @@ public class CountersPutAi extends CountersAi { best = ComputerUtilCard.getBestAI(aiCreat); if (best != null) { sa.getTargets().add(best); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -204,28 +204,28 @@ public class CountersPutAi extends CountersAi { if (!ai.getCounters().isEmpty()) { if (!eachExisting || ai.getPoisonCounters() < 5) { sa.getTargets().add(ai); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if ("Never".equals(logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("AlwaysWithNoTgt".equals(logic)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("AristocratCounters".equals(logic)) { return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); } else if ("PayEnergy".equals(logic)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("PayEnergyConservatively".equals(logic)) { boolean onlyInCombat = ai.getController().isAI() && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT); @@ -234,10 +234,10 @@ public class CountersPutAi extends CountersAi { if (playAggro) { // aggro profiles ignore conservative play for this AI logic - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (ph.inCombat() && source != null) { if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } else if (ai.getGame().getCombat().isBlocking(source)) { // when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(source); @@ -247,7 +247,7 @@ public class CountersPutAi extends CountersAi { int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount(); if (source.getNetToughness() + numActivations > totBlkPower || source.getNetPower() + numActivations >= totBlkToughness) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } } } else if (sa.getSubAbility() != null @@ -257,18 +257,18 @@ public class CountersPutAi extends CountersAi { // Bristling Hydra: save from death using a ping activation if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) { AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) { // outside of combat, this logic only works if the relevant AI profile option is enabled // and if there is enough energy saved if (!onlyInCombat) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } else if (logic.equals("MarkOppCreature")) { if (!ph.is(PhaseType.END_OF_TURN)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); } Predicate predicate = CardPredicates.hasCounter(CounterType.getType(type)); @@ -280,12 +280,12 @@ public class CountersPutAi extends CountersAi { Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats); sa.resetTargets(); sa.getTargets().add(bestCreat); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else if (logic.equals("CheckDFC")) { // for cards like Ludevic's Test Subject if (!source.canTransform(null)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (logic.startsWith("MoveCounter")) { return doMoveCounterLogic(ai, sa, ph); @@ -294,8 +294,15 @@ public class CountersPutAi extends CountersAi { if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) { // don't use this for mana until after combat AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2); + return new AiAbilityDecision(25, AiPlayDecision.WaitForMain2); } - return willActivate; + + if (willActivate) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } else if (logic.equals("ChargeToBestCMC")) { return doChargeToCMCLogic(ai, sa); } else if (logic.equals("ChargeToBestOppControlledCMC")) { @@ -305,14 +312,14 @@ public class CountersPutAi extends CountersAi { } if (!sa.metConditions() && sa.getSubAbility() == null) { - return false; + return new AiAbilityDecision(100, AiPlayDecision.ConditionsNotMet); } if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility()); if (!prot.isEmpty()) { sa.getTargets().add(prot.get(0)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } } @@ -320,13 +327,13 @@ public class CountersPutAi extends CountersAi { CardCollection creatsYouCtrl = ai.getCreaturesInPlay(); List leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness); if (leastToughness.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // TODO If Creature that would be Bolstered for some reason is useless, also return False } if (sa.hasParam("Monstrosity") && source.isMonstrous()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO handle proper calculation of X values based on Cost @@ -341,7 +348,7 @@ public class CountersPutAi extends CountersAi { Combat combat = game.getCombat(); if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return doCombatAdaptLogic(source, amount, combat); } @@ -368,12 +375,12 @@ public class CountersPutAi extends CountersAi { // This will "rewind" clockwork cards when they fall to 50% power or below, consider improving if (curCtrs > Math.ceil(maxCtrs / 2.0)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } amount = Math.min(amount, maxCtrs - curCtrs); if (amount <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -385,14 +392,14 @@ public class CountersPutAi extends CountersAi { .mapToInt(Card::getCMC) .max().orElse(0); if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } // don't use it if no counters to add if (amount <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Polukranos".equals(logic)) { @@ -419,20 +426,20 @@ public class CountersPutAi extends CountersAi { } } if (!canSurvive) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } found = true; break; } if (!found) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if ("AtOppEOT".equals(logic)) { if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -443,17 +450,19 @@ public class CountersPutAi extends CountersAi { if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) { // only evaluates case where all tokens are placed on a single target if (sa.getMinTargets() < 2) { - if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) { + AiAbilityDecision decision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa); + if (decision.willingToPlay()) { Card c = sa.getTargetCard(); if (sa.getTargets().size() > 1) { sa.resetTargets(); sa.getTargets().add(c); } sa.addDividedAllocation(c, amount); - return true; + return decision; } else { - if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies - return false; + if (!hasSacCost) { + // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies + return decision; } } } @@ -497,7 +506,7 @@ public class CountersPutAi extends CountersAi { } if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow), @@ -506,9 +515,9 @@ public class CountersPutAi extends CountersAi { && sa.isPwAbility() && sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class) && sa.isTargetNumberValid() - && sa.getTargets().size() == 0 + && sa.getTargets().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (sourceName.equals("Abzan Charm")) { @@ -530,11 +539,11 @@ public class CountersPutAi extends CountersAi { } } if (left == 0) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } sa.resetTargets(); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // target loop @@ -542,7 +551,7 @@ public class CountersPutAi extends CountersAi { if (list.isEmpty()) { if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // TODO is this good enough? for up to amounts? break; @@ -574,10 +583,9 @@ public class CountersPutAi extends CountersAi { // check if other choice will already be played increasesCharmOutcome = !choices.get(0).getTargets().isEmpty(); } - if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense + if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense || ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) { - if (abCost == null || abCost == Cost.Zero - || (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) { + if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) { // only use at opponent EOT unless it is free choice = chooseBoonTarget(list, type); } @@ -591,7 +599,7 @@ public class CountersPutAi extends CountersAi { if (choice == null) { // can't find anything left if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // TODO is this good enough? for up to amounts? break; @@ -607,14 +615,14 @@ public class CountersPutAi extends CountersAi { choice = null; } if (sa.getTargets().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { final List cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); // Don't activate Curse abilities on my cards and non-curse abilities // on my opponents if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } final int currCounters = cards.get(0).getCounters(CounterType.get(type)); @@ -622,46 +630,46 @@ public class CountersPutAi extends CountersAi { // activating this ability. if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Instant +1/+1 if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) { if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) { - return false; // only if next turn and cost is reusable + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // Useless since the card already has the keyword (or for another reason) if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } boolean immediately = ComputerUtil.playImmediately(ai, sa); - if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) { - return false; + if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) { + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (immediately) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) { // Don't use non P1P1/M1M1 counters before main 2 if possible if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } } if (ComputerUtil.waitForBlocking(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override @@ -772,17 +780,25 @@ public class CountersPutAi extends CountersAi { } if ("ChargeToBestCMC".equals(aiLogic)) { - if (doChargeToCMCLogic(ai, sa) || mandatory) { + AiAbilityDecision decision = doChargeToCMCLogic(ai, sa); + if (decision.willingToPlay()) { // If the AI logic is to charge to best CMC, we can return true // if the logic was successfully applied or if it's mandatory. - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return decision; + } else if (mandatory) { + // If the logic was not applied and it's mandatory, we return false. + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } else { // If the logic was not applied and it's not mandatory, we return false. return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) { - if (doChargeToOppCtrlCMCLogic(ai, sa) || mandatory) { + AiAbilityDecision decision = doChargeToOppCtrlCMCLogic(ai, sa); + if (decision.willingToPlay()) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else if (mandatory) { + // If the logic was not applied and it's mandatory, we return false. + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } else { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @@ -830,8 +846,9 @@ public class CountersPutAi extends CountersAi { if (type.equals("P1P1")) { nPump = amount; } - if (FightAi.canFightAi(ai, sa, nPump, nPump)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump); + if (decision.willingToPlay()) { + return decision; } } @@ -1135,7 +1152,7 @@ public class CountersPutAi extends CountersAi { return Iterables.getFirst(options, null); } - private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) { + private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) { // Spikes (Tempest) // Try not to do it unless at the end of opponent's turn or the creature is threatened @@ -1148,7 +1165,7 @@ public class CountersPutAi extends CountersAi { || (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat)))); if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa); @@ -1166,45 +1183,45 @@ public class CountersPutAi extends CountersAi { if (bestTgt != null) { sa.getTargets().add(bestTgt); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) { + private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) { if (combat.isAttacking(source)) { if (!combat.isBlocked(source)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { for (Card blockedBy : combat.getBlockers(source)) { if (blockedBy.getNetToughness() > source.getNetPower() && blockedBy.getNetToughness() <= source.getNetPower() + amount) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower); if (source.getNetToughness() <= totBlkPower && source.getNetToughness() + amount > totBlkPower) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } } } else if (combat.isBlocking(source)) { for (Card blocked : combat.getAttackersBlockedBy(source)) { if (blocked.getNetToughness() > source.getNetPower() && blocked.getNetToughness() <= source.getNetPower() + amount) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat); } } int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower); if (source.getNetToughness() <= totAtkPower && source.getNetToughness() + amount > totAtkPower) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override @@ -1215,7 +1232,7 @@ public class CountersPutAi extends CountersAi { return max; } - private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) { + private AiAbilityDecision doChargeToCMCLogic(Player ai, SpellAbility sa) { Card source = sa.getHostCard(); CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES); int numCtrs = source.getCounters(CounterEnumType.CHARGE); @@ -1230,10 +1247,16 @@ public class CountersPutAi extends CountersAi { optimalCMC = cmc; } } - return numCtrs < optimalCMC; + if (numCtrs < optimalCMC) { + // If the AI has less counters than the optimal CMC, it should play the ability. + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If the AI has enough counters or more than the optimal CMC, it should not play the ability. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) { + private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) { Card source = sa.getHostCard(); CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS); int numCtrs = source.getCounters(CounterEnumType.CHARGE); @@ -1247,6 +1270,12 @@ public class CountersPutAi extends CountersAi { optimalCMC = cmc; } } - return numCtrs < optimalCMC; + if (numCtrs < optimalCMC) { + // If the AI has less counters than the optimal CMC, it should play the ability. + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If the AI has enough counters or more than the optimal CMC, it should not play the ability. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java index 8aa688e47c4..c0c2cd75a21 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -50,9 +50,13 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { if (sa.usesTargeting()) { - return doTgt(ai, sa, false); + if (doTgt(ai, sa, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } return super.checkApiLogic(ai, sa); } 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 1aa7f5a70c6..7b3e81a57b8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -75,7 +75,7 @@ public class CountersRemoveAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final String type = sa.getParam("CounterType"); if (sa.usesTargeting()) { @@ -85,14 +85,14 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!type.matches("Any") && !type.matches("All")) { final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type)); if (currCounters < 1) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } return super.checkApiLogic(ai, sa); } - private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) { + private AiAbilityDecision doTgt(Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); final Game game = ai.getGame(); @@ -105,7 +105,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // Filter AI-specific targets if provided @@ -123,7 +123,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardPredicates.hasCounter(CounterEnumType.ICE, 3)); if (!depthsList.isEmpty()) { sa.getTargets().add(depthsList.getFirst()); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -136,7 +136,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!planeswalkerList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else if (type.matches("Any")) { // variable amount for Hex Parasite @@ -146,7 +146,7 @@ public class CountersRemoveAi extends SpellAbilityAi { final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (manaLeft == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } amount = manaLeft; xPay = true; @@ -168,7 +168,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (xPay) { sa.setXManaCostPaid(ice); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } @@ -187,7 +187,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (xPay) { sa.setXManaCostPaid(best.getCurrentLoyalty()); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // some rules only for amount = 1 @@ -204,7 +204,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!aiM1M1List.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // do as P1P1 part @@ -213,7 +213,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!aiUndyingList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // TODO stun counters with canRemoveCounters check @@ -224,7 +224,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardPredicates.hasCounter(CounterEnumType.P1P1)); if (!oppP1P1List.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // fallback to remove any counter from opponent @@ -236,7 +236,7 @@ public class CountersRemoveAi extends SpellAbilityAi { for (final CounterType aType : best.getCounters().keySet()) { if (!ComputerUtil.isNegativeCounter(aType, best)) { sa.getTargets().add(best); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } @@ -257,7 +257,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!aiList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else if (type.equals("P1P1")) { // no special amount for that one yet @@ -275,7 +275,7 @@ public class CountersRemoveAi extends SpellAbilityAi { } if (!aiList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -289,7 +289,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!oppList.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } else if (type.equals("TIME")) { @@ -300,7 +300,7 @@ public class CountersRemoveAi extends SpellAbilityAi { final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (manaLeft == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } amount = manaLeft; xPay = true; @@ -318,7 +318,7 @@ public class CountersRemoveAi extends SpellAbilityAi { if (xPay) { sa.setXManaCostPaid(timeCount); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } if (mandatory) { @@ -327,7 +327,7 @@ public class CountersRemoveAi extends SpellAbilityAi { CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT)); if (!adaptCreats.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Outlast nice target @@ -338,29 +338,27 @@ public class CountersRemoveAi extends SpellAbilityAi { if (!betterTargets.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } sa.getTargets().add(ComputerUtilCard.getWorstAI(list)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } @Override protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { - boolean canTarget = doTgt(aiPlayer, sa, mandatory); - return canTarget ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) - : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + return doTgt(aiPlayer, sa, mandatory); } return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) - : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + : new AiAbilityDecision(0, AiPlayDecision.MandatoryPlay); } /* @@ -374,8 +372,7 @@ public class CountersRemoveAi extends SpellAbilityAi { GameEntity target = (GameEntity) params.get("Target"); CounterType type = (CounterType) params.get("CounterType"); - if (target instanceof Card) { - Card targetCard = (Card) target; + if (target instanceof Card targetCard) { if (targetCard.getController().isOpponentOf(player)) { return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min; } else { @@ -386,8 +383,7 @@ public class CountersRemoveAi extends SpellAbilityAi { return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min; } - } else if (target instanceof Player) { - Player targetPlayer = (Player) target; + } else if (target instanceof Player targetPlayer) { if (targetPlayer.isOpponentOf(player)) { return !type.is(CounterEnumType.POISON) ? max : min; } else { 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 8fb043c1705..d4b33aaa3c7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -21,11 +21,7 @@ import forge.util.collect.FCollectionView; public class DestroyAi extends SpellAbilityAi { @Override public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { - if (checkApiLogic(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } + return checkApiLogic(ai, sa); } @Override @@ -107,7 +103,7 @@ public class DestroyAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); final boolean noRegen = sa.hasParam("NoRegen"); final String logic = sa.getParam("AILogic"); @@ -115,7 +111,7 @@ public class DestroyAi extends SpellAbilityAi { CardCollection list; if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } // Targeting @@ -129,7 +125,7 @@ public class DestroyAi extends SpellAbilityAi { // Assume there where already enough targets chosen by AI Logic Above if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // reset targets before AI Logic part @@ -149,13 +145,17 @@ public class DestroyAi extends SpellAbilityAi { if (maxTargets == 0) { // can't afford X or otherwise target anything - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } if (sa.hasParam("TargetingPlayer")) { Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); sa.setTargetingPlayer(targetingPlayer); - return targetingPlayer.getController().chooseTargetsFor(sa); + if (targetingPlayer.getController().chooseTargetsFor(sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } // AI doesn't destroy own cards if it isn't defined in AI logic @@ -210,7 +210,7 @@ public class DestroyAi extends SpellAbilityAi { // Try to avoid targeting creatures that are dead on board list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // target loop @@ -225,7 +225,7 @@ public class DestroyAi extends SpellAbilityAi { if (list.isEmpty()) { if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // TODO is this good enough? for up to amounts? break; @@ -239,7 +239,7 @@ public class DestroyAi extends SpellAbilityAi { if ("OppDestroyYours".equals(logic)) { Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay()); if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else if (CardLists.getNotType(list, "Land").isEmpty()) { @@ -248,7 +248,7 @@ public class DestroyAi extends SpellAbilityAi { if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) { // Strip Mine, Wasteland - cut short if the relevant logic fails if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else { @@ -258,14 +258,14 @@ public class DestroyAi extends SpellAbilityAi { //option to hold removal instead only applies for single targeted removal if (!sa.isTrigger() && sa.getMaxTargets() == 1) { if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } if (choice == null) { // can't find anything left if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // TODO is this good enough? for up to amounts? break; @@ -302,18 +302,18 @@ public class DestroyAi extends SpellAbilityAi { || !source.getGame().getPhaseHandler().isPlayerTurn(ai) || ai.getLife() <= 5)) { // Basic ai logic for Lethal Vapors - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("Always".equals(logic)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (list.isEmpty() || !CardLists.filterControlledBy(list, ai).isEmpty() || CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java index 3f6d83ea4b1..be072083e9d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java @@ -13,12 +13,12 @@ import java.util.Map; public class DiscoverAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; // prevent infinite loop + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** @@ -38,11 +38,7 @@ public class DiscoverAi extends SpellAbilityAi { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - if (checkApiLogic(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } + return checkApiLogic(ai, sa); } @Override 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 5378d4a3184..a2ae9b5c7ce 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -42,43 +42,46 @@ public class DrawAi extends SpellAbilityAi { * @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { if (!targetAI(ai, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (sa.usesTargeting()) { final Player player = sa.getTargets().getFirstTargetedPlayer(); if (player != null && player.isOpponentOf(ai)) { - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // prevent run-away activations - first time will always return true if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (ComputerUtil.playImmediately(ai, sa)) { - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } if (!canLoot(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) { // Canopy lands and other cards that sacrifice themselves to draw cards - return ai.getCardsIn(ZoneType.Hand).isEmpty() - || (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile + if (ai.getCardsIn(ZoneType.Hand).isEmpty() + || (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5)) { + // TODO: make this configurable in the AI profile + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java index 7860158349d..657c82e3d62 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -275,7 +275,7 @@ public class EffectAi extends SpellAbilityAi { } return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Fight")) { - return FightAi.canFightAi(ai, sa, 0, 0) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return FightAi.canFightAi(ai, sa, 0,0); } else if (logic.equals("Pump")) { sa.resetTargets(); List options = CardUtil.getValidCardsToTarget(sa); diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java index 84982061aa5..1c7c62f6ee0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -24,13 +24,13 @@ public class FightAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { sa.resetTargets(); final Card source = sa.getHostCard(); // everything is defined or targeted above, can't do anything there unless a specific logic is set if (sa.hasParam("Defined") && !sa.usesTargeting()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Get creature lists @@ -42,8 +42,10 @@ public class FightAi extends SpellAbilityAi { // Filter MustTarget requirements StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa); - if (humCreatures.isEmpty()) - return false; //prevent IndexOutOfBoundsException on MOJHOSTO variant + //prevent IndexOutOfBoundsException on MOJHOSTO variant + if (humCreatures.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } // assumes the triggered card belongs to the ai if (sa.hasParam("Defined")) { @@ -54,7 +56,7 @@ public class FightAi extends SpellAbilityAi { } } if (fighter1List.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } Card fighter1 = fighter1List.get(0); for (Card humanCreature : humCreatures) { @@ -62,10 +64,11 @@ public class FightAi extends SpellAbilityAi { && !canKill(humanCreature, fighter1, 0)) { // todo: check min/max targets; see if we picked the best matchup sa.getTargets().add(humanCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; // bail at this point, otherwise the AI will overtarget and waste the activation + // bail at this point, otherwise the AI will overtarget and waste the activation + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (sa.hasParam("TargetsFromDifferentZone")) { @@ -77,12 +80,12 @@ public class FightAi extends SpellAbilityAi { // todo: check min/max targets; see if we picked the best matchup sa.getTargets().add(humanCreature); sa.getTargets().add(aiCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } for (Card creature1 : humCreatures) { for (Card creature2 : humCreatures) { @@ -97,11 +100,11 @@ public class FightAi extends SpellAbilityAi { // todo: check min/max targets; see if we picked the best matchup sa.getTargets().add(creature1); sa.getTargets().add(creature2); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } @Override @@ -110,11 +113,7 @@ public class FightAi extends SpellAbilityAi { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi } - if (checkApiLogic(aiPlayer, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } + return checkApiLogic(aiPlayer, sa); } @Override @@ -132,12 +131,14 @@ public class FightAi extends SpellAbilityAi { } } - if (checkApiLogic(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + AiAbilityDecision decision = checkApiLogic(ai, sa); + if (decision.willingToPlay()) { + return decision; } if (!mandatory) { - return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return decision; } + // if mandatory, we have to play it, so we will try to make a good trade or no trade //try to make a good trade or no trade final Card source = sa.getHostCard(); @@ -153,19 +154,19 @@ public class FightAi extends SpellAbilityAi { if (canKill(aiCreature, humanCreature, 0) && ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) { sa.getTargets().add(humanCreature); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay); } } for (Card humanCreature : humCreatures) { if (!canKill(humanCreature, aiCreature, 0)) { sa.getTargets().add(humanCreature); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } } sa.getTargets().add(humCreatures.get(0)); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } /** @@ -176,7 +177,7 @@ public class FightAi extends SpellAbilityAi { * @param power bonus to power * @return true if fight effect should be played, false otherwise */ - public static boolean canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) { + public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); AbilitySub tgtFight = sa.getSubAbility(); @@ -208,7 +209,7 @@ public class FightAi extends SpellAbilityAi { ComputerUtilCard.sortByEvaluateCreature(aiCreatures); ComputerUtilCard.sortByEvaluateCreature(humCreatures); if (humCreatures.isEmpty() || aiCreatures.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // Evaluate creature pairs for (Card humanCreature : humCreatures) { @@ -238,7 +239,7 @@ public class FightAi extends SpellAbilityAi { tgtFight.resetTargets(); tgtFight.getTargets().add(humanCreature); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { // Other cards that use AILogic PowerDmg and a single target @@ -248,7 +249,7 @@ public class FightAi extends SpellAbilityAi { tgtFight.resetTargets(); tgtFight.getTargets().add(humanCreature); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } else { @@ -261,12 +262,12 @@ public class FightAi extends SpellAbilityAi { sa.getTargets().add(aiCreature); tgtFight.resetTargets(); tgtFight.getTargets().add(humanCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/GoadAi.java b/forge-ai/src/main/java/forge/ai/ability/GoadAi.java index 78f6314f9eb..26391fe46e8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GoadAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GoadAi.java @@ -14,7 +14,7 @@ import java.util.List; public class GoadAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); final Game game = source.getGame(); @@ -24,7 +24,7 @@ public class GoadAi extends SpellAbilityAi { List list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); if (game.getPlayers().size() > 2) { // use this part only in multiplayer @@ -50,7 +50,7 @@ public class GoadAi extends SpellAbilityAi { if (!betterList.isEmpty()) { list = betterList; sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { // single Player, goaded creature would attack ai @@ -71,37 +71,40 @@ public class GoadAi extends SpellAbilityAi { if (!betterList.isEmpty()) { list = betterList; sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } // AI does not find a good creature to goad. // because if it would goad a creature it would attack AI. // AI might not have enough information to block it - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return true; + + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - if (checkApiLogic(ai, sa)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + AiAbilityDecision decision = checkApiLogic(ai, sa); + if (decision.willingToPlay()) { + return decision; } if (!mandatory) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + // mandatory play, so we have to play it if (sa.usesTargeting()) { if (sa.getTargetRestrictions().canTgtPlayer()) { for (Player opp : ai.getOpponents()) { if (sa.canTarget(opp)) { sa.getTargets().add(opp); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } } if (sa.canTarget(ai)) { sa.getTargets().add(ai); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } } else { List list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); @@ -110,7 +113,7 @@ public class GoadAi extends SpellAbilityAi { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list)); - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + return new AiAbilityDecision(30, AiPlayDecision.MandatoryPlay); } } return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); 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 0c9db1599ff..886fe0e20ae 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -126,7 +126,7 @@ public class LifeGainAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String aiLogic = sa.getParamOrDefault("AILogic", ""); @@ -148,12 +148,12 @@ public class LifeGainAi extends SpellAbilityAi { // Ugin AI: always use ultimate if (sourceName.equals("Ugin, the Spirit Dragon")) { // TODO: somehow link with DamageDealAi for cases where +1 = win - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // don't use it if no life to gain if (!activateForCost && lifeAmount <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // don't play if the conditions aren't met, unless it would trigger a // beneficial sub-condition @@ -161,47 +161,52 @@ public class LifeGainAi extends SpellAbilityAi { final AbilitySub abSub = sa.getSubAbility(); if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) { if (!abSub.getConditions().areMet(abSub)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); } } if (!activateForCost && !ai.canGainLife()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // prevent run-away activations - first time will always return true if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (sa.usesTargeting()) { if (!target(ai, sa, true)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } if (ComputerUtil.playImmediately(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (isSorcerySpeed(sa, ai) || sa.getSubAbility() != null || playReusable(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { - return true; // sac costs should be performed at Instant speed when able + // sac costs should be performed at Instant speed when able + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Save instant-speed life-gain unless it is really worth it final float value = 0.9f * lifeAmount / life; if (value < 0.2f) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + if (MyRandom.getRandom().nextFloat() < value) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return MyRandom.getRandom().nextFloat() < value; } /** 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 377cccb3277..ea75d199b68 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java @@ -94,7 +94,7 @@ public class LifeLoseAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String amountStr = sa.getParam("LifeAmount"); final String aiLogic = sa.getParamOrDefault("AILogic", ""); @@ -102,7 +102,7 @@ public class LifeLoseAi extends SpellAbilityAi { if (sa.usesTargeting()) { if (!doTgt(ai, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -115,15 +115,15 @@ public class LifeLoseAi extends SpellAbilityAi { } if (amount <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (ComputerUtil.playImmediately(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } final PlayerCollection tgtPlayers = getPlayers(ai, sa); @@ -132,7 +132,7 @@ public class LifeLoseAi extends SpellAbilityAi { .filter(PlayerPredicates.isOpponentOf(ai).and(PlayerPredicates.lifeLessOrEqualTo(amount))); // killing opponents asap if (!filteredPlayer.isEmpty()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Sacrificing a creature in response to something dangerous is generally good in any phase @@ -144,20 +144,20 @@ public class LifeLoseAi extends SpellAbilityAi { // Don't use loselife before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") && !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } if (isSorcerySpeed(sa, ai) || sa.hasParam("ActivationPhases") || playReusable(ai, sa) || ComputerUtil.activateForCost(sa, ai)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaAi.java index 116813aa551..685254d6572 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaAi.java @@ -87,18 +87,22 @@ public class ManaAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { if (sa.hasParam("AILogic")) { - return true; // handled elsewhere, does not meet the standard requirements + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // handled elsewhere, does not meet the standard requirements } // TODO check if it would be worth it to keep mana open for opponents turn anyway if (ComputerUtil.activateForCost(sa, ai)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() - && sa.getSubAbility() == null && (improvesPosition(ai, sa) || ComputerUtil.playImmediately(ai, sa)); + if (sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() + && sa.getSubAbility() == null && (improvesPosition(ai, sa) || ComputerUtil.playImmediately(ai, sa))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** @@ -276,7 +280,6 @@ public class ManaAi extends SpellAbilityAi { @Override protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final String logic = sa.getParamOrDefault("AILogic", ""); - boolean result = checkApiLogic(ai, sa); - return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return checkApiLogic(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java b/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java index e3e40e171cf..e4c44d582f5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java @@ -81,11 +81,11 @@ public abstract class ManifestBaseAi extends SpellAbilityAi { abstract protected boolean shouldApply(final Card card, final Player ai, final SpellAbility sa); @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final Game game = ai.getGame(); final Card host = sa.getHostCard(); if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) { @@ -98,36 +98,42 @@ public abstract class ManifestBaseAi extends SpellAbilityAi { choices = CardLists.getValidCards(choices, sa.getParam("Choices"), ai, host, sa); } if (choices.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } } else if ("TopOfLibrary".equals(sa.getParamOrDefault("Defined", "TopOfLibrary"))) { // Library is empty, no Manifest final CardCollectionView library = ai.getCardsIn(ZoneType.Library); if (library.isEmpty()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // try not to mill himself with Manifest if (library.size() < 5 && !ai.isCardInPlay("Laboratory Maniac")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!shouldApply(library.getFirst(), ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - // Probably should be a little more discerning on playing during OPPs turn + // TODO Probably should be a little more discerning on playing during OPPs turn if (playReusable(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)) { // Add blockers? - return true; + return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence); } if (sa.isAbility()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return MyRandom.getRandom().nextFloat() < .8; + if ( MyRandom.getRandom().nextFloat() < .8) { + // 80% chance to play a Manifest spell + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // 20% chance to not play a Manifest spell + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/MeldAi.java b/forge-ai/src/main/java/forge/ai/ability/MeldAi.java index 35f943eae76..8a35b87464b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MeldAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MeldAi.java @@ -11,19 +11,24 @@ import forge.game.zone.ZoneType; public class MeldAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) { String primaryMeld = sa.getParam("Primary"); String secondaryMeld = sa.getParam("Secondary"); CardCollectionView cardsOTB = aiPlayer.getCardsIn(ZoneType.Battlefield); if (cardsOTB.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer))); boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer))); - - return hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld); + if (hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld)) { + // If the primary meld card is on the battlefield and both meld cards are owned by the AI, play the ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If the secondary meld card is on the battlefield and it is the one being activated, play the ability + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java index 55aa319e471..a1625910192 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -60,7 +60,7 @@ public class MillAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { /* * TODO: * - logic in targetAI looks dodgy @@ -70,16 +70,17 @@ public class MillAi extends SpellAbilityAi { * effect due to possibility of "lose abilities" effect) */ if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; // prevents mill 0 infinite loop? + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (("You".equals(sa.getParam("Defined")) || "Player".equals(sa.getParam("Defined"))) && ai.getCardsIn(ZoneType.Library).size() < 10) { - return false; // prevent self and each player mill when library is small + // prevent self and each player mill when library is small + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.usesTargeting() && !targetAI(ai, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (sa.hasParam("NumCards") && (sa.getParam("NumCards").equals("X") || sa.getParam("NumCards").equals("Z")) @@ -87,9 +88,11 @@ public class MillAi extends SpellAbilityAi { // Set PayX here to maximum value. final int cardsToDiscard = getNumToDiscard(ai, sa); sa.setXManaCostPaid(cardsToDiscard); - return cardsToDiscard > 0; + if (cardsToDiscard <= 0) { + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java index 1d8eb80d2b7..2a5bdf6cffc 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java @@ -43,27 +43,27 @@ public class PermanentAi extends SpellAbilityAi { * here */ @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); // check on legendary if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) { // TODO check the risk we'd lose the effect with bad timing + // TODO Technically we're not checking if same card in play is also legendary, but this is a good enough approximation if (!source.hasSVar("AILegendaryException")) { - // AiPlayDecision.WouldDestroyLegend - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } else { String specialRule = source.getSVar("AILegendaryException"); if ("TwoCopiesAllowed".equals(specialRule)) { // One extra copy allowed on the battlefield, e.g. Brothers Yamazaki if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(source.getName())) > 1) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } } else if ("AlwaysAllowed".equals(specialRule)) { // Nothing to do here, check for Legendary is disabled } else { // Unknown hint, assume two copies not allowed - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } } } @@ -71,8 +71,7 @@ public class PermanentAi extends SpellAbilityAi { if (source.getType().hasSupertype(Supertype.World)) { CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World"); if (!list.isEmpty()) { - // AiPlayDecision.WouldDestroyWorldEnchantment - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyWorldEnchantment); } } @@ -93,9 +92,8 @@ public class PermanentAi extends SpellAbilityAi { } } } else { - // AiPlayDecision.CantAffordX if (xPay <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } sa.setXManaCostPaid(xPay); } @@ -103,8 +101,7 @@ public class PermanentAi extends SpellAbilityAi { // if mana is zero, but card mana cost does have X, then something is wrong ManaCost cardCost = source.getManaCost(); if (cardCost != null && cardCost.countX() > 0) { - // AiPlayDecision.CantPlayAi - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } } @@ -147,10 +144,10 @@ public class PermanentAi extends SpellAbilityAi { } if (oppCards.size() > 3 && oppCards.size() >= aiCards.size() * 2) { sa.setXManaCostPaid(manaValue); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } for (KeywordInterface ki : source.getKeywords(Keyword.MULTIKICKER)) { @@ -177,7 +174,7 @@ public class PermanentAi extends SpellAbilityAi { sa.clearOptionalKeywordAmount(); // Bail if the card cost was {0} and no multikicker was paid (e.g. Everflowing Chalice). // TODO: update this if there's ever a card where it makes sense to play it for {0} with no multikicker - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } } @@ -213,8 +210,7 @@ public class PermanentAi extends SpellAbilityAi { emptyAbility.setActivatingPlayer(ai); if (!ComputerUtilCost.canPayCost(emptyAbility, ai, true)) { - // AiPlayDecision.AnotherTime - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } @@ -310,10 +306,12 @@ public class PermanentAi extends SpellAbilityAi { } } - return !dontCast; + if (dontCast) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override @@ -333,8 +331,13 @@ public class PermanentAi extends SpellAbilityAi { if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - boolean result = checkApiLogic(ai, sa); - return (result || mandatory) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - } - + AiAbilityDecision decision = checkApiLogic(ai, sa); + if (decision.willingToPlay()) { + return decision; + } else if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } else { + return decision; + } + } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java index 5d3e07985c8..b9485ed790d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java @@ -195,9 +195,10 @@ public class PermanentCreatureAi extends PermanentAi { } @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { - if (!super.checkApiLogic(ai, sa)) { - return false; + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { + AiAbilityDecision decision = super.checkApiLogic(ai, sa); + if (!decision.willingToPlay()) { + return decision; } final Card card = sa.getHostCard(); @@ -220,16 +221,15 @@ public class PermanentCreatureAi extends PermanentAi { // AiPlayDecision.WouldBecomeZeroToughnessCreature if (card.hasStartOfKeyword("etbCounter") || mana.countX() != 0 || card.hasETBTrigger(false) || card.hasETBReplacement() || card.hasSVar("NoZeroToughnessAI")) { - return true; + return decision; } final Card copy = CardCopyService.getLKICopy(card); ComputerUtilCard.applyStaticContPT(game, copy, null); if (copy.getNetToughness() > 0) { - return true; + return decision; } - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldBecomeZeroToughnessCreature); } - } diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java index c7c78431982..d6b54fc092e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilAbility; import forge.game.Game; import forge.game.ability.AbilityFactory; @@ -21,9 +23,11 @@ public class PermanentNoncreatureAi extends PermanentAi { * here */ @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { - if (!super.checkApiLogic(ai, sa)) - return false; + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { + AiAbilityDecision decision = super.checkApiLogic(ai, sa); + if (!decision.willingToPlay()) { + return decision; + } final Card host = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); @@ -41,9 +45,10 @@ public class PermanentNoncreatureAi extends PermanentAi { // TODO: consider replacing the condition with host.hasSVar("OblivionRing") targets = CardLists.filterControlledBy(targets, ai.getOpponents()); } - // AiPlayDecision.AnotherTime - return !targets.isEmpty(); + if (targets.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return decision; } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java index 0e1a2296d20..d859139d1f5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -26,7 +26,7 @@ import java.util.stream.Collectors; public class PlayAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final String logic = sa.getParamOrDefault("AILogic", ""); final Game game = ai.getGame(); @@ -34,11 +34,11 @@ public class PlayAi extends SpellAbilityAi { // don't use this as a response (ReplaySpell logic is an exception, might be called from a subability // while the trigger is on stack) if (!game.getStack().isEmpty() && !"ReplaySpell".equals(logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; // prevent infinite loop + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) { @@ -48,38 +48,46 @@ public class PlayAi extends SpellAbilityAi { int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA); int chanceToActivateInst = 100 - aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT); if (ai.getLandsInPlay().size() < numLandsForJhoira) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Don't spam activate the Instant copying ability all the time to give the AI a chance to use other abilities // Can probably be improved, but as random as MoJhoSto already is, probably not a huge deal for now if ("Instant".equals(sa.getParam("AnySupportedCard")) && MyRandom.percentTrue(chanceToActivateInst)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } List cards = getPlayableCards(sa, ai); if (cards.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } if ("ReplaySpell".equals(logic)) { - return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"), false); + if (ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"), false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } else if (logic.startsWith("NeedsChosenCard")) { int minCMC = 0; if (sa.getPayCosts().getCostMana() != null) { minCMC = sa.getPayCosts().getTotalMana().getCMC(); } cards = CardLists.filter(cards, CardPredicates.greaterCMC(minCMC)); - return chooseSingleCard(ai, sa, cards, sa.hasParam("Optional"), null, null) != null; + if (chooseSingleCard(ai, sa, cards, sa.hasParam("Optional"), null, null) != null) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } } else if ("WithTotalCMC".equals(logic)) { // Try to play only when there are more than three playable cards. if (cards.size() < 3) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); if (sa.costHasManaX()) { int amount = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (amount < ComputerUtilCard.getBestAI(cards).getCMC()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); int totalCMC = 0; for (Card c : cards) { totalCMC += c.getCMC(); @@ -97,10 +105,14 @@ public class PlayAi extends SpellAbilityAi { Card rem = source.getExiledCards().getFirst(); CardTypeView t = rem.getState(CardStateName.Original).getType(); - return t.isPermanent() && !t.isLand(); + if (t.isPermanent() && !t.isLand()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -126,8 +138,7 @@ public class PlayAi extends SpellAbilityAi { return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - boolean result = checkApiLogic(ai, sa); - return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return checkApiLogic(ai, sa); } return new AiAbilityDecision(100, AiPlayDecision.WillPlay); 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 a139c969f02..c3f76f57ed9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -36,18 +36,22 @@ public class PoisonAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } if (sa.usesTargeting()) { sa.resetTargets(); - return tgtPlayer(ai, sa, true); + if (tgtPlayer(ai, sa, true)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java index 26d7cf928ba..b96e784dbb8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java @@ -157,17 +157,21 @@ public class ProtectAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { if (sa.usesTargeting()) { return protectTgtAI(ai, sa, false); } final List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); - if (cards.size() == 0) { - return false; + if (cards.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } else if (cards.size() == 1) { // Affecting single card - return getProtectCreatures(ai, sa).contains(cards.get(0)); + if (getProtectCreatures(ai, sa).contains(cards.get(0))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } /* * when this happens we need to expand AI to consider if its ok @@ -175,14 +179,14 @@ public class ProtectAi extends SpellAbilityAi { * control Card and Pump is a Curse, than maybe use? * } */ - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - private boolean protectTgtAI(final Player ai, final SpellAbility sa, final boolean mandatory) { + private AiAbilityDecision protectTgtAI(final Player ai, final SpellAbility sa, final boolean mandatory) { final Game game = ai.getGame(); if (!mandatory && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) && game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } final Card source = sa.getHostCard(); @@ -216,7 +220,12 @@ public class ProtectAi extends SpellAbilityAi { } if (list.isEmpty()) { - return mandatory && protectMandatoryTarget(ai, sa); + if (mandatory && protectMandatoryTarget(ai, sa)) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } else { + sa.resetTargets(); + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } while (sa.canAddMoreTarget()) { @@ -226,11 +235,13 @@ public class ProtectAi extends SpellAbilityAi { if (list.isEmpty()) { if ((sa.getTargets().size() < tgt.getMinTargets(source, sa)) || sa.getTargets().size() == 0) { if (mandatory) { - return protectMandatoryTarget(ai, sa); + if (protectMandatoryTarget(ai, sa)) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } } sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // TODO is this good enough? for up to amounts? break; @@ -242,7 +253,7 @@ public class ProtectAi extends SpellAbilityAi { list.remove(t); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // protectTgtAI() private static boolean protectMandatoryTarget(final Player ai, final SpellAbility sa) { @@ -311,11 +322,7 @@ public class ProtectAi extends SpellAbilityAi { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { - if (protectTgtAI(ai, sa, mandatory)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); - } + return protectTgtAI(ai, sa, mandatory); } return new AiAbilityDecision(100, AiPlayDecision.WillPlay); @@ -324,11 +331,7 @@ public class ProtectAi extends SpellAbilityAi { @Override public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if (sa.usesTargeting()) { - if (protectTgtAI(ai, sa, false)) { - return new AiAbilityDecision(100, AiPlayDecision.WillPlay); - } else { - return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); - } + return protectTgtAI(ai, sa, false); } return new AiAbilityDecision(100, AiPlayDecision.WillPlay); 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 ddbb32f4eda..38b1e46865d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -56,7 +56,7 @@ public class PumpAi extends PumpAiBase { } else if ("Aristocrat".equals(aiLogic)) { return SpecialAiLogic.doAristocratLogic(ai, sa); } else if (aiLogic.startsWith("AristocratCounters")) { - return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); + return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa).willingToPlay(); } else if (aiLogic.equals("SwitchPT")) { // Some more AI would be even better, but this is a good start to prevent spamming if (sa.isActivatedAbility() && sa.getActivationsThisTurn() > 0 && !sa.usesTargeting()) { @@ -114,7 +114,7 @@ public class PumpAi extends PumpAiBase { } @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final Game game = ai.getGame(); final Card source = sa.getHostCard(); final SpellAbility root = sa.getRootAbility(); @@ -131,14 +131,15 @@ public class PumpAi extends PumpAiBase { if ("Pummeler".equals(aiLogic)) { return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa); } else if (aiLogic.startsWith("AristocratCounters")) { - return true; // the preconditions to this are already tested in checkAiLogic + // the preconditions to this are already tested in checkAiLogic + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("GideonBlackblade".equals(aiLogic)) { return SpecialCardAi.GideonBlackblade.consider(ai, sa); } else if ("MoveCounter".equals(aiLogic)) { final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter); if (moveSA == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final String counterType = moveSA.getParam("CounterType"); @@ -153,7 +154,7 @@ public class PumpAi extends PumpAiBase { if (cType != null) { attr = CardLists.filter(attr, CardPredicates.hasCounter(cType)); if (attr.isEmpty()) { - return false; + return new AiAbilityDecision(0,AiPlayDecision.TargetingFailed); } CardCollection best = CardLists.filter(attr, card -> { int amount = 0; @@ -187,7 +188,7 @@ public class PumpAi extends PumpAiBase { final Card card = ComputerUtilCard.getBestCreatureAI(best); sa.getTargets().add(card); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { final boolean sameCtrl = moveSA.getTargetRestrictions().isSameController(); @@ -196,7 +197,7 @@ public class PumpAi extends PumpAiBase { if (cType != null) { list = CardLists.filter(list, CardPredicates.hasCounter(cType)); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } List oppList = CardLists.filterControlledBy(list, ai.getOpponents()); if (!oppList.isEmpty() && !sameCtrl) { @@ -232,7 +233,7 @@ public class PumpAi extends PumpAiBase { final Card card = ComputerUtilCard.getBestCreatureAI(best); sa.getTargets().add(card); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -244,12 +245,12 @@ public class PumpAi extends PumpAiBase { int numRedMana = ComputerUtilMana.determineLeftoverMana(new SpellAbility.EmptySa(source), ai, "R", false); int currentPower = source.getNetPower(); if (currentPower < 20 && currentPower + numRedMana >= 20) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (!game.getStack().isEmpty() && !sa.isCurse() && !isFight) { @@ -261,7 +262,7 @@ public class PumpAi extends PumpAiBase { final int activations = sa.getActivationsThisTurn(); // don't risk sacrificing a creature just to pump it if (activations >= sacActivations - 1) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); } } @@ -304,7 +305,7 @@ public class PumpAi extends PumpAiBase { } if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } //Untargeted @@ -312,47 +313,51 @@ public class PumpAi extends PumpAiBase { final List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); if (cards.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // when this happens we need to expand AI to consider if its ok for everything? for (final Card card : cards) { if (sa.isCurse()) { if (!card.getController().isOpponentOf(ai)) { - return false; + continue; } if (!containsUsefulKeyword(ai, keywords, card, sa, attack)) { continue; } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!card.getController().isOpponentOf(ai)) { if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) { if (game.getPhaseHandler().is(PhaseType.MAIN1) && isSorcerySpeed(sa, ai) || game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai) || game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) { Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords); - return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped); + if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) { + // If the AI can attack with the pumped creature, then it is worth playing + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (grantsUsefulExtraBlockOpts(ai, sa, card, keywords)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } //Targeted if (!pumpTgtAI(ai, sa, defense, attack, false, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // pumpPlayAI() private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory, @@ -462,7 +467,7 @@ public class PumpAi extends PumpAiBase { } if (isFight) { - return FightAi.canFightAi(ai, sa, attack, defense); + return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay(); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java index 49f09226190..46960a42ab7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java @@ -42,7 +42,7 @@ import java.util.List; public class RegenerateAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final Game game = ai.getGame(); final Combat combat = game.getCombat(); final Card hostCard = sa.getHostCard(); @@ -54,7 +54,7 @@ public class RegenerateAi extends SpellAbilityAi { List targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); if (targetables.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (!game.getStack().isEmpty()) { @@ -87,12 +87,12 @@ public class RegenerateAi extends SpellAbilityAi { } } if (sa.getTargets().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { final List list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // when regenerating more than one is possible try for slightly more value int numToSave = Math.min(2, list.size()); @@ -116,7 +116,11 @@ public class RegenerateAi extends SpellAbilityAi { chance = saved >= numToSave; } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java index ed4bd3840d0..4c043c1c82a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java @@ -15,20 +15,27 @@ import forge.util.MyRandom; public class RevealAi extends RevealAiBase { @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { // we can reuse this function here... final boolean bFlag = revealHandTargetAI(ai, sa, false); if (!bFlag) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1); + // Are we checking for runaway activations? if (playReusable(ai, sa)) { randomReturn = true; } - return randomReturn; + + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java index 53bffcc3bea..d4f21bd8f28 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java @@ -12,11 +12,11 @@ public class RevealHandAi extends RevealAiBase { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { final boolean bFlag = revealHandTargetAI(ai, sa, false); if (!bFlag) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1); @@ -25,7 +25,11 @@ public class RevealHandAi extends RevealAiBase { randomReturn = true; } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java index e6246a8978f..8df46208c30 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java @@ -147,10 +147,10 @@ public class ScryAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { // does Scry make sense with no Library cards? if (ai.getCardsIn(ZoneType.Library).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } double chance = .4; // 40 percent chance of milling with instant speed stuff @@ -181,12 +181,15 @@ public class ScryAi extends SpellAbilityAi { if ("X".equals(sa.getParam("ScryNum")) && sa.getSVar("X").equals("Count$xPaid")) { int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (xPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } sa.getRootAbility().setXManaCostPaid(xPay); } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java index d4f1e1c0fe8..96522e590f1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java @@ -17,19 +17,19 @@ import java.util.Map; public class SetStateAi extends SpellAbilityAi { @Override - protected boolean checkApiLogic(final Player aiPlayer, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player aiPlayer, final SpellAbility sa) { final Card source = sa.getHostCard(); final String mode = sa.getParam("Mode"); // turning face is most likely okay // TODO only do this at beneficial moment (e.g. surprise during combat or morph trigger), might want to reserve mana to protect them from easy removal if ("TurnFaceUp".equals(mode) || "TurnFaceDown".equals(mode)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Prevent transform into legendary creature if copy already exists if (!isSafeToTransformIntoLegendary(aiPlayer, source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } if (sa.getSVar("X").equals("Count$xPaid")) { @@ -38,9 +38,9 @@ public class SetStateAi extends SpellAbilityAi { } if ("Transform".equals(mode) || "Flip".equals(mode)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java index 4a80d8eb364..e4482d8ac9b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java @@ -84,10 +84,10 @@ public class SurveilAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { // Makes no sense to do Surveil when there's nothing in the library if (ai.getCardsIn(ZoneType.Library).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // Only Surveil for life when at decent amount of life remaining @@ -95,10 +95,11 @@ public class SurveilAi extends SpellAbilityAi { if (cost != null && cost.hasSpecificCostType(CostPayLife.class)) { final int maxLife = ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SURVEIL_LIFEPERC_AFTER_PAYING_LIFE); if (!ComputerUtilCost.checkLifeCost(ai, cost, sa.getHostCard(), ai.getStartingLife() * maxLife / 100, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } } + // TODO If EOT and I'm the next turn, the percent should probably be higher double chance = .4; // 40 percent chance for instant speed if (isSorcerySpeed(sa, ai)) { chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT) @@ -111,9 +112,10 @@ public class SurveilAi extends SpellAbilityAi { if (randomReturn) { AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return randomReturn; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java index 7c1893852e7..0dfba35144a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -134,7 +134,7 @@ public class TokenAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) { /* * readParameters() is called in checkPhaseRestrictions */ @@ -142,14 +142,14 @@ public class TokenAi extends SpellAbilityAi { final Player opp = ai.getWeakestOpponent(); if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; // prevent infinite tokens? + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } Card actualToken = spawnToken(ai, sa); // Don't kill AIs Legendary tokens if (actualToken.getType().isLegendary() && ai.isCardInPlay(actualToken.getName())) { // TODO Check if Token is useless due to an aura or counters? - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -157,14 +157,18 @@ public class TokenAi extends SpellAbilityAi { sa.resetTargets(); if (actualToken.getType().hasSubtype("Role")) { - return tgtRoleAura(ai, sa, actualToken, false); + if (tgtRoleAura(ai, sa, actualToken, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } if (tgt.canOnlyTgtOpponent() || "Opponent".equals(sa.getParam("AITgts"))) { if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { if (sa.canTarget(ai)) { @@ -183,7 +187,7 @@ public class TokenAi extends SpellAbilityAi { if (!list.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -201,7 +205,7 @@ public class TokenAi extends SpellAbilityAi { } if (sa.isPwAbility() && alwaysFromPW) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS) && game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && game.getCombat() != null @@ -210,14 +214,18 @@ public class TokenAi extends SpellAbilityAi { && actualToken.isCreature()) { for (Card attacker : game.getCombat().getAttackers()) { if (CombatUtil.canBlock(attacker, actualToken)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } // if the token can't block, then what's the point? - return false; + return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat); } - return MyRandom.getRandom().nextFloat() <= chance; + if (MyRandom.getRandom().nextFloat() <= chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java index 04c51e8848a..39b4428a43a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -54,19 +54,29 @@ public class UntapAi extends SpellAbilityAi { } @Override - protected boolean checkApiLogic(Player ai, SpellAbility sa) { + protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if (sa.usesTargeting()) { - return untapPrefTargeting(ai, sa, false); + if (untapPrefTargeting(ai, sa, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } final List pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); - return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai); + if (pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai)) { + // If the defined card is tapped, or if there are no defined cards, we can play this ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // Otherwise, we can't play this ability + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } } @Override diff --git a/forge-core/src/main/java/forge/CardStorageReader.java b/forge-core/src/main/java/forge/CardStorageReader.java index 317599dde5f..072e32a7ed7 100644 --- a/forge-core/src/main/java/forge/CardStorageReader.java +++ b/forge-core/src/main/java/forge/CardStorageReader.java @@ -253,7 +253,7 @@ public class CardStorageReader { sw.start(); executeLoadTask(result, taskFiles, cdlFiles); sw.stop(); - final long timeOnParse = sw.getTime(); + final long timeOnParse = sw.getTime(TimeUnit.SECONDS); System.out.printf("Read cards: %s files in %d ms (%d parts) %s%n", allFiles.size(), timeOnParse, taskFiles.size(), useThreadPool ? "using thread pool" : "in same thread"); } @@ -267,7 +267,7 @@ public class CardStorageReader { sw.start(); executeLoadTask(result, taskZip, cdlZip); sw.stop(); - final long timeOnParse = sw.getTime(); + final long timeOnParse = sw.getTime(TimeUnit.SECONDS); System.out.printf("Read cards: %s archived files in %d ms (%d parts) %s%n", this.zip.size(), timeOnParse, taskZip.size(), useThreadPool ? "using thread pool" : "in same thread"); }