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);
}
}