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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ import java.util.Map;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
@@ -70,10 +70,10 @@ public class CharmAi extends SpellAbilityAi {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(sa, choices, ai, true, num, min);
if (chosenList.isEmpty() && min != 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -81,7 +81,7 @@ public class CharmAi extends SpellAbilityAi {
sa.setChosenList(chosenList);
if (choiceForOpp) {
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.isSpell()) {
@@ -90,7 +90,11 @@ public class CharmAi extends SpellAbilityAi {
}
// prevent run-away activations - first time will always return true
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
private List<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
*/
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) {
sa.resetTargets();
// search targetable Opponents
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(Iterables.getFirst(oppList, null));
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -140,11 +140,7 @@ public class ChooseCardAi extends SpellAbilityAi {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (checkApiLogic(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return checkApiLogic(ai, sa);
}
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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)
*/
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final boolean bFlag = revealHandTargetAI(ai, sa, false);
if (!bFlag) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
@@ -25,7 +25,11 @@ public class RevealHandAi extends RevealAiBase {
randomReturn = true;
}
return randomReturn;
if (randomReturn) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@Override

View File

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

View File

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

View File

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

View File

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

View File

@@ -54,19 +54,29 @@ public class UntapAi extends SpellAbilityAi {
}
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
}
if (sa.usesTargeting()) {
return untapPrefTargeting(ai, sa, false);
if (untapPrefTargeting(ai, sa, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
final List<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

View File

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