Convert checkApiLogic to AiAbilityDecision

This commit is contained in:
Chris H
2025-07-27 23:48:50 -04:00
parent dce3b4b142
commit df40fd5cb3
50 changed files with 835 additions and 593 deletions

View File

@@ -5,6 +5,8 @@ public enum AiPlayDecision {
WillPlay, WillPlay,
MandatoryPlay, MandatoryPlay,
PlayToEmptyHand, PlayToEmptyHand,
ImpactCombat,
ResponseToStackResolve,
AddBoardPresence, AddBoardPresence,
Removal, Removal,
Tempo, Tempo,
@@ -22,14 +24,18 @@ public enum AiPlayDecision {
CantPlayAi, CantPlayAi,
CantAfford, CantAfford,
CantAffordX, CantAffordX,
DoesntImpactCombat,
DoesntImpactGame,
MissingLogic, MissingLogic,
MissingNeededCards, MissingNeededCards,
TimingRestrictions, TimingRestrictions,
MissingPhaseRestrictions, MissingPhaseRestrictions,
ConditionsNotMet,
NeedsToPlayCriteriaNotMet, NeedsToPlayCriteriaNotMet,
StopRunawayActivations, StopRunawayActivations,
TargetingFailed, TargetingFailed,
CostNotAcceptable, CostNotAcceptable,
LifeInDanger,
WouldDestroyLegend, WouldDestroyLegend,
WouldDestroyOtherPlaneswalker, WouldDestroyOtherPlaneswalker,
WouldBecomeZeroToughnessCreature, WouldBecomeZeroToughnessCreature,
@@ -39,7 +45,7 @@ public enum AiPlayDecision {
public boolean willingToPlay() { public boolean willingToPlay() {
return switch (this) { return switch (this) {
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, Removal, Tempo, CardAdvantage -> true; case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
default -> false; default -> false;
}; };
} }

View File

@@ -1819,18 +1819,18 @@ public class ComputerUtilCard {
* @param sa Pump* or CounterPut* * @param sa Pump* or CounterPut*
* @return * @return
*/ */
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) { public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true); final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
for (final Card card : cards) { for (final Card card : cards) {
if (objects.contains(card)) { if (objects.contains(card)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
} }
} }
// For pumps without targeting restrictions, just return immediately until this is fleshed out. // For pumps without targeting restrictions, just return immediately until this is fleshed out.
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
@@ -1849,11 +1849,11 @@ public class ComputerUtilCard {
} }
if (!sa.isTargetNumberValid()) { if (!sa.isTargetNumberValid()) {
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
public static boolean isUselessCreature(Player ai, Card c) { public static boolean isUselessCreature(Player ai, Card c) {

View File

@@ -214,7 +214,7 @@ public class SpecialAiLogic {
} }
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat) // A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) { public static AiAbilityDecision doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS); final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
@@ -222,14 +222,14 @@ public class SpecialAiLogic {
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1); final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
if (numOtherCreats == 0) { if (numOtherCreats == 0) {
// Cut short if there's nothing to sac at all // Cut short if there's nothing to sac at all
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
} }
// Check if the standard Aristocrats logic applies first (if in the right conditions for it) // Check if the standard Aristocrats logic applies first (if in the right conditions for it)
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source); final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
if (isDeclareBlockers || isThreatened) { if (isDeclareBlockers || isThreatened) {
if (doAristocratLogic(ai, sa)) { if (doAristocratLogic(ai, sa)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -247,7 +247,7 @@ public class SpecialAiLogic {
if (countersSa == null) { if (countersSa == null) {
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?) // Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!"); System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!");
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
} }
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -263,7 +263,7 @@ public class SpecialAiLogic {
relevantCreats.remove(source); relevantCreats.remove(source);
if (relevantCreats.isEmpty()) { if (relevantCreats.isEmpty()) {
// No relevant creatures to sac // No relevant creatures to sac
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa); int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
@@ -287,16 +287,20 @@ public class SpecialAiLogic {
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat)) || (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat))
); );
if (!forcedSacTgts.isEmpty()) { if (!forcedSacTgts.isEmpty()) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs); final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) { if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
return source.getNetCombatDamage() < lethalDmg if (source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg; && source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} else { } else {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else { } else {
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose // We have already attacked. Thus, see if we have a creature to sac that is worse to lose
@@ -309,7 +313,7 @@ public class SpecialAiLogic {
); );
if (sacTgts.isEmpty()) { if (sacTgts.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source); final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
@@ -317,7 +321,10 @@ public class SpecialAiLogic {
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower); final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature // Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT; if (source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else { } else {
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances // We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
@@ -329,7 +336,11 @@ public class SpecialAiLogic {
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card) || ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
); );
return !sacFodder.isEmpty(); if (sacFodder.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }

View File

@@ -400,7 +400,7 @@ public class SpecialCardAi {
// Donate // Donate
public static class Donate { public static class Donate {
public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) { public static AiAbilityDecision considerTargetingOpponent(final Player ai, final SpellAbility sa) {
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter( final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) { if (donateTarget != null) {
@@ -410,7 +410,7 @@ public class SpecialCardAi {
// All opponents have hexproof or something like that // All opponents have hexproof or something like that
if (Iterables.isEmpty(oppList)) { if (Iterables.isEmpty(oppList)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
// filter for player who does not have donate target already // filter for player who does not have donate target already
@@ -428,12 +428,11 @@ public class SpecialCardAi {
if (opp != null) { if (opp != null) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(opp);
return true;
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// No targets found to donate, so do nothing. // No targets found to donate, so do nothing.
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) { public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) {
@@ -452,7 +451,7 @@ public class SpecialCardAi {
// Electrostatic Pummeler // Electrostatic Pummeler
public static class ElectrostaticPummeler { public static class ElectrostaticPummeler {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
Game game = ai.getGame(); Game game = ai.getGame();
Combat combat = game.getCombat(); Combat combat = game.getCombat();
@@ -465,13 +464,13 @@ public class SpecialCardAi {
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) { if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop); int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop);
if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg) if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg)
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
// Do not activate if damage will be prevented // Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) { if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
} }
// Activate Electrostatic Pummeler's pump only as a combat trick // Activate Electrostatic Pummeler's pump only as a combat trick
@@ -480,14 +479,14 @@ public class SpecialCardAi {
// We'll try to deal lethal trample/unblocked damage, so remember the card for attack // We'll try to deal lethal trample/unblocked damage, so remember the card for attack
// and wait until declare blockers step. // and wait until declare blockers step.
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS); AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { } else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) { if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
boolean isBlocking = combat.isBlocking(source); boolean isBlocking = combat.isBlocking(source);
@@ -512,11 +511,11 @@ public class SpecialCardAi {
} }
if (totalDamageToPW >= oppT + loyalty) { if (totalDamageToPW >= oppT + loyalty) {
// Already enough damage to take care of the planeswalker // Already enough damage to take care of the planeswalker
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) { if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
// Can pump to kill the planeswalker, go for it // Can pump to kill the planeswalker, go for it
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
} }
@@ -537,31 +536,31 @@ public class SpecialCardAi {
// We can deal a lot of damage (either a lot of damage directly to the opponent, // We can deal a lot of damage (either a lot of damage directly to the opponent,
// or kill the blocker(s) and damage the opponent at the same time, so go for it // or kill the blocker(s) and damage the opponent at the same time, so go for it
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS); AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
} }
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) { if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
// Can't survive first strike or double strike, don't pump // Can't survive first strike or double strike, don't pump
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) { if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) {
// Can't pump enough to kill the blockers and survive, don't pump // Can't pump enough to kill the blockers and survive, don't pump
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) { if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
// Already enough to kill the blockers and survive, don't overpump // Already enough to kill the blockers and survive, don't overpump
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage() if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage()
&& predictedPT.getLeft() <= oppT) { && predictedPT.getLeft() <= oppT) {
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump // Can't kill or cripple anyone, as well as can't Trample over, so don't pump
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
// If we got here, it should be a favorable combat pump, resulting in at least one // If we got here, it should be a favorable combat pump, resulting in at least one
// opposing creature dying, and hopefully with the Pummeler surviving combat. // opposing creature dying, and hopefully with the Pummeler surviving combat.
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) { public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
@@ -693,13 +692,13 @@ public class SpecialCardAi {
// Gideon Blackblade // Gideon Blackblade
public static class GideonBlackblade { public static class GideonBlackblade {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
sa.resetTargets(); sa.resetTargets();
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa)); CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
if (!otb.isEmpty()) { if (!otb.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(otb)); sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -930,12 +929,12 @@ public class SpecialCardAi {
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll) // Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
public static class LivingDeath { public static class LivingDeath {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
// if there's another reanimator card currently suspended, don't cast a new one until the previous // if there's another reanimator card currently suspended, don't cast a new one until the previous
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End) // one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
for (Card ex : ai.getCardsIn(ZoneType.Exile)) { for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) { if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
@@ -946,7 +945,7 @@ public class SpecialCardAi {
if (aiCreaturesInGY.isEmpty()) { if (aiCreaturesInGY.isEmpty()) {
// nothing in graveyard, so cut short // nothing in graveyard, so cut short
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
for (Card c : ai.getCreaturesInPlay()) { for (Card c : ai.getCreaturesInPlay()) {
@@ -978,17 +977,30 @@ public class SpecialCardAi {
} }
// if we get more value out of this than our opponent does (hopefully), go for it // if we get more value out of this than our opponent does (hopefully), go for it
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold); if ((aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
} }
// Maze's End // Maze's End
public static class MazesEnd { public static class MazesEnd {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler(); PhaseHandler ph = ai.getGame().getPhaseHandler();
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate")); CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty(); if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (availableGates.isEmpty()) {
// No gates available, so don't activate Maze's End
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
} }
public static Card considerCardToGet(final Player ai, final SpellAbility sa) public static Card considerCardToGet(final Player ai, final SpellAbility sa)
@@ -1146,13 +1158,13 @@ public class SpecialCardAi {
// Necropotence // Necropotence
public static class Necropotence { public static class Necropotence {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
Game game = ai.getGame(); Game game = ai.getGame();
int computerHandSize = ai.getZone(ZoneType.Hand).size(); int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize(); int maxHandSize = ai.getMaxHandSize();
if (ai.getCardsIn(ZoneType.Library).isEmpty()) { if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
return false; // nothing to draw from the library return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) { if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
@@ -1160,7 +1172,7 @@ public class SpecialCardAi {
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead? // TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
// (not sure how to detect the presence of such effects yet) // (not sure how to detect the presence of such effects yet)
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
PhaseHandler ph = game.getPhaseHandler(); PhaseHandler ph = game.getPhaseHandler();
@@ -1182,23 +1194,33 @@ public class SpecialCardAi {
// We're in a situation when we have nothing castable in hand, something needs to be done // We're in a situation when we have nothing castable in hand, something needs to be done
if (!blackViseOTB) { if (!blackViseOTB) {
// exile-loot +1 card when at max hand size, hoping to get a workable spell or land // exile-loot +1 card when at max hand size, hoping to get a workable spell or land
return computerHandSize + exiledWithNecro - 1 == maxHandSize; if (computerHandSize + exiledWithNecro - 1 == maxHandSize) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else { } else {
// Loot to 7 in presence of Black Vise, hoping to find what to do // Loot to 7 in presence of Black Vise, hoping to find what to do
// NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead? // NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead?
return computerHandSize + exiledWithNecro <= maxHandSize; if (computerHandSize + exiledWithNecro <= maxHandSize) {
// Loot to 7, hoping to find something playable
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// Loot to 8, hoping to find something playable
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
} else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) { } else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) {
// try not to overdraw in presence of Black Vise // try not to overdraw in presence of Black Vise
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) { } else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) {
// Only draw until we reach max hand size // Only draw until we reach max hand size
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) { } else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) {
// Only activate in AI's own turn (sans the exception above) // Only activate in AI's own turn (sans the exception above)
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -1480,7 +1502,7 @@ public class SpecialCardAi {
// Sorin, Vengeful Bloodlord // Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord { public static class SorinVengefulBloodlord {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY); int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES CardPredicates.CREATURES
@@ -1494,7 +1516,7 @@ public class SpecialCardAi {
CardLists.sortByCmcDesc(creaturesToGet); CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) { if (creaturesToGet.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// pick the best creature that will stay on the battlefield // pick the best creature that will stay on the battlefield
@@ -1510,10 +1532,10 @@ public class SpecialCardAi {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(best); sa.getTargets().add(best);
sa.setXManaCostPaid(best.getCMC()); sa.setXManaCostPaid(best.getCMC());
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} }
@@ -1627,23 +1649,27 @@ public class SpecialCardAi {
// The One Ring // The One Ring
public static class TheOneRing { public static class TheOneRing {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) { if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD); int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN); int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger if (ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1; && ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.LifeInDanger);
} }
} }
// The Scarab God // The Scarab God
public static class TheScarabGod { public static class TheScarabGod {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES)); Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES)); Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
@@ -1654,13 +1680,19 @@ public class SpecialCardAi {
sa.getTargets().add(worstOwnCreat); sa.getTargets().add(worstOwnCreat);
} }
return sa.getTargets().size() > 0; if (!sa.getTargets().isEmpty()) {
// If we have a target, we can play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// No valid targets, can't play this ability
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
} }
// Timetwister // Timetwister
public static class Timetwister { public static class Timetwister {
public static boolean consider(final Player ai, final SpellAbility sa) { public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size(); final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
int maxOppHandSize = 0; int maxOppHandSize = 0;
@@ -1674,7 +1706,14 @@ public class SpecialCardAi {
} }
// use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand // use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand
return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD; if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) {
// if the AI has less than 3 cards in hand or the opponent has more than 3 cards in hand than the AI
// then the AI is willing to play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// otherwise, don't play this ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
} }

View File

@@ -95,14 +95,15 @@ public abstract class SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions); return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
} }
if (!checkApiLogic(ai, sa)) { AiAbilityDecision decision = checkApiLogic(ai, sa);
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); if (!decision.willingToPlay()) {
return decision;
} }
// needs to be after API logic because needs to check possible X Cost? // needs to be after API logic because needs to check possible X Cost?
if (cost != null && !willPayCosts(ai, sa, cost, source)) { if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
} }
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return decision;
} }
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) { protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
@@ -169,12 +170,18 @@ public abstract class SpellAbilityAi {
/** /**
* The rest of the logic not covered by the canPlayAI template is defined here * The rest of the logic not covered by the canPlayAI template is defined here
*/ */
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
return MyRandom.getRandom().nextFloat() < .8f; if (MyRandom.getRandom().nextFloat() < .8f) {
// 80% chance to play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// 20% chance to not play the ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) { public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {

View File

@@ -1,5 +1,7 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -16,7 +18,7 @@ import java.util.Map;
public class AlterAttributeAi extends SpellAbilityAi { public class AlterAttributeAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true")); boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true"));
String[] attributes = sa.getParam("Attributes").split(","); String[] attributes = sa.getParam("Attributes").split(",");
@@ -24,7 +26,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
// TODO add targeting logic // TODO add targeting logic
// needed for Suspected // needed for Suspected
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
@@ -36,7 +38,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
case "Solved": case "Solved":
// there is currently no effect that would un-solve something // there is currently no effect that would un-solve something
if (!c.isSolved() && activate) { if (!c.isSolved() && activate) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
break; break;
case "Suspect": case "Suspect":
@@ -44,21 +46,21 @@ public class AlterAttributeAi extends SpellAbilityAi {
// is Suspected good or bad? // is Suspected good or bad?
// currently Suspected is better // currently Suspected is better
if (!activate) { if (!activate) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
case "Saddle": case "Saddle":
case "Saddled": case "Saddled":
// AI should not try to Saddle again? // AI should not try to Saddle again?
if (c.isSaddled()) { if (c.isSaddled()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override

View File

@@ -21,13 +21,19 @@ import java.util.Map;
public class AmassAi extends SpellAbilityAi { public class AmassAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army"); CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army");
Card host = sa.getHostCard(); Card host = sa.getHostCard();
final Game game = ai.getGame(); final Game game = ai.getGame();
if (!aiArmies.isEmpty()) { if (!aiArmies.isEmpty()) {
return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1)); if (aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1))) {
// If AI has an Army that can receive counters, play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// AI has Armies but none can receive counters, so don't play
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
}
} }
final String type = sa.getParam("Type"); final String type = sa.getParam("Type");
final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army"; final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army";
@@ -36,7 +42,7 @@ public class AmassAi extends SpellAbilityAi {
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false); Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
if (token == null) { if (token == null) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
token.setController(ai, 0); token.setController(ai, 0);
@@ -63,7 +69,11 @@ public class AmassAi extends SpellAbilityAi {
//reset static abilities //reset static abilities
game.getAction().checkStaticAbilities(false); game.getAction().checkStaticAbilities(false);
return result; if (result) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override
@@ -83,8 +93,11 @@ public class AmassAi extends SpellAbilityAi {
@Override @Override
protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean result = mandatory || checkApiLogic(ai, sa); if (mandatory) {
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return checkApiLogic(ai, sa);
} }
@Override @Override

View File

@@ -142,21 +142,22 @@ public class AnimateAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = aiPlayer.getGame(); final Game game = aiPlayer.getGame();
final PhaseHandler ph = game.getPhaseHandler(); final PhaseHandler ph = game.getPhaseHandler();
if (!sa.metConditions() && sa.getSubAbility() == null) { if (!sa.metConditions() && sa.getSubAbility() == null) {
return false; // what is this for? return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet); // what is this for?
} }
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) { if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
// Should I animate a card before i have to sacrifice something better?
if (!isAnimatedThisTurn(aiPlayer, source)) { if (!isAnimatedThisTurn(aiPlayer, source)) {
rememberAnimatedThisTurn(aiPlayer, source); rememberAnimatedThisTurn(aiPlayer, source);
return true; // interrupt sacrifice return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
} }
} }
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) { if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) {
return false; // prevent crewing with equal or better creatures return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable); // prevent crewing with equal or better creatures
} }
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) { if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
@@ -202,16 +203,16 @@ public class AnimateAi extends SpellAbilityAi {
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) { if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) { if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature // Do not try to crew a vehicle which is already a creature
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
Card animatedCopy = becomeAnimated(c, sa); Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer) if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) { && !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
if (ph.getPlayerTurn().isOpponentOf(aiPlayer) if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) { && !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
// also check if maybe there are static effects applied to the animated copy that would matter // also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized) // (e.g. Myth Realized)
@@ -227,11 +228,12 @@ public class AnimateAi extends SpellAbilityAi {
} }
if (bFlag) { if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard()); rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return bFlag; // All of the defined stuff is animated, not very useful return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else { } else {
sa.resetTargets(); sa.resetTargets();
return animateTgtAI(sa).willingToPlay(); return animateTgtAI(sa);
} }
} }

View File

@@ -52,11 +52,11 @@ public class AssembleContraptionAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) { if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) { if (xPay == 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
sa.getRootAbility().setXManaCostPaid(xPay); sa.getRootAbility().setXManaCostPaid(xPay);
} }
@@ -66,7 +66,7 @@ public class AssembleContraptionAi extends SpellAbilityAi {
if(target != null) if(target != null)
sa.getTargets().add(target); sa.getTargets().add(target);
else else
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return super.checkApiLogic(ai, sa); return super.checkApiLogic(ai, sa);

View File

@@ -1,9 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil; import forge.ai.*;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.game.Game; import forge.game.Game;
import forge.game.card.Card; import forge.game.card.Card;
@@ -21,7 +18,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Game game = sa.getHostCard().getGame(); final Game game = sa.getHostCard().getGame();
final SpellAbility topSa = game.getStack().isEmpty() ? null final SpellAbility topSa = game.getStack().isEmpty() ? null
: ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa); : ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
@@ -32,47 +29,50 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// The AI can't otherwise play this ability, but should at least not // The AI can't otherwise play this ability, but should at least not
// miss mandatory activations (e.g. triggers). // miss mandatory activations (e.g. triggers).
return sa.isMandatory(); if (sa.isMandatory()) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
private boolean doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) { private AiAbilityDecision doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
// For cards like Spellskite that retarget spells to itself // For cards like Spellskite that retarget spells to itself
if (topSa == null) { if (topSa == null) {
// nothing on stack, so nothing to target // nothing on stack, so nothing to target
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
final TargetChoices topTargets = topSa.getTargets(); final TargetChoices topTargets = topSa.getTargets();
final Card topHost = topSa.getHostCard(); final Card topHost = topSa.getHostCard();
if (sa.getTargets().size() != 0 && sa.isTrigger()) { if (!sa.getTargets().isEmpty() && sa.isTrigger()) {
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed // something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) { if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
// if this does not target at all or already targets host, no need to redirect it again // if this does not target at all or already targets host, no need to redirect it again
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
for (Card tgt : topTargets.getTargetCards()) { for (Card tgt : topTargets.getTargetCards()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) {
// We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites), // We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites),
// no need to retarget again to another one // no need to retarget again to another one
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) { if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
// make sure not to redirect our own abilities // make sure not to redirect our own abilities
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (!topSa.canTarget(sa.getHostCard())) { if (!topSa.canTarget(sa.getHostCard())) {
// don't try targeting it if we can't legally target the host card with it in the first place // don't try targeting it if we can't legally target the host card with it in the first place
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (!sa.canTarget(topSa)) { if (!sa.canTarget(topSa)) {
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face) // don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) { if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
@@ -85,22 +85,22 @@ public class ChangeTargetsAi extends SpellAbilityAi {
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
&& topTargets.contains(aiPlayer)) { && topTargets.contains(aiPlayer)) {
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life // do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
Card firstCard = topTargets.getFirstTargetedCard(); Card firstCard = topTargets.getFirstTargetedCard();
// if we're not the target don't intervene unless we can steal a buff // if we're not the target don't intervene unless we can steal a buff
if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) { if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
Player firstPlayer = topTargets.getFirstTargetedPlayer(); Player firstPlayer = topTargets.getFirstTargetedPlayer();
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) { if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(topSa); sa.getTargets().add(topSa);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }

View File

@@ -129,14 +129,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
// Checks for "return true" unlike checkAiLogic() // Checks for "return true" unlike checkAiLogic()
multipleCardsToChoose.clear(); multipleCardsToChoose.clear();
String aiLogic = sa.getParam("AILogic"); String aiLogic = sa.getParam("AILogic");
if (aiLogic != null) { if (aiLogic != null) {
if (aiLogic.equals("Always")) { if (aiLogic.equals("Always")) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc. } else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa); return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc. } else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -156,10 +156,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (aiLogic.equals("MazesEnd")) { } else if (aiLogic.equals("MazesEnd")) {
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa); return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
} else if (aiLogic.equals("Pongify")) { } else if (aiLogic.equals("Pongify")) {
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic if (sa.isTargetNumberValid()) {
// Pre-targeted in checkAiLogic
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (aiLogic.equals("ReturnCastable")) { } else if (aiLogic.equals("ReturnCastable")) {
return !sa.getHostCard().getExiledCards().isEmpty() if (!sa.getHostCard().getExiledCards().isEmpty()
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false); && ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
if (sa.isHidden()) { if (sa.isHidden()) {
@@ -258,7 +266,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean. * @return a boolean.
*/ */
private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) { private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and // Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana // have access to more mana
final Cost abCost = sa.getPayCosts(); final Cost abCost = sa.getPayCosts();
@@ -275,7 +283,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} catch (IllegalArgumentException ex) { } catch (IllegalArgumentException ex) {
// This happens when Origin is something like // This happens when Origin is something like
// "Graveyard,Library" (Doomsday) // "Graveyard,Library" (Doomsday)
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
final String destination = sa.getParam("Destination"); final String destination = sa.getParam("Destination");
@@ -284,11 +292,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
// AI currently disabled for these costs // AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa) if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
&& !(destination.equals("Battlefield") && !source.isLand())) { && !(destination.equals("Battlefield") && !source.isLand())) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
} }
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
} }
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) { if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
@@ -297,7 +305,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
CostDiscard cd = (CostDiscard) part; CostDiscard cd = (CostDiscard) part;
// this is mainly for typecycling // this is mainly for typecycling
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) { if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
} }
} }
} }
@@ -305,14 +313,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.isNinjutsu()) { if (sa.isNinjutsu()) {
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) { if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
} }
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) { if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
if (ai.getGame().getCombat() == null) { if (ai.getGame().getCombat() == null) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers(); List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
boolean lowerCMC = false; boolean lowerCMC = false;
@@ -323,7 +331,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
if (!lowerCMC) { if (!lowerCMC) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
} }
@@ -333,15 +341,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
final AbilitySub abSub = sa.getSubAbility(); final AbilitySub abSub = sa.getSubAbility();
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) { if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
if (!abSub.metConditions()) { if (!abSub.metConditions()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet);
} }
} else { } else {
return false; return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet);
} }
} }
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
Iterable<Player> pDefined = Lists.newArrayList(source.getController()); Iterable<Player> pDefined = Lists.newArrayList(source.getController());
@@ -355,7 +363,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} }
if (!sa.isTargetNumberValid()) { if (!sa.isTargetNumberValid()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
pDefined = sa.getTargets().getTargetPlayers(); pDefined = sa.getTargets().getTargetPlayers();
} else { } else {
@@ -399,12 +407,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
if (!activateForCost && list.isEmpty()) { if (!activateForCost && list.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if ("Atarka's Command".equals(sourceName) if ("Atarka's Command".equals(sourceName)
&& (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) { && (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) {
// be strict on playing lands off charms // be strict on playing lands off charms
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
String num = sa.getParamOrDefault("ChangeNum", "1"); String num = sa.getParamOrDefault("ChangeNum", "1");
@@ -412,55 +420,65 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.getSVar("X").equals("Count$xPaid")) { if (sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) return false; if (xPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
xPay = Math.min(xPay, list.size()); xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
} else { } else {
// Figure out the X amount, bail if it's zero (nothing will change zone). // Figure out the X amount, bail if it's zero (nothing will change zone).
int xValue = AbilityUtils.calculateAmount(source, "X", sa); int xValue = AbilityUtils.calculateAmount(source, "X", sa);
if (xValue == 0) { if (xValue == 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
} }
} }
} }
if (sourceName.equals("Temur Sabertooth")) { if (sourceName.equals("Temur Sabertooth")) {
// activated bounce + pump // activated bounce + pump
if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) || boolean pumpDecision = ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible"));
ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility())) { AiAbilityDecision saveDecision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility());
if (pumpDecision || saveDecision.willingToPlay()) {
for (Card c : list) { for (Card c : list) {
if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) { if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
} }
} }
} }
return canBouncePermanent(ai, sa, list) != null; if (canBouncePermanent(ai, sa, list) != null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
if (ComputerUtil.playImmediately(ai, sa)) { if (ComputerUtil.playImmediately(ai, sa)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// don't use fetching to top of library/graveyard before main2 // don't use fetching to top of library/graveyard before main2
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases")) { && !sa.hasParam("ActivationPhases")) {
if (!destination.equals("Battlefield") && !destination.equals("Hand")) { if (!destination.equals("Battlefield") && !destination.equals("Hand")) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// Only tutor something in main1 if hand is almost empty // Only tutor something in main1 if hand is almost empty
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand") if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")
&& !aiLogic.equals("AnyMainPhase")) { && !aiLogic.equals("AnyMainPhase")) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
if (ComputerUtil.waitForBlocking(sa)) { if (ComputerUtil.waitForBlocking(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
final AbilitySub subAb = sa.getSubAbility(); final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb).willingToPlay(); if (subAb == null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
} }
/** /**
@@ -681,7 +699,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean. * @return a boolean.
*/ */
private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility sa) { private static AiAbilityDecision knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Retrieve either this card, or target Cards in Graveyard // Retrieve either this card, or target Cards in Graveyard
final List<ZoneType> origin = Lists.newArrayList(); final List<ZoneType> origin = Lists.newArrayList();
@@ -694,19 +712,19 @@ public class ChangeZoneAi extends SpellAbilityAi {
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination")); final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (!isPreferredTarget(ai, sa, false, false)) { if (!isPreferredTarget(ai, sa, false, false)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} else { } else {
// non-targeted retrieval // non-targeted retrieval
final List<Card> retrieval = sa.knownDetermineDefined(sa.getParam("Defined")); final List<Card> retrieval = sa.knownDetermineDefined(sa.getParam("Defined"));
if (retrieval == null || retrieval.isEmpty()) { if (retrieval == null || retrieval.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// return this card from graveyard: cards like Hammer of Bogardan // return this card from graveyard: cards like Hammer of Bogardan
@@ -717,7 +735,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// (dying or losing control of) // (dying or losing control of)
if (origin.contains(ZoneType.Battlefield)) { if (origin.contains(ZoneType.Battlefield)) {
if (ai.getGame().getStack().isEmpty()) { if (ai.getGame().getStack().isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
final AbilitySub abSub = sa.getSubAbility(); final AbilitySub abSub = sa.getSubAbility();
@@ -730,7 +748,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!(destination.equals(ZoneType.Exile) if (!(destination.equals(ZoneType.Exile)
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic")))) && (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
&& !destination.equals(ZoneType.Hand)) { && !destination.equals(ZoneType.Hand)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa); final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa);
@@ -742,13 +760,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
if (!contains) { if (!contains) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
if (destination == ZoneType.Battlefield) { if (destination == ZoneType.Battlefield) {
if (ComputerUtil.isETBprevented(retrieval.get(0))) { if (ComputerUtil.isETBprevented(retrieval.get(0))) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// predict whether something may put a ETBing creature below zero toughness // predict whether something may put a ETBing creature below zero toughness
@@ -758,7 +776,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card copy = CardCopyService.getLKICopy(c); final Card copy = CardCopyService.getLKICopy(c);
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null); ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
if (copy.getNetToughness() <= 0) { if (copy.getNetToughness() <= 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
} }
@@ -772,13 +790,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
if (nothingWillReturn) { if (nothingWillReturn) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
} }
final AbilitySub subAb = sa.getSubAbility(); final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb).willingToPlay(); if (subAb == null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
} }
/* /*
@@ -1816,7 +1838,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return super.chooseSingleAttackableEntity(ai, sa, options, params); return super.chooseSingleAttackableEntity(ai, sa, options, params);
} }
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) { private AiAbilityDecision doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1]; String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
@@ -1835,14 +1857,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(bestRet); sa.getTargets().add(bestRet);
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC()); source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) { private AiAbilityDecision doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
PhaseHandler ph = ai.getGame().getPhaseHandler(); PhaseHandler ph = ai.getGame().getPhaseHandler();
String logic = sa.getParam("AILogic"); String logic = sa.getParam("AILogic");
@@ -1850,7 +1872,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!ph.is(PhaseType.MAIN2)) { if (!ph.is(PhaseType.MAIN2)) {
// Should be given a chance to cast other spells as well as to use a previously upgraded creature // Should be given a chance to cast other spells as well as to use a previously upgraded creature
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
} }
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1]; String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
@@ -1889,12 +1911,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!listGoal.isEmpty()) { if (!listGoal.isEmpty()) {
// make sure we're upgrading sacCMC->goalCMC // make sure we're upgrading sacCMC->goalCMC
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC); source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
// no candidates to upgrade return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return false;
} }
public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) { public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {

View File

@@ -69,11 +69,9 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa); computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
if ("LivingDeath".equals(aiLogic)) { if ("LivingDeath".equals(aiLogic)) {
boolean result = SpecialCardAi.LivingDeath.consider(ai, sa); return SpecialCardAi.LivingDeath.consider(ai, sa);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("Timetwister".equals(aiLogic)) { } else if ("Timetwister".equals(aiLogic)) {
boolean result = SpecialCardAi.Timetwister.consider(ai, sa); return SpecialCardAi.Timetwister.consider(ai, sa);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("RetDiscardedThisTurn".equals(aiLogic)) { } else if ("RetDiscardedThisTurn".equals(aiLogic)) {
boolean result = !ai.getDiscardedThisTurn().isEmpty() && 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); return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);

View File

@@ -18,7 +18,7 @@ import java.util.Map;
public class CharmAi extends SpellAbilityAi { public class CharmAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa); List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
@@ -70,10 +70,10 @@ public class CharmAi extends SpellAbilityAi {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null // Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(sa, choices, ai, true, num, min); chosenList = chooseOptionsAi(sa, choices, ai, true, num, min);
if (chosenList.isEmpty() && min != 0) { if (chosenList.isEmpty() && min != 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else { } else {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
@@ -81,7 +81,7 @@ public class CharmAi extends SpellAbilityAi {
sa.setChosenList(chosenList); sa.setChosenList(chosenList);
if (choiceForOpp) { if (choiceForOpp) {
return true; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (sa.isSpell()) { if (sa.isSpell()) {
@@ -90,7 +90,11 @@ public class CharmAi extends SpellAbilityAi {
} }
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) { private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {

View File

@@ -26,19 +26,19 @@ public class ChooseCardAi extends SpellAbilityAi {
* The rest of the logic not covered by the canPlayAI template is defined here * The rest of the logic not covered by the canPlayAI template is defined here
*/ */
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
// search targetable Opponents // search targetable Opponents
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa)); final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) { if (oppList.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
sa.getTargets().add(Iterables.getFirst(oppList, null)); sa.getTargets().add(Iterables.getFirst(oppList, null));
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
/** /**
@@ -140,11 +140,7 @@ public class ChooseCardAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (checkApiLogic(ai, sa)) { return checkApiLogic(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {

View File

@@ -43,8 +43,13 @@ public class ChooseGenericAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
return sa.hasParam("AILogic"); if (sa.hasParam("AILogic")) {
// This is equivilant to what was here before but feels bad
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/* (non-Javadoc) /* (non-Javadoc)
@@ -52,10 +57,14 @@ public class ChooseGenericAi extends SpellAbilityAi {
*/ */
@Override @Override
public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) { public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player aiPlayer) {
boolean result = sa.isTrigger() AiAbilityDecision decision;
? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()).willingToPlay() if (sa.isTrigger()) {
: checkApiLogic(aiPlayer, sa); decision = doTriggerAINoCost(aiPlayer, sa, sa.isMandatory());
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); } else {
decision = checkApiLogic(aiPlayer, sa);
}
return decision;
} }
@Override @Override
@@ -73,8 +82,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
AiAbilityDecision superDecision = super.doTriggerAINoCost(aiPlayer, sa, mandatory); return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
return superDecision;
} }
@Override @Override

View File

@@ -42,14 +42,17 @@ public class ClashAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
boolean legalAction = true; boolean legalAction = true;
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
legalAction = selectTarget(ai, sa); legalAction = selectTarget(ai, sa);
if (!legalAction) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
return legalAction; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
/* /*

View File

@@ -19,20 +19,25 @@ import java.util.Map;
public class CountersMoveAi extends SpellAbilityAi { public class CountersMoveAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
AiAbilityDecision decision = new AiAbilityDecision(100, AiPlayDecision.WillPlay);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
AiAbilityDecision decision = moveTgtAI(ai, sa); decision = moveTgtAI(ai, sa);
if (!decision.willingToPlay()) { if (!decision.willingToPlay()) {
return false; return decision;
} }
} }
if (!playReusable(ai, sa)) { if (!playReusable(ai, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return MyRandom.getRandom().nextFloat() < .8f; // random success if (MyRandom.getRandom().nextFloat() < .8f) {
return decision;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override

View File

@@ -19,7 +19,7 @@ import java.util.Map;
public class CountersMultiplyAi extends SpellAbilityAi { public class CountersMultiplyAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final CounterType counterType = getCounterType(sa); final CounterType counterType = getCounterType(sa);
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
@@ -51,7 +51,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}); });
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
} else { } else {
return setTargets(ai, sa); return setTargets(ai, sa);
@@ -87,8 +87,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!sa.usesTargeting()) { if (!sa.usesTargeting()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (setTargets(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); AiAbilityDecision decision = setTargets(ai, sa);
if (decision.willingToPlay()) {
return decision;
} else if (mandatory) { } else if (mandatory) {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -98,7 +100,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
.filter(CardPredicates.hasCounters().negate()) .filter(CardPredicates.hasCounters().negate())
.findFirst().orElse(null); .findFirst().orElse(null);
sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch); sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} }
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
@@ -116,7 +118,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
return null; return null;
} }
private boolean setTargets(Player ai, SpellAbility sa) { private AiAbilityDecision setTargets(Player ai, SpellAbility sa) {
final CounterType counterType = getCounterType(sa); final CounterType counterType = getCounterType(sa);
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -172,10 +174,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
// targeting does failed // targeting does failed
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) { if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list, private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list,

View File

@@ -16,7 +16,7 @@ import java.util.Map;
public class CountersProliferateAi extends SpellAbilityAi { public class CountersProliferateAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final List<Card> cperms = Lists.newArrayList(); final List<Card> cperms = Lists.newArrayList();
boolean allyExpOrEnergy = false; boolean allyExpOrEnergy = false;
@@ -68,7 +68,13 @@ public class CountersProliferateAi extends SpellAbilityAi {
})); }));
} }
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy; if (!cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy) {
// AI will play it if there are any counters to proliferate
// or if there are no counters, but AI has experience or energy counters
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override
@@ -92,11 +98,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (checkApiLogic(ai, sa)) { return checkApiLogic(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/* /*

View File

@@ -118,7 +118,7 @@ public class CountersPutAi extends CountersAi {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex // AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be // based on what the expected targets could be
final Cost abCost = sa.getPayCosts(); final Cost abCost = sa.getPayCosts();
@@ -159,7 +159,7 @@ public class CountersPutAi extends CountersAi {
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9)); PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9));
if (!poisonList.isEmpty()) { if (!poisonList.isEmpty()) {
sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife())); sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife()));
return true; return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
} }
} }
@@ -175,7 +175,7 @@ public class CountersPutAi extends CountersAi {
Card best = ComputerUtilCard.getBestAI(oppCreatM1); Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) { if (best != null) {
sa.getTargets().add(best); sa.getTargets().add(best);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa); CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
@@ -195,7 +195,7 @@ public class CountersPutAi extends CountersAi {
best = ComputerUtilCard.getBestAI(aiCreat); best = ComputerUtilCard.getBestAI(aiCreat);
if (best != null) { if (best != null) {
sa.getTargets().add(best); sa.getTargets().add(best);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -204,28 +204,28 @@ public class CountersPutAi extends CountersAi {
if (!ai.getCounters().isEmpty()) { if (!ai.getCounters().isEmpty()) {
if (!eachExisting || ai.getPoisonCounters() < 5) { if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if ("Never".equals(logic)) { if ("Never".equals(logic)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("AlwaysWithNoTgt".equals(logic)) { } else if ("AlwaysWithNoTgt".equals(logic)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("AristocratCounters".equals(logic)) { } else if ("AristocratCounters".equals(logic)) {
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa);
} else if ("PayEnergy".equals(logic)) { } else if ("PayEnergy".equals(logic)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("PayEnergyConservatively".equals(logic)) { } else if ("PayEnergyConservatively".equals(logic)) {
boolean onlyInCombat = ai.getController().isAI() boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT); && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
@@ -234,10 +234,10 @@ public class CountersPutAi extends CountersAi {
if (playAggro) { if (playAggro) {
// aggro profiles ignore conservative play for this AI logic // aggro profiles ignore conservative play for this AI logic
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (ph.inCombat() && source != null) { } else if (ph.inCombat() && source != null) {
if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) { if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) {
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} else if (ai.getGame().getCombat().isBlocking(source)) { } else if (ai.getGame().getCombat().isBlocking(source)) {
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker // when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(source); CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(source);
@@ -247,7 +247,7 @@ public class CountersPutAi extends CountersAi {
int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount(); int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
if (source.getNetToughness() + numActivations > totBlkPower if (source.getNetToughness() + numActivations > totBlkPower
|| source.getNetPower() + numActivations >= totBlkToughness) { || source.getNetPower() + numActivations >= totBlkToughness) {
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
} }
} else if (sa.getSubAbility() != null } else if (sa.getSubAbility() != null
@@ -257,18 +257,18 @@ public class CountersPutAi extends CountersAi {
// Bristling Hydra: save from death using a ping activation // Bristling Hydra: save from death using a ping activation
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) { if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) { } else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
// outside of combat, this logic only works if the relevant AI profile option is enabled // outside of combat, this logic only works if the relevant AI profile option is enabled
// and if there is enough energy saved // and if there is enough energy saved
if (!onlyInCombat) { if (!onlyInCombat) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} else if (logic.equals("MarkOppCreature")) { } else if (logic.equals("MarkOppCreature")) {
if (!ph.is(PhaseType.END_OF_TURN)) { if (!ph.is(PhaseType.END_OF_TURN)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
} }
Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type)); Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type));
@@ -280,12 +280,12 @@ public class CountersPutAi extends CountersAi {
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats); Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(bestCreat); sa.getTargets().add(bestCreat);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else if (logic.equals("CheckDFC")) { } else if (logic.equals("CheckDFC")) {
// for cards like Ludevic's Test Subject // for cards like Ludevic's Test Subject
if (!source.canTransform(null)) { if (!source.canTransform(null)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else if (logic.startsWith("MoveCounter")) { } else if (logic.startsWith("MoveCounter")) {
return doMoveCounterLogic(ai, sa, ph); return doMoveCounterLogic(ai, sa, ph);
@@ -294,8 +294,15 @@ public class CountersPutAi extends CountersAi {
if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) { if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) {
// don't use this for mana until after combat // don't use this for mana until after combat
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2); AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
return new AiAbilityDecision(25, AiPlayDecision.WaitForMain2);
} }
return willActivate;
if (willActivate) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (logic.equals("ChargeToBestCMC")) { } else if (logic.equals("ChargeToBestCMC")) {
return doChargeToCMCLogic(ai, sa); return doChargeToCMCLogic(ai, sa);
} else if (logic.equals("ChargeToBestOppControlledCMC")) { } else if (logic.equals("ChargeToBestOppControlledCMC")) {
@@ -305,14 +312,14 @@ public class CountersPutAi extends CountersAi {
} }
if (!sa.metConditions() && sa.getSubAbility() == null) { if (!sa.metConditions() && sa.getSubAbility() == null) {
return false; return new AiAbilityDecision(100, AiPlayDecision.ConditionsNotMet);
} }
if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility()); CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility());
if (!prot.isEmpty()) { if (!prot.isEmpty()) {
sa.getTargets().add(prot.get(0)); sa.getTargets().add(prot.get(0));
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
} }
@@ -320,13 +327,13 @@ public class CountersPutAi extends CountersAi {
CardCollection creatsYouCtrl = ai.getCreaturesInPlay(); CardCollection creatsYouCtrl = ai.getCreaturesInPlay();
List<Card> leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness); List<Card> leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness);
if (leastToughness.isEmpty()) { if (leastToughness.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
// TODO If Creature that would be Bolstered for some reason is useless, also return False // TODO If Creature that would be Bolstered for some reason is useless, also return False
} }
if (sa.hasParam("Monstrosity") && source.isMonstrous()) { if (sa.hasParam("Monstrosity") && source.isMonstrous()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// TODO handle proper calculation of X values based on Cost // TODO handle proper calculation of X values based on Cost
@@ -341,7 +348,7 @@ public class CountersPutAi extends CountersAi {
Combat combat = game.getCombat(); Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) { if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat); return doCombatAdaptLogic(source, amount, combat);
} }
@@ -368,12 +375,12 @@ public class CountersPutAi extends CountersAi {
// This will "rewind" clockwork cards when they fall to 50% power or below, consider improving // This will "rewind" clockwork cards when they fall to 50% power or below, consider improving
if (curCtrs > Math.ceil(maxCtrs / 2.0)) { if (curCtrs > Math.ceil(maxCtrs / 2.0)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
amount = Math.min(amount, maxCtrs - curCtrs); amount = Math.min(amount, maxCtrs - curCtrs);
if (amount <= 0) { if (amount <= 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
@@ -385,14 +392,14 @@ public class CountersPutAi extends CountersAi {
.mapToInt(Card::getCMC) .mapToInt(Card::getCMC)
.max().orElse(0); .max().orElse(0);
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) { if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
// don't use it if no counters to add // don't use it if no counters to add
if (amount <= 0) { if (amount <= 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if ("Polukranos".equals(logic)) { if ("Polukranos".equals(logic)) {
@@ -419,20 +426,20 @@ public class CountersPutAi extends CountersAi {
} }
} }
if (!canSurvive) { if (!canSurvive) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
found = true; found = true;
break; break;
} }
if (!found) { if (!found) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
if ("AtOppEOT".equals(logic)) { if ("AtOppEOT".equals(logic)) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) { if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -443,17 +450,19 @@ public class CountersPutAi extends CountersAi {
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) { if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target // only evaluates case where all tokens are placed on a single target
if (sa.getMinTargets() < 2) { if (sa.getMinTargets() < 2) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) { AiAbilityDecision decision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa);
if (decision.willingToPlay()) {
Card c = sa.getTargetCard(); Card c = sa.getTargetCard();
if (sa.getTargets().size() > 1) { if (sa.getTargets().size() > 1) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(c); sa.getTargets().add(c);
} }
sa.addDividedAllocation(c, amount); sa.addDividedAllocation(c, amount);
return true; return decision;
} else { } else {
if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies if (!hasSacCost) {
return false; // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
} }
} }
} }
@@ -497,7 +506,7 @@ public class CountersPutAi extends CountersAi {
} }
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) { if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow), // Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
@@ -506,9 +515,9 @@ public class CountersPutAi extends CountersAi {
&& sa.isPwAbility() && sa.isPwAbility()
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class) && sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
&& sa.isTargetNumberValid() && sa.isTargetNumberValid()
&& sa.getTargets().size() == 0 && sa.getTargets().isEmpty()
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) { && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (sourceName.equals("Abzan Charm")) { if (sourceName.equals("Abzan Charm")) {
@@ -530,11 +539,11 @@ public class CountersPutAi extends CountersAi {
} }
} }
if (left == 0) { if (left == 0) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
sa.resetTargets(); sa.resetTargets();
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
// target loop // target loop
@@ -542,7 +551,7 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) { if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else { } else {
// TODO is this good enough? for up to amounts? // TODO is this good enough? for up to amounts?
break; break;
@@ -574,10 +583,9 @@ public class CountersPutAi extends CountersAi {
// check if other choice will already be played // check if other choice will already be played
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty(); increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
} }
if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|| ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) { || ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) {
if (abCost == null || abCost == Cost.Zero if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
|| (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
// only use at opponent EOT unless it is free // only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type); choice = chooseBoonTarget(list, type);
} }
@@ -591,7 +599,7 @@ public class CountersPutAi extends CountersAi {
if (choice == null) { // can't find anything left if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) { if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else { } else {
// TODO is this good enough? for up to amounts? // TODO is this good enough? for up to amounts?
break; break;
@@ -607,14 +615,14 @@ public class CountersPutAi extends CountersAi {
choice = null; choice = null;
} }
if (sa.getTargets().isEmpty()) { if (sa.getTargets().isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} else { } else {
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
// Don't activate Curse abilities on my cards and non-curse abilities // Don't activate Curse abilities on my cards and non-curse abilities
// on my opponents // on my opponents
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) { if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
final int currCounters = cards.get(0).getCounters(CounterType.get(type)); final int currCounters = cards.get(0).getCounters(CounterType.get(type));
@@ -622,46 +630,46 @@ public class CountersPutAi extends CountersAi {
// activating this ability. // activating this ability.
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) { if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// Instant +1/+1 // Instant +1/+1
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) { if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) { if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return false; // only if next turn and cost is reusable return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
// Useless since the card already has the keyword (or for another reason) // Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) { if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
boolean immediately = ComputerUtil.playImmediately(ai, sa); boolean immediately = ComputerUtil.playImmediately(ai, sa);
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
} }
if (immediately) { if (immediately) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) { if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) {
// Don't use non P1P1/M1M1 counters before main 2 if possible // Don't use non P1P1/M1M1 counters before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) { if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
} }
if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) { if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
} }
} }
if (ComputerUtil.waitForBlocking(sa)) { if (ComputerUtil.waitForBlocking(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
@Override @Override
@@ -772,17 +780,25 @@ public class CountersPutAi extends CountersAi {
} }
if ("ChargeToBestCMC".equals(aiLogic)) { if ("ChargeToBestCMC".equals(aiLogic)) {
if (doChargeToCMCLogic(ai, sa) || mandatory) { AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) {
// If the AI logic is to charge to best CMC, we can return true // If the AI logic is to charge to best CMC, we can return true
// if the logic was successfully applied or if it's mandatory. // if the logic was successfully applied or if it's mandatory.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return decision;
} else if (mandatory) {
// If the logic was not applied and it's mandatory, we return false.
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else { } else {
// If the logic was not applied and it's not mandatory, we return false. // If the logic was not applied and it's not mandatory, we return false.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) { } else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
if (doChargeToOppCtrlCMCLogic(ai, sa) || mandatory) { AiAbilityDecision decision = doChargeToOppCtrlCMCLogic(ai, sa);
if (decision.willingToPlay()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (mandatory) {
// If the logic was not applied and it's mandatory, we return false.
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else { } else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@@ -830,8 +846,9 @@ public class CountersPutAi extends CountersAi {
if (type.equals("P1P1")) { if (type.equals("P1P1")) {
nPump = amount; nPump = amount;
} }
if (FightAi.canFightAi(ai, sa, nPump, nPump)) { AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); if (decision.willingToPlay()) {
return decision;
} }
} }
@@ -1135,7 +1152,7 @@ public class CountersPutAi extends CountersAi {
return Iterables.getFirst(options, null); return Iterables.getFirst(options, null);
} }
private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) { private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
// Spikes (Tempest) // Spikes (Tempest)
// Try not to do it unless at the end of opponent's turn or the creature is threatened // Try not to do it unless at the end of opponent's turn or the creature is threatened
@@ -1148,7 +1165,7 @@ public class CountersPutAi extends CountersAi {
|| (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat)))); || (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) { if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
return false; return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
} }
CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa); CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
@@ -1166,45 +1183,45 @@ public class CountersPutAi extends CountersAi {
if (bestTgt != null) { if (bestTgt != null) {
sa.getTargets().add(bestTgt); sa.getTargets().add(bestTgt);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) { private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) {
if (combat.isAttacking(source)) { if (combat.isAttacking(source)) {
if (!combat.isBlocked(source)) { if (!combat.isBlocked(source)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else { } else {
for (Card blockedBy : combat.getBlockers(source)) { for (Card blockedBy : combat.getBlockers(source)) {
if (blockedBy.getNetToughness() > source.getNetPower() if (blockedBy.getNetToughness() > source.getNetPower()
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) { && blockedBy.getNetToughness() <= source.getNetPower() + amount) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower); int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
if (source.getNetToughness() <= totBlkPower if (source.getNetToughness() <= totBlkPower
&& source.getNetToughness() + amount > totBlkPower) { && source.getNetToughness() + amount > totBlkPower) {
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
} }
} else if (combat.isBlocking(source)) { } else if (combat.isBlocking(source)) {
for (Card blocked : combat.getAttackersBlockedBy(source)) { for (Card blocked : combat.getAttackersBlockedBy(source)) {
if (blocked.getNetToughness() > source.getNetPower() if (blocked.getNetToughness() > source.getNetPower()
&& blocked.getNetToughness() <= source.getNetPower() + amount) { && blocked.getNetToughness() <= source.getNetPower() + amount) {
return true; return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} }
} }
int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower); int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower);
if (source.getNetToughness() <= totAtkPower if (source.getNetToughness() <= totAtkPower
&& source.getNetToughness() + amount > totAtkPower) { && source.getNetToughness() + amount > totAtkPower) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override
@@ -1215,7 +1232,7 @@ public class CountersPutAi extends CountersAi {
return max; return max;
} }
private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) { private AiAbilityDecision doChargeToCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES); CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
int numCtrs = source.getCounters(CounterEnumType.CHARGE); int numCtrs = source.getCounters(CounterEnumType.CHARGE);
@@ -1230,10 +1247,16 @@ public class CountersPutAi extends CountersAi {
optimalCMC = cmc; optimalCMC = cmc;
} }
} }
return numCtrs < optimalCMC; if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) { private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS); CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
int numCtrs = source.getCounters(CounterEnumType.CHARGE); int numCtrs = source.getCounters(CounterEnumType.CHARGE);
@@ -1247,6 +1270,12 @@ public class CountersPutAi extends CountersAi {
optimalCMC = cmc; optimalCMC = cmc;
} }
} }
return numCtrs < optimalCMC; if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
} }

View File

@@ -50,9 +50,13 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
return doTgt(ai, sa, false); if (doTgt(ai, sa, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
return super.checkApiLogic(ai, sa); return super.checkApiLogic(ai, sa);
} }

View File

@@ -75,7 +75,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final String type = sa.getParam("CounterType"); final String type = sa.getParam("CounterType");
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
@@ -85,14 +85,14 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!type.matches("Any") && !type.matches("All")) { if (!type.matches("Any") && !type.matches("All")) {
final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type)); final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type));
if (currCounters < 1) { if (currCounters < 1) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
return super.checkApiLogic(ai, sa); return super.checkApiLogic(ai, sa);
} }
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) { private AiAbilityDecision doTgt(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -105,7 +105,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa); CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
// Filter AI-specific targets if provided // Filter AI-specific targets if provided
@@ -123,7 +123,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardPredicates.hasCounter(CounterEnumType.ICE, 3)); CardPredicates.hasCounter(CounterEnumType.ICE, 3));
if (!depthsList.isEmpty()) { if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst()); sa.getTargets().add(depthsList.getFirst());
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -136,7 +136,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!planeswalkerList.isEmpty()) { if (!planeswalkerList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList)); sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else if (type.matches("Any")) { } else if (type.matches("Any")) {
// variable amount for Hex Parasite // variable amount for Hex Parasite
@@ -146,7 +146,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (manaLeft == 0) { if (manaLeft == 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
} }
amount = manaLeft; amount = manaLeft;
xPay = true; xPay = true;
@@ -168,7 +168,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) { if (xPay) {
sa.setXManaCostPaid(ice); sa.setXManaCostPaid(ice);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
@@ -187,7 +187,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) { if (xPay) {
sa.setXManaCostPaid(best.getCurrentLoyalty()); sa.setXManaCostPaid(best.getCurrentLoyalty());
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// some rules only for amount = 1 // some rules only for amount = 1
@@ -204,7 +204,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiM1M1List.isEmpty()) { if (!aiM1M1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// do as P1P1 part // do as P1P1 part
@@ -213,7 +213,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiUndyingList.isEmpty()) { if (!aiUndyingList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// TODO stun counters with canRemoveCounters check // TODO stun counters with canRemoveCounters check
@@ -224,7 +224,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardPredicates.hasCounter(CounterEnumType.P1P1)); CardPredicates.hasCounter(CounterEnumType.P1P1));
if (!oppP1P1List.isEmpty()) { if (!oppP1P1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// fallback to remove any counter from opponent // fallback to remove any counter from opponent
@@ -236,7 +236,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
for (final CounterType aType : best.getCounters().keySet()) { for (final CounterType aType : best.getCounters().keySet()) {
if (!ComputerUtil.isNegativeCounter(aType, best)) { if (!ComputerUtil.isNegativeCounter(aType, best)) {
sa.getTargets().add(best); sa.getTargets().add(best);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
@@ -257,7 +257,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiList.isEmpty()) { if (!aiList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else if (type.equals("P1P1")) { } else if (type.equals("P1P1")) {
// no special amount for that one yet // no special amount for that one yet
@@ -275,7 +275,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
} }
if (!aiList.isEmpty()) { if (!aiList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -289,7 +289,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!oppList.isEmpty()) { if (!oppList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList)); sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} else if (type.equals("TIME")) { } else if (type.equals("TIME")) {
@@ -300,7 +300,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (manaLeft == 0) { if (manaLeft == 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
} }
amount = manaLeft; amount = manaLeft;
xPay = true; xPay = true;
@@ -318,7 +318,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) { if (xPay) {
sa.setXManaCostPaid(timeCount); sa.setXManaCostPaid(timeCount);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
if (mandatory) { if (mandatory) {
@@ -327,7 +327,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT)); CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
if (!adaptCreats.isEmpty()) { if (!adaptCreats.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats)); sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// Outlast nice target // Outlast nice target
@@ -338,29 +338,27 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!betterTargets.isEmpty()) { if (!betterTargets.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets)); sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats)); sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
sa.getTargets().add(ComputerUtilCard.getWorstAI(list)); sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
@Override @Override
protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { protected AiAbilityDecision doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
boolean canTarget = doTgt(aiPlayer, sa, mandatory); return doTgt(aiPlayer, sa, mandatory);
return canTarget ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) return mandatory ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); : new AiAbilityDecision(0, AiPlayDecision.MandatoryPlay);
} }
/* /*
@@ -374,8 +372,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
GameEntity target = (GameEntity) params.get("Target"); GameEntity target = (GameEntity) params.get("Target");
CounterType type = (CounterType) params.get("CounterType"); CounterType type = (CounterType) params.get("CounterType");
if (target instanceof Card) { if (target instanceof Card targetCard) {
Card targetCard = (Card) target;
if (targetCard.getController().isOpponentOf(player)) { if (targetCard.getController().isOpponentOf(player)) {
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min; return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
} else { } else {
@@ -386,8 +383,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min; return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
} }
} else if (target instanceof Player) { } else if (target instanceof Player targetPlayer) {
Player targetPlayer = (Player) target;
if (targetPlayer.isOpponentOf(player)) { if (targetPlayer.isOpponentOf(player)) {
return !type.is(CounterEnumType.POISON) ? max : min; return !type.is(CounterEnumType.POISON) ? max : min;
} else { } else {

View File

@@ -21,11 +21,7 @@ import forge.util.collect.FCollectionView;
public class DestroyAi extends SpellAbilityAi { public class DestroyAi extends SpellAbilityAi {
@Override @Override
public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) {
if (checkApiLogic(ai, sa)) { return checkApiLogic(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override
@@ -107,7 +103,7 @@ public class DestroyAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final boolean noRegen = sa.hasParam("NoRegen"); final boolean noRegen = sa.hasParam("NoRegen");
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
@@ -115,7 +111,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list; CardCollection list;
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
// Targeting // Targeting
@@ -129,7 +125,7 @@ public class DestroyAi extends SpellAbilityAi {
// Assume there where already enough targets chosen by AI Logic Above // Assume there where already enough targets chosen by AI Logic Above
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) { if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// reset targets before AI Logic part // reset targets before AI Logic part
@@ -149,13 +145,17 @@ public class DestroyAi extends SpellAbilityAi {
if (maxTargets == 0) { if (maxTargets == 0) {
// can't afford X or otherwise target anything // can't afford X or otherwise target anything
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
} }
if (sa.hasParam("TargetingPlayer")) { if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0); Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer); sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa); if (targetingPlayer.getController().chooseTargetsFor(sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
// AI doesn't destroy own cards if it isn't defined in AI logic // AI doesn't destroy own cards if it isn't defined in AI logic
@@ -210,7 +210,7 @@ public class DestroyAi extends SpellAbilityAi {
// Try to avoid targeting creatures that are dead on board // Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa); list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
// target loop // target loop
@@ -225,7 +225,7 @@ public class DestroyAi extends SpellAbilityAi {
if (list.isEmpty()) { if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else { } else {
// TODO is this good enough? for up to amounts? // TODO is this good enough? for up to amounts?
break; break;
@@ -239,7 +239,7 @@ public class DestroyAi extends SpellAbilityAi {
if ("OppDestroyYours".equals(logic)) { if ("OppDestroyYours".equals(logic)) {
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay()); Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) { if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
} else if (CardLists.getNotType(list, "Land").isEmpty()) { } else if (CardLists.getNotType(list, "Land").isEmpty()) {
@@ -248,7 +248,7 @@ public class DestroyAi extends SpellAbilityAi {
if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) { if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) {
// Strip Mine, Wasteland - cut short if the relevant logic fails // Strip Mine, Wasteland - cut short if the relevant logic fails
if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) { if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
} else { } else {
@@ -258,14 +258,14 @@ public class DestroyAi extends SpellAbilityAi {
//option to hold removal instead only applies for single targeted removal //option to hold removal instead only applies for single targeted removal
if (!sa.isTrigger() && sa.getMaxTargets() == 1) { if (!sa.isTrigger() && sa.getMaxTargets() == 1) {
if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) { if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} }
if (choice == null) { // can't find anything left if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) { if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else { } else {
// TODO is this good enough? for up to amounts? // TODO is this good enough? for up to amounts?
break; break;
@@ -302,18 +302,18 @@ public class DestroyAi extends SpellAbilityAi {
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai) || !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|| ai.getLife() <= 5)) { || ai.getLife() <= 5)) {
// Basic ai logic for Lethal Vapors // Basic ai logic for Lethal Vapors
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("Always".equals(logic)) { } else if ("Always".equals(logic)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (list.isEmpty() if (list.isEmpty()
|| !CardLists.filterControlledBy(list, ai).isEmpty() || !CardLists.filterControlledBy(list, ai).isEmpty()
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) { || CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
@Override @Override

View File

@@ -13,12 +13,12 @@ import java.util.Map;
public class DiscoverAi extends SpellAbilityAi { public class DiscoverAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
return true; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/** /**
@@ -38,11 +38,7 @@ public class DiscoverAi extends SpellAbilityAi {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (checkApiLogic(ai, sa)) { return checkApiLogic(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override

View File

@@ -42,43 +42,46 @@ public class DrawAi extends SpellAbilityAi {
* @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility) * @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (!targetAI(ai, sa, false)) { if (!targetAI(ai, sa, false)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
final Player player = sa.getTargets().getFirstTargetedPlayer(); final Player player = sa.getTargets().getFirstTargetedPlayer();
if (player != null && player.isOpponentOf(ai)) { if (player != null && player.isOpponentOf(ai)) {
return true; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (ComputerUtil.playImmediately(ai, sa)) { if (ComputerUtil.playImmediately(ai, sa)) {
return true; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// Don't tap creatures that may be able to block // Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) { if (ComputerUtil.waitForBlocking(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
if (!canLoot(ai, sa)) { if (!canLoot(ai, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) { if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) {
// Canopy lands and other cards that sacrifice themselves to draw cards // Canopy lands and other cards that sacrifice themselves to draw cards
return ai.getCardsIn(ZoneType.Hand).isEmpty() if (ai.getCardsIn(ZoneType.Hand).isEmpty()
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile || (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5)) {
// TODO: make this configurable in the AI profile
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} }
return true; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/* /*

View File

@@ -275,7 +275,7 @@ public class EffectAi extends SpellAbilityAi {
} }
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("Fight")) { } else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0, 0) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return FightAi.canFightAi(ai, sa, 0,0);
} else if (logic.equals("Pump")) { } else if (logic.equals("Pump")) {
sa.resetTargets(); sa.resetTargets();
List<Card> options = CardUtil.getValidCardsToTarget(sa); List<Card> options = CardUtil.getValidCardsToTarget(sa);

View File

@@ -24,13 +24,13 @@ public class FightAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
sa.resetTargets(); sa.resetTargets();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
// everything is defined or targeted above, can't do anything there unless a specific logic is set // everything is defined or targeted above, can't do anything there unless a specific logic is set
if (sa.hasParam("Defined") && !sa.usesTargeting()) { if (sa.hasParam("Defined") && !sa.usesTargeting()) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// Get creature lists // Get creature lists
@@ -42,8 +42,10 @@ public class FightAi extends SpellAbilityAi {
// Filter MustTarget requirements // Filter MustTarget requirements
StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa); StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa);
if (humCreatures.isEmpty()) //prevent IndexOutOfBoundsException on MOJHOSTO variant
return false; //prevent IndexOutOfBoundsException on MOJHOSTO variant if (humCreatures.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
// assumes the triggered card belongs to the ai // assumes the triggered card belongs to the ai
if (sa.hasParam("Defined")) { if (sa.hasParam("Defined")) {
@@ -54,7 +56,7 @@ public class FightAi extends SpellAbilityAi {
} }
} }
if (fighter1List.isEmpty()) { if (fighter1List.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
Card fighter1 = fighter1List.get(0); Card fighter1 = fighter1List.get(0);
for (Card humanCreature : humCreatures) { for (Card humanCreature : humCreatures) {
@@ -62,10 +64,11 @@ public class FightAi extends SpellAbilityAi {
&& !canKill(humanCreature, fighter1, 0)) { && !canKill(humanCreature, fighter1, 0)) {
// todo: check min/max targets; see if we picked the best matchup // todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(humanCreature); sa.getTargets().add(humanCreature);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
return false; // bail at this point, otherwise the AI will overtarget and waste the activation // bail at this point, otherwise the AI will overtarget and waste the activation
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
if (sa.hasParam("TargetsFromDifferentZone")) { if (sa.hasParam("TargetsFromDifferentZone")) {
@@ -77,12 +80,12 @@ public class FightAi extends SpellAbilityAi {
// todo: check min/max targets; see if we picked the best matchup // todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(humanCreature); sa.getTargets().add(humanCreature);
sa.getTargets().add(aiCreature); sa.getTargets().add(aiCreature);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
for (Card creature1 : humCreatures) { for (Card creature1 : humCreatures) {
for (Card creature2 : humCreatures) { for (Card creature2 : humCreatures) {
@@ -97,11 +100,11 @@ public class FightAi extends SpellAbilityAi {
// todo: check min/max targets; see if we picked the best matchup // todo: check min/max targets; see if we picked the best matchup
sa.getTargets().add(creature1); sa.getTargets().add(creature1);
sa.getTargets().add(creature2); sa.getTargets().add(creature2);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
@Override @Override
@@ -110,11 +113,7 @@ public class FightAi extends SpellAbilityAi {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // e.g. Hunt the Weak, the AI logic was already checked through canFightAi
} }
if (checkApiLogic(aiPlayer, sa)) { return checkApiLogic(aiPlayer, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override
@@ -132,12 +131,14 @@ public class FightAi extends SpellAbilityAi {
} }
} }
if (checkApiLogic(ai, sa)) { AiAbilityDecision decision = checkApiLogic(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); if (decision.willingToPlay()) {
return decision;
} }
if (!mandatory) { if (!mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return decision;
} }
// if mandatory, we have to play it, so we will try to make a good trade or no trade
//try to make a good trade or no trade //try to make a good trade or no trade
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -153,19 +154,19 @@ public class FightAi extends SpellAbilityAi {
if (canKill(aiCreature, humanCreature, 0) if (canKill(aiCreature, humanCreature, 0)
&& ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) { && ComputerUtilCard.evaluateCreature(humanCreature) > ComputerUtilCard.evaluateCreature(aiCreature)) {
sa.getTargets().add(humanCreature); sa.getTargets().add(humanCreature);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay);
} }
} }
for (Card humanCreature : humCreatures) { for (Card humanCreature : humCreatures) {
if (!canKill(humanCreature, aiCreature, 0)) { if (!canKill(humanCreature, aiCreature, 0)) {
sa.getTargets().add(humanCreature); sa.getTargets().add(humanCreature);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} }
} }
sa.getTargets().add(humCreatures.get(0)); sa.getTargets().add(humCreatures.get(0));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} }
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} }
/** /**
@@ -176,7 +177,7 @@ public class FightAi extends SpellAbilityAi {
* @param power bonus to power * @param power bonus to power
* @return true if fight effect should be played, false otherwise * @return true if fight effect should be played, false otherwise
*/ */
public static boolean canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) { public static AiAbilityDecision canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
AbilitySub tgtFight = sa.getSubAbility(); AbilitySub tgtFight = sa.getSubAbility();
@@ -208,7 +209,7 @@ public class FightAi extends SpellAbilityAi {
ComputerUtilCard.sortByEvaluateCreature(aiCreatures); ComputerUtilCard.sortByEvaluateCreature(aiCreatures);
ComputerUtilCard.sortByEvaluateCreature(humCreatures); ComputerUtilCard.sortByEvaluateCreature(humCreatures);
if (humCreatures.isEmpty() || aiCreatures.isEmpty()) { if (humCreatures.isEmpty() || aiCreatures.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
// Evaluate creature pairs // Evaluate creature pairs
for (Card humanCreature : humCreatures) { for (Card humanCreature : humCreatures) {
@@ -238,7 +239,7 @@ public class FightAi extends SpellAbilityAi {
tgtFight.resetTargets(); tgtFight.resetTargets();
tgtFight.getTargets().add(humanCreature); tgtFight.getTargets().add(humanCreature);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else { } else {
// Other cards that use AILogic PowerDmg and a single target // Other cards that use AILogic PowerDmg and a single target
@@ -248,7 +249,7 @@ public class FightAi extends SpellAbilityAi {
tgtFight.resetTargets(); tgtFight.resetTargets();
tgtFight.getTargets().add(humanCreature); tgtFight.getTargets().add(humanCreature);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} else { } else {
@@ -261,12 +262,12 @@ public class FightAi extends SpellAbilityAi {
sa.getTargets().add(aiCreature); sa.getTargets().add(aiCreature);
tgtFight.resetTargets(); tgtFight.resetTargets();
tgtFight.getTargets().add(humanCreature); tgtFight.getTargets().add(humanCreature);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
/** /**

View File

@@ -14,7 +14,7 @@ import java.util.List;
public class GoadAi extends SpellAbilityAi { public class GoadAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = source.getGame(); final Game game = source.getGame();
@@ -24,7 +24,7 @@ public class GoadAi extends SpellAbilityAi {
List<Card> list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa); List<Card> list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) if (list.isEmpty())
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
if (game.getPlayers().size() > 2) { if (game.getPlayers().size() > 2) {
// use this part only in multiplayer // use this part only in multiplayer
@@ -50,7 +50,7 @@ public class GoadAi extends SpellAbilityAi {
if (!betterList.isEmpty()) { if (!betterList.isEmpty()) {
list = betterList; list = betterList;
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else { } else {
// single Player, goaded creature would attack ai // single Player, goaded creature would attack ai
@@ -71,37 +71,40 @@ public class GoadAi extends SpellAbilityAi {
if (!betterList.isEmpty()) { if (!betterList.isEmpty()) {
list = betterList; list = betterList;
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
// AI does not find a good creature to goad. // AI does not find a good creature to goad.
// because if it would goad a creature it would attack AI. // because if it would goad a creature it would attack AI.
// AI might not have enough information to block it // AI might not have enough information to block it
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
@Override @Override
protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected AiAbilityDecision doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (checkApiLogic(ai, sa)) { AiAbilityDecision decision = checkApiLogic(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); if (decision.willingToPlay()) {
return decision;
} }
if (!mandatory) { if (!mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// mandatory play, so we have to play it
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (sa.getTargetRestrictions().canTgtPlayer()) { if (sa.getTargetRestrictions().canTgtPlayer()) {
for (Player opp : ai.getOpponents()) { for (Player opp : ai.getOpponents()) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} }
} }
if (sa.canTarget(ai)) { if (sa.canTarget(ai)) {
sa.getTargets().add(ai); sa.getTargets().add(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} }
} else { } else {
List<Card> list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa); List<Card> list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
@@ -110,7 +113,7 @@ public class GoadAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list)); sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(list));
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(30, AiPlayDecision.MandatoryPlay);
} }
} }
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);

View File

@@ -126,7 +126,7 @@ public class LifeGainAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String aiLogic = sa.getParamOrDefault("AILogic", ""); final String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -148,12 +148,12 @@ public class LifeGainAi extends SpellAbilityAi {
// Ugin AI: always use ultimate // Ugin AI: always use ultimate
if (sourceName.equals("Ugin, the Spirit Dragon")) { if (sourceName.equals("Ugin, the Spirit Dragon")) {
// TODO: somehow link with DamageDealAi for cases where +1 = win // TODO: somehow link with DamageDealAi for cases where +1 = win
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// don't use it if no life to gain // don't use it if no life to gain
if (!activateForCost && lifeAmount <= 0) { if (!activateForCost && lifeAmount <= 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// don't play if the conditions aren't met, unless it would trigger a // don't play if the conditions aren't met, unless it would trigger a
// beneficial sub-condition // beneficial sub-condition
@@ -161,47 +161,52 @@ public class LifeGainAi extends SpellAbilityAi {
final AbilitySub abSub = sa.getSubAbility(); final AbilitySub abSub = sa.getSubAbility();
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) { if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
if (!abSub.getConditions().areMet(abSub)) { if (!abSub.getConditions().areMet(abSub)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet);
} }
} else { } else {
return false; return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet);
} }
} }
if (!activateForCost && !ai.canGainLife()) { if (!activateForCost && !ai.canGainLife()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// prevent run-away activations - first time will always return true // prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (!target(ai, sa, true)) { if (!target(ai, sa, true)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} }
if (ComputerUtil.playImmediately(ai, sa)) { if (ComputerUtil.playImmediately(ai, sa)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (isSorcerySpeed(sa, ai) if (isSorcerySpeed(sa, ai)
|| sa.getSubAbility() != null || playReusable(ai, sa)) { || sa.getSubAbility() != null || playReusable(ai, sa)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) { if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
return true; // sac costs should be performed at Instant speed when able // sac costs should be performed at Instant speed when able
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// Save instant-speed life-gain unless it is really worth it // Save instant-speed life-gain unless it is really worth it
final float value = 0.9f * lifeAmount / life; final float value = 0.9f * lifeAmount / life;
if (value < 0.2f) { if (value < 0.2f) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (MyRandom.getRandom().nextFloat() < value) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return MyRandom.getRandom().nextFloat() < value;
} }
/** /**

View File

@@ -94,7 +94,7 @@ public class LifeLoseAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String amountStr = sa.getParam("LifeAmount"); final String amountStr = sa.getParam("LifeAmount");
final String aiLogic = sa.getParamOrDefault("AILogic", ""); final String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -102,7 +102,7 @@ public class LifeLoseAi extends SpellAbilityAi {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (!doTgt(ai, sa, false)) { if (!doTgt(ai, sa, false)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} }
@@ -115,15 +115,15 @@ public class LifeLoseAi extends SpellAbilityAi {
} }
if (amount <= 0) { if (amount <= 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (ComputerUtil.playImmediately(ai, sa)) { if (ComputerUtil.playImmediately(ai, sa)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
final PlayerCollection tgtPlayers = getPlayers(ai, sa); final PlayerCollection tgtPlayers = getPlayers(ai, sa);
@@ -132,7 +132,7 @@ public class LifeLoseAi extends SpellAbilityAi {
.filter(PlayerPredicates.isOpponentOf(ai).and(PlayerPredicates.lifeLessOrEqualTo(amount))); .filter(PlayerPredicates.isOpponentOf(ai).and(PlayerPredicates.lifeLessOrEqualTo(amount)));
// killing opponents asap // killing opponents asap
if (!filteredPlayer.isEmpty()) { if (!filteredPlayer.isEmpty()) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// Sacrificing a creature in response to something dangerous is generally good in any phase // Sacrificing a creature in response to something dangerous is generally good in any phase
@@ -144,20 +144,20 @@ public class LifeLoseAi extends SpellAbilityAi {
// Don't use loselife before main 2 if possible // Don't use loselife before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) { && !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
} }
// Don't tap creatures that may be able to block // Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) { if (ComputerUtil.waitForBlocking(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
if (isSorcerySpeed(sa, ai) || sa.hasParam("ActivationPhases") || playReusable(ai, sa) if (isSorcerySpeed(sa, ai) || sa.hasParam("ActivationPhases") || playReusable(ai, sa)
|| ComputerUtil.activateForCost(sa, ai)) { || ComputerUtil.activateForCost(sa, ai)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/* /*

View File

@@ -87,18 +87,22 @@ public class ManaAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
return true; // handled elsewhere, does not meet the standard requirements return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // handled elsewhere, does not meet the standard requirements
} }
// TODO check if it would be worth it to keep mana open for opponents turn anyway // TODO check if it would be worth it to keep mana open for opponents turn anyway
if (ComputerUtil.activateForCost(sa, ai)) { if (ComputerUtil.activateForCost(sa, ai)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource() if (sa.getPayCosts().hasNoManaCost() && sa.getPayCosts().isReusuableResource()
&& sa.getSubAbility() == null && (improvesPosition(ai, sa) || ComputerUtil.playImmediately(ai, sa)); && sa.getSubAbility() == null && (improvesPosition(ai, sa) || ComputerUtil.playImmediately(ai, sa))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/** /**
@@ -276,7 +280,6 @@ public class ManaAi extends SpellAbilityAi {
@Override @Override
protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) { protected AiAbilityDecision canPlayAI(Player ai, SpellAbility sa) {
final String logic = sa.getParamOrDefault("AILogic", ""); final String logic = sa.getParamOrDefault("AILogic", "");
boolean result = checkApiLogic(ai, sa); return checkApiLogic(ai, sa);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }

View File

@@ -81,11 +81,11 @@ public abstract class ManifestBaseAi extends SpellAbilityAi {
abstract protected boolean shouldApply(final Card card, final Player ai, final SpellAbility sa); abstract protected boolean shouldApply(final Card card, final Player ai, final SpellAbility sa);
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) { if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) {
@@ -98,36 +98,42 @@ public abstract class ManifestBaseAi extends SpellAbilityAi {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), ai, host, sa); choices = CardLists.getValidCards(choices, sa.getParam("Choices"), ai, host, sa);
} }
if (choices.isEmpty()) { if (choices.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
} }
} else if ("TopOfLibrary".equals(sa.getParamOrDefault("Defined", "TopOfLibrary"))) { } else if ("TopOfLibrary".equals(sa.getParamOrDefault("Defined", "TopOfLibrary"))) {
// Library is empty, no Manifest // Library is empty, no Manifest
final CardCollectionView library = ai.getCardsIn(ZoneType.Library); final CardCollectionView library = ai.getCardsIn(ZoneType.Library);
if (library.isEmpty()) if (library.isEmpty())
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
// try not to mill himself with Manifest // try not to mill himself with Manifest
if (library.size() < 5 && !ai.isCardInPlay("Laboratory Maniac")) { if (library.size() < 5 && !ai.isCardInPlay("Laboratory Maniac")) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (!shouldApply(library.getFirst(), ai, sa)) { if (!shouldApply(library.getFirst(), ai, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
// Probably should be a little more discerning on playing during OPPs turn // TODO Probably should be a little more discerning on playing during OPPs turn
if (playReusable(ai, sa)) { if (playReusable(ai, sa)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)) { if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Add blockers? // Add blockers?
return true; return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence);
} }
if (sa.isAbility()) { if (sa.isAbility()) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return MyRandom.getRandom().nextFloat() < .8; if ( MyRandom.getRandom().nextFloat() < .8) {
// 80% chance to play a Manifest spell
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// 20% chance to not play a Manifest spell
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override

View File

@@ -11,19 +11,24 @@ import forge.game.zone.ZoneType;
public class MeldAi extends SpellAbilityAi { public class MeldAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
String primaryMeld = sa.getParam("Primary"); String primaryMeld = sa.getParam("Primary");
String secondaryMeld = sa.getParam("Secondary"); String secondaryMeld = sa.getParam("Secondary");
CardCollectionView cardsOTB = aiPlayer.getCardsIn(ZoneType.Battlefield); CardCollectionView cardsOTB = aiPlayer.getCardsIn(ZoneType.Battlefield);
if (cardsOTB.isEmpty()) { if (cardsOTB.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer))); boolean hasPrimaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(primaryMeld).and(CardPredicates.isOwner(aiPlayer)));
boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer))); boolean hasSecondaryMeld = cardsOTB.anyMatch(CardPredicates.nameEquals(secondaryMeld).and(CardPredicates.isOwner(aiPlayer)));
if (hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld)) {
return hasPrimaryMeld && hasSecondaryMeld && sa.getHostCard().getName().equals(primaryMeld); // If the primary meld card is on the battlefield and both meld cards are owned by the AI, play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the secondary meld card is on the battlefield and it is the one being activated, play the ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override

View File

@@ -60,7 +60,7 @@ public class MillAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
/* /*
* TODO: * TODO:
* - logic in targetAI looks dodgy * - logic in targetAI looks dodgy
@@ -70,16 +70,17 @@ public class MillAi extends SpellAbilityAi {
* effect due to possibility of "lose abilities" effect) * effect due to possibility of "lose abilities" effect)
*/ */
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevents mill 0 infinite loop? return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (("You".equals(sa.getParam("Defined")) || "Player".equals(sa.getParam("Defined"))) if (("You".equals(sa.getParam("Defined")) || "Player".equals(sa.getParam("Defined")))
&& ai.getCardsIn(ZoneType.Library).size() < 10) { && ai.getCardsIn(ZoneType.Library).size() < 10) {
return false; // prevent self and each player mill when library is small // prevent self and each player mill when library is small
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (sa.usesTargeting() && !targetAI(ai, sa, false)) { if (sa.usesTargeting() && !targetAI(ai, sa, false)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
if (sa.hasParam("NumCards") && (sa.getParam("NumCards").equals("X") || sa.getParam("NumCards").equals("Z")) if (sa.hasParam("NumCards") && (sa.getParam("NumCards").equals("X") || sa.getParam("NumCards").equals("Z"))
@@ -87,9 +88,11 @@ public class MillAi extends SpellAbilityAi {
// Set PayX here to maximum value. // Set PayX here to maximum value.
final int cardsToDiscard = getNumToDiscard(ai, sa); final int cardsToDiscard = getNumToDiscard(ai, sa);
sa.setXManaCostPaid(cardsToDiscard); sa.setXManaCostPaid(cardsToDiscard);
return cardsToDiscard > 0; if (cardsToDiscard <= 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) { private boolean targetAI(final Player ai, final SpellAbility sa, final boolean mandatory) {

View File

@@ -43,27 +43,27 @@ public class PermanentAi extends SpellAbilityAi {
* here * here
*/ */
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
// check on legendary // check on legendary
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) { if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
// TODO check the risk we'd lose the effect with bad timing // TODO check the risk we'd lose the effect with bad timing
// TODO Technically we're not checking if same card in play is also legendary, but this is a good enough approximation
if (!source.hasSVar("AILegendaryException")) { if (!source.hasSVar("AILegendaryException")) {
// AiPlayDecision.WouldDestroyLegend return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
return false;
} else { } else {
String specialRule = source.getSVar("AILegendaryException"); String specialRule = source.getSVar("AILegendaryException");
if ("TwoCopiesAllowed".equals(specialRule)) { if ("TwoCopiesAllowed".equals(specialRule)) {
// One extra copy allowed on the battlefield, e.g. Brothers Yamazaki // One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(source.getName())) > 1) { if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(source.getName())) > 1) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
} }
} else if ("AlwaysAllowed".equals(specialRule)) { } else if ("AlwaysAllowed".equals(specialRule)) {
// Nothing to do here, check for Legendary is disabled // Nothing to do here, check for Legendary is disabled
} else { } else {
// Unknown hint, assume two copies not allowed // Unknown hint, assume two copies not allowed
return false; return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
} }
} }
} }
@@ -71,8 +71,7 @@ public class PermanentAi extends SpellAbilityAi {
if (source.getType().hasSupertype(Supertype.World)) { if (source.getType().hasSupertype(Supertype.World)) {
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World"); CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
if (!list.isEmpty()) { if (!list.isEmpty()) {
// AiPlayDecision.WouldDestroyWorldEnchantment return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyWorldEnchantment);
return false;
} }
} }
@@ -93,9 +92,8 @@ public class PermanentAi extends SpellAbilityAi {
} }
} }
} else { } else {
// AiPlayDecision.CantAffordX
if (xPay <= 0) { if (xPay <= 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
} }
sa.setXManaCostPaid(xPay); sa.setXManaCostPaid(xPay);
} }
@@ -103,8 +101,7 @@ public class PermanentAi extends SpellAbilityAi {
// if mana is zero, but card mana cost does have X, then something is wrong // if mana is zero, but card mana cost does have X, then something is wrong
ManaCost cardCost = source.getManaCost(); ManaCost cardCost = source.getManaCost();
if (cardCost != null && cardCost.countX() > 0) { if (cardCost != null && cardCost.countX() > 0) {
// AiPlayDecision.CantPlayAi return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
return false;
} }
} }
@@ -147,10 +144,10 @@ public class PermanentAi extends SpellAbilityAi {
} }
if (oppCards.size() > 3 && oppCards.size() >= aiCards.size() * 2) { if (oppCards.size() > 3 && oppCards.size() >= aiCards.size() * 2) {
sa.setXManaCostPaid(manaValue); sa.setXManaCostPaid(manaValue);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
for (KeywordInterface ki : source.getKeywords(Keyword.MULTIKICKER)) { for (KeywordInterface ki : source.getKeywords(Keyword.MULTIKICKER)) {
@@ -177,7 +174,7 @@ public class PermanentAi extends SpellAbilityAi {
sa.clearOptionalKeywordAmount(); sa.clearOptionalKeywordAmount();
// Bail if the card cost was {0} and no multikicker was paid (e.g. Everflowing Chalice). // Bail if the card cost was {0} and no multikicker was paid (e.g. Everflowing Chalice).
// TODO: update this if there's ever a card where it makes sense to play it for {0} with no multikicker // TODO: update this if there's ever a card where it makes sense to play it for {0} with no multikicker
return false; return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
} }
} }
@@ -213,8 +210,7 @@ public class PermanentAi extends SpellAbilityAi {
emptyAbility.setActivatingPlayer(ai); emptyAbility.setActivatingPlayer(ai);
if (!ComputerUtilCost.canPayCost(emptyAbility, ai, true)) { if (!ComputerUtilCost.canPayCost(emptyAbility, ai, true)) {
// AiPlayDecision.AnotherTime return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
return false;
} }
} }
@@ -310,10 +306,12 @@ public class PermanentAi extends SpellAbilityAi {
} }
} }
return !dontCast; if (dontCast) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
@Override @Override
@@ -333,8 +331,13 @@ public class PermanentAi extends SpellAbilityAi {
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) { if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
boolean result = checkApiLogic(ai, sa); AiAbilityDecision decision = checkApiLogic(ai, sa);
return (result || mandatory) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); if (decision.willingToPlay()) {
} return decision;
} else if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
return decision;
}
}
} }

View File

@@ -195,9 +195,10 @@ public class PermanentCreatureAi extends PermanentAi {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (!super.checkApiLogic(ai, sa)) { AiAbilityDecision decision = super.checkApiLogic(ai, sa);
return false; if (!decision.willingToPlay()) {
return decision;
} }
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
@@ -220,16 +221,15 @@ public class PermanentCreatureAi extends PermanentAi {
// AiPlayDecision.WouldBecomeZeroToughnessCreature // AiPlayDecision.WouldBecomeZeroToughnessCreature
if (card.hasStartOfKeyword("etbCounter") || mana.countX() != 0 if (card.hasStartOfKeyword("etbCounter") || mana.countX() != 0
|| card.hasETBTrigger(false) || card.hasETBReplacement() || card.hasSVar("NoZeroToughnessAI")) { || card.hasETBTrigger(false) || card.hasETBReplacement() || card.hasSVar("NoZeroToughnessAI")) {
return true; return decision;
} }
final Card copy = CardCopyService.getLKICopy(card); final Card copy = CardCopyService.getLKICopy(card);
ComputerUtilCard.applyStaticContPT(game, copy, null); ComputerUtilCard.applyStaticContPT(game, copy, null);
if (copy.getNetToughness() > 0) { if (copy.getNetToughness() > 0) {
return true; return decision;
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.WouldBecomeZeroToughnessCreature);
} }
} }

View File

@@ -1,5 +1,7 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
@@ -21,9 +23,11 @@ public class PermanentNoncreatureAi extends PermanentAi {
* here * here
*/ */
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (!super.checkApiLogic(ai, sa)) AiAbilityDecision decision = super.checkApiLogic(ai, sa);
return false; if (!decision.willingToPlay()) {
return decision;
}
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -41,9 +45,10 @@ public class PermanentNoncreatureAi extends PermanentAi {
// TODO: consider replacing the condition with host.hasSVar("OblivionRing") // TODO: consider replacing the condition with host.hasSVar("OblivionRing")
targets = CardLists.filterControlledBy(targets, ai.getOpponents()); targets = CardLists.filterControlledBy(targets, ai.getOpponents());
} }
// AiPlayDecision.AnotherTime if (targets.isEmpty()) {
return !targets.isEmpty(); return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
return true; return decision;
} }
} }

View File

@@ -26,7 +26,7 @@ import java.util.stream.Collectors;
public class PlayAi extends SpellAbilityAi { public class PlayAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final String logic = sa.getParamOrDefault("AILogic", ""); final String logic = sa.getParamOrDefault("AILogic", "");
final Game game = ai.getGame(); final Game game = ai.getGame();
@@ -34,11 +34,11 @@ public class PlayAi extends SpellAbilityAi {
// don't use this as a response (ReplaySpell logic is an exception, might be called from a subability // don't use this as a response (ReplaySpell logic is an exception, might be called from a subability
// while the trigger is on stack) // while the trigger is on stack)
if (!game.getStack().isEmpty() && !"ReplaySpell".equals(logic)) { if (!game.getStack().isEmpty() && !"ReplaySpell".equals(logic)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) { if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) {
@@ -48,38 +48,46 @@ public class PlayAi extends SpellAbilityAi {
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA); int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
int chanceToActivateInst = 100 - aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT); int chanceToActivateInst = 100 - aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT);
if (ai.getLandsInPlay().size() < numLandsForJhoira) { if (ai.getLandsInPlay().size() < numLandsForJhoira) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
// Don't spam activate the Instant copying ability all the time to give the AI a chance to use other abilities // Don't spam activate the Instant copying ability all the time to give the AI a chance to use other abilities
// Can probably be improved, but as random as MoJhoSto already is, probably not a huge deal for now // Can probably be improved, but as random as MoJhoSto already is, probably not a huge deal for now
if ("Instant".equals(sa.getParam("AnySupportedCard")) && MyRandom.percentTrue(chanceToActivateInst)) { if ("Instant".equals(sa.getParam("AnySupportedCard")) && MyRandom.percentTrue(chanceToActivateInst)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
List<Card> cards = getPlayableCards(sa, ai); List<Card> cards = getPlayableCards(sa, ai);
if (cards.isEmpty()) { if (cards.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
if ("ReplaySpell".equals(logic)) { if ("ReplaySpell".equals(logic)) {
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"), false); if (ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"), false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else if (logic.startsWith("NeedsChosenCard")) { } else if (logic.startsWith("NeedsChosenCard")) {
int minCMC = 0; int minCMC = 0;
if (sa.getPayCosts().getCostMana() != null) { if (sa.getPayCosts().getCostMana() != null) {
minCMC = sa.getPayCosts().getTotalMana().getCMC(); minCMC = sa.getPayCosts().getTotalMana().getCMC();
} }
cards = CardLists.filter(cards, CardPredicates.greaterCMC(minCMC)); cards = CardLists.filter(cards, CardPredicates.greaterCMC(minCMC));
return chooseSingleCard(ai, sa, cards, sa.hasParam("Optional"), null, null) != null; if (chooseSingleCard(ai, sa, cards, sa.hasParam("Optional"), null, null) != null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
} else if ("WithTotalCMC".equals(logic)) { } else if ("WithTotalCMC".equals(logic)) {
// Try to play only when there are more than three playable cards. // Try to play only when there are more than three playable cards.
if (cards.size() < 3) if (cards.size() < 3)
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
if (sa.costHasManaX()) { if (sa.costHasManaX()) {
int amount = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); int amount = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (amount < ComputerUtilCard.getBestAI(cards).getCMC()) if (amount < ComputerUtilCard.getBestAI(cards).getCMC())
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
int totalCMC = 0; int totalCMC = 0;
for (Card c : cards) { for (Card c : cards) {
totalCMC += c.getCMC(); totalCMC += c.getCMC();
@@ -97,10 +105,14 @@ public class PlayAi extends SpellAbilityAi {
Card rem = source.getExiledCards().getFirst(); Card rem = source.getExiledCards().getFirst();
CardTypeView t = rem.getState(CardStateName.Original).getType(); CardTypeView t = rem.getState(CardStateName.Original).getType();
return t.isPermanent() && !t.isLand(); if (t.isPermanent() && !t.isLand()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
/** /**
@@ -126,8 +138,7 @@ public class PlayAi extends SpellAbilityAi {
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
boolean result = checkApiLogic(ai, sa); return checkApiLogic(ai, sa);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);

View File

@@ -36,18 +36,22 @@ public class PoisonAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility) * forge.game.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
// Don't tap creatures that may be able to block // Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) { if (ComputerUtil.waitForBlocking(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
return tgtPlayer(ai, sa, true); if (tgtPlayer(ai, sa, true)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
/* /*

View File

@@ -157,17 +157,21 @@ public class ProtectAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
return protectTgtAI(ai, sa, false); return protectTgtAI(ai, sa, false);
} }
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (cards.size() == 0) { if (cards.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} else if (cards.size() == 1) { } else if (cards.size() == 1) {
// Affecting single card // Affecting single card
return getProtectCreatures(ai, sa).contains(cards.get(0)); if (getProtectCreatures(ai, sa).contains(cards.get(0))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
/* /*
* when this happens we need to expand AI to consider if its ok * when this happens we need to expand AI to consider if its ok
@@ -175,14 +179,14 @@ public class ProtectAi extends SpellAbilityAi {
* control Card and Pump is a Curse, than maybe use? * control Card and Pump is a Curse, than maybe use?
* } * }
*/ */
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
private boolean protectTgtAI(final Player ai, final SpellAbility sa, final boolean mandatory) { private AiAbilityDecision protectTgtAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Game game = ai.getGame(); final Game game = ai.getGame();
if (!mandatory && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS) if (!mandatory && game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& game.getStack().isEmpty()) { && game.getStack().isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
} }
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -216,7 +220,12 @@ public class ProtectAi extends SpellAbilityAi {
} }
if (list.isEmpty()) { if (list.isEmpty()) {
return mandatory && protectMandatoryTarget(ai, sa); if (mandatory && protectMandatoryTarget(ai, sa)) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
while (sa.canAddMoreTarget()) { while (sa.canAddMoreTarget()) {
@@ -226,11 +235,13 @@ public class ProtectAi extends SpellAbilityAi {
if (list.isEmpty()) { if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(source, sa)) || sa.getTargets().size() == 0) { if ((sa.getTargets().size() < tgt.getMinTargets(source, sa)) || sa.getTargets().size() == 0) {
if (mandatory) { if (mandatory) {
return protectMandatoryTarget(ai, sa); if (protectMandatoryTarget(ai, sa)) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
} }
sa.resetTargets(); sa.resetTargets();
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else { } else {
// TODO is this good enough? for up to amounts? // TODO is this good enough? for up to amounts?
break; break;
@@ -242,7 +253,7 @@ public class ProtectAi extends SpellAbilityAi {
list.remove(t); list.remove(t);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} // protectTgtAI() } // protectTgtAI()
private static boolean protectMandatoryTarget(final Player ai, final SpellAbility sa) { private static boolean protectMandatoryTarget(final Player ai, final SpellAbility sa) {
@@ -311,11 +322,7 @@ public class ProtectAi extends SpellAbilityAi {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else { } else {
if (protectTgtAI(ai, sa, mandatory)) { return protectTgtAI(ai, sa, mandatory);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
@@ -324,11 +331,7 @@ public class ProtectAi extends SpellAbilityAi {
@Override @Override
public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) { public AiAbilityDecision chkAIDrawback(SpellAbility sa, Player ai) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (protectTgtAI(ai, sa, false)) { return protectTgtAI(ai, sa, false);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); return new AiAbilityDecision(100, AiPlayDecision.WillPlay);

View File

@@ -56,7 +56,7 @@ public class PumpAi extends PumpAiBase {
} else if ("Aristocrat".equals(aiLogic)) { } else if ("Aristocrat".equals(aiLogic)) {
return SpecialAiLogic.doAristocratLogic(ai, sa); return SpecialAiLogic.doAristocratLogic(ai, sa);
} else if (aiLogic.startsWith("AristocratCounters")) { } else if (aiLogic.startsWith("AristocratCounters")) {
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa); return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa).willingToPlay();
} else if (aiLogic.equals("SwitchPT")) { } else if (aiLogic.equals("SwitchPT")) {
// Some more AI would be even better, but this is a good start to prevent spamming // Some more AI would be even better, but this is a good start to prevent spamming
if (sa.isActivatedAbility() && sa.getActivationsThisTurn() > 0 && !sa.usesTargeting()) { if (sa.isActivatedAbility() && sa.getActivationsThisTurn() > 0 && !sa.usesTargeting()) {
@@ -114,7 +114,7 @@ public class PumpAi extends PumpAiBase {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final SpellAbility root = sa.getRootAbility(); final SpellAbility root = sa.getRootAbility();
@@ -131,14 +131,15 @@ public class PumpAi extends PumpAiBase {
if ("Pummeler".equals(aiLogic)) { if ("Pummeler".equals(aiLogic)) {
return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa); return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa);
} else if (aiLogic.startsWith("AristocratCounters")) { } else if (aiLogic.startsWith("AristocratCounters")) {
return true; // the preconditions to this are already tested in checkAiLogic // the preconditions to this are already tested in checkAiLogic
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("GideonBlackblade".equals(aiLogic)) { } else if ("GideonBlackblade".equals(aiLogic)) {
return SpecialCardAi.GideonBlackblade.consider(ai, sa); return SpecialCardAi.GideonBlackblade.consider(ai, sa);
} else if ("MoveCounter".equals(aiLogic)) { } else if ("MoveCounter".equals(aiLogic)) {
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter); final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
if (moveSA == null) { if (moveSA == null) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
final String counterType = moveSA.getParam("CounterType"); final String counterType = moveSA.getParam("CounterType");
@@ -153,7 +154,7 @@ public class PumpAi extends PumpAiBase {
if (cType != null) { if (cType != null) {
attr = CardLists.filter(attr, CardPredicates.hasCounter(cType)); attr = CardLists.filter(attr, CardPredicates.hasCounter(cType));
if (attr.isEmpty()) { if (attr.isEmpty()) {
return false; return new AiAbilityDecision(0,AiPlayDecision.TargetingFailed);
} }
CardCollection best = CardLists.filter(attr, card -> { CardCollection best = CardLists.filter(attr, card -> {
int amount = 0; int amount = 0;
@@ -187,7 +188,7 @@ public class PumpAi extends PumpAiBase {
final Card card = ComputerUtilCard.getBestCreatureAI(best); final Card card = ComputerUtilCard.getBestCreatureAI(best);
sa.getTargets().add(card); sa.getTargets().add(card);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} else { } else {
final boolean sameCtrl = moveSA.getTargetRestrictions().isSameController(); final boolean sameCtrl = moveSA.getTargetRestrictions().isSameController();
@@ -196,7 +197,7 @@ public class PumpAi extends PumpAiBase {
if (cType != null) { if (cType != null) {
list = CardLists.filter(list, CardPredicates.hasCounter(cType)); list = CardLists.filter(list, CardPredicates.hasCounter(cType));
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
List<Card> oppList = CardLists.filterControlledBy(list, ai.getOpponents()); List<Card> oppList = CardLists.filterControlledBy(list, ai.getOpponents());
if (!oppList.isEmpty() && !sameCtrl) { if (!oppList.isEmpty() && !sameCtrl) {
@@ -232,7 +233,7 @@ public class PumpAi extends PumpAiBase {
final Card card = ComputerUtilCard.getBestCreatureAI(best); final Card card = ComputerUtilCard.getBestCreatureAI(best);
sa.getTargets().add(card); sa.getTargets().add(card);
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
@@ -244,12 +245,12 @@ public class PumpAi extends PumpAiBase {
int numRedMana = ComputerUtilMana.determineLeftoverMana(new SpellAbility.EmptySa(source), ai, "R", false); int numRedMana = ComputerUtilMana.determineLeftoverMana(new SpellAbility.EmptySa(source), ai, "R", false);
int currentPower = source.getNetPower(); int currentPower = source.getNetPower();
if (currentPower < 20 && currentPower + numRedMana >= 20) { if (currentPower < 20 && currentPower + numRedMana >= 20) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (!game.getStack().isEmpty() && !sa.isCurse() && !isFight) { if (!game.getStack().isEmpty() && !sa.isCurse() && !isFight) {
@@ -261,7 +262,7 @@ public class PumpAi extends PumpAiBase {
final int activations = sa.getActivationsThisTurn(); final int activations = sa.getActivationsThisTurn();
// don't risk sacrificing a creature just to pump it // don't risk sacrificing a creature just to pump it
if (activations >= sacActivations - 1) { if (activations >= sacActivations - 1) {
return false; return new AiAbilityDecision(0, AiPlayDecision.ConditionsNotMet);
} }
} }
@@ -304,7 +305,7 @@ public class PumpAi extends PumpAiBase {
} }
if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) { if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
//Untargeted //Untargeted
@@ -312,47 +313,51 @@ public class PumpAi extends PumpAiBase {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa); final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (cards.isEmpty()) { if (cards.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
// when this happens we need to expand AI to consider if its ok for everything? // when this happens we need to expand AI to consider if its ok for everything?
for (final Card card : cards) { for (final Card card : cards) {
if (sa.isCurse()) { if (sa.isCurse()) {
if (!card.getController().isOpponentOf(ai)) { if (!card.getController().isOpponentOf(ai)) {
return false; continue;
} }
if (!containsUsefulKeyword(ai, keywords, card, sa, attack)) { if (!containsUsefulKeyword(ai, keywords, card, sa, attack)) {
continue; continue;
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
if (!card.getController().isOpponentOf(ai)) { if (!card.getController().isOpponentOf(ai)) {
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) { if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) { } else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
if (game.getPhaseHandler().is(PhaseType.MAIN1) && isSorcerySpeed(sa, ai) || if (game.getPhaseHandler().is(PhaseType.MAIN1) && isSorcerySpeed(sa, ai) ||
game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai) || game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai) ||
game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) { game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords); Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped); if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
// If the AI can attack with the pumped creature, then it is worth playing
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (grantsUsefulExtraBlockOpts(ai, sa, card, keywords)) { } else if (grantsUsefulExtraBlockOpts(ai, sa, card, keywords)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
//Targeted //Targeted
if (!pumpTgtAI(ai, sa, defense, attack, false, false)) { if (!pumpTgtAI(ai, sa, defense, attack, false, false)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
return true; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} // pumpPlayAI() } // pumpPlayAI()
private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory, private boolean pumpTgtAI(final Player ai, final SpellAbility sa, final int defense, final int attack, final boolean mandatory,
@@ -462,7 +467,7 @@ public class PumpAi extends PumpAiBase {
} }
if (isFight) { if (isFight) {
return FightAi.canFightAi(ai, sa, attack, defense); return FightAi.canFightAi(ai, sa, attack, defense).willingToPlay();
} }
} }

View File

@@ -42,7 +42,7 @@ import java.util.List;
public class RegenerateAi extends SpellAbilityAi { public class RegenerateAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
final Combat combat = game.getCombat(); final Combat combat = game.getCombat();
final Card hostCard = sa.getHostCard(); final Card hostCard = sa.getHostCard();
@@ -54,7 +54,7 @@ public class RegenerateAi extends SpellAbilityAi {
List<Card> targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); List<Card> targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (targetables.isEmpty()) { if (targetables.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
if (!game.getStack().isEmpty()) { if (!game.getStack().isEmpty()) {
@@ -87,12 +87,12 @@ public class RegenerateAi extends SpellAbilityAi {
} }
} }
if (sa.getTargets().isEmpty()) { if (sa.getTargets().isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
} else { } else {
final List<Card> list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa); final List<Card> list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
// when regenerating more than one is possible try for slightly more value // when regenerating more than one is possible try for slightly more value
int numToSave = Math.min(2, list.size()); int numToSave = Math.min(2, list.size());
@@ -116,7 +116,11 @@ public class RegenerateAi extends SpellAbilityAi {
chance = saved >= numToSave; chance = saved >= numToSave;
} }
return chance; if (chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override

View File

@@ -15,20 +15,27 @@ import forge.util.MyRandom;
public class RevealAi extends RevealAiBase { public class RevealAi extends RevealAiBase {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
// we can reuse this function here... // we can reuse this function here...
final boolean bFlag = revealHandTargetAI(ai, sa, false); final boolean bFlag = revealHandTargetAI(ai, sa, false);
if (!bFlag) { if (!bFlag) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1); boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
// Are we checking for runaway activations?
if (playReusable(ai, sa)) { if (playReusable(ai, sa)) {
randomReturn = true; randomReturn = true;
} }
return randomReturn;
if (randomReturn) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override

View File

@@ -12,11 +12,11 @@ public class RevealHandAi extends RevealAiBase {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility) * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/ */
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final boolean bFlag = revealHandTargetAI(ai, sa, false); final boolean bFlag = revealHandTargetAI(ai, sa, false);
if (!bFlag) { if (!bFlag) {
return false; return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} }
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1); boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
@@ -25,7 +25,11 @@ public class RevealHandAi extends RevealAiBase {
randomReturn = true; randomReturn = true;
} }
return randomReturn; if (randomReturn) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} }
@Override @Override

View File

@@ -147,10 +147,10 @@ public class ScryAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
// does Scry make sense with no Library cards? // does Scry make sense with no Library cards?
if (ai.getCardsIn(ZoneType.Library).isEmpty()) { if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
double chance = .4; // 40 percent chance of milling with instant speed stuff double chance = .4; // 40 percent chance of milling with instant speed stuff
@@ -181,12 +181,15 @@ public class ScryAi extends SpellAbilityAi {
if ("X".equals(sa.getParam("ScryNum")) && sa.getSVar("X").equals("Count$xPaid")) { if ("X".equals(sa.getParam("ScryNum")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()); int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) { if (xPay == 0) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
} }
sa.getRootAbility().setXManaCostPaid(xPay); sa.getRootAbility().setXManaCostPaid(xPay);
} }
return randomReturn; if (randomReturn) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override

View File

@@ -17,19 +17,19 @@ import java.util.Map;
public class SetStateAi extends SpellAbilityAi { public class SetStateAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player aiPlayer, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player aiPlayer, final SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String mode = sa.getParam("Mode"); final String mode = sa.getParam("Mode");
// turning face is most likely okay // turning face is most likely okay
// TODO only do this at beneficial moment (e.g. surprise during combat or morph trigger), might want to reserve mana to protect them from easy removal // TODO only do this at beneficial moment (e.g. surprise during combat or morph trigger), might want to reserve mana to protect them from easy removal
if ("TurnFaceUp".equals(mode) || "TurnFaceDown".equals(mode)) { if ("TurnFaceUp".equals(mode) || "TurnFaceDown".equals(mode)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
// Prevent transform into legendary creature if copy already exists // Prevent transform into legendary creature if copy already exists
if (!isSafeToTransformIntoLegendary(aiPlayer, source)) { if (!isSafeToTransformIntoLegendary(aiPlayer, source)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
} }
if (sa.getSVar("X").equals("Count$xPaid")) { if (sa.getSVar("X").equals("Count$xPaid")) {
@@ -38,9 +38,9 @@ public class SetStateAi extends SpellAbilityAi {
} }
if ("Transform".equals(mode) || "Flip".equals(mode)) { if ("Transform".equals(mode) || "Flip".equals(mode)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override

View File

@@ -84,10 +84,10 @@ public class SurveilAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
// Makes no sense to do Surveil when there's nothing in the library // Makes no sense to do Surveil when there's nothing in the library
if (ai.getCardsIn(ZoneType.Library).isEmpty()) { if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
return false; return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
} }
// Only Surveil for life when at decent amount of life remaining // Only Surveil for life when at decent amount of life remaining
@@ -95,10 +95,11 @@ public class SurveilAi extends SpellAbilityAi {
if (cost != null && cost.hasSpecificCostType(CostPayLife.class)) { if (cost != null && cost.hasSpecificCostType(CostPayLife.class)) {
final int maxLife = ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SURVEIL_LIFEPERC_AFTER_PAYING_LIFE); final int maxLife = ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.SURVEIL_LIFEPERC_AFTER_PAYING_LIFE);
if (!ComputerUtilCost.checkLifeCost(ai, cost, sa.getHostCard(), ai.getStartingLife() * maxLife / 100, sa)) { if (!ComputerUtilCost.checkLifeCost(ai, cost, sa.getHostCard(), ai.getStartingLife() * maxLife / 100, sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
} }
} }
// TODO If EOT and I'm the next turn, the percent should probably be higher
double chance = .4; // 40 percent chance for instant speed double chance = .4; // 40 percent chance for instant speed
if (isSorcerySpeed(sa, ai)) { if (isSorcerySpeed(sa, ai)) {
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT) chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
@@ -111,9 +112,10 @@ public class SurveilAi extends SpellAbilityAi {
if (randomReturn) { if (randomReturn) {
AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
return randomReturn; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
@Override @Override

View File

@@ -134,7 +134,7 @@ public class TokenAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
/* /*
* readParameters() is called in checkPhaseRestrictions * readParameters() is called in checkPhaseRestrictions
*/ */
@@ -142,14 +142,14 @@ public class TokenAi extends SpellAbilityAi {
final Player opp = ai.getWeakestOpponent(); final Player opp = ai.getWeakestOpponent();
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite tokens? return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
Card actualToken = spawnToken(ai, sa); Card actualToken = spawnToken(ai, sa);
// Don't kill AIs Legendary tokens // Don't kill AIs Legendary tokens
if (actualToken.getType().isLegendary() && ai.isCardInPlay(actualToken.getName())) { if (actualToken.getType().isLegendary() && ai.isCardInPlay(actualToken.getName())) {
// TODO Check if Token is useless due to an aura or counters? // TODO Check if Token is useless due to an aura or counters?
return false; return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
} }
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -157,14 +157,18 @@ public class TokenAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
if (actualToken.getType().hasSubtype("Role")) { if (actualToken.getType().hasSubtype("Role")) {
return tgtRoleAura(ai, sa, actualToken, false); if (tgtRoleAura(ai, sa, actualToken, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
if (tgt.canOnlyTgtOpponent() || "Opponent".equals(sa.getParam("AITgts"))) { if (tgt.canOnlyTgtOpponent() || "Opponent".equals(sa.getParam("AITgts"))) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
} else { } else {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} else { } else {
if (sa.canTarget(ai)) { if (sa.canTarget(ai)) {
@@ -183,7 +187,7 @@ public class TokenAi extends SpellAbilityAi {
if (!list.isEmpty()) { if (!list.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list)); sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
} else { } else {
return false; return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
} }
} }
@@ -201,7 +205,7 @@ public class TokenAi extends SpellAbilityAi {
} }
if (sa.isPwAbility() && alwaysFromPW) { if (sa.isPwAbility() && alwaysFromPW) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS) } else if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
&& game.getCombat() != null && game.getCombat() != null
@@ -210,14 +214,18 @@ public class TokenAi extends SpellAbilityAi {
&& actualToken.isCreature()) { && actualToken.isCreature()) {
for (Card attacker : game.getCombat().getAttackers()) { for (Card attacker : game.getCombat().getAttackers()) {
if (CombatUtil.canBlock(attacker, actualToken)) { if (CombatUtil.canBlock(attacker, actualToken)) {
return true; return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} }
} }
// if the token can't block, then what's the point? // if the token can't block, then what's the point?
return false; return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} }
return MyRandom.getRandom().nextFloat() <= chance; if (MyRandom.getRandom().nextFloat() <= chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} }
/** /**

View File

@@ -54,19 +54,29 @@ public class UntapAi extends SpellAbilityAi {
} }
@Override @Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) { protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
return untapPrefTargeting(ai, sa, false); if (untapPrefTargeting(ai, sa, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} }
final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai); if (pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai)) {
// If the defined card is tapped, or if there are no defined cards, we can play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// Otherwise, we can't play this ability
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
} }
@Override @Override

View File

@@ -253,7 +253,7 @@ public class CardStorageReader {
sw.start(); sw.start();
executeLoadTask(result, taskFiles, cdlFiles); executeLoadTask(result, taskFiles, cdlFiles);
sw.stop(); sw.stop();
final long timeOnParse = sw.getTime(); final long timeOnParse = sw.getTime(TimeUnit.SECONDS);
System.out.printf("Read cards: %s files in %d ms (%d parts) %s%n", allFiles.size(), timeOnParse, taskFiles.size(), useThreadPool ? "using thread pool" : "in same thread"); System.out.printf("Read cards: %s files in %d ms (%d parts) %s%n", allFiles.size(), timeOnParse, taskFiles.size(), useThreadPool ? "using thread pool" : "in same thread");
} }
@@ -267,7 +267,7 @@ public class CardStorageReader {
sw.start(); sw.start();
executeLoadTask(result, taskZip, cdlZip); executeLoadTask(result, taskZip, cdlZip);
sw.stop(); sw.stop();
final long timeOnParse = sw.getTime(); final long timeOnParse = sw.getTime(TimeUnit.SECONDS);
System.out.printf("Read cards: %s archived files in %d ms (%d parts) %s%n", this.zip.size(), timeOnParse, taskZip.size(), useThreadPool ? "using thread pool" : "in same thread"); System.out.printf("Read cards: %s archived files in %d ms (%d parts) %s%n", this.zip.size(), timeOnParse, taskZip.size(), useThreadPool ? "using thread pool" : "in same thread");
} }