diff --git a/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java b/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java new file mode 100644 index 00000000000..dcc7f0a2b9b --- /dev/null +++ b/forge-ai/src/main/java/forge/ai/AiAbilityDecision.java @@ -0,0 +1,25 @@ +package forge.ai; + +public class AiAbilityDecision { + private static int MIN_RATING = 30; + + private final int rating; + private final AiPlayDecision decision; + + public AiAbilityDecision(int rating, AiPlayDecision decision) { + this.rating = rating; + this.decision = decision; + } + + public int getRating() { + return rating; + } + + public AiPlayDecision getDecision() { + return decision; + } + + public boolean willingToPlay() { + return rating > MIN_RATING && decision.willingToPlay(); + } +} diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index fe9c4f526a1..d9c5907f2a5 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -1017,7 +1017,7 @@ public class AiController { Sentry.setExtra("Card", card.getName()); Sentry.setExtra("SA", sa.toString()); - boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa); + boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa).willingToPlay(); // remove added extra Sentry.removeExtra("Card"); @@ -1395,7 +1395,7 @@ public class AiController { if (spell instanceof SpellApiBased) { boolean chance = false; if (withoutPayingManaCost) { - chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory); + chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay(); } else { chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory); } diff --git a/forge-ai/src/main/java/forge/ai/AiPlayDecision.java b/forge-ai/src/main/java/forge/ai/AiPlayDecision.java index 692badb6bd8..5c518d720f3 100644 --- a/forge-ai/src/main/java/forge/ai/AiPlayDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiPlayDecision.java @@ -1,15 +1,33 @@ package forge.ai; public enum AiPlayDecision { - WillPlay, + // Play decision reasons + WillPlay, + MandatoryPlay, + PlayToEmptyHand, + AddBoardPresence, + Removal, + Tempo, + CardAdvantage, + + // Play later decisions + WaitForCombat, + WaitForMain2, + WaitForEndOfTurn, + StackNotEmpty, + AnotherTime, + + // Don't play decision reasons, CantPlaySa, CantPlayAi, CantAfford, CantAffordX, - WaitForMain2, - AnotherTime, + MissingLogic, MissingNeededCards, + TimingRestrictions, + MissingPhaseRestrictions, NeedsToPlayCriteriaNotMet, + StopRunawayActivations, TargetingFailed, CostNotAcceptable, WouldDestroyLegend, @@ -17,5 +35,12 @@ public enum AiPlayDecision { WouldBecomeZeroToughnessCreature, WouldDestroyWorldEnchantment, BadEtbEffects, - CurseEffects + CurseEffects; + + public boolean willingToPlay() { + return switch (this) { + case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, Removal, Tempo, CardAdvantage -> true; + default -> false; + }; + } } \ No newline at end of file diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java index b2aabd58c50..acac8b4dada 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java @@ -1503,7 +1503,7 @@ public class ComputerUtilMana { AbilitySub sub = m.getSubAbility(); // We really shouldn't be hardcoding names here. ChkDrawback should just return true for them if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) { - if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) { + if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) { continue; } needsLimitedResources = true; // TODO: check for good drawbacks (gainLife) @@ -1583,7 +1583,7 @@ public class ComputerUtilMana { // don't use abilities with dangerous drawbacks AbilitySub sub = m.getSubAbility(); if (sub != null) { - if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) { + if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) { continue; } } diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java index 0bb9c6f0dae..38681aa8d60 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java +++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java @@ -360,10 +360,10 @@ public class SpecialAiLogic { // FIXME: We're emulating the UnlessCost on the SA to run the proper checks. // This is hacky, but it works. Perhaps a cleaner way exists? sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost")); - willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa); + willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa).willingToPlay(); sa.getMapParams().remove("UnlessCost"); } else { - willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa); + willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa).willingToPlay(); } return willPlay; } diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java index 40023febd19..1a9ccb90e37 100644 --- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java +++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java @@ -78,16 +78,17 @@ public class SpecialCardAi { // Arena and Magus of the Arena public static class Arena { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { final Game game = ai.getGame(); + // TODO This is basically removal, so we may want to play this at other times if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) { - return false; // at opponent's EOT only, to conserve mana + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); } CardCollection aiCreatures = ai.getCreaturesInPlay(); if (aiCreatures.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } for (Player opp : ai.getOpponents()) { @@ -111,11 +112,11 @@ public class SpecialCardAi { if (canKillAll) { sa.getTargets().clear(); sa.getTargets().add(aiCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.Removal); } } } - return sa.isTargetNumberValid(); + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -203,7 +204,7 @@ public class SpecialCardAi { // Chain of Acid public static class ChainOfAcid { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { List AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS); List OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), @@ -213,13 +214,22 @@ public class SpecialCardAi { // which it can only distinguish by their CMC, considering >CMC higher value). // Currently ensures that the AI will still have lands provided that the human player goes to // destroy all the AI's lands in order (to avoid manalock). - return !OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2; + if (!OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2) { + // If there are enough lands, target the worst non-creature permanent of the opponent + Card worstOppPerm = ComputerUtilCard.getWorstAI(OppPerms); + if (worstOppPerm != null) { + sa.resetTargets(); + sa.getTargets().add(worstOppPerm); + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + } + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } // Chain of Smog public static class ChainOfSmog { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { if (ai.getCardsIn(ZoneType.Hand).isEmpty()) { // to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point) // TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a @@ -235,10 +245,10 @@ public class SpecialCardAi { sa.getParent().resetTargets(); sa.getParent().getTargets().add(targOpp); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -426,17 +436,17 @@ public class SpecialCardAi { return false; } - public static boolean considerDonatingPermanent(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) { Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); if (donateTarget != null) { sa.resetTargets(); sa.getTargets().add(donateTarget); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first."); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -628,15 +638,15 @@ public class SpecialCardAi { // Fell the Mighty public static class FellTheMighty { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { CardCollection aiList = ai.getCreaturesInPlay(); if (aiList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } CardLists.sortByPowerAsc(aiList); Card lowest = aiList.get(0); if (!sa.canTarget(lowest)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), @@ -646,9 +656,9 @@ public class SpecialCardAi { if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) { sa.resetTargets(); sa.getTargets().add(lowest); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -695,13 +705,13 @@ public class SpecialCardAi { // Goblin Polka Band public static class GoblinPolkaBand { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { int maxPotentialTgts = ai.getOpponents().getCreaturesInPlay().filter(CardPredicates.UNTAPPED).size(); int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false); int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts); if (numTgts == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // Set Announce @@ -711,7 +721,7 @@ public class SpecialCardAi { List validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true); sa.resetTargets(); sa.getTargets().addAll(Aggregates.random(validTgts, numTgts)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -1042,29 +1052,33 @@ public class SpecialCardAi { return exiledWith == null || (tgt != null && ComputerUtilCard.evaluateCreature(tgt) > ComputerUtilCard.evaluateCreature(exiledWith)); } - public static boolean considerCopy(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision considerCopy(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); final Card exiledWith = source.getImprintedCards().isEmpty() ? null : source.getImprintedCards().getFirst(); if (exiledWith == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // We want to either be able to attack with the creature, or keep it until our opponent's end of turn as a // potential blocker - return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith) + if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith) || (ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && ai.getGame().getCombat() != null - && !ai.getGame().getCombat().getAttackers().isEmpty()); + && !ai.getGame().getCombat().getAttackers().isEmpty())) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } // Momir Vig, Simic Visionary Avatar public static class MomirVigAvatar { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { Card source = sa.getHostCard(); if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } // In MoJhoSto, prefer Jhoira sorcery ability from time to time @@ -1075,7 +1089,7 @@ public class SpecialCardAi { int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA); if (ai.getLandsInPlay().size() >= numLandsForJhoira && MyRandom.percentTrue(chanceToPrefJhoira)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } } @@ -1084,7 +1098,7 @@ public class SpecialCardAi { // Some basic strategy for Momir if (tokenSize < 2) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } if (tokenSize > 11) { @@ -1093,7 +1107,7 @@ public class SpecialCardAi { sa.setXManaCostPaid(tokenSize); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -1304,7 +1318,7 @@ public class SpecialCardAi { } } - public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision considerSecondTarget(final Player ai, final SpellAbility sa) { Card firstTgt = sa.getParent().getTargetCard(); CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter( CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa))); @@ -1312,89 +1326,105 @@ public class SpecialCardAi { if (secondTgt != null) { sa.resetTargets(); sa.getTargets().add(secondTgt); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } // Price of Progress public static class PriceOfProgress { - public static boolean consider(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) { // Don't play in early game - opponent likely still has lands to play if (ai.getGame().getPhaseHandler().getTurn() < 10) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size(); + // TODO Better if we actually calculate the true damage + boolean willDieToPCasting = (ai.getLife() <= aiLands * 2); + if (!willDieToPCasting) { + boolean hasBridge = false; + for (Card c : ai.getCardsIn(ZoneType.Battlefield)) { + // Do we have a card in play that makes us want to empty out hand? + if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) { + hasBridge = true; + break; + } + } - boolean hasBridge = false; - for (Card c : ai.getCardsIn(ZoneType.Battlefield)) { - // Do we have a card in play that makes us want to empty out hand? - if (c.hasSVar("PreferredHandSize") && ai.getCardsIn(ZoneType.Hand).size() > Integer.parseInt(c.getSVar("PreferredHandSize"))) { - hasBridge = true; - break; + // Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll + // even if suboptimal play, but don't waste the card too early even then! + if (hasBridge) { + return new AiAbilityDecision(100, AiPlayDecision.PlayToEmptyHand); } } - // Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll - // even if suboptimal play, but don't waste the card too early even then! - if ((hasBridge) && (ai.getGame().getPhaseHandler().getTurn() >= 10)) { - return true; - } - + boolean willPlay = true; for (Player opp : ai.getOpponents()) { int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size(); + // Don't if no enemy nonbasic lands + if (oppLands == 0) { + willPlay = false; + continue; + } + // Always if enemy would die and we don't! // TODO : predict actual damage instead of assuming it'll be 2*lands // Don't if we lose, unless we lose anyway to unblocked creatures next turn - if ((ai.getLife() <= aiLands * 2) && + if (willDieToPCasting && (!(ComputerUtil.aiLifeInDanger(ai, true, 0)) && ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2))) { - return false; + willPlay = false; } // Do if we can win - if ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2) { - return true; + if (opp.getLife() <= oppLands * 2) { + return new AiAbilityDecision(1000, AiPlayDecision.WillPlay); } // Don't if we'd lose a larger percentage of our remaining life than enemy if ((aiLands / ((double) ai.getLife())) > (oppLands / ((double) ai.getOpponentsSmallestLifeTotal()))) { - return false; - } - // Don't if no enemy nonbasic lands - if (oppLands == 0) { - return false; + willPlay = false; } + // Don't if loss is equal in percentage but we lose more points if (((aiLands / ((double) ai.getLife())) == (oppLands / ((double) ai.getOpponentsSmallestLifeTotal()))) && (aiLands > oppLands)) { - return false; + willPlay = false; } } - return true; + if (willPlay) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } // Sarkhan the Mad public static class SarkhanTheMad { - public static boolean considerDig(final Player ai, final SpellAbility sa) { - return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1; + public static AiAbilityDecision considerDig(final Player ai, final SpellAbility sa) { + if (sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) { + public static AiAbilityDecision considerMakeDragon(final Player ai, final SpellAbility sa) { // TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier? CardCollection creatures = ai.getCreaturesInPlay(); boolean hasValidTgt = !CardLists.filter(creatures, t -> t.getNetPower() < 5 && t.getNetToughness() < 5).isEmpty(); if (hasValidTgt) { Card worstCreature = ComputerUtilCard.getWorstCreatureAI(creatures); sa.getTargets().add(worstCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) { int minLife = weakestOpp.getLife(); @@ -1705,12 +1735,12 @@ public class SpecialCardAi { // Volrath's Shapeshifter public static class VolrathsShapeshifter { - 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(); if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) { // try not to do this too early to at least attempt to avoid situations where the AI // would cast a spell which would ruin the shapeshifting - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard); @@ -1726,11 +1756,15 @@ public class SpecialCardAi { if (topGY == null || !topGY.isCreature() || ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) { - return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false); + if ( numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) { diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java index 3cbe146141b..744aff70a7c 100644 --- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java @@ -39,28 +39,33 @@ import forge.util.collect.FCollectionView; */ public abstract class SpellAbilityAi { - public final boolean canPlayAIWithSubs(final Player aiPlayer, final SpellAbility sa) { - if (!canPlayAI(aiPlayer, sa)) { - return false; + public final AiAbilityDecision canPlayAIWithSubs(final Player aiPlayer, final SpellAbility sa) { + AiAbilityDecision decision = canPlayAI(aiPlayer, sa); + if (!decision.willingToPlay()) { + return decision; } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb); + if (subAb == null) { + return decision; + } + + return chkDrawbackWithSubs(aiPlayer, subAb); } /** * Handles the AI decision to play a "main" SpellAbility */ - protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } return canPlayWithoutRestrict(ai, sa); } - protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayWithoutRestrict(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); final Cost cost = sa.getPayCosts(); @@ -72,7 +77,7 @@ public abstract class SpellAbilityAi { if (!checkConditions(ai, sa, sa.getConditions())) { SpellAbility sub = sa.getSubAbility(); if (sub != null && !checkConditions(ai, sub, sub.getConditions())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.NeedsToPlayCriteriaNotMet); } } @@ -81,23 +86,23 @@ public abstract class SpellAbilityAi { final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai) && !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize(); if (!checkAiLogic(ai, sa, logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions); } } else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions); } if (!checkApiLogic(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // needs to be after API logic because needs to check possible X Cost? if (cost != null && !willPayCosts(ai, sa, cost, source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) { @@ -166,9 +171,10 @@ public abstract class SpellAbilityAi { */ protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; // prevent infinite loop + return false; } - return MyRandom.getRandom().nextFloat() < .8f; // random success + + return MyRandom.getRandom().nextFloat() < .8f; } public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { @@ -183,28 +189,48 @@ public abstract class SpellAbilityAi { return sa.isTargetNumberValid(); } - return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory); + return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory).willingToPlay(); } - public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { - if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) { - return false; + public final AiAbilityDecision doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + AiAbilityDecision decision = doTriggerAINoCost(aiPlayer, sa, mandatory); + if (!decision.willingToPlay() && !"Always".equals(sa.getParam("AILogic"))) { + return decision; } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory; - } + if (subAb == null) { + if (decision.willingToPlay()) { + return decision; + } + + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + decision = chkDrawbackWithSubs(aiPlayer, subAb); + if (decision.willingToPlay()) { + return decision; + } + + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } /** * Handles the AI decision to play a triggered SpellAbility */ - protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { - if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) { - return true; + protected AiAbilityDecision doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + AiAbilityDecision decision = canPlayWithoutRestrict(aiPlayer, sa); + if (decision.willingToPlay() && (!mandatory || sa.isTargetNumberValid())) { + // This is a weird check. Why do we care if its not mandatory if we WANT to do it? + return decision; } // not mandatory, short way out if (!mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // invalid target might prevent it @@ -220,30 +246,30 @@ public abstract class SpellAbilityAi { if (sa.canTarget(p)) { sa.resetTargets(); sa.getTargets().add(p); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** * Handles the AI decision to play a sub-SpellAbility */ - public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { // sub-SpellAbility might use targets too if (sa.usesTargeting()) { // no Candidates, no adding to Stack if (!sa.getTargetRestrictions().hasCandidates(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // but if it does, it should override this function System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method"); - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -304,9 +330,18 @@ public abstract class SpellAbilityAi { * @param ab * @return */ - public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { + public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { final AbilitySub subAb = ab.getSubAbility(); - return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb)); + AiAbilityDecision decision = SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer); + if (!decision.willingToPlay()) { + return decision; + } + + if (subAb == null) { + return decision; + } + + return chkDrawbackWithSubs(aiPlayer, subAb); } public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { diff --git a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java index 2c4374f46d6..c7ee9dc9ab9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.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,78 +18,74 @@ import java.util.Map; public class ActivateAbilityAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { - // AI cannot use this properly until he can use SAs during Humans turn - + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final Player opp = ai.getStrongestOpponent(); List list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card")); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } if (!sa.usesTargeting()) { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); - if (!defined.contains(opp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } else { sa.resetTargets(); if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Player opp = ai.getStrongestOpponent(); - final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); if (null == tgt) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); - - return defined.contains(opp); + if (defined.contains(opp)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } else { sa.resetTargets(); sa.getTargets().add(opp); } - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { - // AI cannot use this properly until he can use SAs during Humans turn + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { final Card source = sa.getHostCard(); - - boolean randomReturn = true; - if (!sa.usesTargeting()) { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); - if (defined.contains(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { sa.resetTargets(); sa.getTargets().add(ai.getWeakestOpponent()); } - - return randomReturn; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/AddPhaseAi.java b/forge-ai/src/main/java/forge/ai/ability/AddPhaseAi.java index 6220daa4059..7f12fbfbe5d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AddPhaseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AddPhaseAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -11,8 +13,8 @@ import forge.game.spellability.SpellAbility; public class AddPhaseAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return false; + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java b/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java index 951e0aa90e0..858e4f1af0f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AddTurnAi.java @@ -17,6 +17,8 @@ */ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.player.Player; @@ -38,7 +40,7 @@ import java.util.List; public class AddTurnAi extends SpellAbilityAi { @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); Player opp = targetableOpps.min(PlayerPredicates.compareByLife()); @@ -47,41 +49,41 @@ public class AddTurnAi extends SpellAbilityAi { if (sa.canTarget(ai) && (mandatory || !ai.getGame().getReplacementHandler().wouldExtraTurnBeSkipped(ai))) { sa.getTargets().add(ai); } else if (mandatory) { - for (final Player ally : ai.getAllies()) { + for (final Player ally : ai.getAllies()) { if (sa.canTarget(ally)) { - sa.getTargets().add(ally); - break; + sa.getTargets().add(ally); + break; } - } + } if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { final List tgtPlayers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa); for (final Player p : tgtPlayers) { if (p.isOpponentOf(ai) && !mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // TODO: improve ai for Sage of Hours - return StringUtils.isNumeric(sa.getParam("NumTurns")); - // not sure if the AI should be playing with cards that give the - // Human more turns. + if (!StringUtils.isNumeric(sa.getParam("NumTurns"))) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return doTriggerAINoCost(aiPlayer, sa, false); + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/AdvanceCrankAi.java b/forge-ai/src/main/java/forge/ai/ability/AdvanceCrankAi.java index 10b4e4bf9d0..3143ce41769 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AdvanceCrankAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AdvanceCrankAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.card.CardLists; import forge.game.card.CardPredicates; @@ -11,12 +13,12 @@ import forge.game.zone.ZoneType; public class AdvanceCrankAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { int nextSprocket = (ai.getCrankCounter() % 3) + 1; int crankCount = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isContraptionOnSprocket(nextSprocket)); - //Could evaluate whether we actually want to crank those, but this is probably fine for now. - if(crankCount < 2) - return false; + if (crankCount < 2) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } return super.canPlayAI(ai, sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java b/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java index 6387c89ebed..ae4ef08e796 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AlwaysPlayAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.player.PlayerActionConfirmMode; @@ -13,8 +15,8 @@ public class AlwaysPlayAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return true; + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @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 a73cb26c46f..12d2a140da9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java @@ -3,6 +3,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -28,9 +30,7 @@ public class AmassAi extends SpellAbilityAi { return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1)); } final String type = sa.getParam("Type"); - StringBuilder sb = new StringBuilder("b_0_0_"); - sb.append(sa.getOriginalParam("Type").toLowerCase()).append("_army"); - final String tokenScript = sb.toString(); + final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army"; final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa); Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false); @@ -82,8 +82,9 @@ public class AmassAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return mandatory || checkApiLogic(ai, sa); + 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); } @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 00a45305b27..e5b77a7af8c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java @@ -231,41 +231,44 @@ public class AnimateAi extends SpellAbilityAi { return bFlag; // All of the defined stuff is animated, not very useful } else { sa.resetTargets(); - return animateTgtAI(sa); + return animateTgtAI(sa).willingToPlay(); } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { if (sa.usesTargeting()) { sa.resetTargets(); return animateTgtAI(sa); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + AiAbilityDecision decision; if (sa.usesTargeting()) { - if(animateTgtAI(sa)) - return true; - else if (!mandatory) - return false; + decision = animateTgtAI(sa); + if (decision.willingToPlay()) { + return decision; + } else if (!mandatory) { + return decision; + } else { // fallback if animate is mandatory sa.resetTargets(); List list = CardUtil.getValidCardsToTarget(sa); if (list.isEmpty()) { - return false; + return decision; } Card toAnimate = ComputerUtilCard.getWorstAI(list); rememberAnimatedThisTurn(aiPlayer, toAnimate); sa.getTargets().add(toAnimate); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override @@ -273,7 +276,7 @@ public class AnimateAi extends SpellAbilityAi { return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2); } - private boolean animateTgtAI(final SpellAbility sa) { + private AiAbilityDecision animateTgtAI(final SpellAbility sa) { final Player ai = sa.getActivatingPlayer(); final PhaseHandler ph = ai.getGame().getPhaseHandler(); final String logic = sa.getParamOrDefault("AILogic", ""); @@ -295,7 +298,7 @@ public class AnimateAi extends SpellAbilityAi { // list is empty, no possible targets if (list.isEmpty() && !alwaysActivatePWAbility) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // something is used for animate into creature @@ -362,7 +365,7 @@ public class AnimateAi extends SpellAbilityAi { // data is empty, no good targets if (data.isEmpty() && !alwaysActivatePWAbility) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // get the best creature to be animated @@ -385,13 +388,13 @@ public class AnimateAi extends SpellAbilityAi { holdAnimatedTillMain2(ai, worst); if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) { releaseHeldTillMain2(ai, worst); - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } rememberAnimatedThisTurn(ai, worst); sa.getTargets().add(worst); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (logic.equals("SetPT")) { @@ -403,7 +406,7 @@ public class AnimateAi extends SpellAbilityAi { && (buffed.getNetPower() - worst.getNetPower() >= 3 || !ComputerUtilCard.doesCreatureAttackAI(ai, worst))) { sa.getTargets().add(worst); rememberAnimatedThisTurn(ai, worst); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -415,7 +418,7 @@ public class AnimateAi extends SpellAbilityAi { boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated); boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated); if (isValuableAttacker || isValuableBlocker) - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } @@ -425,7 +428,7 @@ public class AnimateAi extends SpellAbilityAi { if(worst != null) { sa.getTargets().add(worst); rememberAnimatedThisTurn(ai, worst); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -435,7 +438,7 @@ public class AnimateAi extends SpellAbilityAi { if(best != null) { sa.getTargets().add(best); rememberAnimatedThisTurn(ai, best); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -443,7 +446,7 @@ public class AnimateAi extends SpellAbilityAi { // two are the only things // that animate a target. Those can just use AI:RemoveDeck:All until // this can do a reasonably good job of picking a good target - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } public static Card becomeAnimated(final Card card, final SpellAbility sa) { diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java index 0d8536b2089..0607f11d9bd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAllAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -9,24 +11,30 @@ import forge.game.spellability.SpellAbility; public class AnimateAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { String logic = sa.getParamOrDefault("AILogic", ""); if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) { // TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate for (Card c : aiPlayer.getCreaturesInPlay()) { if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } - return "Always".equals(logic); + if ("Always".equals(logic)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // end animateAllCanPlayAI() @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(aiPlayer, sa); + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return canPlayAI(aiPlayer, 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 e4c3c5a5c46..4f329dd153f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AssembleContraptionAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AssembleContraptionAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.GameEntity; @@ -16,30 +18,32 @@ import java.util.List; public class AssembleContraptionAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { - //Pulls double duty as the OpenAttraction API. Same logic; usually good to do as long as we have the appropriate cards. + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { CardCollectionView deck = getDeck(ai, sa); if(deck.isEmpty()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); - if(!super.canPlayAI(ai, sa)) - return false; + AiAbilityDecision superDecision = super.canPlayAI(ai, sa); + if (!superDecision.willingToPlay()) + return superDecision; if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) { int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); xPay = Math.max(xPay, deck.size()); if (xPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } sa.getRootAbility().setXManaCostPaid(xPay); } if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) { - return getGoodReassembleTarget(ai, sa) != null; + if (getGoodReassembleTarget(ai, sa) == null) { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private static CardCollectionView getDeck(Player ai, SpellAbility sa) { @@ -92,18 +96,16 @@ public class AssembleContraptionAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - if(getDeck(aiPlayer, sa).isEmpty()) - return false; - - return super.chkAIDrawback(sa, aiPlayer); + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if(!mandatory && getDeck(aiPlayer, sa).isEmpty()) + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return super.doTriggerAINoCost(aiPlayer, sa, mandatory); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - if(!mandatory && getDeck(aiPlayer, sa).isEmpty()) - return false; - - return super.doTriggerAINoCost(aiPlayer, sa, mandatory); + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { + if(getDeck(aiPlayer, sa).isEmpty()) + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + return super.chkAIDrawback(sa, aiPlayer); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java b/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java index 43a62a9259f..89c5694aca8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -10,11 +12,11 @@ import java.util.Map; public class AssignGroupAi extends SpellAbilityAi { - protected boolean canPlayAI(Player ai, SpellAbility sa) { + @Override + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { // TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar), // otherwise the AI considers the card playable. - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells, Map params) { diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index eccac5925d0..4d8d03c1e70 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -45,23 +45,23 @@ public class AttachAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); // TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures // and gaining card advantage if (source.hasKeyword("MayFlashSac") && !ai.canCastSorcery()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions); } if (abCost != null) { // AI currently disabled for these costs if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } } @@ -70,20 +70,21 @@ public class AttachAi extends SpellAbilityAi { // TODO: Add some extra checks for where the AI may want to cast a replacement aura // on another creature and keep it when the original enchanted creature is useless - return false; + return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend); } // prevent run-away activations - first time will always return true if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } // Attach spells always have a target final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt != null) { sa.resetTargets(); - if (!attachPreference(sa, tgt, false)) { - return false; + AiAbilityDecision attachDecision = attachPreference(sa, tgt, false); + if (!attachDecision.willingToPlay()) { + return attachDecision; } } @@ -94,7 +95,7 @@ public class AttachAi extends SpellAbilityAi { } if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai))) && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) { @@ -102,7 +103,7 @@ public class AttachAi extends SpellAbilityAi { final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (xPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } sa.setXManaCostPaid(xPay); @@ -112,10 +113,10 @@ public class AttachAi extends SpellAbilityAi { final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source); effectExile.setActivatingPlayer(ai); final List targets = CardUtil.getValidCardsToTarget(effectExile); - return !targets.isEmpty(); + return !targets.isEmpty() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) { @@ -955,9 +956,8 @@ public class AttachAi extends SpellAbilityAi { * @return true, if successful */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { final Card card = sa.getHostCard(); - // Check if there are any valid targets List targets = new ArrayList<>(); final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt == null) { @@ -969,23 +969,43 @@ public class AttachAi extends SpellAbilityAi { if (!mandatory && card.isEquipment() && !targets.isEmpty()) { Card newTarget = (Card) targets.get(0); - //don't equip human creatures if (newTarget.getController().isOpponentOf(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - - //don't equip a worse creature if (card.isEquipping()) { Card oldTarget = card.getEquipping(); if (ComputerUtilCard.evaluateCreature(oldTarget) > ComputerUtilCard.evaluateCreature(newTarget)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + boolean stacking = !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName()); + if (!stacking) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - // don't equip creatures that don't gain anything - return !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName()); } } + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } - return true; + @Override + public AiAbilityDecision chkAIDrawback(final SpellAbility sa, final Player ai) { + if (sa.isTrigger() && sa.usesTargeting()) { + CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); + CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa); + Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null); + if (tgt != null) { + sa.resetTargets(); + sa.getTargets().add(tgt); + } + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } + } else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null + && sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private static boolean isAuraSpell(final SpellAbility sa) { @@ -1005,13 +1025,13 @@ public class AttachAi extends SpellAbilityAi { * the mandatory * @return true, if successful */ - private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) { + private static AiAbilityDecision attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) { GameObject o; boolean spellCanTargetPlayer = false; if (isAuraSpell(sa)) { Card source = sa.getHostCard(); if (!source.hasKeyword(Keyword.ENCHANT)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) { String ko = ki.getOriginal(); @@ -1036,11 +1056,11 @@ public class AttachAi extends SpellAbilityAi { } if (o == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } sa.getTargets().add(o); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -1692,25 +1712,6 @@ public class AttachAi extends SpellAbilityAi { return chosen; } - @Override - public boolean chkAIDrawback(final SpellAbility sa, final Player ai) { - // TODO for targeting optional Halvar trigger, needs to be coordinated with PumpAi to make it playable - if (sa.isTrigger() && sa.usesTargeting()) { - CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); - CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa); - Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null); - if (tgt != null) { - sa.resetTargets(); - sa.getTargets().add(tgt); - } - return sa.isTargetNumberValid(); - } else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null - && sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) { - // Living Weapon or similar - return true; - } - return false; - } @Override public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { diff --git a/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java b/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java index 9bd5d028cf0..862cc9d48f8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BalanceAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; @@ -11,7 +13,7 @@ import forge.util.MyRandom; public class BalanceAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { String logic = sa.getParam("AILogic"); int diff = 0; Player opp = aiPlayer.getWeakestOpponent(); @@ -37,7 +39,7 @@ public class BalanceAi extends SpellAbilityAi { if (diff < 0) { // Don't sacrifice permanents even if opponent has a ton of cards in hand - return false; + return new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi); } final CardCollectionView humHand = opp.getCardsIn(ZoneType.Hand); @@ -45,6 +47,7 @@ public class BalanceAi extends SpellAbilityAi { diff += 0.5 * (humHand.size() - compHand.size()); // Larger differential == more chance to actually cast this spell - return diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10; + boolean willPlay = diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10; + return new AiAbilityDecision(willPlay ? 100 : 0, willPlay ? forge.ai.AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java index b9e019382b3..68c59c5ef79 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -16,55 +18,51 @@ import forge.game.zone.ZoneType; public class BecomesBlockedAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { final Card source = sa.getHostCard(); final TargetRestrictions tgt = sa.getTargetRestrictions(); final Game game = aiPlayer.getGame(); if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS) || !game.getPhaseHandler().getPlayerTurn().isOpponentOf(aiPlayer)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (tgt != null) { - sa.resetTargets(); - CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents()); - list = CardLists.getTargetableCards(list, sa); - list = CardLists.getNotKeyword(list, Keyword.TRAMPLE); + sa.resetTargets(); + CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents()); + list = CardLists.getTargetableCards(list, sa); + list = CardLists.getNotKeyword(list, Keyword.TRAMPLE); - while (sa.canAddMoreTarget()) { - Card choice = null; + while (sa.canAddMoreTarget()) { + Card choice = null; - if (list.isEmpty()) { - return false; - } + if (list.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } - choice = ComputerUtilCard.getBestCreatureAI(list); + choice = ComputerUtilCard.getBestCreatureAI(list); - if (choice == null) { // can't find anything left - return false; - } + if (choice == null) { // can't find anything left + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } - list.remove(choice); - sa.getTargets().add(choice); - } + list.remove(choice); + sa.getTargets().add(choice); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // TODO - implement AI - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - boolean chance; - + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { // TODO - implement AI - chance = false; - - return chance; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java index 2d07529b09e..e8b3457315a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; import forge.ai.AiAttackController; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -17,7 +19,7 @@ import java.util.List; public class BidLifeAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { final Card source = sa.getHostCard(); final Game game = source.getGame(); TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -26,31 +28,31 @@ public class BidLifeAi extends SpellAbilityAi { if (tgt.canTgtCreature()) { List list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } Card c = ComputerUtilCard.getBestCreatureAI(list); if (sa.canTarget(c)) { sa.getTargets().add(c); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else if (tgt.getZone().contains(ZoneType.Stack)) { if (game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final SpellAbility topSA = game.getStack().peekAbility(); if (!topSA.isCounterableBy(sa) || aiPlayer.equals(topSA.getActivatingPlayer())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.canTargetSpellAbility(topSA)) { sa.getTargets().add(topSA); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } } boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - return chance; + return new AiAbilityDecision(chance ? 100 : 0, chance ? AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/BondAi.java b/forge-ai/src/main/java/forge/ai/ability/BondAi.java index ba040031401..8b9949dbff9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BondAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BondAi.java @@ -17,6 +17,8 @@ */ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -46,8 +48,8 @@ public final class BondAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return true; + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // end bondCanPlayAI() @Override @@ -56,7 +58,7 @@ public final class BondAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/BranchAi.java b/forge-ai/src/main/java/forge/ai/ability/BranchAi.java index 0de37578305..31932b40d9a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/BranchAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/BranchAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpecialAiLogic; import forge.ai.SpecialCardAi; @@ -21,16 +23,18 @@ public class BranchAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { final String aiLogic = sa.getParamOrDefault("AILogic", ""); if ("GrislySigil".equals(aiLogic)) { - return SpecialCardAi.GrislySigil.consider(aiPlayer, sa); + boolean result = SpecialCardAi.GrislySigil.consider(aiPlayer, sa); + return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } else if ("BranchCounter".equals(aiLogic)) { - return SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); // Bring the Ending, Anticognition (hacky implementation) + boolean result = SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); + return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } else if ("TgtAttacker".equals(aiLogic)) { final Combat combat = aiPlayer.getGame().getCombat(); if (combat == null || combat.getAttackingPlayer() != aiPlayer) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final CardCollection attackers = combat.getAttackers(); @@ -45,16 +49,20 @@ public class BranchAi extends SpellAbilityAi { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackers)); } - return sa.isTargetNumberValid(); + return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } // TODO: expand for other cases where the AI is needed to make a decision on a branch - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return canPlayAI(aiPlayer, sa) || mandatory; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + AiAbilityDecision decision = canPlayAI(aiPlayer, sa); + if (decision.willingToPlay() || mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/CannotPlayAi.java b/forge-ai/src/main/java/forge/ai/ability/CannotPlayAi.java index c19c871f7c6..8009f70991c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CannotPlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CannotPlayAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -10,15 +12,15 @@ public class CannotPlayAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return false; + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - return canPlayAI(aiPlayer, sa); + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java index 1187dc81cdc..f31f5e6d022 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeCombatantsAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.GameEntity; import forge.game.player.Player; @@ -15,34 +17,36 @@ public class ChangeCombatantsAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // TODO: Extend this if possible for cards that have this as an activated ability - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(aiPlayer, sa); + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { final String logic = sa.getParamOrDefault("AILogic", ""); if (logic.equals("WeakestOppExceptCtrl")) { PlayerCollection targetableOpps = aiPlayer.getOpponents(); targetableOpps.remove(sa.getHostCard().getController()); if (targetableOpps.isEmpty()) { - 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 @@ -63,4 +67,3 @@ public class ChangeCombatantsAi extends SpellAbilityAi { return (T)weakestTargetableOpp; } } - 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 a029144183e..193e723191b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java @@ -178,7 +178,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * @return a boolean. */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { if (sa.isHidden()) { return hiddenOriginPlayDrawbackAI(aiPlayer, sa); } @@ -197,7 +197,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { String aiLogic = sa.getParamOrDefault("AILogic", ""); if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) { @@ -206,10 +206,10 @@ public class ChangeZoneAi extends SpellAbilityAi { } if ("Always".equals(aiLogic)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("IfNotBuffed".equals(aiLogic)) { if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) { - return true; // debuffed by opponent's auras to the level that it becomes useless + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } int delta = 0; for (Card enc : sa.getHostCard().getEnchantedBy()) { @@ -219,9 +219,17 @@ public class ChangeZoneAi extends SpellAbilityAi { delta++; } } - return delta <= 0; + if (delta <= 0) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if ("SaviorOfOllenbock".equals(aiLogic)) { - return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa); + if (SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (sa.isHidden()) { @@ -452,7 +460,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb); + return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb).willingToPlay(); } /** @@ -464,7 +472,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. */ - private static boolean hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) { + private static AiAbilityDecision hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) { // if putting cards from hand to library and parent is drawing cards // make sure this will actually do something: final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -476,11 +484,11 @@ public class ChangeZoneAi extends SpellAbilityAi { } else if (!isCurse && sa.canTarget(aiPlayer)) { sa.getTargets().add(aiPlayer); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -494,7 +502,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * a boolean. * @return a boolean. */ - private static boolean hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) { + private static AiAbilityDecision hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) { // Fetching should occur fairly often as it helps cast more spells, and // have access to more mana @@ -507,7 +515,7 @@ public class ChangeZoneAi extends SpellAbilityAi { * to make sub-optimal choices (waste bounce) than to make obvious mistakes * (bounce useful permanent). */ - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -545,15 +553,15 @@ public class ChangeZoneAi extends SpellAbilityAi { pDefined = sa.getTargets().getTargetPlayers(); if (Iterables.isEmpty(pDefined)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa); } @@ -567,10 +575,10 @@ public class ChangeZoneAi extends SpellAbilityAi { } if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // *********** Utility functions for Hidden ******************** @@ -770,7 +778,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } final AbilitySub subAb = sa.getSubAbility(); - return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb); + return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb).willingToPlay(); } /* @@ -843,16 +851,26 @@ public class ChangeZoneAi extends SpellAbilityAi { * a {@link forge.game.spellability.SpellAbility} object. * @return a boolean. */ - private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) { + private static AiAbilityDecision knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) { if ("MimicVat".equals(sa.getParam("AILogic"))) { - return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa); + if (SpecialCardAi.MimicVat.considerExile(aiPlayer, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (!sa.usesTargeting()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return isPreferredTarget(aiPlayer, sa, false, true); + if (!isPreferredTarget(aiPlayer, sa, false, true)) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } else { + // if we are here, we have a target + // so we can play the ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } /** @@ -1493,7 +1511,7 @@ public class ChangeZoneAi extends SpellAbilityAi { } } if (choice == null) { // can't find anything left - if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { + if (sa.getTargets().isEmpty() || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) { sa.resetTargets(); return false; } @@ -1521,13 +1539,21 @@ public class ChangeZoneAi extends SpellAbilityAi { * a boolean. * @return a boolean. */ - private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) { + private static AiAbilityDecision knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) { final String logic = sa.getParamOrDefault("AILogic", ""); if ("DeathgorgeScavenger".equals(logic)) { - return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa); + if (SpecialCardAi.DeathgorgeScavenger.consider(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if ("ExtraplanarLens".equals(logic)) { - return SpecialCardAi.ExtraplanarLens.consider(ai, sa); + if (SpecialCardAi.ExtraplanarLens.consider(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if ("ExileCombatThreat".equals(logic)) { return doExileCombatThreatLogic(ai, sa); } @@ -1539,14 +1565,27 @@ public class ChangeZoneAi extends SpellAbilityAi { if (!list.isEmpty()) { final Card attachedTo = list.get(0); // This code is for the Dragon auras - return !attachedTo.getController().isOpponentOf(ai); + if (!attachedTo.getController().isOpponentOf(ai)) { + // If the AI is not the controller of the attachedTo card, then it is not a valid target. + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If the AI is the controller of the attachedTo card, then it is a valid target. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } } else if (isPreferredTarget(ai, sa, mandatory, true)) { // do nothing - } else return isUnpreferredTarget(ai, sa, mandatory); + } else { + if (isUnpreferredTarget(ai, sa, mandatory)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If the AI is not the controller of the attachedTo card, then it is not a valid target. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List origin, SpellAbility sa, CardCollection fetchList, Player player, final Player decider) { @@ -1858,7 +1897,7 @@ public class ChangeZoneAi extends SpellAbilityAi { return false; } - public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) { @SuppressWarnings("unchecked") Map originalParams = (Map)sa.getReplacingObject(AbilityKey.OriginalParams); SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause); @@ -1867,13 +1906,13 @@ public class ChangeZoneAi extends SpellAbilityAi { if (Objects.equals(ZoneType.Hand, destination)) { // If the commander is being moved to your hand, don't replace since its easier to cast it again - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect) if (sa.getHostCard().getName().contains("Squee, the Immortal") && (destination == ZoneType.Graveyard || destination == ZoneType.Exile)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) { @@ -1882,28 +1921,38 @@ public class ChangeZoneAi extends SpellAbilityAi { if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin")) && "Battlefield".equals(causeSub.getParam("Destination"))) { // A blink effect implemented using ChangeZone API - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else // This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to // return the commander to the Command zone. if (subApi == ApiType.DelayedTrigger) { SpellAbility exec = causeSub.getAdditionalAbility("Execute"); if (exec != null && exec.getApi() == ApiType.ChangeZone) { // A blink effect implemented using a delayed trigger - return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination")); + if (!"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } + } else { + if (causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card)) + || !causeSa.getActivatingPlayer().equals(aiPlayer)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - } else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card)) - || !causeSa.getActivatingPlayer().equals(aiPlayer); } - // Normally we want the commander back in Command zone to recast him later - return true; + // Normally we want the commander back in Command zone to recast it later + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) { + public static AiAbilityDecision doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) { final Combat combat = aiPlayer.getGame().getCombat(); if (combat == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } Card choice = null; @@ -1938,9 +1987,9 @@ public class ChangeZoneAi extends SpellAbilityAi { if (choice != null) { sa.getTargets().add(choice); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) { 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 ba17e0546b2..33d03af40b9 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; @@ -19,7 +21,7 @@ import java.util.Map; public class ChangeZoneAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { // Change Zone All, can be any type moving from one zone to another final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); @@ -32,14 +34,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll"); if (!aiLogicAllowsDiscard) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); } } } @@ -59,31 +61,31 @@ public class ChangeZoneAllAi extends SpellAbilityAi { // Ugin AI: always try to sweep before considering +1 if (sourceName.equals("Ugin, the Spirit Dragon")) { - return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType); + boolean result = SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa); computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa); if ("LivingDeath".equals(aiLogic)) { - // Living Death AI - return SpecialCardAi.LivingDeath.consider(ai, sa); + boolean result = SpecialCardAi.LivingDeath.consider(ai, sa); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("Timetwister".equals(aiLogic)) { - // Timetwister AI - return SpecialCardAi.Timetwister.consider(ai, sa); + boolean result = SpecialCardAi.Timetwister.consider(ai, sa); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("RetDiscardedThisTurn".equals(aiLogic)) { - // e.g. Shadow of the Grave - return ai.getDiscardedThisTurn().size() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN); + 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); } else if ("ExileGraveyards".equals(aiLogic)) { for (Player opp : ai.getOpponents()) { CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard); CardCollection creats = CardLists.filter(cardsGY, CardPredicates.CREATURES); - if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) { PlayerCollection players = ai.getOpponents(); players.add(ai); @@ -98,68 +100,48 @@ public class ChangeZoneAllAi extends SpellAbilityAi { bestTgt = player; } } - if (bestTgt != null) { sa.resetTargets(); sa.getTargets().add(bestTgt); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO improve restrictions on when the AI would want to use this // spBounceAll has some AI we can compare to. if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) { if (!sa.usesTargeting()) { - // TODO: improve logic for non-targeted SAs of this type (most are currently AI:RemoveDeck:All, e.g. Memory Jar) - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { - // search targetable Opponents final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - if (oppList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } - - // get the one with the most handsize Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin)); - - // set the target if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) { sa.resetTargets(); sa.getTargets().add(oppTarget); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } } } else if (origin.equals(ZoneType.Battlefield)) { - // this statement is assuming the AI is trying to use this spell offensively - // if the AI is using it defensively, then something else needs to occur - // if only creatures are affected evaluate both lists and pass only - // if human creatures are more valuable if (sa.usesTargeting()) { - // search targetable Opponents final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - if (oppList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } - - // get the one with the most in graveyard - // zone is visible so evaluate which would be hurt the most Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin)); - - // set the target if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) { sa.resetTargets(); sa.getTargets().add(oppTarget); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } computerType = new CardCollection(); } - int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units) int nonCreatureEvalThreshold = 3; // CMC difference if (ai.getController().isAI()) { @@ -181,103 +163,80 @@ public class ChangeZoneAllAi extends SpellAbilityAi { && game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) { // Life is in serious danger, return all creatures from the battlefield to wherever // so they don't deal lethal damage - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard .evaluateCreatureList(oppType)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - } // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human - // permanents are more valuable - else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard + } else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard .evaluatePermanentList(oppType)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - - // Don't cast during main1? if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions); } } else if (origin.equals(ZoneType.Graveyard)) { if (sa.usesTargeting()) { - // search targetable Opponents final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - if (oppList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } - - // get the one with the most in graveyard - // zone is visible so evaluate which would be hurt the most Player oppTarget = Collections.max(oppList, AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa)); - - // set the target if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) { sa.resetTargets(); sa.getTargets().add(oppTarget); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } } else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) { - return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size()) + boolean result = (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size()) && !ComputerUtil.isPlayingReanimator(ai); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (origin.equals(ZoneType.Exile)) { if (aiLogic.startsWith("DiscardAllAndRetExiled")) { int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size(); int curHandSize = ai.getCardsIn(ZoneType.Hand).size(); - - // minimum card advantage unless the hand will be fully reloaded int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0; boolean noDiscard = aiLogic.contains(".noDiscard"); - if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) { if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) { - // Try to gain some card advantage if the card will die anyway - // TODO: ideally, should evaluate the hand value and not discard good hands to it - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - - return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize()); + boolean result = (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize()); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (origin.equals(ZoneType.Stack)) { - // TODO - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - if (destination.equals(ZoneType.Battlefield)) { if (sa.hasParam("GainControl")) { - // Check if the cards are valuable enough if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) { if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard .evaluateCreatureList(oppType)) < 400) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are less valuable - else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard + } else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard .evaluatePermanentList(oppType)) < 6) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { - // don't activate if human gets more back than AI does if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) { if (ComputerUtilCard.evaluateCreatureList(computerType) <= (ComputerUtilCard .evaluateCreatureList(oppType) + 100)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are less valuable - else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard + } else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard .evaluatePermanentList(oppType) + 2)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } - - return (((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance); + boolean result = ((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** @@ -292,11 +251,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi { * @return a boolean. */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // if putting cards from hand to library and parent is drawing cards // make sure this will actually do something: - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) @@ -328,127 +287,92 @@ public class ChangeZoneAllAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, final SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, final SpellAbility sa, boolean mandatory) { // Change Zone All, can be any type moving from one zone to another final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0); if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) { - // TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's - // Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and - // there is no specific AI to support playing it in a smarter way. Feel free to expand. - return ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES); + boolean result = ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - CardCollectionView humanType = ai.getOpponents().getCardsIn(origin); humanType = AbilityUtils.filterListByType(humanType, sa.getParam("ChangeType"), sa); - CardCollectionView computerType = ai.getCardsIn(origin); computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa); - - // TODO improve restrictions on when the AI would want to use this - // spBounceAll has some AI we can compare to. if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) { if (sa.usesTargeting()) { - // search targetable Opponents final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - if (oppList.isEmpty()) { if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) { sa.resetTargets(); sa.getTargets().add(ai); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } - - // get the one with the most handsize Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin)); - - // set the target if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) { sa.resetTargets(); sa.getTargets().add(oppTarget); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } } } else if (origin.equals(ZoneType.Battlefield)) { - // if mandatory, no need to evaluate if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - // this statement is assuming the AI is trying to use this spell offensively - // if the AI is using it defensively, then something else needs to occur - // if only creatures are affected evaluate both lists and pass only - // if human creatures are more valuable if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) { if (ComputerUtilCard.evaluateCreatureList(computerType) >= ComputerUtilCard.evaluateCreatureList(humanType)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are more valuable - else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) { - return false; + } else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (origin.equals(ZoneType.Graveyard)) { if (sa.usesTargeting()) { - // search targetable Opponents final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); - if (oppList.isEmpty()) { if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) { sa.resetTargets(); sa.getTargets().add(ai); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return sa.isTargetNumberValid(); + return sa.isTargetNumberValid() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } - - // get the one with the most in graveyard - // zone is visible so evaluate which would be hurt the most Player oppTarget = oppList.max( AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa)); - - // set the target if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) { sa.resetTargets(); sa.getTargets().add(oppTarget); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } } - } else if (origin.equals(ZoneType.Exile)) { - - } else if (origin.equals(ZoneType.Stack)) { - // currently only exists indirectly (e.g. Summary Dismissal via PlayAi) } - if (destination.equals(ZoneType.Battlefield)) { - // if mandatory, no need to evaluate if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (sa.hasParam("GainControl")) { - // Check if the cards are valuable enough if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) { - return (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1; - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are less valuable - return (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard + boolean result = (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + boolean result = (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard .evaluatePermanentList(humanType)) >= 1; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - - // don't activate if human gets more back than AI does if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) { - return ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType); - } // otherwise evaluate both lists by CMC and pass only if human - // permanents are less valuable - return ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType); + boolean result = ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + boolean result = ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } 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 bbe10fcd12d..de299dad1cd 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java @@ -276,10 +276,10 @@ public class CharmAi extends SpellAbilityAi { } @Override - public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { + public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) { // choices were already targeted if (ab.getRootAbility().getChosenList() != null) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } return super.chkDrawbackWithSubs(aiPlayer, ab); } 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 c211df16fc6..1f957f42dd8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java @@ -135,11 +135,16 @@ public class ChooseCardAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if (sa.hasParam("AILogic") && !checkAiLogic(ai, sa, sa.getParam("AILogic"))) { - return false; + 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/ChooseCardNameAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java index 0d760959026..260757a2efa 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java @@ -22,16 +22,20 @@ import java.util.Map; public class ChooseCardNameAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { if (sa.hasParam("AILogic")) { // Don't tap creatures that may be able to block if (ComputerUtil.waitForBlocking(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat); } String logic = sa.getParam("AILogic"); if (logic.equals("CursedScroll")) { - return SpecialCardAi.CursedScroll.consider(ai, sa); + if (SpecialCardAi.CursedScroll.consider(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -43,13 +47,13 @@ public class ChooseCardNameAi extends SpellAbilityAi { sa.getTargets().add(ai); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { String aiLogic = sa.getParamOrDefault("AILogic", ""); if ("PithingNeedle".equals(aiLogic)) { // Make sure theres something in play worth Needlings. @@ -57,18 +61,29 @@ public class ChooseCardNameAi extends SpellAbilityAi { CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa); if (oppPerms.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms); if (card != null) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // 5 percent chance to cast per opposing card with a non mana ability - return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size(); + if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } + + if (mandatory) { + // If mandatory, then we will play it. + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If not mandatory, then we won't play it. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return mandatory; } /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean) diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java index e893cc2b6ca..bf64a60d5a3 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseColorAi.java @@ -16,35 +16,45 @@ import forge.util.MyRandom; public class ChooseColorAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final PhaseHandler ph = game.getPhaseHandler(); if (!sa.hasParam("AILogic")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingLogic); } final String logic = sa.getParam("AILogic"); if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if ("Nykthos, Shrine to Nyx".equals(sourceName)) { - return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa); + if (SpecialCardAi.NykthosShrineToNyx.consider(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if ("Oona, Queen of the Fae".equals(sourceName)) { if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } // Set PayX here to maximum value. sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if ("Addle".equals(sourceName)) { - return !ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty(); + // TODO Why is this not in the AI logic? + // Why are we specifying the weakest opponent? + if (!ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); + } } if (logic.equals("MostExcessOpponentControls")) { @@ -54,10 +64,10 @@ public class ChooseColorAi extends SpellAbilityAi { int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist); if (excess > 4) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("MostProminentInComputerDeck")) { if ("Astral Cornucopia".equals(sourceName)) { // activate in Main 2 hoping that the extra mana surplus will make a difference @@ -65,22 +75,31 @@ public class ChooseColorAi extends SpellAbilityAi { CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.NONLAND_PERMANENTS); - return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai); + if (!permanents.isEmpty() && ph.is(PhaseType.MAIN2, ai)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); + } } } else if (logic.equals("HighestDevotionToColor")) { // currently only works more or less reliably in Main2 to cast own spells if (!ph.is(PhaseType.MAIN2, ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } } boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - return chance; + return new AiAbilityDecision( + chance ? 100 : 0, + chance ? AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(ai, sa); + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java index 92a5d969980..3ae3ade26ec 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.Direction; import forge.game.Game; @@ -18,11 +20,11 @@ public class ChooseDirectionAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final String logic = sa.getParam("AILogic"); final Game game = sa.getActivatingPlayer().getGame(); if (logic == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingLogic); } else { if ("Aminatou".equals(logic)) { CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS); @@ -33,19 +35,24 @@ public class ChooseDirectionAi extends SpellAbilityAi { CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right)); int leftValue = Aggregates.sum(left, Card::getCMC); int rightValue = Aggregates.sum(right, Card::getCMC); - return aiValue <= leftValue && aiValue <= rightValue; + if (aiValue <= leftValue && aiValue <= rightValue) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } } - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return canPlayAI(ai, sa); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(ai, sa); + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java index 31746c3e52c..22f78bb0e32 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseEvenOddAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; import forge.ai.AiAttackController; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -9,9 +11,9 @@ import forge.util.MyRandom; public class ChooseEvenOddAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { if (!sa.hasParam("AILogic")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingLogic); } if (sa.usesTargeting()) { sa.resetTargets(); @@ -19,16 +21,22 @@ public class ChooseEvenOddAi extends SpellAbilityAi { if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(ai, sa); + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return canPlayAI(ai, sa); } - } 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 fdf3afba56a..fb650cafe77 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericAi.java @@ -29,7 +29,7 @@ public class ChooseGenericAi extends SpellAbilityAi { return true; } else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) { for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) { - if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) { + if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb).willingToPlay()) { return true; } } @@ -51,27 +51,30 @@ public class ChooseGenericAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa); + 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); } @Override - protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) { for (final Player p : aiPlayer.getOpponents()) { if (p.canBeTargetedBy(sa)) { sa.resetTargets(); sa.getTargets().add(p); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return true; // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6 + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6 } if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - - return super.doTriggerAINoCost(aiPlayer, sa, mandatory); + AiAbilityDecision superDecision = super.doTriggerAINoCost(aiPlayer, sa, mandatory); + return superDecision; } @Override @@ -262,7 +265,7 @@ public class ChooseGenericAi extends SpellAbilityAi { List filtered = Lists.newArrayList(); // filter first for the spells which can be done for (SpellAbility sp : spells) { - if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) { + if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp).willingToPlay()) { filtered.add(sp); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java index 99b10c6e5ff..c33dfa85e4d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseNumberAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.AiAttackController; -import forge.ai.ComputerUtilCard; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -11,11 +9,11 @@ import forge.util.MyRandom; public class ChooseNumberAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { String aiLogic = sa.getParamOrDefault("AILogic", ""); if (aiLogic.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingLogic); } else if (aiLogic.equals("SweepCreatures")) { int maxChoiceLimit = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Max"), sa); int ownCreatureCount = aiPlayer.getCreaturesInPlay().size(); @@ -30,17 +28,24 @@ public class ChooseNumberAi extends SpellAbilityAi { } if (refOpp == null) { - return false; // no opponent has any creatures + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } int evalAI = ComputerUtilCard.evaluateCreatureList(aiPlayer.getCreaturesInPlay()); int evalOpp = ComputerUtilCard.evaluateCreatureList(refOpp.getCreaturesInPlay()); if (aiPlayer.getLifeLostLastTurn() + aiPlayer.getLifeLostThisTurn() == 0 && evalAI > evalOpp) { - return false; // we're not pressured and our stuff seems better, don't do it yet + // we're not pressured and our stuff seems better, don't do it yet + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } - return ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit); + if (ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit)) { + // we have more creatures than the opponent, or we have less than the opponent but more than the max choice limit + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // we have less creatures than the opponent and less than the max choice limit + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (sa.usesTargeting()) { @@ -49,16 +54,23 @@ public class ChooseNumberAi extends SpellAbilityAi { if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(ai, sa); + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } { + return canPlayAI(ai, sa); + } } - } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java b/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java index d2622f900b4..5a91ec79668 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChoosePlayerAi.java @@ -2,6 +2,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.player.Player; @@ -15,17 +17,17 @@ import java.util.Map; public class ChoosePlayerAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { - return true; + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return canPlayAI(ai, sa); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { return canPlayAI(ai, sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java index a3a2cc489b7..e778312f28d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseSourceAi.java @@ -1,10 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Iterables; -import forge.ai.AiAttackController; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.GameObject; import forge.game.ability.AbilityUtils; @@ -32,7 +29,7 @@ public class ChooseSourceAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(final Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, SpellAbility sa) { // TODO: AI Support! Currently this is copied from AF ChooseCard. // When implementing AI, I believe AI also needs to be made aware of the damage sources chosen // to be prevented (e.g. so the AI doesn't attack with a creature that will not deal any damage @@ -44,7 +41,7 @@ public class ChooseSourceAi extends SpellAbilityAi { if (abCost != null) { if (!willPayCosts(ai, sa, abCost, source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } @@ -54,7 +51,7 @@ public class ChooseSourceAi extends SpellAbilityAi { if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } if (sa.hasParam("AILogic")) { @@ -63,11 +60,11 @@ public class ChooseSourceAi extends SpellAbilityAi { if (!game.getStack().isEmpty()) { final SpellAbility topStack = game.getStack().peekAbility(); if (sa.hasParam("Choices") && !topStack.matchesValid(topStack.getHostCard(), sa.getParam("Choices").split(","))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } final ApiType threatApi = topStack.getApi(); if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } final Card threatSource = topStack.getHostCard(); @@ -79,13 +76,17 @@ public class ChooseSourceAi extends SpellAbilityAi { } if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack); - return ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0; + if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield); if (sa.hasParam("Choices")) { @@ -98,11 +99,13 @@ public class ChooseSourceAi extends SpellAbilityAi { } return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0; }); - return !choices.isEmpty(); + if (choices.isEmpty()) { + 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/ChooseTypeAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java index 89f6451822c..982962457fc 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java @@ -21,20 +21,34 @@ import java.util.Set; public class ChooseTypeAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { String aiLogic = sa.getParamOrDefault("AILogic", ""); if (aiLogic.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingLogic); } else if ("MostProminentComputerControls".equals(aiLogic)) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) { - return doMirrorEntityLogic(aiPlayer, sa); + if (doMirrorEntityLogic(aiPlayer, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } + + + if (!chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty(); } else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) { - return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty(); + return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty() + ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("MostProminentOppControls".equals(aiLogic)) { - return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty(); + return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty() + ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } return doTriggerAINoCost(aiPlayer, sa, false); @@ -101,7 +115,7 @@ public class ChooseTypeAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { boolean isCurse = sa.isCurse(); if (sa.usesTargeting()) { @@ -133,16 +147,16 @@ public class ChooseTypeAi extends SpellAbilityAi { } if (!sa.isTargetNumberValid()) { - return false; // nothing to target? + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) { if (p.isOpponentOf(ai) && !mandatory && !isCurse) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private String chooseType(SpellAbility sa, CardCollectionView cards) { 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 254da6006db..64e66ea380b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java @@ -2,6 +2,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -22,14 +24,15 @@ public class ClashAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { boolean legalAction = true; if (sa.usesTargeting()) { legalAction = selectTarget(aiPlayer, sa); } - return legalAction; + return legalAction ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } /* @@ -104,7 +107,6 @@ public class ClashAi extends SpellAbilityAi { } } - return sa.getTargets().size() > 0; + return !sa.getTargets().isEmpty(); } - } diff --git a/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java b/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java index 2e4b32bf240..a76f5bda152 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ClassLevelUpAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.ai.SpellApiToAi; import forge.game.ability.AbilityUtils; @@ -12,7 +14,8 @@ import forge.game.trigger.TriggerType; public class ClassLevelUpAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + // TODO does leveling up affect combat? Otherwise wait for Main2 Card host = sa.getHostCard(); final int level = host.getClassLevel() + 1; for (StaticAbility stAb : host.getStaticAbilities()) { @@ -26,11 +29,11 @@ public class ClassLevelUpAi extends SpellAbilityAi { } SpellAbility effect = t.ensureAbility(); if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java index 0fd433286ce..86ffb6a9408 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -18,7 +20,7 @@ import java.util.Map; public class CloneAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final Game game = source.getGame(); @@ -37,7 +39,7 @@ public class CloneAi extends SpellAbilityAi { // "Can I use this to block something?" if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions); } PhaseHandler phase = game.getPhaseHandler(); @@ -66,18 +68,19 @@ public class CloneAi extends SpellAbilityAi { } if (!bFlag) { // All of the defined stuff is cloned, not very useful - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } else { sa.resetTargets(); useAbility &= cloneTgtAI(sa); } - return useAbility; + return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // end cloneCanPlayAI() @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // AI should only activate this during Human's turn boolean chance = true; @@ -85,11 +88,12 @@ public class CloneAi extends SpellAbilityAi { chance = cloneTgtAI(sa); } - return chance; + return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { Card host = sa.getHostCard(); boolean chance = true; @@ -111,7 +115,11 @@ public class CloneAi extends SpellAbilityAi { // Eventually, we can call the trigger of ETB abilities with // not mandatory as part of the checks to cast something - return chance || mandatory; + if (mandatory || chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java b/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java index 79f8c63880c..96d3b6cebf5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ConniveAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilMana; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -14,16 +11,16 @@ import forge.game.zone.ZoneType; public class ConniveAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { if (!ai.canDraw()) { - return false; // can't draw anything + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } Card host = sa.getHostCard(); final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa); if (num == 0) { - return false; // Won't do anything + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); @@ -41,7 +38,7 @@ public class ConniveAi extends SpellAbilityAi { sa.resetTargets(); while (sa.canAddMoreTarget()) { if ((list.isEmpty() && sa.isTargetNumberValid() && !sa.getTargets().isEmpty())) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (list.isEmpty()) { @@ -53,7 +50,7 @@ public class ConniveAi extends SpellAbilityAi { if (list.isEmpty()) { // Not mandatory, or the the list was regenerated and is still empty, // so return whether or not we found enough targets - return sa.isTargetNumberValid(); + return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } Card choice = ComputerUtilCard.getBestCreatureAI(list); @@ -66,13 +63,17 @@ public class ConniveAi extends SpellAbilityAi { list.clear(); } } - return !sa.getTargets().isEmpty() && sa.isTargetNumberValid(); + if (!sa.getTargets().isEmpty() && sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!ai.canDraw() && !mandatory) { - return false; // can't draw anything + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } boolean preferred = true; @@ -85,7 +86,7 @@ public class ConniveAi extends SpellAbilityAi { while (sa.canAddMoreTarget()) { if (mandatory) { if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (list.isEmpty() && preferred) { @@ -98,14 +99,13 @@ public class ConniveAi extends SpellAbilityAi { // Still an empty list, but we have to choose something (mandatory); expand targeting to // include AI's own cards to see if there's anything targetable (e.g. Plague Belcher). list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); - preferred = false; } } if (list.isEmpty()) { // Not mandatory, or the the list was regenerated and is still empty, // so return whether or not we found enough targets - return sa.isTargetNumberValid(); + return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi); } Card choice = ComputerUtilCard.getBestCreatureAI(list); @@ -118,7 +118,10 @@ public class ConniveAi extends SpellAbilityAi { list.clear(); } } - return true; + return new AiAbilityDecision( + sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0, + sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed + ); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java index 82ab4d67455..9fd40ca6a8a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java @@ -1,9 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Lists; -import forge.ai.ComputerUtilCard; -import forge.ai.SpecialCardAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -21,7 +19,7 @@ public class ControlExchangeAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, final SpellAbility sa) { Card object1 = null; Card object2 = null; final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -41,35 +39,48 @@ public class ControlExchangeAi extends SpellAbilityAi { sa.getTargets().add(object2); } if (object1 == null || object2 == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (ComputerUtilCard.evaluateCreature(object1) > ComputerUtilCard.evaluateCreature(object2) + 40) { sa.getTargets().add(object1); - return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + + if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) { + // if the AI has already activated this ability this turn, it is less likely to do so again + // this is to prevent the AI from trading away its best cards too often + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // if the AI has not activated this ability this turn, it is more likely to do so again + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { if (mandatory) { - return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid(); + AiAbilityDecision decision = chkAIDrawback(sa, aiPlayer); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return decision; } else { return canPlayAI(aiPlayer, sa); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { if (!sa.usesTargeting()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -90,7 +101,7 @@ public class ControlExchangeAi extends SpellAbilityAi { list = CardLists.getTargetableCards(list, sa); if (list.isEmpty()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); Card best = ComputerUtilCard.getBestAI(list); @@ -106,7 +117,7 @@ public class ControlExchangeAi extends SpellAbilityAi { // Defined card is better than this one, try to avoid trade if (!best.equals(realBest)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -115,10 +126,10 @@ public class ControlExchangeAi extends SpellAbilityAi { return doTrigTwoTargetsLogic(aiPlayer, sa, best); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) { + private AiAbilityDecision doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) { final TargetRestrictions tgt = sa.getTargetRestrictions(); final int creatureThreshold = 100; // TODO: make this configurable from the AI profile final int nonCreatureThreshold = 2; @@ -130,30 +141,30 @@ public class ControlExchangeAi extends SpellAbilityAi { list = CardLists.getTargetableCards(list, sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } Card aiWorst = ComputerUtilCard.getWorstAI(list); if (aiWorst == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (aiWorst != bestFirstTgt) { if (bestFirstTgt.isCreature() && aiWorst.isCreature()) { if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) { sa.getTargets().add(aiWorst); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { // TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) { sa.getTargets().add(aiWorst); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } sa.clearTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java index 175e10719c1..d9367376656 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java @@ -65,7 +65,7 @@ import java.util.Map; */ public class ControlGainAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, final SpellAbility sa) { final List lose = Lists.newArrayList(); if (sa.hasParam("LoseControl")) { @@ -81,22 +81,30 @@ public class ControlGainAi extends SpellAbilityAi { if (sa.hasParam("AllValid")) { CardCollectionView tgtCards = opponents.getCardsIn(ZoneType.Battlefield); tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa); - return !tgtCards.isEmpty(); + + if (tgtCards.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } + } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { sa.resetTargets(); if (sa.hasParam("TargetingPlayer")) { Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), 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); + } } if (tgt.canOnlyTgtOpponent()) { List oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa)); if (oppList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (tgt.isRandomTarget()) { @@ -111,12 +119,12 @@ public class ControlGainAi extends SpellAbilityAi { if (lose.contains("EOT") && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && !sa.isTrigger()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.hasParam("Defined")) { // no need to target, we'll pick up the target from Defined - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } CardCollection list = opponents.getCardsIn(ZoneType.Battlefield); @@ -165,7 +173,7 @@ public class ControlGainAi extends SpellAbilityAi { }); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } int creatures = 0, artifacts = 0, planeswalkers = 0, lands = 0, enchantments = 0; @@ -194,7 +202,7 @@ public class ControlGainAi extends SpellAbilityAi { if (list.isEmpty()) { if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // TODO is this good enough? for up to amounts? break; @@ -257,39 +265,41 @@ public class ControlGainAi extends SpellAbilityAi { } } - return true; + return new AiAbilityDecision( + sa.isTargetNumberValid() ? 100 : 0, + sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { - if (sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) { + if (sa.hasParam("TargetingPlayer") || (mandatory && !this.canPlayAI(ai, sa).willingToPlay())) { if (sa.getTargetRestrictions().canOnlyTgtOpponent()) { List oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); if (oppList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } sa.getTargets().add(Aggregates.random(oppList)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } List list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } sa.getTargets().add(ComputerUtilCard.getWorstAI(list)); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, final Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, final Player ai) { final Game game = ai.getGame(); // Special card logic that is processed elsewhere @@ -305,7 +315,7 @@ public class ControlGainAi extends SpellAbilityAi { CardCollectionView tgtCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield); tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa); if (tgtCards.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } final List lose = Lists.newArrayList(); @@ -314,8 +324,12 @@ public class ControlGainAi extends SpellAbilityAi { lose.addAll(Lists.newArrayList(sa.getParam("LoseControl").split(","))); } - return !lose.contains("EOT") - || !game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS); + if (lose.contains("EOT") + && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); + } else { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } else { return this.canPlayAI(ai, sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java index 7d6dc51bd1c..91dda4ee13c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainVariantAi.java @@ -18,6 +18,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -41,24 +43,23 @@ import java.util.Map; */ public class ControlGainVariantAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, final SpellAbility sa) { String logic = sa.getParam("AILogic"); if ("GainControlOwns".equals(logic)) { List list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), crd -> crd.isCreature() && !crd.getController().equals(crd.getOwner())); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } for (final Card c : list) { if (ai.equals(c.getController())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } } - return true; - + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java index ed43935f146..12ad2167acb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java @@ -22,13 +22,13 @@ import java.util.function.Predicate; public class CopyPermanentAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { Card source = sa.getHostCard(); PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); String aiLogic = sa.getParamOrDefault("AILogic", ""); if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } if ("MomirAvatar".equals(aiLogic)) { @@ -36,31 +36,55 @@ public class CopyPermanentAi extends SpellAbilityAi { } else if ("MimicVat".equals(aiLogic)) { return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa); } else if ("AtEOT".equals(aiLogic)) { - return ph.is(PhaseType.END_OF_TURN); + if (ph.is(PhaseType.END_OF_TURN)) { + if (ph.getPlayerTurn() == aiPlayer) { + // If it's the AI's turn, it can activate at EOT + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If it's not the AI's turn, it can't activate at EOT + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } else { + // Not at EOT phase + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); + } } else if ("AtOppEOT".equals(aiLogic)) { - return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer; + if (ph.is(PhaseType.END_OF_TURN)) { + if (ph.getPlayerTurn() != aiPlayer) { + // If it's not the AI's turn, it can activate at EOT + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // If it's the AI's turn, it can't activate at EOT + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); + } + } else { + // Not at EOT phase + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); + } } else if ("DuplicatePerms".equals(aiLogic)) { final List valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); if (valid.size() < 2) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } if (sa.hasParam("Defined")) { // If there needs to be an imprinted card, don't activate the ability if nothing was imprinted yet (e.g. Mimic Vat) if (sa.getParam("Defined").equals("Imprinted.ExiledWithSource") && source.getImprintedCards().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } } if (sa.isEmbalm() || sa.isEternalize()) { // E.g. Vizier of Many Faces: check to make sure it makes sense to make the token now - if (ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa) != AiPlayDecision.WillPlay) { - return false; + AiPlayDecision decision = ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa); + + if (decision != AiPlayDecision.WillPlay) { + return new AiAbilityDecision(0, decision); } } @@ -75,29 +99,37 @@ public class CopyPermanentAi extends SpellAbilityAi { sa.resetTargets(); Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); sa.setTargetingPlayer(targetingPlayer); - return targetingPlayer.getController().chooseTargetsFor(sa); + if (targetingPlayer.getController().chooseTargetsFor(sa)) { + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if (sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer()) { if (!sa.isCurse()) { if (sa.canTarget(aiPlayer)) { sa.getTargets().add(aiPlayer); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { for (Player p : aiPlayer.getYourTeam()) { if (sa.canTarget(p)) { sa.getTargets().add(p); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { for (Player p : aiPlayer.getOpponents()) { if (sa.canTarget(p)) { sa.getTargets().add(p); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { return doTriggerAINoCost(aiPlayer, sa, false); @@ -105,7 +137,7 @@ public class CopyPermanentAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) { final Card host = sa.getHostCard(); final Player activator = sa.getActivatingPlayer(); final Game game = host.getGame(); @@ -128,13 +160,13 @@ public class CopyPermanentAi extends SpellAbilityAi { //Nothing to target if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate()); if (betterList.isEmpty()) { if (!mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { list = betterList; @@ -146,7 +178,7 @@ public class CopyPermanentAi extends SpellAbilityAi { if (felidarGuardian.size() > 0) { // can copy a Felidar Guardian and combo off, so let's do it sa.getTargets().add(felidarGuardian.get(0)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -155,9 +187,9 @@ public class CopyPermanentAi extends SpellAbilityAi { list = CardLists.canSubsequentlyTarget(list, sa); if (list.isEmpty()) { - if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { + 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; @@ -177,9 +209,9 @@ public class CopyPermanentAi extends SpellAbilityAi { } if (choice == null) { // can't find anything left - if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { + 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; @@ -194,20 +226,22 @@ public class CopyPermanentAi extends SpellAbilityAi { choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host, sa); Collection betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory); if (betterChoices.isEmpty()) { - return mandatory; + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - } else { - // if no targeting, it should always be ok } if ("TriggeredCardController".equals(sa.getParam("Controller"))) { Card trigCard = (Card)sa.getTriggeringObject(AbilityKey.Card); if (!mandatory && trigCard != null && trigCard.getController().isOpponentOf(aiPlayer)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) diff --git a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java index 9ed64ee8db1..c7763c79d3e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java @@ -17,14 +17,15 @@ import java.util.Map; public class CopySpellAbilityAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { Game game = aiPlayer.getGame(); int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK); int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF); String logic = sa.getParamOrDefault("AILogic", ""); if (game.getStack().isEmpty()) { - return sa.isMandatory() || "Always".equals(logic); + boolean result = sa.isMandatory() || "Always".equals(logic); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final SpellAbility top = game.getStack().peekAbility(); @@ -44,12 +45,12 @@ public class CopySpellAbilityAi extends SpellAbilityAi { && !"AlwaysIfViable".equals(logic) && !"OnceIfViable".equals(logic) && !"AlwaysCopyActivatedAbilities".equals(logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("OnceIfViable".equals(logic)) { if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -57,31 +58,31 @@ public class CopySpellAbilityAi extends SpellAbilityAi { // Filter AI-specific targets if provided if ("OnlyOwned".equals(sa.getParam("AITgts"))) { if (!top.getActivatingPlayer().equals(aiPlayer)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (top.isWrapper() || top.isActivatedAbility()) { // Shouldn't even try with triggered or wrapped abilities at this time, will crash - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (top.getApi() == ApiType.CopySpellAbility) { // Don't try to copy a copy ability, too complex for the AI to handle - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (top.getApi() == ApiType.Mana) { // would lead to Stack Overflow by trying to play this again - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) { if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) { // If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but // it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) { // Mana spent is not copied, so these spells generally do nothing when copied. - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) { // Don't try to copy anything you can't understand how to handle - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle. @@ -100,31 +101,49 @@ public class CopySpellAbilityAi extends SpellAbilityAi { if (decision == AiPlayDecision.WillPlay) { sa.getTargets().add(top); AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } + return new AiAbilityDecision(0, decision); } } // the AI should not miss mandatory activations - return sa.isMandatory() || "Always".equals(logic); + boolean result = sa.isMandatory() || "Always".equals(logic); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { // the AI should not miss mandatory activations (e.g. Precursor Golem trigger) String logic = sa.getParamOrDefault("AILogic", ""); - return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable + + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (logic.contains("Always")) { + // If the logic is "Always" or "AlwaysIfViable", we will always play this ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { if ("ChainOfSmog".equals(sa.getParam("AILogic"))) { return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa); } else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) { return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa); - } - return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer)); + } + AiAbilityDecision decision = canPlayAI(aiPlayer, sa); + if (!decision.willingToPlay()) { + if (sa.isMandatory()) { + return super.chkAIDrawback(sa, aiPlayer); + } + } + return decision; } @Override @@ -138,7 +157,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi { // Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then // run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents. if ("ChainOfAcid".equals(sa.getParam("AILogic"))) { - return SpecialCardAi.ChainOfAcid.consider(player, sa); + return SpecialCardAi.ChainOfAcid.consider(player, sa).willingToPlay(); } return true; diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java index b89c5682e17..3bf569eca3f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java @@ -30,7 +30,7 @@ import forge.util.collect.FCollectionView; public class CounterAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { boolean toReturn = true; final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); @@ -40,22 +40,22 @@ public class CounterAi extends SpellAbilityAi { SpellAbility tgtSA = null; if (game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (abCost != null) { // AI currently disabled for these costs if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } if ("Force of Will".equals(sourceName)) { if (!SpecialCardAi.ForceOfWill.consider(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -63,19 +63,19 @@ public class CounterAi extends SpellAbilityAi { final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa); if ((topSA.isSpell() && !topSA.isCounterableBy(sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) { // might as well check for player's friendliness - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa); } // check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided if (sa.hasParam("AITgts") && (topSA.getHostCard() == null || !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (sa.hasParam("CounterNoManaSpell") && topSA.getTotalManaSpent() > 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) { @@ -84,7 +84,7 @@ public class CounterAi extends SpellAbilityAi { CostDiscard discardCost = unlessCost.getCostPartByType(CostDiscard.class); if ("Hand".equals(discardCost.getType())) { if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -100,10 +100,11 @@ public class CounterAi extends SpellAbilityAi { tgtCMC += topSA.getPayCosts().getTotalMana().countX() > 0 ? 3 : 0; // TODO: somehow determine the value of X paid and account for it? } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { - return false; + // This spell doesn't target. Must be a "Coutner All" or "Counter trigger" type of ability. + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null; @@ -122,13 +123,13 @@ public class CounterAi extends SpellAbilityAi { } if (toPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } if (toPay <= usableManaSources) { // If this is a reusable Resource, feel free to play it most of the time if (!playReusable(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } @@ -147,15 +148,15 @@ public class CounterAi extends SpellAbilityAi { if (sa.hasParam("AILogic")) { String logic = sa.getParam("AILogic"); if ("Never".equals(logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts int minCMC = Integer.parseInt(logic.substring(7)); if (tgtCMC < minCMC) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if ("NullBrooch".equals(logic)) { if (!SpecialCardAi.NullBrooch.consider(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -234,40 +235,40 @@ public class CounterAi extends SpellAbilityAi { } if (dontCounter) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return toReturn; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return doTriggerAINoCost(aiPlayer, sa, true); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Game game = ai.getGame(); if (sa.usesTargeting()) { if (game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } sa.resetTargets(); if (mandatory && !sa.canAddMoreTarget()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } Pair pair = chooseTargetSpellAbility(game, sa, ai, mandatory); SpellAbility tgtSA = pair.getLeft(); if (tgtSA == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } sa.getTargets().add(tgtSA); if (!mandatory && !pair.getRight()) { // If not mandatory and not preferred, bail out after setting target - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null; @@ -288,14 +289,14 @@ public class CounterAi extends SpellAbilityAi { if (!mandatory) { if (toPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); } if (toPay <= usableManaSources) { // If this is a reusable Resource, feel free to play it most // of the time if (!playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } } @@ -312,7 +313,7 @@ public class CounterAi extends SpellAbilityAi { // force the Human into making decisions) // But really it should be more picky about how it counters things - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } public Pair chooseTargetSpellAbility(Game game, SpellAbility sa, Player ai, boolean mandatory) { @@ -381,7 +382,7 @@ public class CounterAi extends SpellAbilityAi { } // no reason to pay if we don't plan to confirm - if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) { + if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false).willingToPlay()) { return false; } // TODO check hasFizzled diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java index e3b9a6ed7fc..ea7a896b62a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java @@ -45,13 +45,13 @@ public abstract class CountersAi extends SpellAbilityAi { *

* * @param list - * a {@link forge.CardList} object. + * a {@link CardCollectionView} object. * @param type - * a {@link java.lang.String} object. + * a {@link String} object. * @param amount * a int. - * @param newParam TODO - * @return a {@link forge.game.card.Card} object. + * @param ai a {@link Player} object. + * @return a {@link Card} object. */ public static Card chooseCursedTarget(final CardCollectionView list, final String type, final int amount, final Player ai) { Card choice; @@ -65,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi { // try to kill the best killable creature, or reduce the best one // but try not to target a Undying Creature final List killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING); - if (killable.size() > 0) { + if (!killable.isEmpty()) { choice = ComputerUtilCard.getBestCreatureAI(killable); } else { choice = ComputerUtilCard.getBestCreatureAI(list); @@ -83,10 +83,10 @@ public abstract class CountersAi extends SpellAbilityAi { *

* * @param list - * a {@link forge.CardList} object. + * a {@link CardCollectionView} object. * @param type - * a {@link java.lang.String} object. - * @return a {@link forge.game.card.Card} object. + * a {@link String} object. + * @return a {@link Card} object. */ public static Card chooseBoonTarget(final CardCollectionView list, final String type) { Card choice = null; 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 e32c32ab106..a5acefb25b0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java @@ -1,9 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Iterables; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.*; @@ -24,7 +22,8 @@ public class CountersMoveAi extends SpellAbilityAi { protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { if (sa.usesTargeting()) { sa.resetTargets(); - if (!moveTgtAI(ai, sa)) { + AiAbilityDecision decision = moveTgtAI(ai, sa); + if (!decision.willingToPlay()) { return false; } } @@ -109,12 +108,13 @@ public class CountersMoveAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { sa.resetTargets(); - if (!moveTgtAI(ai, sa) && !mandatory) { - return false; + AiAbilityDecision decision = moveTgtAI(ai, sa); + if (!decision.willingToPlay() && !mandatory) { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (!sa.isTargetNumberValid() && mandatory) { @@ -122,18 +122,18 @@ public class CountersMoveAi extends SpellAbilityAi { List tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); if (tgtCards.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } final Card card = ComputerUtilCard.getWorstAI(tgtCards); sa.getTargets().add(card); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { // no target Probably something like Graft if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } final Card host = sa.getHostCard(); @@ -145,7 +145,7 @@ public class CountersMoveAi extends SpellAbilityAi { final List destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa); if (srcCards.isEmpty() || destCards.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } final Card src = srcCards.get(0); @@ -153,21 +153,21 @@ public class CountersMoveAi extends SpellAbilityAi { // for such Trigger, do not move counter to another players creature if (!dest.getController().equals(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (ComputerUtilCard.isUselessCreature(ai, dest)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (dest.hasSVar("EndOfTurnLeavePlay")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (cType != null) { if (!dest.canReceiveCounters(cType)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final int amount = calcAmount(sa, cType); int a = src.getCounters(cType); if (a < amount) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final Card srcCopy = CardCopyService.getLKICopy(src); @@ -181,27 +181,31 @@ public class CountersMoveAi extends SpellAbilityAi { int newEval = ComputerUtilCard.evaluateCreature(srcCopy) + ComputerUtilCard.evaluateCreature(destCopy); if (newEval < oldEval) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } // check for some specific AI preferences if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) { - return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0; + if (!cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } // no target - return true; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if (sa.usesTargeting()) { sa.resetTargets(); return moveTgtAI(ai, sa); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private static int calcAmount(final SpellAbility sa, final CounterType cType) { @@ -226,7 +230,7 @@ public class CountersMoveAi extends SpellAbilityAi { return amount; } - private boolean moveTgtAI(final Player ai, final SpellAbility sa) { + private AiAbilityDecision moveTgtAI(final Player ai, final SpellAbility sa) { final Card host = sa.getHostCard(); final Game game = ai.getGame(); final String type = sa.getParam("CounterType"); @@ -244,7 +248,7 @@ public class CountersMoveAi extends SpellAbilityAi { if (destCards.isEmpty()) { // something went wrong - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } final Card dest = destCards.get(0); @@ -253,7 +257,7 @@ public class CountersMoveAi extends SpellAbilityAi { tgtCards.remove(dest); if (cType != null && !dest.canReceiveCounters(cType)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // prefered logic for this: try to steal counter @@ -285,7 +289,7 @@ public class CountersMoveAi extends SpellAbilityAi { if (card != null) { sa.getTargets().add(card); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -329,14 +333,14 @@ public class CountersMoveAi extends SpellAbilityAi { if (card != null) { sa.getTargets().add(card); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else if (sa.getMaxTargets() == 2) { // TODO - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { // SA uses target for Defined // Source => Targeted @@ -344,12 +348,12 @@ public class CountersMoveAi extends SpellAbilityAi { if (srcCards.isEmpty()) { // something went wrong - return false; + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); } final Card src = srcCards.get(0); if (cType != null && src.getCounters(cType) <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } Card lkiWithCounters = CardCopyService.getLKICopy(src); @@ -402,14 +406,14 @@ public class CountersMoveAi extends SpellAbilityAi { if (card != null) { sa.getTargets().add(card); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger()) || (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger()); if (!isMandatoryTrigger) { // no good target - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -439,10 +443,10 @@ public class CountersMoveAi extends SpellAbilityAi { if (card != null) { sa.getTargets().add(card); - 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/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java index 6fbcf7bc5c4..259a18271d8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java @@ -1,9 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Lists; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.*; @@ -85,24 +83,25 @@ public class CountersMultiplyAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!sa.usesTargeting()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (setTargets(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (mandatory) { CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } Card safeMatch = list.stream() .filter(CardPredicates.hasCounters().negate()) .findFirst().orElse(null); sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return mandatory; + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private CounterType getCounterType(SpellAbility sa) { 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 8a4dc0ab193..f806b820dad 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java @@ -72,24 +72,31 @@ public class CountersProliferateAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { boolean chance = true; // TODO Make sure Human has poison counters or there are some counters // we want to proliferate - return chance; + return new AiAbilityDecision( + chance ? 100 : 0, + chance ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi + ); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if ("Always".equals(sa.getParam("AILogic"))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return checkApiLogic(ai, sa); + if (checkApiLogic(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* 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 176f5ccfc47..7d87be6e651 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -53,8 +53,7 @@ public class CountersPutAi extends CountersAi { // disable moving counters (unless a specialized AI logic supports it) for (final CostPart part : cost.getCostParts()) { - if (part instanceof CostRemoveCounter) { - final CostRemoveCounter remCounter = (CostRemoveCounter) part; + if (part instanceof CostRemoveCounter remCounter) { final CounterType counterType = remCounter.counter; if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) { return false; @@ -666,7 +665,7 @@ public class CountersPutAi extends CountersAi { } @Override - public boolean chkAIDrawback(final SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(final SpellAbility sa, Player ai) { boolean chance = true; final Game game = ai.getGame(); Card choice = null; @@ -701,9 +700,9 @@ public class CountersPutAi extends CountersAi { while (sa.canAddMoreTarget()) { if (list.isEmpty()) { if (!sa.isTargetNumberValid() - || sa.getTargets().size() == 0) { + || sa.getTargets().isEmpty()) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { break; } @@ -724,9 +723,9 @@ public class CountersPutAi extends CountersAi { } if (choice == null) { // can't find anything left - if ((!sa.isTargetNumberValid()) || (sa.getTargets().size() == 0)) { + 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; @@ -741,11 +740,14 @@ public class CountersPutAi extends CountersAi { } } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); final Card source = sa.getHostCard(); final String aiLogic = sa.getParamOrDefault("AILogic", ""); @@ -770,9 +772,20 @@ public class CountersPutAi extends CountersAi { } if ("ChargeToBestCMC".equals(aiLogic)) { - return doChargeToCMCLogic(ai, sa) || mandatory; + if (doChargeToCMCLogic(ai, sa) || mandatory) { + // 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); + } 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)) { - return doChargeToOppCtrlCMCLogic(ai, sa) || mandatory; + if (doChargeToOppCtrlCMCLogic(ai, sa) || mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (!sa.usesTargeting()) { @@ -801,7 +814,7 @@ public class CountersPutAi extends CountersAi { sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class)); if (playerList.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // try to choose player with less creatures @@ -818,7 +831,7 @@ public class CountersPutAi extends CountersAi { nPump = amount; } if (FightAi.canFightAi(ai, sa, nPump, nPump)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -839,7 +852,7 @@ public class CountersPutAi extends CountersAi { if (mandatory) { // When things are mandatory, gotta handle a little differently if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) { - return true; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (list.isEmpty() && preferred) { @@ -859,7 +872,9 @@ public class CountersPutAi extends CountersAi { if (list.isEmpty()) { // Not mandatory, or the the list was regenerated and is still empty, // so return whether or not we found enough targets - return sa.isTargetNumberValid(); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } Card choice = null; @@ -912,7 +927,7 @@ public class CountersPutAi extends CountersAi { } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java index cd04e931f90..ef58eeefb10 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAllAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; import com.google.common.collect.Lists; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -22,7 +24,7 @@ import java.util.Map; public class CountersPutAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, 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(); @@ -47,25 +49,25 @@ public class CountersPutAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for these costs if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } if (logic.equals("AtEOTOrBlock")) { if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } } else if (logic.equals("AtOppEOT")) { if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } } @@ -94,20 +96,20 @@ public class CountersPutAllAi extends SpellAbilityAi { if (curse) { if (type.equals("M1M1")) { final List killable = CardLists.filter(hList, c -> c.getNetToughness() <= amount); - if (!(killable.size() > 2)) { - return false; + if (killable.size() <= 2) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { // make sure compy doesn't harm his stuff more than human's // stuff if (cList.size() > hList.size()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else { // human has more things that will benefit, don't play if (hList.size() >= cList.size()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } //Check for cards that could profit from the ability @@ -125,20 +127,33 @@ public class CountersPutAllAi extends SpellAbilityAi { } } if (!combatants) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } - if (playReusable(ai, sa)) { - return chance; + if (!chance) { + // if the AI has already activated this ability this turn, it is less likely to do so again + // this is to prevent the AI from trading away its best cards too often + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } - return ((MyRandom.getRandom().nextFloat() < .6667) && chance); + if (playReusable(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (MyRandom.getRandom().nextFloat() < .6667) { + // if the AI has not activated this ability this turn, it is more likely to do so again + // this is to prevent the AI from trading away its best cards too often + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // if the AI has not activated this ability this turn, it is less likely to do so again + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return canPlayAI(ai, sa); } /* (non-Javadoc) @@ -150,7 +165,7 @@ public class CountersPutAllAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { if (sa.usesTargeting()) { List players = Lists.newArrayList(); if (!sa.isCurse()) { @@ -168,11 +183,23 @@ public class CountersPutAllAi extends SpellAbilityAi { preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer); sa.resetTargets(); sa.getTargets().add(p); - return preferred || mandatory; + if (preferred) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } } - return mandatory || canPlayAI(aiPlayer, sa); + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return canPlayAI(aiPlayer, sa); } } 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 6867c696d48..8aa688e47c4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java @@ -18,9 +18,7 @@ package forge.ai.ability; import com.google.common.collect.Iterables; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.*; @@ -180,11 +178,27 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { - return doTgt(ai, sa, mandatory); + if (doTgt(ai, sa, mandatory)) { + // if we can target, then we can play it + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } + } else { + // if we can't target, then we can't play it + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } + if (mandatory) { + // if mandatory, just play it + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // if not mandatory, check if we can play it + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return mandatory; } /* 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 7501a0424a8..1aa7f5a70c6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; @@ -24,9 +26,9 @@ import java.util.function.Predicate; public class CountersRemoveAi extends SpellAbilityAi { @Override - protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayWithoutRestrict(final Player ai, final SpellAbility sa) { if ("Always".equals(sa.getParam("AILogic"))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } return super.canPlayWithoutRestrict(ai, sa); } @@ -351,11 +353,14 @@ public class CountersRemoveAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { - return doTgt(aiPlayer, sa, mandatory); + boolean canTarget = doTgt(aiPlayer, sa, mandatory); + return canTarget ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return mandatory; + return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) + : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java index f505555979f..4ae50c61851 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java @@ -17,26 +17,26 @@ import java.util.function.Predicate; public class DamageAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { // AI needs to be expanded, since this function can be pretty complex // based on what the expected targets could be final Card source = sa.getHostCard(); // prevent run-away activations - first time will always return true if (MyRandom.getRandom().nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } // abCost stuff that should probably be centralized... final Cost abCost = sa.getPayCosts(); if (abCost != null) { // AI currently disabled for some costs if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } // wait until stack is empty (prevents duplicate kills) if (!ai.getGame().getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty); } int x = -1; @@ -51,11 +51,15 @@ public class DamageAllAi extends SpellAbilityAi { if (x == -1) { if (determineOppToKill(ai, sa, source, dmg) != null) { // we already know we can kill a player, so go for it - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // look for other value in this (damaging creatures or // creatures + player, e.g. Pestilence, etc.) - return evaluateDamageAll(ai, sa, source, dmg) > 0; + if (evaluateDamageAll(ai, sa, source, dmg) > 0) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { int best = -1, best_x = -1; Player bestOpp = determineOppToKill(ai, sa, source, x); @@ -81,9 +85,9 @@ public class DamageAllAi extends SpellAbilityAi { if (sa.getSVar(damage).equals("Count$xPaid")) { sa.setXManaCostPaid(best_x); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -185,7 +189,7 @@ public class DamageAllAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { final Card source = sa.getHostCard(); final String validP = sa.getParamOrDefault("ValidPlayers", ""); @@ -211,21 +215,21 @@ public class DamageAllAi extends SpellAbilityAi { } // Don't get yourself killed if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // if we can kill human, do it if ((validP.equals("Player") || validP.equals("Opponent") || validP.contains("Targeted")) && (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) > ComputerUtilCard .evaluateCreatureList(humanList)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -258,7 +262,7 @@ public class DamageAllAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); final String validP = sa.getParamOrDefault("ValidPlayers", ""); @@ -287,24 +291,24 @@ public class DamageAllAi extends SpellAbilityAi { // If it's not mandatory check a few things if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Don't get yourself killed if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // if we can kill human, do it if ((validP.equals("Player") || validP.contains("Opponent") || validP.contains("Targeted")) && (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) + 50 >= ComputerUtilCard .evaluateCreatureList(humanList)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index 0ef629a96a9..1e804d34ab8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -38,7 +38,7 @@ import java.util.Map; public class DamageDealAi extends DamageAiBase { @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { final SpellAbility root = sa.getRootAbility(); final String damage = sa.getParam("NumDmg"); Card source = sa.getHostCard(); @@ -65,15 +65,19 @@ public class DamageDealAi extends DamageAiBase { continue; // in case the calculation gets messed up somewhere } root.setSVar("EnergyToPay", "Number$" + dmg); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.getSVar(damage).equals("Count$xPaid")) { // Life Drain if ("XLifeDrain".equals(logic)) { - return doXLifeDrainLogic(ai, sa); + if (doXLifeDrainLogic(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } // Set PayX here to maximum value. @@ -83,11 +87,15 @@ public class DamageDealAi extends DamageAiBase { dmg--; // the card will be spent casting the spell, so actual damage is 1 less } } - return damageTargetAI(ai, sa, dmg, true); + if (damageTargetAI(ai, sa, dmg, true)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Cost abCost = sa.getPayCosts(); final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); @@ -108,7 +116,7 @@ public class DamageDealAi extends DamageAiBase { boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0); boolean isLethal = sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife(); if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -134,10 +142,10 @@ public class DamageDealAi extends DamageAiBase { if (shouldTgtP(ai, sa, maxDmg, false)) { sa.resetTargets(); sa.getTargets().add(maxDamaged); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } } @@ -154,7 +162,7 @@ public class DamageDealAi extends DamageAiBase { if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { for (Card potentialAtkr : ai.getCreaturesInPlay()) { if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -175,16 +183,24 @@ public class DamageDealAi extends DamageAiBase { * Mostly used to ping the player with remaining counters. The issue with * stacked effects might appear here. */ - return damageTargetAI(ai, sa, n, true); + if (damageTargetAI(ai, sa, n, true)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } else { /* * Only ping when stack is clear to avoid hassle of evaluating stacked effects * like protection/pumps or over-killing target. */ - return ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false); + if (ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty); + } } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if ("NinThePainArtist".equals(logic)) { // Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard @@ -193,11 +209,15 @@ public class DamageDealAi extends DamageAiBase { if (doTarget) { Card tgt = sa.getTargetCard(); if (tgt != null) { - return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController(); + if (ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); + } } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); } if (sourceName.equals("Sorin, Grim Nemesis")) { @@ -209,35 +229,35 @@ public class DamageDealAi extends DamageAiBase { continue; // in case the calculation gets messed up somewhere } sa.setXManaCostPaid(dmg); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (dmg <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // temporarily disabled until better AI if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Try to chain damage/debuff effects @@ -248,13 +268,13 @@ public class DamageDealAi extends DamageAiBase { int extraDmg = chainDmg.getValue(); boolean willTargetIfChained = damageTargetAI(ai, sa, dmg + extraDmg, false); if (!willTargetIfChained) { - return false; // won't play it even in chain + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); // won't play it even in chain } else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) { // we're trying to chain a pump spell to a damage spell targeting a player, that won't work // so run an additional check to ensure that we want to cast the current spell separately sa.resetTargets(); if (!damageTargetAI(ai, sa, dmg, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } else { // we are about to decide to play this damage spell; if there's something chained to it, reserve mana for @@ -264,7 +284,7 @@ public class DamageDealAi extends DamageAiBase { } } else if (!damageTargetAI(ai, sa, dmg, false)) { // simple targeting when there is no spell chaining plan - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) || @@ -288,10 +308,12 @@ public class DamageDealAi extends DamageAiBase { if ("DiscardCMCX".equals(sa.getParam("AILogic"))) { final int cmc = sa.getXManaCostPaid(); - return ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc)); + if (!ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc))) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** @@ -932,14 +954,14 @@ public class DamageDealAi extends DamageAiBase { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); final String damage = sa.getParam("NumDmg"); int dmg = calculateDamageAmount(sa, source, damage); // Remove all damage if (sa.hasParam("Remove")) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) { @@ -950,10 +972,18 @@ public class DamageDealAi extends DamageAiBase { if (!sa.usesTargeting()) { // If it's not mandatory check a few things - return mandatory || damageChooseNontargeted(ai, sa, dmg); + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (damageChooseNontargeted(ai, sa, dmg)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { if (!damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) { @@ -976,7 +1006,7 @@ public class DamageDealAi extends DamageAiBase { } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private static int calculateDamageAmount(SpellAbility sa, Card source, String damage) { diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java index 5140062169f..8fa5446216d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpecialCardAi; import forge.game.ability.AbilityUtils; import forge.game.player.Player; @@ -14,7 +16,7 @@ public class DamageEachAi extends DamageAiBase { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final String logic = sa.getParam("AILogic"); PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); @@ -22,30 +24,41 @@ public class DamageEachAi extends DamageAiBase { if (sa.usesTargeting() && weakestOpp != null) { if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); - sa.getTargets().add(weakestOpp); - return weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife(); + if (weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife()) { + sa.getTargets().add(weakestOpp); + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } final String damage = sa.getParam("NumDmg"); final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa); - return shouldTgtP(ai, sa, iDmg, false); + + if (shouldTgtP(ai, sa, iDmg, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // check AI life before playing this drawback? - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(ai, sa); + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java index 106af35c80d..7f2697882ca 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.GameObject; import forge.game.ability.AbilityUtils; @@ -24,7 +21,7 @@ import java.util.List; public class DamagePreventAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card hostCard = sa.getHostCard(); final Game game = ai.getGame(); final Combat combat = game.getCombat(); @@ -33,7 +30,7 @@ public class DamagePreventAi extends SpellAbilityAi { final Cost cost = sa.getPayCosts(); if (!willPayCosts(ai, sa, cost, hostCard)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -70,7 +67,7 @@ public class DamagePreventAi extends SpellAbilityAi { chance = flag; } else { // if nothing on the stack, and it's not declare // blockers. no need to prevent - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } // non-targeted @@ -120,7 +117,7 @@ public class DamagePreventAi extends SpellAbilityAi { targetables = CardLists.getTargetableCards(targetables, sa); if (targetables.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES); ComputerUtilCard.sortByEvaluateCreature(combatants); @@ -137,11 +134,15 @@ public class DamagePreventAi extends SpellAbilityAi { sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(hostCard, sa.getParam("Amount"), sa)); } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { boolean chance = false; final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt == null) { @@ -151,7 +152,11 @@ public class DamagePreventAi extends SpellAbilityAi { chance = preventDamageMandatoryTarget(ai, sa, mandatory); } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java b/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java index b12e262580f..267e2088e2d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DayTimeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -11,24 +13,34 @@ import java.util.Map; public class DayTimeAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); if ((sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) || sa.getPayCosts().hasManaCost()) { // If it involves a cost that may put us at a disadvantage, better activate before own turn if possible if (!isSorcerySpeed(sa, aiPlayer)) { - return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer; + if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); + } } else { - return ph.is(PhaseType.MAIN2, aiPlayer); // Give other things a chance to be cast (e.g. Celestus) + if (ph.is(PhaseType.MAIN2, aiPlayer)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return true; // TODO: more logic if it's ever a bad idea to trigger this (when non-mandatory) + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java index ac7939f30e6..cd8b74f16b8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DebuffAi.java @@ -1,10 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Lists; -import forge.ai.AiAttackController; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -26,27 +23,27 @@ import java.util.List; public class DebuffAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, final SpellAbility sa) { // if there is no target and host card isn't in play, don't activate final Card source = sa.getHostCard(); final Game game = ai.getGame(); if (!sa.usesTargeting() && !source.isInPlay()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final Cost cost = sa.getPayCosts(); // temporarily disabled until AI is improved if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } final PhaseHandler ph = game.getPhaseHandler(); @@ -58,7 +55,7 @@ public class DebuffAi extends SpellAbilityAi { // Instant-speed pumps should not be cast outside of combat when the // stack is empty, unless there are specific activation phase requirements if (!isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.AnotherTime); } } @@ -66,7 +63,7 @@ public class DebuffAi extends SpellAbilityAi { List cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); final Combat combat = game.getCombat(); - return cards.stream().anyMatch(c -> { + if (cards.stream().anyMatch(c -> { if (c.getController().equals(sa.getActivatingPlayer()) || combat == null) return false; @@ -75,21 +72,34 @@ public class DebuffAi extends SpellAbilityAi { } // don't add duplicate negative keywords return sa.hasParam("Keywords") && c.hasAnyKeyword(Arrays.asList(sa.getParam("Keywords").split(" & "))); - }); + })) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { - return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false); + if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if (!sa.usesTargeting()) { // TODO - copied from AF_Pump.pumpDrawbackAI() - what should be here? } else { - return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false); + if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } // debuffDrawbackAI() /** @@ -234,18 +244,24 @@ public class DebuffAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final List kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<>(); if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } else { - return debuffTgtAI(ai, sa, kws, mandatory); + if (debuffTgtAI(ai, sa, kws, mandatory)) { + 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/DelayedTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java index 59234bfa3e2..0ae14fde337 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DelayedTriggerAi.java @@ -15,43 +15,56 @@ import forge.game.zone.ZoneType; public class DelayedTriggerAi extends SpellAbilityAi { @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if ("Always".equals(sa.getParam("AILogic"))) { - // TODO: improve ai - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } SpellAbility trigsa = sa.getAdditionalAbility("Execute"); if (trigsa == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } trigsa.setActivatingPlayer(ai); if (trigsa instanceof AbilitySub) { return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); } else { - return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + if (decision == AiPlayDecision.WillPlay) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { SpellAbility trigsa = sa.getAdditionalAbility("Execute"); if (trigsa == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); trigsa.setActivatingPlayer(ai); if (!sa.hasParam("OptionalDecider")) { - return aic.doTrigger(trigsa, true); + if (aic.doTrigger(trigsa, true)) { + // If the trigger is mandatory, we can play it + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { - return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You")); + if (aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { // Card-specific logic String logic = sa.getParamOrDefault("AILogic", ""); if (logic.equals("SpellCopy")) { @@ -90,9 +103,9 @@ public class DelayedTriggerAi extends SpellAbilityAi { }); if (count == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (logic.equals("NarsetRebound")) { // should be done in Main2, but it might broke for other cards //if (phase.getPhase().isBefore(PhaseType.MAIN2)) { @@ -125,10 +138,10 @@ public class DelayedTriggerAi extends SpellAbilityAi { }); if (count == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (logic.equals("SaveCreature")) { CardCollection ownCreatures = ai.getCreaturesInPlay(); @@ -142,19 +155,25 @@ public class DelayedTriggerAi extends SpellAbilityAi { if (!ownCreatures.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestAI(ownCreatures)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } // Generic logic SpellAbility trigsa = sa.getAdditionalAbility("Execute"); if (trigsa == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } trigsa.setActivatingPlayer(ai); - return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + + AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + if (decision == AiPlayDecision.WillPlay) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } 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 f11bf1dbde9..8fb043c1705 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -20,8 +20,12 @@ import forge.util.collect.FCollectionView; public class DestroyAi extends SpellAbilityAi { @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { - return checkApiLogic(ai, sa); + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { + if (checkApiLogic(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override @@ -313,7 +317,7 @@ public class DestroyAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final boolean noRegen = sa.hasParam("NoRegen"); if (sa.usesTargeting()) { sa.resetTargets(); @@ -321,7 +325,7 @@ public class DestroyAi extends SpellAbilityAi { CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty() || list.size() < sa.getMinTargets()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Try to avoid targeting creatures that are dead on board @@ -349,7 +353,7 @@ public class DestroyAi extends SpellAbilityAi { list.removeAll(preferred); if (preferred.isEmpty() && !mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } while (sa.canAddMoreTarget()) { @@ -357,12 +361,12 @@ public class DestroyAi extends SpellAbilityAi { if (!sa.isMinTargetChosen()) { if (!mandatory) { sa.resetTargets(); - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } else { break; } } else { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { Card c = ComputerUtilCard.getBestAI(preferred); @@ -397,9 +401,18 @@ public class DestroyAi extends SpellAbilityAi { } } - return sa.isTargetNumberValid(); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + sa.resetTargets(); + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } else { - return mandatory; + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java index 78cdfcdb64b..2f82c6e34e6 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java @@ -23,21 +23,21 @@ public class DestroyAllAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } return doMassRemovalLogic(ai, sa); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return doMassRemovalLogic(aiPlayer, sa); } @Override - protected boolean canPlayAI(final Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, 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(); @@ -46,13 +46,13 @@ public class DestroyAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for some costs if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } // prevent run-away activations - first time will always return true if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } final String aiLogic = sa.getParamOrDefault("AILogic", ""); @@ -64,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi { return doMassRemovalLogic(ai, sa); } - public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) { + public static AiAbilityDecision doMassRemovalLogic(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String logic = sa.getParamOrDefault("AILogic", ""); @@ -72,7 +72,7 @@ public class DestroyAllAi extends SpellAbilityAi { final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1); if (logic.equals("Always")) { - return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } String valid = sa.getParamOrDefault("ValidCards", ""); @@ -92,7 +92,7 @@ public class DestroyAllAi extends SpellAbilityAi { opplist = CardLists.filter(opplist, predicate); ailist = CardLists.filter(ailist, predicate); if (opplist.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.usesTargeting()) { @@ -101,7 +101,7 @@ public class DestroyAllAi extends SpellAbilityAi { sa.getTargets().add(opponent); ailist.clear(); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -110,30 +110,35 @@ public class DestroyAllAi extends SpellAbilityAi { int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.size()); int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.size()); - return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave); + if (numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else if (numAiCanSave < ailist.size() && (opplist.size() - numOppsCanSave < ailist.size() - numAiCanSave)) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } // If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what! if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) && (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat())) && ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // if only creatures are affected evaluate both lists and pass only if human creatures are more valuable if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) { if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); } // test whether the human can kill the ai next turn @@ -146,39 +151,42 @@ public class DestroyAllAi extends SpellAbilityAi { } } if (!containsAttacker) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } AiBlockController block = new AiBlockController(ai, false); block.assignBlockersForCombat(combat); if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // only lands involved else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) { if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) { - return true; + // TODO Should care about any land recursion, not just Crucible of Worlds + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } // evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes CardCollection aiCreatures = ai.getCreaturesInPlay(); CardCollection oppCreatures = opponent.getCreaturesInPlay(); if (!oppCreatures.isEmpty()) { if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // check if the AI would lose more lands than the opponent would if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - 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/DigAi.java b/forge-ai/src/main/java/forge/ai/ability/DigAi.java index 930ea118cca..73faf8fb2e0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigAi.java @@ -26,20 +26,20 @@ public class DigAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Card host = sa.getHostCard(); Player libraryOwner = ai; if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.usesTargeting()) { sa.resetTargets(); if (!sa.canTarget(opp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(opp); libraryOwner = opp; @@ -47,14 +47,14 @@ public class DigAi extends SpellAbilityAi { // return false if nothing to dig into if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Never".equals(sa.getParam("AILogic"))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("AtOppEOT".equals(sa.getParam("AILogic"))) { if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -62,14 +62,14 @@ public class DigAi extends SpellAbilityAi { if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) { int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa); if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // Don't use draw abilities before main 2 if possible if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") && !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final String num = sa.getParam("DigNum"); @@ -87,14 +87,14 @@ public class DigAi extends SpellAbilityAi { int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()) - manaToSave; if (numCards <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } root.setXManaCostPaid(numCards); } } if (playReusable(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if ((!game.getPhaseHandler().getNextTurn().equals(ai) @@ -102,24 +102,28 @@ public class DigAi extends SpellAbilityAi { && !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai) && (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW)) && !ComputerUtil.activateForCost(sa, ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("MadSarkhanDigDmg".equals(sa.getParam("AILogic"))) { return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa); } - - return !ComputerUtil.preventRunAwayActivations(sa); + + if (ComputerUtil.preventRunAwayActivations(sa)) { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } else { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // TODO: improve this check in ways that may be specific to a subability return canPlayAI(aiPlayer, sa); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); Player opp = targetableOpps.min(PlayerPredicates.compareByLife()); @@ -137,12 +141,16 @@ public class DigAi extends SpellAbilityAi { int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]); int numCards = ComputerUtilCost.getMaxXValue(sa, ai, true) - manaToSave; if (numCards <= 0) { - return mandatory; + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(100, AiPlayDecision.CantPlayAi); + } } root.setXManaCostPaid(numCards); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java index 08ce3d26562..8c7853cc756 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigMultipleAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.AiAttackController; -import forge.ai.ComputerUtil; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -20,7 +18,7 @@ public class DigMultipleAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); final Card host = sa.getHostCard(); @@ -29,7 +27,7 @@ public class DigMultipleAi extends SpellAbilityAi { if (sa.usesTargeting()) { sa.resetTargets(); if (!opp.canBeTargetedBy(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(opp); libraryOwner = opp; @@ -37,14 +35,14 @@ public class DigMultipleAi extends SpellAbilityAi { // return false if nothing to dig into if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Never".equals(sa.getParam("AILogic"))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("AtOppEOT".equals(sa.getParam("AILogic"))) { if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -52,18 +50,18 @@ public class DigMultipleAi extends SpellAbilityAi { if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) { int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa); if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // Don't use draw abilities before main 2 if possible if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") && !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (playReusable(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if ((!game.getPhaseHandler().getNextTurn().equals(ai) @@ -71,14 +69,18 @@ public class DigMultipleAi extends SpellAbilityAi { && !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai) && (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW)) && !ComputerUtil.activateForCost(sa, ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return !ComputerUtil.preventRunAwayActivations(sa); + if (ComputerUtil.preventRunAwayActivations(sa)) { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } else { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); @@ -89,7 +91,7 @@ public class DigMultipleAi extends SpellAbilityAi { } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) diff --git a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java index 4e1cbf3a454..6fbe3be6c5d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DigUntilAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.AiAttackController; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.card.CardPredicates; @@ -19,7 +17,7 @@ import java.util.Map; public class DigUntilAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { Card source = sa.getHostCard(); final String logic = sa.getParamOrDefault("AILogic", ""); double chance = .4; // 40 percent chance with instant speed stuff @@ -42,7 +40,7 @@ public class DigUntilAi extends SpellAbilityAi { // material in the library after using it several times. // TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard? if (ai.getCardsIn(ZoneType.Library).size() < 20) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Land.Basic".equals(sa.getParam("Valid")) && ai.getZone(ZoneType.Hand).contains(CardPredicates.LANDS_PRODUCING_MANA)) { @@ -52,7 +50,7 @@ public class DigUntilAi extends SpellAbilityAi { // This is important for Replenish/Living Death type decks if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().isPlayerTurn(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -60,7 +58,7 @@ public class DigUntilAi extends SpellAbilityAi { if (sa.usesTargeting()) { sa.resetTargets(); if (!sa.canTarget(opp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(opp); libraryOwner = opp; @@ -68,7 +66,7 @@ public class DigUntilAi extends SpellAbilityAi { if (sa.hasParam("Valid")) { final String valid = sa.getParam("Valid"); if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid, source.getController(), source, sa).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -80,7 +78,7 @@ public class DigUntilAi extends SpellAbilityAi { if (root.getXManaCostPaid() == null) { int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (numCards <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } root.setXManaCostPaid(numCards); } @@ -88,15 +86,20 @@ public class DigUntilAi extends SpellAbilityAi { // return false if nothing to dig into if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); - return randomReturn; + + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { sa.resetTargets(); if (sa.isCurse()) { @@ -116,7 +119,7 @@ public class DigUntilAi extends SpellAbilityAi { } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java index d9874f2443b..9774ed6c29d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java @@ -26,7 +26,7 @@ import forge.util.collect.FCollectionView; public class DiscardAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final Cost abCost = sa.getPayCosts(); @@ -34,23 +34,30 @@ public class DiscardAi extends SpellAbilityAi { // temporarily disabled until better AI if (!willPayCosts(ai, sa, abCost, source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Chandra, Flamecaller".equals(sourceName)) { final int hand = ai.getCardsIn(ZoneType.Hand).size(); - return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand)); + + + + if (MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (aiLogic.equals("VolrathsShapeshifter")) { return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa); } - final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0; + final boolean humanHasHand = !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty(); if (sa.usesTargeting()) { if (!discardTargetAI(ai, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { // TODO: Add appropriate restrictions @@ -64,7 +71,7 @@ public class DiscardAi extends SpellAbilityAi { } else { // defined to the human, so that's fine as long the human has cards if (!humanHasHand) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else { @@ -78,12 +85,12 @@ public class DiscardAi extends SpellAbilityAi { final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()), ai.getWeakestOpponent() .getCardsIn(ZoneType.Hand).size()); if (cardsToDiscard < 1) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.setXManaCostPaid(cardsToDiscard); } else { if (AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa) < 1) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -113,7 +120,7 @@ public class DiscardAi extends SpellAbilityAi { } } if (numDiscard == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -121,27 +128,31 @@ public class DiscardAi extends SpellAbilityAi { // Don't use discard abilities before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (aiLogic.equals("AnyPhaseIfFavored")) { if (ai.getGame().getCombat() != null) { if (ai.getCardsIn(ZoneType.Hand).size() < ai.getGame().getCombat().getDefenderPlayerByAttacker(source).getCardsIn(ZoneType.Hand).size()) { - return false; + 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.CantPlayAi); } boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn()); // some other variables here, like handsize vs. maxHandSize - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } private boolean discardTargetAI(final Player ai, final SpellAbility sa) { @@ -166,7 +177,7 @@ public class DiscardAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); Player opp = targetableOpps.min(PlayerPredicates.compareByLife()); @@ -176,7 +187,7 @@ public class DiscardAi extends SpellAbilityAi { } else if (mandatory && sa.canTarget(ai)) { sa.getTargets().add(ai); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else { @@ -184,7 +195,7 @@ public class DiscardAi extends SpellAbilityAi { if ("AtLeast2".equals(sa.getParam("AILogic"))) { final List players = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa); if (players.isEmpty() || players.get(0).getCardsIn(ZoneType.Hand).size() < 2) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -196,18 +207,22 @@ public class DiscardAi extends SpellAbilityAi { } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { // Drawback AI improvements // if parent draws cards, make sure cards in hand + cards drawn > 0 if (sa.usesTargeting()) { - return discardTargetAI(ai, sa); + if (discardTargetAI(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } // TODO: check for some extra things - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) { 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 19caeb40ca1..3f6d83ea4b1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.AiPlayDecision; -import forge.ai.ComputerUtil; -import forge.ai.PlayerControllerAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; @@ -36,8 +33,16 @@ public class DiscoverAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { - return mandatory || checkApiLogic(ai, sa); + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (checkApiLogic(ai, sa)) { + 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/DrainManaAi.java b/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java index 957f45691e1..fafe4a7b480 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrainManaAi.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; @@ -12,7 +14,7 @@ import java.util.List; public class DrainManaAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { // AI cannot use this properly until he can use SAs during Humans turn final Card source = sa.getHostCard(); @@ -25,40 +27,48 @@ public class DrainManaAi extends SpellAbilityAi { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); if (!defined.contains(opp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { sa.resetTargets(); sa.getTargets().add(opp); } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Player opp = ai.getWeakestOpponent(); final Card source = sa.getHostCard(); if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); - return defined.contains(opp); + if (defined.contains(opp)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } else { sa.resetTargets(); sa.getTargets().add(opp); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { // AI cannot use this properly until he can use SAs during Humans turn final Card source = sa.getHostCard(); @@ -68,13 +78,17 @@ public class DrainManaAi extends SpellAbilityAi { final List defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); if (defined.contains(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { sa.resetTargets(); sa.getTargets().add(ai.getWeakestOpponent()); } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } } 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 ae3183ac0de..5378d4a3184 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java @@ -175,8 +175,12 @@ public class DrawAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { - return targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay()); + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { + if (targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay())) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } /** @@ -534,12 +538,16 @@ public class DrawAi extends SpellAbilityAi { } // drawTargetAI() @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return targetAI(ai, sa, mandatory); + if (targetAI(ai, sa, mandatory)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } /* (non-Javadoc) 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 1d114441b57..7860158349d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java @@ -35,7 +35,7 @@ import java.util.Map; public class EffectAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(final Player ai,final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, final SpellAbility sa) { final Game game = ai.getGame(); boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667; String logic = ""; @@ -45,12 +45,12 @@ public class EffectAi extends SpellAbilityAi { final PhaseHandler phase = game.getPhaseHandler(); if (logic.equals("BeginningOfOppTurn")) { if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } randomReturn = true; } else if (logic.equals("EndOfOppTurn")) { if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } randomReturn = true; } else if (logic.equals("KeepOppCreatsLandsTapped")) { @@ -64,20 +64,20 @@ public class EffectAi extends SpellAbilityAi { worthHolding = true; } if (!worthHolding) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } randomReturn = true; } } else if (logic.equals("RestrictBlocking")) { if (!phase.isPlayerTurn(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN) || phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.getPayCosts().getTotalMana().countX() > 0 && sa.getHostCard().getSVar("X").equals("Count$xPaid")) { // Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans. final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai, sa.isTrigger()) / 2; - if (xPay == 0) { return false; } + if (xPay == 0) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.setXManaCostPaid(xPay); } @@ -90,23 +90,27 @@ public class EffectAi extends SpellAbilityAi { int potentialDmg = 0; List currentAttackers = new ArrayList<>(); - if (possibleBlockers.isEmpty()) { return false; } + if (possibleBlockers.isEmpty()) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } for (final Card creat : possibleAttackers) { if (CombatUtil.canAttack(creat, opp) && possibleBlockers.size() > 1) { potentialDmg += creat.getCurrentPower(); - if (potentialDmg >= oppLife) { return true; } + if (potentialDmg >= oppLife) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } if (combat != null && combat.isAttacking(creat)) { currentAttackers.add(creat); } } - return currentAttackers.size() > possibleBlockers.size(); + if (currentAttackers.size() > possibleBlockers.size()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if (logic.equals("Fog")) { FogAi fogAi = new FogAi(); - if (!fogAi.canPlayAI(ai, sa)) { - return false; + if (!fogAi.canPlayAI(ai, sa).willingToPlay()) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -124,14 +128,14 @@ public class EffectAi extends SpellAbilityAi { } if (!canTgt) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { List list = game.getCombat().getAttackers(); list = CardLists.getTargetableCards(list, sa); Card target = ComputerUtilCard.getBestCreatureAI(list); if (target == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(target); } @@ -139,7 +143,7 @@ public class EffectAi extends SpellAbilityAi { randomReturn = true; } else if (logic.equals("ChainVeil")) { if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) || ai.getPlaneswalkersInPlay().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } randomReturn = true; } else if (logic.equals("WillCastCreature") && ai.isAI()) { @@ -150,17 +154,17 @@ public class EffectAi extends SpellAbilityAi { randomReturn = true; } else if (logic.equals("Main1")) { if (phase.getPhase().isBefore(PhaseType.MAIN1)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } randomReturn = true; } else if (logic.equals("Main2")) { if (phase.getPhase().isBefore(PhaseType.MAIN2)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } randomReturn = true; } else if (logic.equals("Evasion")) { if (!phase.isPlayerTurn(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } boolean shouldPlay = false; @@ -185,10 +189,10 @@ public class EffectAi extends SpellAbilityAi { break; } - return shouldPlay; + return shouldPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("RedirectSpellDamageFromPlayer")) { if (game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } boolean threatened = false; for (final SpellAbilityStackInstance stackInst : game.getStack()) { @@ -204,7 +208,7 @@ public class EffectAi extends SpellAbilityAi { randomReturn = threatened; } else if (logic.equals("Prevent")) { // prevent burn spell from opponent if (game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final SpellAbility saTop = game.getStack().peekAbility(); final Card host = saTop.getHostCard(); @@ -215,10 +219,10 @@ public class EffectAi extends SpellAbilityAi { final ApiType type = saTop.getApi(); if (type == ApiType.DealDamage || type == ApiType.DamageAll) { // burn spell sa.getTargets().add(saTop); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("NoGain")) { // basic logic to cancel GainLife on stack if (!game.getStack().isEmpty()) { @@ -228,14 +232,14 @@ public class EffectAi extends SpellAbilityAi { while (topStack != null) { if (topStack.getApi() == ApiType.GainLife) { if ("You".equals(topStack.getParam("Defined")) || topStack.isTargeting(activator) || (!topStack.usesTargeting() && !topStack.hasParam("Defined"))) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else if (topStack.getApi() == ApiType.DealDamage && topStack.getHostCard().hasKeyword(Keyword.LIFELINK)) { Card host = topStack.getHostCard(); for (GameEntity target : topStack.getTargets().getTargetEntities()) { if (ComputerUtilCombat.predictDamageTo(target, AbilityUtils.calculateAmount(host, topStack.getParam("NumDmg"), topStack), host, false) > 0) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } @@ -249,11 +253,11 @@ public class EffectAi extends SpellAbilityAi { final Player attackingPlayer = combat.getAttackingPlayer(); if (attackingPlayer.isOpponentOf(ai) && attackingPlayer.canGainLife()) { if (ComputerUtilCombat.checkAttackerLifelinkDamage(combat) > 0) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("NonCastCreature")) { // TODO: add support for more cases with more convoluted API setups if (!game.getStack().isEmpty()) { @@ -265,13 +269,13 @@ public class EffectAi extends SpellAbilityAi { boolean reanimator = "true".equalsIgnoreCase(topStack.getSVar("IsReanimatorCard")); if (changeZone && (toBattlefield || reanimator)) { if ("Creature".equals(topStack.getParam("ChangeType")) || topStack.getParamOrDefault("Defined", "").contains("Creature")) - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Fight")) { - return FightAi.canFightAi(ai, sa, 0, 0); + return FightAi.canFightAi(ai, sa, 0, 0) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Pump")) { sa.resetTargets(); List options = CardUtil.getValidCardsToTarget(sa); @@ -281,45 +285,44 @@ public class EffectAi extends SpellAbilityAi { } if (!options.isEmpty() && phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Burn")) { - // for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack) SpellAbility burn = sa.getSubAbility(); - return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn); + return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn).willingToPlay() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("YawgmothsWill")) { - return SpecialCardAi.YawgmothsWill.consider(ai, sa); + return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.startsWith("NeedCreatures")) { if (ai.getCreaturesInPlay().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (logic.contains(":")) { String[] k = logic.split(":"); int i = Integer.parseInt(k[1]); - return ai.getCreaturesInPlay().size() >= i; + return ai.getCreaturesInPlay().size() >= i ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (logic.equals("ReplaySpell")) { CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Graveyard), sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa); if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (logic.equals("PeaceTalks")) { Player nextPlayer = game.getNextPlayerAfter(ai); // If opponent doesn't have creatures, preventing attacks don't mean as much if (nextPlayer.getCreaturesInPlay().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Only cast Peace Talks after you attack just in case you have creatures if (!phase.is(PhaseType.MAIN2)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Create a pseudo combat and see if my life is in danger - return randomReturn; + return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Bribe")) { Card host = sa.getHostCard(); Combat combat = game.getCombat(); @@ -327,9 +330,9 @@ public class EffectAi extends SpellAbilityAi { && phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("CantRegenerate")) { if (sa.usesTargeting()) { CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); @@ -350,19 +353,19 @@ public class EffectAi extends SpellAbilityAi { }); if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO check Stack for Effects that would destroy the selected card? sa.getTargets().add(ComputerUtilCard.getBestAI(list)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (sa.getParent() != null) { // sub ability should be okay - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("Self".equals(sa.getParam("RememberObjects"))) { // the ones affecting itself are Nimbus cards, were opponent can activate this effect Card host = sa.getHostCard(); if (!host.canBeDestroyed()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } Map runParams = AbilityKey.mapFromAffected(sa.getHostCard()); @@ -370,18 +373,18 @@ public class EffectAi extends SpellAbilityAi { List repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other); // no Destroy Replacement, or one non-Regeneration one like Totem-Armor if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else { //no AILogic - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("False".equals(sa.getParam("Stackable"))) { @@ -390,7 +393,7 @@ public class EffectAi extends SpellAbilityAi { name = sa.getHostCard().getName() + "'s Effect"; } if (sa.getActivatingPlayer().isCardInCommand(name)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -406,20 +409,20 @@ public class EffectAi extends SpellAbilityAi { break; } } - return canTgt; + return canTgt ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else { sa.getTargets().add(ai); } } - return randomReturn; + return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { if (sa.hasParam("AILogic")) { - if (canPlayAI(aiPlayer, sa)) { - return true; // if false, fall through further to do the mandatory stuff + if (canPlayAI(aiPlayer, sa).willingToPlay()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -431,7 +434,7 @@ public class EffectAi extends SpellAbilityAi { if (!oppPerms.isEmpty()) { sa.resetTargets(); sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (mandatory) { @@ -441,11 +444,11 @@ public class EffectAi extends SpellAbilityAi { if (!aiPerms.isEmpty()) { sa.resetTargets(); sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } return super.doTriggerAINoCost(aiPlayer, sa, mandatory); diff --git a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java index 6e5579272ea..fac07a46841 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java @@ -17,9 +17,7 @@ */ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.card.CardLists; import forge.game.combat.CombatUtil; @@ -45,19 +43,17 @@ public final class EncodeAi extends SpellAbilityAi { *

* @param sa * a {@link forge.game.spellability.SpellAbility} object. - * @param af - * a {@link forge.game.ability.AbilityFactory} object. * * @return a boolean. */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return true; + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { - return true; + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* diff --git a/forge-ai/src/main/java/forge/ai/ability/EndTurnAi.java b/forge-ai/src/main/java/forge/ai/ability/EndTurnAi.java index ad6e0e0bb01..9ec0a72c26c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EndTurnAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EndTurnAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -12,18 +14,22 @@ import forge.game.spellability.SpellAbility; public class EndTurnAi extends SpellAbilityAi { @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return mandatory; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { return false; } + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return false; + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/EndureAi.java b/forge-ai/src/main/java/forge/ai/ability/EndureAi.java index fc4f9f4dc0d..31e57904de5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/EndureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/EndureAi.java @@ -2,6 +2,8 @@ package forge.ai.ability; import com.google.common.collect.Sets; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -22,19 +24,19 @@ public class EndureAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // Support for possible targeted Endure (e.g. target creature endures X) if (sa.usesTargeting()) { Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield)); if (bestCreature == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); sa.getTargets().add(bestCreature); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } public static boolean shouldPutCounters(Player ai, SpellAbility sa) { @@ -121,7 +123,7 @@ public class EndureAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { // Support for possible targeted Endure (e.g. target creature endures X) if (sa.usesTargeting()) { CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), @@ -129,12 +131,16 @@ public class EndureAi extends SpellAbilityAi { if (!list.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return canPlayAI(aiPlayer, sa) || mandatory; + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return canPlayAI(aiPlayer, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java index ef3996b70c3..85eb8f044b5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java @@ -15,19 +15,19 @@ public class ExploreAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // Explore with a target (e.g. Enter the Unknown) if (sa.usesTargeting()) { Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield)); if (bestCreature == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); sa.getTargets().add(bestCreature); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } public static boolean shouldPutInGraveyard(Card topCard, Player ai) { @@ -64,19 +64,23 @@ public class ExploreAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { CardCollection list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa); if (!list.isEmpty()) { sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } - return canPlayAI(aiPlayer, sa) || mandatory; + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return canPlayAI(aiPlayer, 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 8c403f07f80..84982061aa5 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java @@ -105,26 +105,38 @@ public class FightAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { if ("Always".equals(sa.getParam("AILogic"))) { - return true; // e.g. Hunt the Weak, the AI logic was already checked through canFightAi + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi } - return checkApiLogic(aiPlayer, sa); + if (checkApiLogic(aiPlayer, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final String aiLogic = sa.getParamOrDefault("AILogic", ""); if (aiLogic.equals("Grothama")) { - return mandatory ? true : SpecialCardAi.GrothamaAllDevouring.consider(ai, sa); + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + if (SpecialCardAi.GrothamaAllDevouring.consider(ai, sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (checkApiLogic(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } //try to make a good trade or no trade @@ -132,7 +144,7 @@ public class FightAi extends SpellAbilityAi { List humCreatures = ai.getOpponents().getCreaturesInPlay(); humCreatures = CardLists.getTargetableCards(humCreatures, sa); if (humCreatures.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } //assumes the triggered card belongs to the ai if (sa.hasParam("Defined")) { @@ -141,19 +153,19 @@ public class FightAi extends SpellAbilityAi { if (canKill(aiCreature, humanCreature, 0) && ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) { sa.getTargets().add(humanCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } for (Card humanCreature : humCreatures) { if (!canKill(humanCreature, aiCreature, 0)) { sa.getTargets().add(humanCreature); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } sa.getTargets().add(humCreatures.get(0)); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java b/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java index 1dce017bc3a..a932d697657 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -13,52 +15,56 @@ public class FlipACoinAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { if (sa.hasParam("AILogic")) { String ailogic = sa.getParam("AILogic"); if (ailogic.equals("Never")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (ailogic.equals("PhaseOut")) { if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (ailogic.equals("Bangchuckers")) { if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); for (Player o : ai.getOpponents()) { if (sa.canTarget(o) && o.canLoseLife() && !o.cantLoseForZeroOrLessLife()) { sa.getTargets().add(o); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } for (Card c : ai.getOpponents().getCreaturesInPlay()) { if (sa.canTarget(c)) { sa.getTargets().add(c); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (ailogic.equals("KillOrcs")) { if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); for (Card c : ai.getOpponents().getCreaturesInPlay()) { if (sa.canTarget(c)) { sa.getTargets().add(c); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return sa.isTargetNumberValid(); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return canPlayAI(ai, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java b/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java index 0a142a221ce..1f5348b59e1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FlipOntoBattlefieldAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; @@ -14,26 +16,43 @@ import java.util.Map; public class FlipOntoBattlefieldAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { PhaseHandler ph = sa.getHostCard().getGame().getPhaseHandler(); String logic = sa.getParamOrDefault("AILogic", ""); if (!isSorcerySpeed(sa, aiPlayer) && sa.getPayCosts().hasManaCost()) { - return ph.is(PhaseType.END_OF_TURN); + if (ph.is(PhaseType.END_OF_TURN)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn); + } } if ("DamageCreatures".equals(logic)) { int maxToughness = Integer.parseInt(sa.getSubAbility().getParam("NumDmg")); CardCollectionView rightToughness = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), card -> card.getNetToughness() <= maxToughness && card.canBeDestroyed()); - return !rightToughness.isEmpty(); + + if (rightToughness.isEmpty()) { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } else { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } - return !aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty(); + if (!aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield).isEmpty()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return canPlayAI(aiPlayer, sa) || mandatory; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return canPlayAI(aiPlayer, sa); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/FogAi.java b/forge-ai/src/main/java/forge/ai/ability/FogAi.java index 7ee2f2eef0f..7b912c915d2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java @@ -22,36 +22,36 @@ public class FogAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Game game = ai.getGame(); final Card hostCard = sa.getHostCard(); final Combat combat = game.getCombat(); // Don't cast it, if the effect is already in place if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO Test if we can even Fog successfully if (handleMemoryCheck(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Only cast when Stack is empty, so Human uses spells/abilities first if (!game.getStack().isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO Only cast outside of combat if I won't be able to cast inside of combat if (combat == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // AI should only activate this during Opponents Declare Blockers phase if (!game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) || !game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { // TODO Be careful of effects that don't let you cast spells during combat - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } int remainingLife = ComputerUtilCombat.lifeThatWouldRemain(ai, combat); @@ -61,28 +61,32 @@ public class FogAi extends SpellAbilityAi { int fogs = countAvailableFogs(ai); if (fogs > 2 && dmg > 2) { // Playing a fog deck. If you got them play them. - return true; + return new AiAbilityDecision(100, AiPlayDecision.Tempo); } if (dmg > 2 && hostCard.hasKeyword(Keyword.BUYBACK) && CardLists.count(ai.getCardsIn(ZoneType.Battlefield), Card::isLand) > 3) { // Constant mists sacrifices a land to buyback. But if AI is running it, they are probably ok sacrificing some lands - return true; + return new AiAbilityDecision(100, AiPlayDecision.Tempo); } if ("SeriousDamage".equals(sa.getParam("AILogic"))) { if (dmg > ai.getLife() / 4) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.Tempo); } else if (dmg >= 5) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.Tempo); } else if (ai.getLife() < ai.getStartingLife() / 3) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.Tempo); } } // TODO Compare to poison counters? // Cast it if life is in danger - return ComputerUtilCombat.lifeInDanger(ai, game.getCombat()); + if (ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) { + return new AiAbilityDecision(100, AiPlayDecision.Tempo); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } private boolean handleMemoryCheck(Player ai, SpellAbility sa) { @@ -137,7 +141,7 @@ public class FogAi extends SpellAbilityAi { @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { // AI should only activate this during Human's turn boolean chance; final Game game = ai.getGame(); @@ -149,11 +153,15 @@ public class FogAi extends SpellAbilityAi { chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE); } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { final Game game = aiPlayer.getGame(); boolean chance; if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getWeakestOpponent())) { @@ -162,6 +170,10 @@ public class FogAi extends SpellAbilityAi { chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE); } - return chance || mandatory; + if (mandatory || chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java index dd59e096ea1..02d88ebeb0b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GameLossAi.java @@ -1,15 +1,17 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; public class GameLossAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Player opp = ai.getStrongestOpponent(); if (opp.cantLose()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Only one SA Lose the Game card right now, which is Door to Nothingness @@ -17,14 +19,14 @@ public class GameLossAi extends SpellAbilityAi { if (sa.usesTargeting() && sa.canTarget(opp)) { sa.resetTargets(); sa.getTargets().add(opp); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { Player loser = ai; // Phage the Untouchable @@ -33,7 +35,7 @@ public class GameLossAi extends SpellAbilityAi { } if (!mandatory && (loser == ai || loser.cantLose())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.usesTargeting() && sa.canTarget(loser)) { @@ -41,6 +43,6 @@ public class GameLossAi extends SpellAbilityAi { sa.getTargets().add(loser); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/GameWinAi.java b/forge-ai/src/main/java/forge/ai/ability/GameWinAi.java index 9586916f190..d88b94bc24b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GameWinAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GameWinAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -10,20 +12,25 @@ public class GameWinAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { - return !ai.cantWin(); + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { + if (ai.cantWin()) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + // If the AI can win the game, it should play this ability. + // This is a special case where the AI should always play the ability if it can win. // TODO Check conditions are met on card (e.g. Coalition Victory) // TODO Consider likelihood of SA getting countered + return new AiAbilityDecision(10000, AiPlayDecision.WillPlay); // In general, don't return true. // But this card wins the game, I can make an exception for that } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } 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 7f1bca5e4f1..78f6314f9eb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/GoadAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/GoadAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -86,36 +84,36 @@ public class GoadAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (checkApiLogic(ai, sa)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (!mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.usesTargeting()) { if (sa.getTargetRestrictions().canTgtPlayer()) { for (Player opp : ai.getOpponents()) { if (sa.canTarget(opp)) { sa.getTargets().add(opp); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } if (sa.canTarget(ai)) { sa.getTargets().add(ai); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { List list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); if (list.isEmpty()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list)); - 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/HauntAi.java b/forge-ai/src/main/java/forge/ai/ability/HauntAi.java index 3943e091530..5bfc2a4f712 100644 --- a/forge-ai/src/main/java/forge/ai/ability/HauntAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/HauntAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -15,7 +17,7 @@ import java.util.List; public class HauntAi extends SpellAbilityAi { @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card card = sa.getHostCard(); final Game game = ai.getGame(); if (sa.usesTargeting() && !card.isToken()) { @@ -24,12 +26,12 @@ public class HauntAi extends SpellAbilityAi { // nothing to haunt if (creats.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final List oppCreats = CardLists.filterControlledBy(creats, ai.getOpponents()); sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppCreats.isEmpty() ? creats : oppCreats)); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } \ No newline at end of file diff --git a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java index eb28a5ad961..2d3607ff9a2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java @@ -10,60 +10,65 @@ public class ImmediateTriggerAi extends SpellAbilityAi { // TODO: this class is largely reused from DelayedTriggerAi, consider updating @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { String logic = sa.getParamOrDefault("AILogic", ""); if (logic.equals("Always")) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } SpellAbility trigsa = sa.getAdditionalAbility("Execute"); if (trigsa == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } trigsa.setActivatingPlayer(ai); if (trigsa instanceof AbilitySub) { return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa); - } else { - return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); } + + AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + if (decision == AiPlayDecision.WillPlay) { + return new AiAbilityDecision(100, decision); + } + + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { // always add to stack, targeting happens after payment if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } SpellAbility trigsa = sa.getAdditionalAbility("Execute"); if (trigsa == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); trigsa.setActivatingPlayer(ai); - return aic.doTrigger(trigsa, !"You".equals(sa.getParamOrDefault("OptionalDecider", "You"))); + return aic.doTrigger(trigsa, !"You".equals(sa.getParamOrDefault("OptionalDecider", "You"))) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { String logic = sa.getParamOrDefault("AILogic", ""); if (logic.equals("Always")) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } SpellAbility trigsa = sa.getAdditionalAbility("Execute"); if (trigsa == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (logic.equals("WeakerCreature")) { Card ownCreature = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay()); if (ownCreature == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } int eval = ComputerUtilCard.evaluateCreature(ownCreature); @@ -75,12 +80,12 @@ public class ImmediateTriggerAi extends SpellAbilityAi { } } if (!foundWorse) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } trigsa.setActivatingPlayer(ai); - return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa); + return ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa) == AiPlayDecision.WillPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java index e7c8c69d463..9ec25e13f8f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseType; @@ -15,10 +17,10 @@ public class InvestigateAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); - - return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer; + boolean result = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/LearnAi.java b/forge-ai/src/main/java/forge/ai/ability/LearnAi.java index b465cb77c01..15fa903ee27 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LearnAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LearnAi.java @@ -1,9 +1,7 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.PlayerControllerAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.card.CardLists; @@ -17,19 +15,21 @@ import java.util.Map; public class LearnAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // For the time being, Learn is treated as universally positive due to being optional - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(aiPlayer, sa); + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return canPlayAI(aiPlayer, sa); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return canPlayAI(aiPlayer, sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java index 4f498b4bd6b..bbf6873533e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -21,8 +23,8 @@ public class LegendaryRuleAi extends SpellAbilityAi { * @see forge.card.ability.SpellAbilityAi#canPlayAI(forge.game.player.Player, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return false; // should not get here + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // should not get here } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java index 71cdad4a7be..1faab704ba8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.player.PlayerCollection; @@ -18,9 +20,9 @@ public class LifeExchangeAi extends SpellAbilityAi { * forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { if (!aiPlayer.canGainLife()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final int myLife = aiPlayer.getLife(); @@ -42,19 +44,20 @@ public class LifeExchangeAi extends SpellAbilityAi { // never target self, that would be silly for exchange sa.getTargets().add(opponent); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // if life is in danger, always activate if (myLife < 5 && hLife > myLife) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // cost includes sacrifice probably, so make sure it's worth it chance &= (hLife > (myLife + 8)); - return MyRandom.getRandom().nextFloat() < .6667 && chance; + boolean result = MyRandom.getRandom().nextFloat() < .6667 && chance; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** @@ -71,7 +74,7 @@ public class LifeExchangeAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); Player opp = targetableOpps.max(PlayerPredicates.compareByLife()); if (sa.usesTargeting()) { @@ -82,10 +85,10 @@ public class LifeExchangeAi extends SpellAbilityAi { sa.getTargets().add(ai); } } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java index f79e25b44e7..ecfa17619b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java @@ -21,35 +21,35 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { * forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final Game game = ai.getGame(); if ("Tree of Redemption".equals(sourceName)) { if (!ai.canGainLife()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // someone controls "Rain of Gore" or "Sulfuric Vortex", lifegain is bad in that case if (game.isCardInPlay("Rain of Gore") || game.isCardInPlay("Sulfuric Vortex")) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // an opponent controls "Tainted Remedy", lifegain is bad in that case for (Player op : ai.getOpponents()) { if (op.isCardInPlay("Tainted Remedy")) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (ComputerUtil.waitForBlocking(sa) || ai.getLife() + 1 >= source.getNetToughness() || (ai.getLife() > 5 && !ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if ("Tree of Perdition".equals(sourceName)) { boolean shouldDo = false; if (ComputerUtil.waitForBlocking(sa)) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); for (Player op : ai.getOpponents()) { // if oppoent can't be targeted, or it can't lose life, try another one @@ -80,7 +80,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { } } - return shouldDo; + return shouldDo ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("Evra, Halcyon Witness".equals(sourceName)) { int aiLife = ai.getLife(); @@ -92,7 +92,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { Player def = game.getCombat().getDefenderPlayerByAttacker(source); if (game.getCombat().isUnblocked(source) && def.canLoseLife() && aiLife >= def.getLife() && source.getNetPower() < def.getLife()) { // Unblocked Evra which can deal lethal damage - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (ai.getController().isAI() && aiLife > source.getNetPower() && source.hasKeyword(Keyword.LIFELINK)) { int dangerMin = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD)); int dangerMax = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD)); @@ -100,7 +100,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { int lifeInDanger = dangerDiff <= 0 ? dangerMin : MyRandom.getRandom().nextInt(dangerDiff) + dangerMin; if (source.getNetPower() >= lifeInDanger && ai.canGainLife() && ComputerUtil.lifegainPositive(ai, source)) { // Blocked or unblocked Evra which will get bigger *and* we're getting our life back through Lifelink - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } @@ -109,10 +109,10 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { if (source.getNetPower() > aiLife) { // Only makes sense if the AI can actually gain life from this if (!ai.canGainLife()) - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); if (ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // check the top of stack @@ -120,13 +120,13 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { if (!stack.isEmpty()) { SpellAbility saTop = stack.peekAbility(); if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /** @@ -143,17 +143,17 @@ public class LifeExchangeVariantAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); if (sa.usesTargeting()) { sa.resetTargets(); if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } 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 385fb7ec569..0c9db1599ff 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java @@ -216,13 +216,13 @@ public class LifeGainAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { // If the Target is gaining life, target self. // if the Target is modifying how much life is gained, this needs to be // handled better if (sa.usesTargeting()) { if (!target(ai, sa, mandatory)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } @@ -233,11 +233,11 @@ public class LifeGainAi extends SpellAbilityAi { sa.setXManaCostPaid(xPay); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return doTriggerAINoCost(ai, sa, true); } 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 4c4f2b7096b..377cccb3277 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; @@ -26,7 +28,7 @@ public class LifeLoseAi extends SpellAbilityAi { * SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { final PlayerCollection tgtPlayers = getPlayers(ai, sa); final Card source = sa.getHostCard(); @@ -48,14 +50,13 @@ public class LifeLoseAi extends SpellAbilityAi { } if (tgtPlayers.contains(ai) && amount > 0 && amount + 3 > ai.getLife()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - if (sa.usesTargeting()) { - return doTgt(ai, sa, false); + boolean result = doTgt(ai, sa, false); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* @@ -166,11 +167,11 @@ public class LifeLoseAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { if (sa.usesTargeting()) { if (!doTgt(ai, sa, mandatory)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -191,7 +192,15 @@ public class LifeLoseAi extends SpellAbilityAi { : AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa); // For cards like Foul Imp, ETB you lose life - return mandatory || !tgtPlayers.contains(ai) || amount <= 0 || amount + 3 <= ai.getLife(); + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + + if (!tgtPlayers.contains(ai) || amount <= 0 || amount + 3 <= ai.getLife()) { + 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/LifeSetAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java index ee700b99dd9..6bdb34f5aa4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilAbility; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardPredicates; @@ -19,7 +17,7 @@ import forge.util.MyRandom; public class LifeSetAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final int myLife = ai.getLife(); final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife()); @@ -29,12 +27,12 @@ public class LifeSetAi extends SpellAbilityAi { // Don't use setLife before main 2 if possible if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO add AI logic for that if (sa.hasParam("Redistribute")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO handle proper calculation of X values based on Cost and what would be paid @@ -61,7 +59,7 @@ public class LifeSetAi extends SpellAbilityAi { // possibly add a combo here for Magister Sphinx and // Higedetsu's (sp?) Second Rite if (opponent == null || amount > hlife || !opponent.canLoseLife()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(opponent); } else { @@ -72,34 +70,35 @@ public class LifeSetAi extends SpellAbilityAi { } else if (amount > myLife && ai.canGainLife()) { sa.getTargets().add(ai); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else { if (sa.getParam("Defined").equals("Player")) { if (amount == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (myLife > amount) { // will decrease computer's life if ((myLife < 5) || ((myLife - amount) > (hlife - amount))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } if (amount <= myLife) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // if life is in danger, always activate if (myLife < 3 && amount > myLife && ai.canGainLife()) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return MyRandom.getRandom().nextFloat() < .6667 && chance; + boolean result = MyRandom.getRandom().nextFloat() < .6667 && chance; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final int myLife = ai.getLife(); final PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final Player opponent = targetableOpps.max(PlayerPredicates.compareByLife()); @@ -109,7 +108,7 @@ public class LifeSetAi extends SpellAbilityAi { // TODO add AI logic for that if (sa.hasParam("Redistribute")) { - return mandatory; + return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final String amountStr = sa.getParam("LifeAmount"); @@ -127,12 +126,13 @@ public class LifeSetAi extends SpellAbilityAi { // special cases when amount can't be calculated without targeting first if (amount == 0 && "TargetedPlayer$StartingLife/HalfDown".equals(source.getSVar(amountStr))) { // e.g. Torgaar, Famine Incarnate - return doHalfStartingLifeLogic(ai, opponent, sa) || mandatory; + boolean result = doHalfStartingLifeLogic(ai, opponent, sa); + return result || mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sourceName.equals("Eternity Vessel") && (ai.getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vampire Hexmage")) || (source.getCounters(CounterEnumType.CHARGE) == 0))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // If the Target is gaining life, target self. @@ -142,7 +142,7 @@ public class LifeSetAi extends SpellAbilityAi { sa.resetTargets(); if (tgt.canOnlyTgtOpponent()) { if (opponent == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(opponent); } else { @@ -153,12 +153,12 @@ public class LifeSetAi extends SpellAbilityAi { } else if (amount > myLife || mandatory) { sa.getTargets().add(ai); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private boolean doHalfStartingLifeLogic(Player ai, Player opponent, SpellAbility sa) { 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 d37cfd60f36..116813aa551 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManaAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManaAi.java @@ -112,13 +112,14 @@ public class ManaAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { final String logic = sa.getParamOrDefault("AILogic", ""); if (logic.startsWith("ManaRitual")) { - return doManaRitualLogic(aiPlayer, sa, true); + boolean result = doManaRitualLogic(aiPlayer, sa, true); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // Dark Ritual and other similar instants/sorceries that add mana to mana pool @@ -271,4 +272,11 @@ public class ManaAi extends SpellAbilityAi { } return !lose; } + + @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); + } } 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 3bbe9b44a44..e3e40e171cf 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java @@ -1,10 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Iterables; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -26,10 +23,10 @@ import java.util.Map; public abstract class ManifestBaseAi extends SpellAbilityAi { @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { // Manifest doesn't have any "Pay X to manifest X triggers" - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) 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 bbd1a4ff12d..35f943eae76 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MeldAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MeldAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.card.CardCollectionView; import forge.game.card.CardPredicates; @@ -25,7 +27,7 @@ public class MeldAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } \ No newline at end of file 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 6a58a861341..5de87e1378e 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java @@ -2,10 +2,7 @@ package forge.ai.ability; import com.google.common.collect.Lists; import com.google.common.collect.Maps; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCost; -import forge.ai.SpecialCardAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardCollectionView; @@ -165,14 +162,14 @@ public class MillAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - return targetAI(aiPlayer, sa, true); + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { + return targetAI(aiPlayer, sa, true) ? new AiAbilityDecision(100, forge.ai.AiPlayDecision.WillPlay) : new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (!targetAI(aiPlayer, sa, mandatory)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.hasParam("NumCards") && (sa.getParam("NumCards").equals("X") && sa.getSVar("X").equals("Count$xPaid"))) { @@ -180,7 +177,7 @@ public class MillAi extends SpellAbilityAi { sa.setXManaCostPaid(getNumToDiscard(aiPlayer, sa)); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) diff --git a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java index 01cc8709143..780272fa3a8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java @@ -1,18 +1,15 @@ package forge.ai.ability; import com.google.common.collect.Iterables; -import forge.ai.AiCardMemory; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.card.CardLists; -import forge.game.card.CardUtil; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.keyword.Keyword; +import forge.game.phase.PhaseType; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -22,37 +19,40 @@ import java.util.Map; public class MustBlockAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { final Card source = sa.getHostCard(); final Game game = aiPlayer.getGame(); final Combat combat = game.getCombat(); final boolean onlyLethal = !"AllowNonLethal".equals(sa.getParam("AILogic")); if (combat == null || !combat.isAttacking(source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { // The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards // may result in overextending and losing the attacker - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final List list = determineGoodBlockers(source, aiPlayer, combat.getDefenderPlayerByAttacker(source), sa, onlyLethal,false); if (!list.isEmpty()) { final Card blocker = ComputerUtilCard.getBestCreatureAI(list); + if (blocker == null) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } sa.getTargets().add(blocker); AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { if (sa.hasParam("DefinedAttacker")) { // The AI can't handle "target creature blocks another target creature" abilities yet - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Otherwise it's a standard targeted "target creature blocks CARDNAME" ability, so use the main canPlayAI code path @@ -60,14 +60,19 @@ public class MustBlockAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); + // only use on creatures that can attack + if (!ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + Card attacker = source; if (sa.hasParam("DefinedAttacker")) { final List cards = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedAttacker"), sa); if (cards.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } attacker = cards.get(0); @@ -76,13 +81,21 @@ public class MustBlockAi extends SpellAbilityAi { boolean chance = false; if (sa.usesTargeting()) { - List list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true); - if (list.isEmpty() && mandatory) { - list = CardUtil.getValidCardsToTarget(sa); + final List list = determineGoodBlockers(attacker, ai, ai.getWeakestOpponent(), sa, true, true); + if (list.isEmpty()) { + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } final Card blocker = ComputerUtilCard.getBestCreatureAI(list); if (blocker == null) { - return sa.isTargetNumberValid(); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } if (!mandatory && sa.isKeyword(Keyword.PROVOKE) && blocker.isTapped()) { @@ -93,7 +106,7 @@ public class MustBlockAi extends SpellAbilityAi { if (defender != null && combat.getAttackingPlayer().equals(ai) && defender.canLoseLife() && !defender.cantLoseForZeroOrLessLife() && ComputerUtilCombat.lifeThatWouldRemain(defender, combat) <= 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } @@ -102,12 +115,16 @@ public class MustBlockAi extends SpellAbilityAi { chance = true; } else if (sa.hasParam("Choices")) { // currently choice is attacked player - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } private List determineBlockerFromList(final Card attacker, final Player ai, Iterable options, SpellAbility sa, diff --git a/forge-ai/src/main/java/forge/ai/ability/MutateAi.java b/forge-ai/src/main/java/forge/ai/ability/MutateAi.java index 73c644b84fc..066d9798224 100644 --- a/forge-ai/src/main/java/forge/ai/ability/MutateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/MutateAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.card.CardCollectionView; import forge.game.card.CardLists; @@ -17,7 +15,7 @@ import java.util.function.Predicate; public class MutateAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { CardCollectionView mutateTgts = CardLists.getTargetableCards(aiPlayer.getCreaturesInPlay(), sa); mutateTgts = ComputerUtil.getSafeTargets(aiPlayer, sa, mutateTgts); @@ -32,7 +30,7 @@ public class MutateAi extends SpellAbilityAi { ); if (mutateTgts.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Choose the best target @@ -41,7 +39,7 @@ public class MutateAi extends SpellAbilityAi { Card mutateTgt = ComputerUtilCard.getBestCreatureAI(mutateTgts); sa.getTargets().add(mutateTgt); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java index eeb25f8f4f9..ac976c5795c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PeekAndRevealAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.AiAttackController; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; -import forge.ai.SpellApiToAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.card.CardCollection; import forge.game.phase.PhaseHandler; @@ -27,20 +24,20 @@ public class PeekAndRevealAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { if (sa instanceof AbilityStatic) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } String logic = sa.getParamOrDefault("AILogic", ""); if ("Main2".equals(logic)) { if (aiPlayer.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if ("EndOfOppTurn".equals(logic)) { PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); if (!(ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // So far this only appears on Triggers, but will expand @@ -50,32 +47,32 @@ public class PeekAndRevealAi extends SpellAbilityAi { Player libraryOwner = aiPlayer; if (!willPayCosts(aiPlayer, sa, sa.getPayCosts(), host)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.usesTargeting()) { sa.resetTargets(); //todo: evaluate valid targets if (!sa.canTarget(opp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getTargets().add(opp); libraryOwner = opp; } if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("X".equals(sa.getParam("PeekAmount")) && sa.getSVar("X").equals("Count$xPaid")) { int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger()); if (xPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.getRootAbility().setXManaCostPaid(xPay); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) @@ -93,7 +90,7 @@ public class PeekAndRevealAi extends SpellAbilityAi { } AbilitySub subAb = sa.getSubAbility(); - return subAb != null && SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(player, subAb); + return subAb != null && SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(player, subAb).willingToPlay(); } } 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 5313bef01ee..1d8eb80d2b7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCost; -import forge.ai.ComputerUtilMana; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.card.CardStateName; import forge.card.CardType.Supertype; import forge.card.mana.ManaCost; @@ -320,24 +317,24 @@ public class PermanentAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - final Card source = sa.getHostCard(); - final Cost cost = sa.getPayCosts(); - + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!sa.metConditions()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.hasParam("AILogic") && !checkAiLogic(ai, sa, sa.getParam("AILogic"))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } + final Cost cost = sa.getPayCosts(); + final Card source = sa.getHostCard(); if (cost != null && !willPayCosts(ai, sa, cost, source)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return checkApiLogic(ai, sa) || mandatory; + boolean result = checkApiLogic(ai, sa); + return (result || mandatory) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java b/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java index ea5cb13c3ed..2933f9a34a7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PhasesAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.GameEntity; import forge.game.ability.AbilityUtils; @@ -23,7 +21,7 @@ import java.util.function.Predicate; public class PhasesAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // This still needs to be fleshed out final TargetRestrictions tgt = sa.getTargetRestrictions(); final Card source = sa.getHostCard(); @@ -36,55 +34,76 @@ public class PhasesAi extends SpellAbilityAi { if (tgtCards.contains(source)) { // Protect it from something final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source); - return isThreatened; - } else { - // Card def = tgtCards.get(0); - // Phase this out if it might attack me, or before it can be - // declared as a blocker + if (isThreatened) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else { if (!phasesPrefTargeting(tgt, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { final TargetRestrictions tgt = sa.getTargetRestrictions(); if (tgt == null) { - return mandatory; + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (phasesPrefTargeting(tgt, sa, mandatory)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (mandatory) { // not enough preferred targets, but mandatory so keep going: - return sa.isTargetNumberValid() || phasesUnpreferredTargeting(aiPlayer.getGame(), sa, mandatory); + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // no valid targets, but mandatory so try to find something + if (phasesUnpreferredTargeting(aiPlayer.getGame(), sa, mandatory)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + sa.resetTargets(); + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { final TargetRestrictions tgt = sa.getTargetRestrictions(); boolean randomReturn = true; - if (tgt == null) { - - } else { + if (tgt != null) { if (!phasesPrefTargeting(tgt, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); } } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } private boolean phasesPrefTargeting(final TargetRestrictions tgt, final SpellAbility sa, 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 b38f0d1707e..0e1a2296d20 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java @@ -115,20 +115,22 @@ public class PlayAi extends SpellAbilityAi { * @return a boolean. */ @Override - protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) { if (sa.usesTargeting()) { if (!sa.hasParam("AILogic")) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("ReplaySpell".equals(sa.getParam("AILogic"))) { - return ComputerUtil.targetPlayableSpellCard(ai, getPlayableCards(sa, ai), sa, sa.hasParam("WithoutManaCost"), mandatory); + boolean result = ComputerUtil.targetPlayableSpellCard(ai, getPlayableCards(sa, ai), sa, sa.hasParam("WithoutManaCost"), mandatory); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return checkApiLogic(ai, sa); + boolean result = checkApiLogic(ai, sa); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : 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/PoisonAi.java b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java index 4e3a4428b14..a139c969f02 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PoisonAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -55,30 +57,26 @@ public class PoisonAi extends SpellAbilityAi { * forge.game.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + boolean result; if (sa.usesTargeting()) { - return tgtPlayer(ai, sa, mandatory); + result = tgtPlayer(ai, sa, mandatory); } else if (mandatory || !ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) { // mandatory or ai is uneffected - return true; + result = true; } else { // currently there are no optional Trigger final PlayerCollection players = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa); if (players.isEmpty()) { - return false; - } - // not affected, don't care - if (!players.contains(ai)) { - return true; - } - - Player max = players.max(PlayerPredicates.compareByPoison()); - if (ai.getPoisonCounters() == max.getPoisonCounters()) { - // ai is one of the max - return false; + result = false; + } else if (!players.contains(ai)) { + result = true; + } else { + Player max = players.max(PlayerPredicates.compareByPoison()); + result = ai.getPoisonCounters() != max.getPoisonCounters(); } } - return true; + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private boolean tgtPlayer(Player ai, SpellAbility sa, boolean mandatory) { diff --git a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java index fa96446e376..1bacde7f3e7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -20,7 +22,7 @@ public class PowerExchangeAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, final SpellAbility sa) { Card c1 = null; Card c2 = null; final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -41,27 +43,28 @@ public class PowerExchangeAi extends SpellAbilityAi { sa.getTargets().add(c2); } if (c1 == null || c2 == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (sa.isMandatory() || ComputerUtilCard.evaluateCreature(c1) > ComputerUtilCard.evaluateCreature(c2) + 40) { sa.getTargets().add(c1); - return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + boolean result = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { return canPlayAI(aiPlayer, sa); } - 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 8f1031bc3cf..26d7cf928ba 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java @@ -305,25 +305,33 @@ public class ProtectAi extends SpellAbilityAi { } // protectMandatoryTarget() @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } else { - return protectTgtAI(ai, sa, mandatory); + if (protectTgtAI(ai, sa, mandatory)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // protectTriggerAI @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { if (sa.usesTargeting()) { - return protectTgtAI(ai, sa, false); + if (protectTgtAI(ai, sa, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // protectDrawbackAI() } diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java index 6352447e814..b504a225e5a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAllAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.card.Card; import forge.game.cost.Cost; @@ -9,25 +11,25 @@ import forge.game.spellability.SpellAbility; public class ProtectAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card hostCard = sa.getHostCard(); // if there is no target and host card isn't in play, don't activate if (!sa.usesTargeting() && !hostCard.isInPlay()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final Cost cost = sa.getPayCosts(); // temporarily disabled until better AI if (!willPayCosts(ai, sa, cost, hostCard)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // protectAllCanPlayAI() @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + 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 3679ebc069e..ddbb32f4eda 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java @@ -435,7 +435,7 @@ public class PumpAi extends PumpAiBase { } else if (sa.getParam("AILogic").equals("SacOneEach")) { // each player sacrifices one permanent, e.g. Vaevictis, Asmadi the Dire - grab the worst for allied and // the best for opponents - return SacrificeAi.doSacOneEachLogic(ai, sa); + return SacrificeAi.doSacOneEachLogic(ai, sa).willingToPlay(); } else if (sa.getParam("AILogic").equals("Destroy")) { List tgts = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); if (tgts.isEmpty()) { @@ -628,7 +628,7 @@ public class PumpAi extends PumpAiBase { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final SpellAbility root = sa.getRootAbility(); final String numDefense = sa.getParamOrDefault("NumDef", ""); final String numAttack = sa.getParamOrDefault("NumAtt", ""); @@ -667,17 +667,17 @@ public class PumpAi extends PumpAiBase { if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else { - return pumpTgtAI(ai, sa, defense, attack, mandatory, true); + boolean result = pumpTgtAI(ai, sa, defense, attack, mandatory, true); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - - return true; } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { final SpellAbility root = sa.getRootAbility(); final Card source = sa.getHostCard(); @@ -700,10 +700,10 @@ public class PumpAi extends PumpAiBase { continue; // in case the calculation gets messed up somewhere } root.setSVar("EnergyToPay", "Number$" + minus); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } int attack; @@ -735,17 +735,25 @@ public class PumpAi extends PumpAiBase { } if (sa.usesTargeting()) { - return pumpTgtAI(ai, sa, defense, attack, false, true); + if (pumpTgtAI(ai, sa, defense, attack, false, true)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } if (source.isCreature()) { if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + if (source.getNetToughness() + defense > 0) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return source.getNetToughness() + defense > 0; } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java index 2399b4aea95..0cfa25d9c1c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.ComputerUtilCost; +import forge.ai.*; import forge.game.Game; import forge.game.GameObject; import forge.game.ability.AbilityUtils; @@ -29,7 +26,7 @@ public class PumpAllAi extends PumpAiBase { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(final Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, final SpellAbility sa) { final Card source = sa.getHostCard(); final Game game = ai.getGame(); final Combat combat = game.getCombat(); @@ -40,17 +37,17 @@ public class PumpAllAi extends PumpAiBase { PhaseHandler ph = ai.getGame().getPhaseHandler(); if (!(ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai) || (!ph.getPlayerTurn().equals(ai) && ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (ComputerUtil.preventRunAwayActivations(sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (abCost != null && source.hasSVar("AIPreference")) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, true)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -60,13 +57,13 @@ public class PumpAllAi extends PumpAiBase { if (sa.canTarget(opp) && sa.isCurse()) { sa.resetTargets(); sa.getTargets().add(opp); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if (sa.canTarget(ai) && !sa.isCurse()) { sa.resetTargets(); sa.getTargets().add(ai); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } @@ -100,7 +97,7 @@ public class PumpAllAi extends PumpAiBase { || phase.isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()) || game.getReplacementHandler().isPreventCombatDamageThisTurn()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } int totalPower = 0; for (Card c : human) { @@ -110,47 +107,57 @@ public class PumpAllAi extends PumpAiBase { totalPower += Math.min(c.getNetPower(), power * -1); if (phase == PhaseType.COMBAT_DECLARE_BLOCKERS && combat.isUnblocked(c)) { if (ComputerUtilCombat.lifeInDanger(sa.getActivatingPlayer(), combat)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } totalPower += Math.min(c.getNetPower(), power * -1); } if (totalPower >= power * -2) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // -X/-0 end if (comp.isEmpty() && ComputerUtil.activateForCost(sa, ai)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // evaluate both lists and pass only if human creatures are more valuable - return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human); + boolean result = (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // end Curse if (!game.getStack().isEmpty()) { - return pumpAgainstRemoval(ai, sa, comp); + boolean result = pumpAgainstRemoval(ai, sa, comp); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return ai.getCreaturesInPlay().anyMatch(c -> c.isValid(valid, source.getController(), source, sa) + boolean result = ai.getCreaturesInPlay().anyMatch(c -> c.isValid(valid, source.getController(), source, sa) && ComputerUtilCard.shouldPumpCard(ai, sa, c, defense, power, keywords)); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // pumpAllCanPlayAI() @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { - return true; + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { // it might help so take it if (!sa.usesTargeting() && !sa.isCurse() && sa.hasParam("ValidCards") && sa.getParam("ValidCards").contains("YouCtrl")) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // important to call canPlay first so targets are added if needed - return canPlayAI(ai, sa) || mandatory; + AiAbilityDecision decision = canPlayAI(ai, sa); + if (decision == null) { + return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + if (mandatory && decision.getDecision() != AiPlayDecision.WillPlay) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + return decision; } boolean pumpAgainstRemoval(Player ai, SpellAbility sa, List comp) { diff --git a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java index bdce6b58dd1..2d17ed4988b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java @@ -23,7 +23,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList final PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); final Card source = sa.getHostCard(); @@ -33,13 +33,13 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { && (sa.getPayCosts().hasTapCost() || sa.getPayCosts().hasManaCost())) { // If it has an associated cost, try to only do this before own turn if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } // Do it once per turn, generally (may be improved later) if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -61,7 +61,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { } else if (canTgtHuman) { sa.getTargets().add(opp); } else { - return false; // could not find a valid target + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); // could not find a valid target } if (!canTgtHuman || !canTgtAI) { @@ -73,16 +73,26 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi { AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { // Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList - return canPlayAI(ai, sa) || mandatory; + + AiAbilityDecision decision = canPlayAI(ai, sa); + if (decision.willingToPlay()) { + return decision; + } + + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + + return decision; } /* (non-Javadoc) 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 fc8dd326e04..49f09226190 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RegenerateAi.java @@ -17,10 +17,7 @@ */ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCombat; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.Game; import forge.game.GameObject; import forge.game.ability.AbilityUtils; @@ -123,17 +120,15 @@ public class RegenerateAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - boolean chance = false; - + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + boolean chance; if (sa.usesTargeting()) { chance = regenMandatoryTarget(ai, sa, mandatory); } else { // If there's no target on the trigger, just say yes. chance = true; } - - return chance; + return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private static boolean regenMandatoryTarget(final Player ai, final SpellAbility sa, final boolean mandatory) { diff --git a/forge-ai/src/main/java/forge/ai/ability/RemoveFromCombatAi.java b/forge-ai/src/main/java/forge/ai/ability/RemoveFromCombatAi.java index 5d1abd1e177..007262a37d0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RemoveFromCombatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RemoveFromCombatAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -8,33 +10,34 @@ import forge.game.spellability.SpellAbility; public class RemoveFromCombatAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // disabled for the AI for now. Only for Gideon Jura at this time. - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // AI should only activate this during Human's turn if ("RemoveBestAttacker".equals(sa.getParam("AILogic"))) { - return aiPlayer.getGame().getCombat() != null && aiPlayer.getGame().getCombat().getDefenders().contains(aiPlayer); + boolean result = aiPlayer.getGame().getCombat() != null && aiPlayer.getGame().getCombat().getDefenders().contains(aiPlayer); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO - implement AI - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { boolean chance; // TODO - implement AI chance = false; - return chance; + return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java index 52205aecc40..3dd5acd4c0a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatAi.java @@ -17,27 +17,31 @@ import java.util.Map; public class RepeatAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); String logic = sa.getParamOrDefault("AILogic", ""); if (sa.usesTargeting()) { if (!sa.canTarget(opp)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.resetTargets(); sa.getTargets().add(opp); } if ("MaxX".equals(logic) || "MaxXAtOppEOT".equals(logic)) { if ("MaxXAtOppEOT".equals(logic) && !(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // Set PayX here to maximum value. final int max = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); sa.setXManaCostPaid(max); - return max > 0; + if (max <= 0) { + return new AiAbilityDecision(0, AiPlayDecision.CantAffordX); + } else { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override @@ -47,7 +51,7 @@ public class RepeatAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { String logic = sa.getParamOrDefault("AILogic", ""); if (sa.usesTargeting()) { @@ -65,9 +69,9 @@ public class RepeatAi extends SpellAbilityAi { if (best != null) { sa.resetTargets(); sa.getTargets().add(best); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); @@ -76,7 +80,7 @@ public class RepeatAi extends SpellAbilityAi { sa.resetTargets(); sa.getTargets().add(opp); } else if (!mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -85,10 +89,18 @@ public class RepeatAi extends SpellAbilityAi { final SpellAbility repeat = sa.getAdditionalAbility("RepeatSubAbility"); if (repeat == null) { - return mandatory; + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); - return aic.doTrigger(repeat, mandatory); + if (aic.doTrigger(repeat, mandatory)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java index 6d70c502c64..a409f3b44b8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.SpecialCardAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.AbilityUtils; import forge.game.card.*; import forge.game.phase.PhaseType; @@ -21,27 +19,31 @@ public class RepeatEachAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { String logic = sa.getParam("AILogic"); if ("PriceOfProgress".equals(logic)) { return SpecialCardAi.PriceOfProgress.consider(aiPlayer, sa); } else if ("Never".equals(logic)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if ("CloneAllTokens".equals(logic)) { List humTokenCreats = CardLists.filter(aiPlayer.getOpponents().getCreaturesInPlay(), CardPredicates.TOKEN); List compTokenCreats = aiPlayer.getTokensInPlay(); - return compTokenCreats.size() > humTokenCreats.size(); + if (compTokenCreats.size() > humTokenCreats.size()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if ("BalanceLands".equals(logic)) { if (aiPlayer.getLandsInPlay().size() >= 5) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } List opponents = aiPlayer.getOpponents(); for(Player opp : opponents) { if (opp.getLandsInPlay().size() < 4) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } else if ("AllPlayerLoseLife".equals(logic)) { @@ -58,7 +60,7 @@ public class RepeatEachAi extends SpellAbilityAi { // if playing it would cause AI to lose most life, don't do that if (lossYou + 5 > aiPlayer.getLife()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -76,21 +78,29 @@ public class RepeatEachAi extends SpellAbilityAi { } } } - // would not hit opponent, don't do that - return hitOpp; + + if (hitOpp) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else if ("EquipAll".equals(logic)) { if (aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1, aiPlayer)) { final CardCollection unequipped = CardLists.filter(aiPlayer.getCardsIn(ZoneType.Battlefield), card -> card.isEquipment() && card.getAttachedTo() != sa.getHostCard()); - return !unequipped.isEmpty(); + if (!unequipped.isEmpty()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // TODO Add some normal AI variability here - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/RestartGameAi.java b/forge-ai/src/main/java/forge/ai/ability/RestartGameAi.java index 78fcd5bacfd..33a2c4b0264 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RestartGameAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RestartGameAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtil; -import forge.ai.ComputerUtilCard; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.CardCollection; import forge.game.card.CardLists; import forge.game.player.Player; @@ -20,18 +18,18 @@ public class RestartGameAi extends SpellAbilityAi { * forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { if (ComputerUtil.aiLifeInDanger(ai, true, 0)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // check if enough good permanents will be available to be returned, so AI can "autowin" CardCollection exiled = CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Exile), "Permanent.nonAura+IsRemembered", ai, sa.getHostCard(), sa); if (ComputerUtilCard.evaluatePermanentList(exiled) > 20) { - 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/RevealAi.java b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java index e1f275ad9af..ed4bd3840d0 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; import forge.ai.AiPlayDecision; import forge.ai.PlayerControllerAi; import forge.game.ability.AbilityUtils; @@ -31,7 +32,7 @@ public class RevealAi extends RevealAiBase { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { // logic to see if it should reveal Miracle Card if (sa.hasParam("MiracleCost")) { final Card c = sa.getHostCard(); @@ -44,12 +45,14 @@ public class RevealAi extends RevealAiBase { spell = (Spell) spell.copyWithDefinedCost(new Cost(sa.getParam("MiracleCost"), false)); - if (AiPlayDecision.WillPlay == ((PlayerControllerAi) ai.getController()).getAi() - .canPlayFromEffectAI(spell, false, false)) { - return true; + AiPlayDecision decision = ((PlayerControllerAi) ai.getController()).getAi() + .canPlayFromEffectAI(spell, false, false); + + if (AiPlayDecision.WillPlay == decision) { + return new AiAbilityDecision(100, decision); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if ("Kefnet".equals(sa.getParam("AILogic"))) { @@ -58,7 +61,7 @@ public class RevealAi extends RevealAiBase { ); if (c == null || (!c.isInstant() && !c.isSorcery())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } for (SpellAbility s : c.getBasicSpells()) { Spell spell = (Spell) s.copy(ai); @@ -68,21 +71,21 @@ public class RevealAi extends RevealAiBase { // use hard coded reduce cost spell.putParam("ReduceCost", "2"); + AiPlayDecision decision = ((PlayerControllerAi) ai.getController()).getAi() + .canPlayFromEffectAI(spell, false, false); - if (AiPlayDecision.WillPlay == ((PlayerControllerAi) ai.getController()).getAi() - .canPlayFromEffectAI(spell, false, false)) { - return true; + if (AiPlayDecision.WillPlay == decision) { + return new AiAbilityDecision(100, decision); } } - return false; - + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!revealHandTargetAI(ai, sa, mandatory)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java b/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java index ea2f415866d..3a0e5d10722 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealAiBase.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.player.PlayerCollection; @@ -41,8 +43,8 @@ public abstract class RevealAiBase extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { revealHandTargetAI(ai, sa, false); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } 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 5935d301ce2..53bffcc3bea 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RevealHandAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.game.player.Player; import forge.game.spellability.SpellAbility; import forge.util.MyRandom; @@ -27,8 +29,11 @@ public class RevealHandAi extends RevealAiBase { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return revealHandTargetAI(ai, sa, mandatory); + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + if (revealHandTargetAI(ai, sa, mandatory)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - } diff --git a/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java index e079cbb09ed..b2d6657c25c 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RollDiceAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.Game; import forge.game.card.Card; @@ -15,7 +17,7 @@ import java.util.Map; public class RollDiceAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { Card source = sa.getHostCard(); Game game = aiPlayer.getGame(); PhaseHandler ph = game.getPhaseHandler(); @@ -23,25 +25,30 @@ public class RollDiceAi extends SpellAbilityAi { String logic = sa.getParamOrDefault("AILogic", ""); if (logic.equals("Combat")) { - return ph.inCombat() && ((game.getCombat().isAttacking(source) && game.getCombat().isUnblocked(source)) || game.getCombat().isBlocking(source)); + boolean result = ph.inCombat() && ((game.getCombat().isAttacking(source) && game.getCombat().isUnblocked(source)) || game.getCombat().isBlocking(source)); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("CombatEarly")) { - return ph.inCombat() && (game.getCombat().isAttacking(source) || game.getCombat().isBlocking(source)); + boolean result = ph.inCombat() && (game.getCombat().isAttacking(source) || game.getCombat().isBlocking(source)); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("Main2")) { - return ph.is(PhaseType.MAIN2, aiPlayer); + boolean result = ph.is(PhaseType.MAIN2, aiPlayer); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (logic.equals("AtOppEOT")) { - return ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN); + boolean result = ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (cost != null && (sa.getPayCosts().hasManaCost() || sa.getPayCosts().hasTapCost())) { - return ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN); + boolean result = ph.getNextTurn() == aiPlayer && ph.is(PhaseType.END_OF_TURN); + return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java index b466eaa2351..82bbc3a8b76 100644 --- a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java @@ -1,10 +1,7 @@ package forge.ai.ability; -import forge.ai.AiController; -import forge.ai.AiProps; -import forge.ai.PlayerControllerAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -18,17 +15,19 @@ public class RollPlanarDiceAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { if (ai.getGame().getActivePlanes() == null) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } for (Card c : ai.getGame().getActivePlanes()) { if (willRollOnPlane(ai, c)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } private boolean willRollOnPlane(Player ai, Card plane) { @@ -162,7 +161,7 @@ public class RollPlanarDiceAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // for potential implementation of drawback checks? return canPlayAI(aiPlayer, sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java index bdcf719dab8..ed3037a96be 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCost; import forge.ai.SpellAbilityAi; @@ -25,29 +27,31 @@ import java.util.Map; public class SacrificeAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { return sacrificeTgtAI(ai, sa, false); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { // AI should only activate this during Human's turn return sacrificeTgtAI(ai, sa, false); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - // Improve AI for triggers. If source is a creature with: - // When ETB, sacrifice a creature. Check to see if the AI has something to sacrifice + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + AiAbilityDecision decision = sacrificeTgtAI(ai, sa, mandatory); + if (decision.willingToPlay()) { + return decision; + } - // Eventually, we can call the trigger of ETB abilities with not - // mandatory as part of the checks to cast something - - return sacrificeTgtAI(ai, sa, mandatory) || mandatory; + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + return decision; } - private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa, boolean mandatory) { + private AiAbilityDecision sacrificeTgtAI(final Player ai, final SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); final boolean destroy = sa.hasParam("Destroy"); final String aiLogic = sa.getParamOrDefault("AILogic", ""); @@ -60,15 +64,15 @@ public class SacrificeAi extends SpellAbilityAi { if (mandatory && sa.canTarget(ai)) { sa.resetTargets(); sa.getTargets().add(ai); - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final Player opp = targetableOpps.max(PlayerPredicates.compareByLife()); sa.resetTargets(); sa.getTargets().add(opp); if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } String num = sa.getParamOrDefault("Amount" , "1"); final int amount = AbilityUtils.calculateAmount(source, num, sa); @@ -77,7 +81,7 @@ public class SacrificeAi extends SpellAbilityAi { for (Card c : list) { if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (!destroy) { @@ -85,12 +89,12 @@ public class SacrificeAi extends SpellAbilityAi { } else { if (!CardLists.getKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) { // human can choose to destroy indestructibles - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (list.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (num.equals("X") && sa.getSVar(num).equals("Count$xPaid")) { @@ -103,7 +107,7 @@ public class SacrificeAi extends SpellAbilityAi { // If the Human has at least half rounded up of the amount to be // sacrificed, cast the spell if (!sa.isTrigger() && list.size() < half) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -131,7 +135,7 @@ public class SacrificeAi extends SpellAbilityAi { // Since all of the cards have AI:RemoveDeck:All, I enabled 1 for 1 // (or X for X) trades for special decks - return humanList.size() >= amount; + return humanList.size() >= amount ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else if (defined.equals("You")) { List computerList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, sa.getActivatingPlayer(), source, sa); for (Card c : computerList) { @@ -149,16 +153,20 @@ public class SacrificeAi extends SpellAbilityAi { break; } } - return c.hasSVar("SacMe") || isLethal; + + if (c.hasSVar("SacMe") || isLethal) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } if (c.hasSVar("SacMe") || ComputerUtilCard.evaluateCreature(c) <= 135) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override @@ -166,9 +174,8 @@ public class SacrificeAi extends SpellAbilityAi { return true; } - public static boolean doSacOneEachLogic(Player ai, SpellAbility sa) { + public static AiAbilityDecision doSacOneEachLogic(Player ai, SpellAbility sa) { Game game = ai.getGame(); - sa.resetTargets(); for (Player p : game.getPlayers()) { CardCollection targetable = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa)); @@ -200,7 +207,7 @@ public class SacrificeAi extends SpellAbilityAi { } } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java index 9619c281426..b9d3aedc368 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAllAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCost; -import forge.ai.SpecialCardAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.card.Card; import forge.game.cost.Cost; import forge.game.player.Player; @@ -13,7 +10,7 @@ import forge.util.MyRandom; public class SacrificeAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, 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(); @@ -23,31 +20,37 @@ public class SacrificeAllAi extends SpellAbilityAi { if (abCost != null) { // AI currently disabled for some costs if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantAfford); } } if (logic.equals("HellionEruption")) { if (ai.getCreaturesInPlay().size() < 5 || ai.getCreaturesInPlay().size() * 150 < ComputerUtilCard.evaluateCreatureList(ai.getCreaturesInPlay())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (logic.equals("MadSarkhanDragon")) { return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa); } - if (!DestroyAllAi.doMassRemovalLogic(ai, sa)) { - return false; + AiAbilityDecision decision = DestroyAllAi.doMassRemovalLogic(ai, sa); + if (!decision.willingToPlay()) { + return decision; } // prevent run-away activations - first time will always return true boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); - return ((MyRandom.getRandom().nextFloat() < .9667) && chance); + if (MyRandom.getRandom().nextFloat() < .9667 && chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { //TODO: Add checks for bad outcome - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } } 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 18cf6b05dc3..e6246a8978f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java @@ -1,9 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCost; -import forge.ai.ComputerUtilMana; -import forge.ai.SpecialCardAi; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.ability.ApiType; import forge.game.card.Card; import forge.game.card.CardLists; @@ -24,7 +21,7 @@ public class ScryAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { // ability is targeted sa.resetTargets(); @@ -51,19 +48,27 @@ 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.CantPlayAi); } sa.getRootAbility().setXManaCostPaid(xPay); } - return mandatory || sa.isTargetNumberValid(); + if (mandatory) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } + + if (sa.isTargetNumberValid()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } // scryTargetAI() @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return doTriggerAINoCost(ai, sa, false); } 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 73b9f4e2daa..d4f1e1c0fe8 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.card.CardStateName; import forge.game.ability.AbilityUtils; import forge.game.card.*; @@ -51,9 +49,9 @@ public class SetStateAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { // Gross generalization, but this always considers alternate states more powerful - return !sa.getHostCard().isInAlternateState(); + return sa.getHostCard().isInAlternateState() ? new AiAbilityDecision(0, AiPlayDecision.CantPlayAi) : new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java index 50684e8874d..9bc58f90f63 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.phase.PhaseType; import forge.game.player.Player; @@ -10,20 +12,28 @@ import java.util.Map; public class ShuffleAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + // TODO Does the AI know what's on top of the deck and is it something useful? + // + String logic = sa.getParamOrDefault("AILogic", ""); if (logic.equals("Always")) { // We may want to play this for the subability, e.g. Mind's Desire - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (logic.equals("OwnMain2")) { - return aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer); + if (aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer)) { + // We may want to play this for the subability, e.g. Mind's Desire + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); + } } // not really sure when the compy would use this; maybe only after a human // deliberately put a card on top of their library - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); /* - * if (!ComputerUtil.canPayCost(sa)) return false; + * if (!ComputerUtil.canPayCost(sa)) return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); * * Card source = sa.getHostCard(); * @@ -38,20 +48,24 @@ public class ShuffleAi extends SpellAbilityAi { } @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return shuffleTargetAI(sa); } - private boolean shuffleTargetAI(final SpellAbility sa) { + private AiAbilityDecision shuffleTargetAI(final SpellAbility sa) { /* * Shuffle at the end of some other effect where we'd usually shuffle * inside that effect, but can't for some reason. */ - return sa.getParent() != null; + if (sa.getParent() != null) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } // shuffleTargetAI() @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { return shuffleTargetAI(sa); } diff --git a/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java b/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java index 500706b9c83..9face1ab00b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SkipPhaseAi.java @@ -1,5 +1,6 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; import forge.ai.AiAttackController; import forge.ai.SpellAbilityAi; import forge.game.player.Player; @@ -10,12 +11,12 @@ import java.util.Map; public class SkipPhaseAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { return targetPlayer(aiPlayer, sa, false); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { return targetPlayer(aiPlayer, sa, mandatory); } @@ -24,7 +25,7 @@ public class SkipPhaseAi extends SpellAbilityAi { return true; } - public boolean targetPlayer(Player ai, SpellAbility sa, boolean mandatory) { + private AiAbilityDecision targetPlayer(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai); sa.resetTargets(); @@ -38,9 +39,9 @@ public class SkipPhaseAi extends SpellAbilityAi { sa.getTargets().add(ai); } else { - return false; + return new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi); } } - return true; + return new AiAbilityDecision(100, forge.ai.AiPlayDecision.WillPlay); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/SkipTurnAi.java b/forge-ai/src/main/java/forge/ai/ability/SkipTurnAi.java index fb7f3415e1a..db654339ef1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SkipTurnAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SkipTurnAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -9,15 +11,19 @@ public class SkipTurnAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { - return "Always".equals(sa.getParam("AILogic")); + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { + if ("Always".equals(sa.getParam("AILogic"))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return canPlayAI(aiPlayer, sa); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java b/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java index 958f17bd91e..f385245896b 100644 --- a/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.cost.Cost; import forge.game.player.Player; @@ -10,12 +12,12 @@ import forge.util.collect.FCollectionView; public class StoreSVarAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { - return true; + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { if (sa instanceof WrappedAbility) { SpellAbility origSa = ((WrappedAbility)sa).getWrappedAbility(); if (origSa.getHostCard().getName().equals("Maralen of the Mornsong Avatar")) { @@ -23,7 +25,7 @@ public class StoreSVarAi extends SpellAbilityAi { } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @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 67277aadc0c..4a80d8eb364 100644 --- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java @@ -21,13 +21,12 @@ public class SurveilAi extends SpellAbilityAi { * @see forge.ai.SpellAbilityAi#doTriggerAINoCost(forge.game.player.Player, forge.game.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary? sa.resetTargets(); sa.getTargets().add(ai); } - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* @@ -35,7 +34,7 @@ public class SurveilAi extends SpellAbilityAi { * @see forge.ai.SpellAbilityAi#chkAIDrawback(forge.game.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { return doTriggerAINoCost(ai, sa, false); } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java index 7a2b6c2d8eb..f186fdb909f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java @@ -19,7 +19,7 @@ import forge.util.collect.FCollectionView; public class TapAi extends TapAiBase { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final PhaseHandler phase = ai.getGame().getPhaseHandler(); final Player turn = phase.getPlayerTurn(); @@ -32,20 +32,20 @@ public class TapAi extends TapAiBase { // Aggro Brains are willing to use TapEffects aggressively instead of defensively AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); if (!aic.getBooleanProperty(AiProps.PLAY_AGGRO)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { // Don't tap down after blockers - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else if (!playReusable(ai, sa)) { // Generally don't want to tap things with an Instant during Players turn outside of combat - 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.CantPlayAi); } final Card source = sa.getHostCard(); @@ -59,7 +59,7 @@ public class TapAi extends TapAiBase { } if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (!sa.usesTargeting()) { @@ -70,12 +70,18 @@ public class TapAi extends TapAiBase { untap = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); } - boolean bFlag = false; + int value = 0; for (final Card c : untap) { - bFlag |= c.isUntapped(); + if (c.isUntapped()) { + value += ComputerUtilCard.evaluateCreature(c); + } } - return bFlag; + if (value > 0) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { // X controls the minimum targets if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) { @@ -85,7 +91,11 @@ public class TapAi extends TapAiBase { } sa.resetTargets(); - return tapPrefTargeting(ai, source, sa, false); + if (tapPrefTargeting(ai, source, sa, false)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java index cd361036c00..d11228a9271 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java @@ -275,32 +275,40 @@ public abstract class TapAiBase extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); } final List pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); // might be from ETBreplacement - return pDefined.isEmpty() || !pDefined.get(0).isInPlay() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai); + if (pDefined.isEmpty() || !pDefined.get(0).isInPlay() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { sa.resetTargets(); if (tapPrefTargeting(ai, source, sa, mandatory)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (mandatory) { // not enough preferred targets, but mandatory so keep going: - return tapUnpreferredTargeting(ai, sa, mandatory); + if (tapUnpreferredTargeting(ai, sa, mandatory)) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { final Card source = sa.getHostCard(); final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer"); @@ -309,7 +317,11 @@ public abstract class TapAiBase extends SpellAbilityAi { Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); sa.setTargetingPlayer(targetingPlayer); sa.getTargets().clear(); - return targetingPlayer.getController().chooseTargetsFor(sa); + if (targetingPlayer.getController().chooseTargetsFor(sa)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } boolean randomReturn = true; @@ -318,11 +330,10 @@ public abstract class TapAiBase extends SpellAbilityAi { // target section, maybe pull this out? sa.resetTargets(); if (!tapPrefTargeting(ai, source, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return randomReturn; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } - } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java index 1e54900c19e..a9a0424f91d 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapAllAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCombat; import forge.ai.SpellAbilityAi; import forge.game.Game; @@ -21,7 +23,7 @@ import java.util.List; public class TapAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(final Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(final Player ai, SpellAbility sa) { // If tapping all creatures do it either during declare attackers of AIs turn // or during upkeep/begin combat? @@ -30,7 +32,7 @@ public class TapAllAi extends SpellAbilityAi { final Game game = ai.getGame(); if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final String valid = sa.getParamOrDefault("ValidCards", ""); @@ -51,31 +53,31 @@ public class TapAllAi extends SpellAbilityAi { if (logic.startsWith("AtLeast")) { int num = AbilityUtils.calculateAmount(source, logic.substring(7), sa); if (validTappables.size() < num) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } } if (MyRandom.getRandom().nextFloat() > Math.pow(.6667, sa.getActivationsThisTurn())) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (validTappables.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final List human = CardLists.filterControlledBy(validTappables, opp); final List compy = CardLists.filterControlledBy(validTappables, ai); if (human.size() <= compy.size()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } // in AI's turn, check if there are possible attackers, before tapping blockers if (game.getPhaseHandler().isPlayerTurn(ai)) { validTappables = ai.getCardsIn(ZoneType.Battlefield); final boolean any = validTappables.anyMatch(c -> CombatUtil.canAttack(c) && ComputerUtilCombat.canAttackNextTurn(c)); - return any; + return any ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } private CardCollectionView getTapAllTargets(final String valid, final Card source, SpellAbility sa) { @@ -87,7 +89,7 @@ public class TapAllAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) { final Card source = sa.getHostCard(); final String valid = sa.getParamOrDefault("ValidCards", ""); @@ -106,7 +108,7 @@ public class TapAllAi extends SpellAbilityAi { } if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } boolean rr = false; @@ -118,9 +120,9 @@ public class TapAllAi extends SpellAbilityAi { final int human = CardLists.count(validTappables, CardPredicates.isControlledByAnyOf(ai.getYourTeam())); final int compy = CardLists.count(validTappables, CardPredicates.isControlledByAnyOf(ai.getOpponents())); if (human > compy) { - return rr; + return rr ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java index e052b4737c9..54fea9bb00f 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.game.ability.AbilityUtils; import forge.game.card.Card; import forge.game.player.Player; @@ -12,7 +14,7 @@ public class TapOrUntapAi extends TapAiBase { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card source = sa.getHostCard(); boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); @@ -27,16 +29,20 @@ public class TapOrUntapAi extends TapAiBase { } if (!bFlag) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } else { sa.resetTargets(); if (!tapPrefTargeting(ai, source, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAllAi.java index 28461b07c81..2c85e552473 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TapOrUntapAllAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -14,10 +16,10 @@ public class TapOrUntapAllAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // Only Turnabout currently uses this, it's hardcoded to always return false // Looks like Faces of the Past could also use this - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java b/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java index 70def99ebfe..deba2d58ee4 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; import com.google.common.collect.Iterables; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtil; import forge.ai.SpellAbilityAi; import forge.game.card.Card; @@ -17,12 +19,17 @@ import java.util.Map; public class TimeTravelAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { boolean hasSuspendedCards = aiPlayer.getCardsIn(ZoneType.Exile).anyMatch(CardPredicates.hasSuspend()); boolean hasRelevantCardsOTB = aiPlayer.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.hasCounter(CounterEnumType.TIME)); - // TODO: add more logic for cards which may need it - return hasSuspendedCards || hasRelevantCardsOTB; + if (hasSuspendedCards || hasRelevantCardsOTB) { + // If there are cards with Time counters, we can play this ability + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + // No cards to add/remove Time counters from, so don't play this ability + 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 da00d007f2f..7c1893852e7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java @@ -76,7 +76,7 @@ public class TokenAi extends SpellAbilityAi { final AbilitySub sub = sa.getSubAbility(); // useful // no token created - return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkAIDrawback(sub, ai)); // planeswalker plus ability or sub-ability is + return pwPlus || (sub != null && SpellApiToAi.Converter.get(sub).chkAIDrawback(sub, ai).willingToPlay()); // planeswalker plus ability or sub-ability is } String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString()); @@ -252,7 +252,7 @@ public class TokenAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { Card actualToken = spawnToken(ai, sa); final TargetRestrictions tgt = sa.getTargetRestrictions(); @@ -260,13 +260,18 @@ public class TokenAi extends SpellAbilityAi { sa.resetTargets(); if (actualToken.getType().hasSubtype("Role")) { - return tgtRoleAura(ai, sa, actualToken, mandatory); + if (tgtRoleAura(ai, sa, actualToken, mandatory)) { + // Targeting handled in tgtRoleAura + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); + } } if (tgt.canOnlyTgtOpponent()) { PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); if (mandatory && targetableOpps.isEmpty()) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } Player opp = targetableOpps.min(PlayerPredicates.compareByLife()); sa.getTargets().add(opp); @@ -290,23 +295,27 @@ public class TokenAi extends SpellAbilityAi { } } if (x <= 0 && !mandatory) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (mandatory) { // Necessary because the AI goes into this method twice, first to set up targets (with mandatory=true) // and then the second time to confirm the trigger (where mandatory may be set to false). - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } if ("OnlyOnAlliedAttack".equals(sa.getParam("AILogic"))) { Combat combat = ai.getGame().getCombat(); - return combat != null && combat.getAttackingPlayer() != null - && !combat.getAttackingPlayer().isOpponentOf(ai); + if (combat != null && combat.getAttackingPlayer() != null + && !combat.getAttackingPlayer().isOpponentOf(ai)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } /* (non-Javadoc) * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String) diff --git a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java index 248602264a7..5b954e4fbc7 100644 --- a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; import forge.ai.AiAttackController; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -15,7 +17,7 @@ import java.util.List; public class TwoPilesAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { final Card card = sa.getHostCard(); ZoneType zone = null; @@ -32,7 +34,7 @@ public class TwoPilesAi extends SpellAbilityAi { if (sa.canTarget(opp)) { sa.getTargets().add(opp); } else { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } @@ -49,6 +51,10 @@ public class TwoPilesAi extends SpellAbilityAi { } pool = CardLists.getValidCards(pool, valid, card.getController(), card, sa); int size = pool.size(); - return size > 2; + if (size > 2) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } diff --git a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java index 09bba16dd2f..114a53c85f2 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java @@ -1,8 +1,6 @@ package forge.ai.ability; -import forge.ai.ComputerUtilCard; -import forge.ai.ComputerUtilCost; -import forge.ai.SpellAbilityAi; +import forge.ai.*; import forge.game.GameObject; import forge.game.ability.AbilityUtils; import forge.game.card.Card; @@ -20,7 +18,7 @@ public class UnattachAllAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { // prevent run-away activations - first time will always return true boolean chance = MyRandom.getRandom().nextFloat() <= .9; @@ -33,7 +31,7 @@ public class UnattachAllAi extends SpellAbilityAi { final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); if (xPay == 0) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } sa.setXManaCostPaid(xPay); @@ -41,17 +39,21 @@ public class UnattachAllAi extends SpellAbilityAi { if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) && !"Curse".equals(sa.getParam("AILogic"))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return chance; + if (chance) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { final Card card = sa.getHostCard(); // Check if there are any valid targets List targets = new ArrayList<>(); @@ -63,21 +65,25 @@ public class UnattachAllAi extends SpellAbilityAi { Card newTarget = (Card) targets.get(0); //don't equip opponent creatures if (!newTarget.getController().equals(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } //don't equip a worse creature if (card.isEquipping()) { Card oldTarget = card.getEquipping(); - return ComputerUtilCard.evaluateCreature(oldTarget) <= ComputerUtilCard.evaluateCreature(newTarget); + if (ComputerUtilCard.evaluateCreature(oldTarget) <= ComputerUtilCard.evaluateCreature(newTarget)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { // AI should only activate this during Human's turn return canPlayAI(ai, sa); } 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 818edf8aca4..04c51e8848a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java @@ -70,41 +70,53 @@ public class UntapAi extends SpellAbilityAi { } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { if (!sa.usesTargeting()) { if (mandatory) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("Never".equals(sa.getParam("AILogic"))) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } final List pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), 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)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } } else { if (untapPrefTargeting(ai, sa, mandatory)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if (mandatory) { // not enough preferred targets, but mandatory so keep going: - return untapUnpreferredTargeting(sa, mandatory); + if (untapUnpreferredTargeting(sa, mandatory)) { + return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - public boolean chkAIDrawback(SpellAbility sa, Player ai) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { boolean randomReturn = true; if (!sa.usesTargeting()) { // who cares if its already untapped, it's only a subability? } else { if (!untapPrefTargeting(ai, sa, false)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } - return randomReturn; + if (randomReturn) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } /** diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java index a16f0cfc763..c72b2f7e656 100644 --- a/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.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.ApiType; import forge.game.card.Card; @@ -14,35 +16,39 @@ import forge.game.zone.ZoneType; public class UntapAllAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { final Card source = sa.getHostCard(); final AbilitySub abSub = sa.getSubAbility(); if (abSub != null) { if (ApiType.AddPhase == abSub.getApi() && source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_END)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.TAPPED); final String valid = sa.getParamOrDefault("ValidCards", ""); list = CardLists.getValidCards(list, valid, source.getController(), source, sa); // don't untap if only opponent benefits - return list.anyMatch(CardPredicates.isControlledByAnyOf(aiPlayer.getYourTeam())); + if (list.anyMatch(CardPredicates.isControlledByAnyOf(aiPlayer.getYourTeam()))) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { Card source = sa.getHostCard(); if (sa.hasParam("ValidCards")) { String valid = sa.getParam("ValidCards"); CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.TAPPED); list = CardLists.getValidCards(list, valid, source.getController(), source, sa); - return mandatory || !list.isEmpty(); + return (mandatory || !list.isEmpty()) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } - return mandatory; + return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/VentureAi.java b/forge-ai/src/main/java/forge/ai/ability/VentureAi.java index 904ce2e3278..5c29443ce4a 100644 --- a/forge-ai/src/main/java/forge/ai/ability/VentureAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/VentureAi.java @@ -1,6 +1,7 @@ package forge.ai.ability; import com.google.common.collect.Lists; +import forge.ai.AiAbilityDecision; import forge.ai.AiPlayDecision; import forge.ai.PlayerControllerAi; import forge.ai.SpellAbilityAi; @@ -16,23 +17,37 @@ import java.util.Map; public class VentureAi extends SpellAbilityAi { @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // If this has a mana cost, do it at opponent's EOT if able to prevent spending mana early; if sorcery, do it in Main2 PhaseHandler ph = aiPlayer.getGame().getPhaseHandler(); if (sa.getPayCosts().hasManaCost() || sa.getPayCosts().hasTapCost()) { if (isSorcerySpeed(sa, aiPlayer)) { - return ph.is(PhaseType.MAIN2, aiPlayer); + if (ph.is(PhaseType.MAIN2, aiPlayer)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } else { - return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer; + if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } } } - - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return mandatory || canPlayAI(aiPlayer, sa); + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + if (mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } + AiAbilityDecision decision = canPlayAI(aiPlayer, sa); + if (decision == null) { + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); + } + return decision; } @Override @@ -46,20 +61,20 @@ public class VentureAi extends SpellAbilityAi { List viableRooms = Lists.newArrayList(); for (SpellAbility room : spells) { - if (player.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player? + if (player.getController().isAI()) { room.setActivatingPlayer(player); - if (((PlayerControllerAi)player.getController()).getAi().canPlaySa(room) == AiPlayDecision.WillPlay) { + AiPlayDecision playDecision = ((PlayerControllerAi)player.getController()).getAi().canPlaySa(room); + if (playDecision == AiPlayDecision.WillPlay) { viableRooms.add(room); } } } if (!viableRooms.isEmpty()) { - // choose a room at random from the ones that are deemed playable return Aggregates.random(viableRooms); } - return Aggregates.random(spells); // If we're here, we should choose at least something, so choose a random thing then + return Aggregates.random(spells); } } diff --git a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java index c31126beb28..c23852a5568 100644 --- a/forge-ai/src/main/java/forge/ai/ability/VoteAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/VoteAi.java @@ -1,6 +1,8 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.SpellAbilityAi; import forge.game.card.Card; import forge.game.card.CardLists; @@ -16,32 +18,40 @@ public class VoteAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player aiPlayer, SpellAbility sa) { // TODO: add ailogic String logic = sa.getParam("AILogic"); final Card host = sa.getHostCard(); if ("Always".equals(logic)) { - return true; + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } else if ("Judgment".equals(logic)) { - return !CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield), - sa.getParam("VoteCard"), host.getController(), host, sa).isEmpty(); + if (!CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield), + sa.getParam("VoteCard"), host.getController(), host, sa).isEmpty()) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards); + } } else if ("Torture".equals(logic)) { - return aiPlayer.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.MAIN1); + if (aiPlayer.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.MAIN1)) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2); + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player) */ @Override - public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { + public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { return canPlayAI(aiPlayer, sa); } @Override - protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } @Override diff --git a/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java index ff061ff580a..a04ccbf0131 100644 --- a/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java @@ -1,5 +1,7 @@ package forge.ai.ability; +import forge.ai.AiAbilityDecision; +import forge.ai.AiPlayDecision; import forge.ai.ComputerUtilCard; import forge.ai.SpellAbilityAi; import forge.game.ability.AbilityUtils; @@ -17,7 +19,7 @@ public class ZoneExchangeAi extends SpellAbilityAi { * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) */ @Override - protected boolean canPlayAI(Player ai, final SpellAbility sa) { + protected AiAbilityDecision canPlayAI(Player ai, final SpellAbility sa) { Card object1 = null; Card object2 = null; final Card source = sa.getHostCard(); @@ -35,25 +37,29 @@ public class ZoneExchangeAi extends SpellAbilityAi { } object2 = ComputerUtilCard.getBestAI(list); if (object1 == null || object2 == null || !object1.isInZone(zone1) || !object1.getOwner().equals(ai)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } if (type.equals("Aura")) { Card c = object1.getEnchantingCard(); if (!c.canBeAttached(object2, sa)) { - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } } if (object2.getCMC() > object1.getCMC()) { - return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); + if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); + } else { + return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations); + } } - return false; + return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } /* (non-Javadoc) * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean) */ @Override - protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { - return true; + protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { + return new AiAbilityDecision(100, AiPlayDecision.WillPlay); } }