() {
@Override
@@ -285,7 +320,7 @@ public class AiAttackController {
}
continue;
}
- if (c.hasKeyword("Vigilance")) {
+ if (c.hasKeyword(Keyword.VIGILANCE)) {
vigilantes.add(c);
notNeededAsBlockers.remove(c); // they will be re-added later
if (canBlockAnAttacker(c, opponentsAttackers, false)) {
@@ -327,7 +362,7 @@ public class AiAttackController {
// In addition, if the computer guesses it needs no blockers, make sure
// that
// it won't be surprised by Exalted
- final int humanExaltedBonus = countExaltedBonus(opp);
+ final int humanExaltedBonus = opp.countExaltedBonus();
if (humanExaltedBonus > 0) {
final boolean finestHour = opp.isCardInPlay("Finest Hour");
@@ -512,7 +547,7 @@ public class AiAttackController {
int trampleDamage = 0;
for (Card attacker : blockedAttackers) {
- if (attacker.hasKeyword("Trample")) {
+ if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int damage = ComputerUtilCombat.getAttack(attacker);
for (Card blocker : this.blockers) {
if (CombatUtil.canBlock(attacker, blocker)) {
@@ -586,13 +621,6 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object.
*/
public final void declareAttackers(final Combat combat) {
- // if this method is called multiple times during a turn,
- // it will always return the same value
- // randomInt is used so that the computer doesn't always
- // do the same thing on turn 3 if he had the same creatures in play
- // I know this is a little confusing
-
- random.setSeed(ai.getGame().getPhaseHandler().getTurn() + AiAttackController.randomInt);
if (this.attackers.isEmpty()) {
return;
@@ -705,20 +733,15 @@ public class AiAttackController {
// Exalted
if (combat.getAttackers().isEmpty()) {
- boolean exalted = false;
- int exaltedCount = 0;
- for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
- if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) {
- exalted = true;
- break;
- }
- if (c.getName().equals("Finest Hour") && ai.getGame().getPhaseHandler().isFirstCombat()) {
- exalted = true;
- break;
- }
- if (c.hasKeyword("Exalted")) {
- exaltedCount++;
- if (exaltedCount > 2) {
+ boolean exalted = ai.countExaltedBonus() > 2;
+
+ if (!exalted) {
+ for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
+ if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) {
+ exalted = true;
+ break;
+ }
+ if (c.getName().equals("Finest Hour") && ai.getGame().getPhaseHandler().isFirstCombat()) {
exalted = true;
break;
}
@@ -1025,24 +1048,6 @@ public class AiAttackController {
}
} // getAttackers()
- /**
- *
- * countExaltedBonus.
- *
- *
- * @param player
- * a {@link forge.game.player.Player} object.
- * @return a int.
- */
- public final static int countExaltedBonus(final Player player) {
- int bonus = 0;
- for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
- bonus += c.getAmountOfKeyword("Exalted");
- }
-
- return bonus;
- }
-
/**
*
* getAttack.
@@ -1055,7 +1060,7 @@ public class AiAttackController {
public final static int getAttack(final Card c) {
int n = c.getNetCombatDamage();
- if (c.hasKeyword("Double Strike")) {
+ if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
n *= 2;
}
@@ -1086,7 +1091,7 @@ public class AiAttackController {
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
if ((attacker.hasSVar("NonCombatPriority"))
- && (!attacker.hasKeyword("Vigilance"))) {
+ && (!attacker.hasKeyword(Keyword.VIGILANCE))) {
// For each level of priority, enemy has to have life as much as the creature's power
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
// the lower the priroity, the less willing the AI is to use the creature for attacking.
@@ -1138,7 +1143,7 @@ public class AiAttackController {
&& CombatUtil.canBlock(attacker, defender)) {
numberOfPossibleBlockers += 1;
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
- && !(attacker.hasKeyword("Undying") && attacker.getCounters(CounterType.P1P1) == 0)) {
+ && !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
// see if the defending creature is of higher or lower
// value. We don't want to attack only to lose value
@@ -1154,14 +1159,11 @@ public class AiAttackController {
if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) {
canKillAllDangerous = false;
} else {
- for (KeywordInterface inst : defender.getKeywords()) {
- String keyword = inst.getOriginal();
- if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
- canKillAllDangerous = false;
- break;
- // there is a creature that can survive an attack from this creature
- // and combat will have negative effects
- }
+ if (defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT)
+ || defender.hasKeyword(Keyword.LIFELINK)) {
+ canKillAllDangerous = false;
+ // there is a creature that can survive an attack from this creature
+ // and combat will have negative effects
}
// Check if maybe we are too reckless in adding this attacker
@@ -1185,7 +1187,7 @@ public class AiAttackController {
}
}
- if (!attacker.hasKeyword("Vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
+ if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false;
canBeKilled = true;
canBeKilledByOne = true;
@@ -1271,7 +1273,7 @@ public class AiAttackController {
// creature would leave the battlefield
// no pain in exerting it
shouldExert = true;
- } else if (c.hasKeyword("Vigilance")) {
+ } else if (c.hasKeyword(Keyword.VIGILANCE)) {
// Free exert - why not?
shouldExert = true;
}
@@ -1334,7 +1336,7 @@ public class AiAttackController {
}
}
- if (!shouldExert && random.nextBoolean()) {
+ if (!shouldExert && MyRandom.getRandom().nextBoolean()) {
// TODO Improve when the AI wants to use Exert powers
shouldExert = true;
}
diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java
index f5ee0467fb0..e948d3cebfc 100644
--- a/forge-ai/src/main/java/forge/ai/AiBlockController.java
+++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java
@@ -22,9 +22,11 @@ import com.google.common.base.Predicates;
import forge.card.CardStateName;
import forge.game.CardTraitBase;
import forge.game.GameEntity;
+import forge.game.GlobalRuleChange;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
+import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -117,11 +119,11 @@ public class AiBlockController {
// If I don't have any planeswalkers then sorting doesn't really matter
if (defenders.size() == 1) {
- final CardCollection attackers = combat.getAttackersOf(defenders.get(0));
+ final CardCollection attackers = combat.getAttackersOf(defenders.get(0));
// Begin with the attackers that pose the biggest threat
ComputerUtilCard.sortByEvaluateCreature(attackers);
CardLists.sortByPowerDesc(attackers);
- //move cards like Phage the Untouchable to the front
+ //move cards like Phage the Untouchable to the front
Collections.sort(attackers, new Comparator() {
@Override
public int compare(final Card o1, final Card o2) {
@@ -142,16 +144,16 @@ public class AiBlockController {
// defend planeswalkers with more loyalty before planeswalkers with less loyalty
// if planeswalker will be too difficult to defend don't even bother
for (GameEntity defender : defenders) {
- if (defender instanceof Card) {
- final CardCollection attackers = combat.getAttackersOf(defender);
- // Begin with the attackers that pose the biggest threat
- CardLists.sortByPowerDesc(attackers);
- for (final Card c : attackers) {
- sortedAttackers.add(c);
- }
- } else if (defender instanceof Player && defender.equals(ai)){
- firstAttacker = combat.getAttackersOf(defender);
- }
+ if (defender instanceof Card) {
+ final CardCollection attackers = combat.getAttackersOf(defender);
+ // Begin with the attackers that pose the biggest threat
+ CardLists.sortByPowerDesc(attackers);
+ for (final Card c : attackers) {
+ sortedAttackers.add(c);
+ }
+ } else if (defender instanceof Player && defender.equals(ai)) {
+ firstAttacker = combat.getAttackersOf(defender);
+ }
}
if (bLifeInDanger) {
@@ -180,7 +182,7 @@ public class AiBlockController {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
- || attacker.hasKeyword("Menace")) {
+ || attacker.hasKeyword(Keyword.MENACE)) {
continue;
}
@@ -204,11 +206,11 @@ public class AiBlockController {
&& !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers);
// check whether it's better to block a creature without trample to absorb more damage
- if (attacker.hasKeyword("Trample")) {
+ if (attacker.hasKeyword(Keyword.TRAMPLE)) {
boolean doNotBlock = false;
for (Card other : attackersLeft) {
if (other.equals(attacker) || !CombatUtil.canBlock(other, blocker)
- || other.hasKeyword("Trample")
+ || other.hasKeyword(Keyword.TRAMPLE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|| ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false)
|| other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
@@ -231,11 +233,10 @@ public class AiBlockController {
// 3.Blockers that can destroy the attacker and have an upside when dying
killingBlockers = getKillingBlockers(combat, attacker, blockers);
for (Card b : killingBlockers) {
- if ((b.hasKeyword("Undying") && b.getCounters(CounterType.P1P1) == 0)
- || b.hasSVar("SacMe")
- || (b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1)
- || (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0)
- || b.hasSVar("EndOfTurnLeavePlay")) {
+ if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe")
+ || (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
+ || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
+ || b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
break;
}
@@ -245,7 +246,7 @@ public class AiBlockController {
if (b.hasSVar("SacMe") && Integer.parseInt(b.getSVar("SacMe")) > 3) {
blocker = b;
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
- blockedButUnkilled.add(attacker);
+ blockedButUnkilled.add(attacker);
}
break;
}
@@ -293,8 +294,7 @@ public class AiBlockController {
// 6. Blockers that don't survive until the next turn anyway
for (final Card attacker : attackersLeft) {
- if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
- || attacker.hasKeyword("Menace")
+ if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -304,12 +304,12 @@ public class AiBlockController {
final List blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
for (Card b : blockers) {
- if ((b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1)
- || (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0)
- || b.hasSVar("EndOfTurnLeavePlay")) {
+ if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
+ || (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
+ || b.hasSVar("EndOfTurnLeavePlay")) {
blocker = b;
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
- blockedButUnkilled.add(attacker);
+ blockedButUnkilled.add(attacker);
}
break;
}
@@ -531,7 +531,7 @@ public class AiBlockController {
// Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) {
- if (!attacker.hasKeyword("Menace") && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
+ if (!attacker.hasKeyword(Keyword.MENACE) && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
continue;
}
@@ -590,7 +590,7 @@ public class AiBlockController {
for (final Card attacker : attackersLeft) {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
- || attacker.hasKeyword("Menace")
+ || attacker.hasKeyword(Keyword.MENACE)
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -645,7 +645,7 @@ public class AiBlockController {
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
- || attacker.hasKeyword("Menace")
+ || attacker.hasKeyword(Keyword.MENACE)
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
attackers.remove(0);
makeChumpBlocks(combat, attackers);
@@ -657,7 +657,7 @@ public class AiBlockController {
final Card blocker = ComputerUtilCard.getWorstCreatureAI(chumpBlockers);
// check if it's better to block a creature with lower power and without trample
- if (attacker.hasKeyword("Trample")) {
+ if (attacker.hasKeyword(Keyword.TRAMPLE)) {
final int damageAbsorbed = blocker.getLethalDamage();
if (attacker.getNetCombatDamage() > damageAbsorbed) {
for (Card other : attackers) {
@@ -665,7 +665,7 @@ public class AiBlockController {
continue;
}
if (other.getNetCombatDamage() >= damageAbsorbed
- && !other.hasKeyword("Trample")
+ && !other.hasKeyword(Keyword.TRAMPLE)
&& !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
&& !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
&& CombatUtil.canBlock(other, blocker, combat)) {
@@ -696,7 +696,7 @@ public class AiBlockController {
for (final Card attacker : currentAttackers) {
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
- && !attacker.hasKeyword("Menace")
+ && !attacker.hasKeyword(Keyword.MENACE)
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
}
@@ -729,7 +729,7 @@ public class AiBlockController {
List chumpBlockers;
- List tramplingAttackers = CardLists.getKeyword(attackers, "Trample");
+ List tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
// TODO - should check here for a "rampage-like" trigger that replaced
@@ -738,7 +738,7 @@ public class AiBlockController {
for (final Card attacker : tramplingAttackers) {
- if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) && !combat.isBlocked(attacker))
+ if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) && !combat.isBlocked(attacker))
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue;
@@ -772,32 +772,42 @@ public class AiBlockController {
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
blockers.removeAll(combat.getBlockers(attacker));
+ // Don't add any blockers that won't kill the attacker because the damage would be prevented by a static effect
+ if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
+ blockers = CardLists.filter(blockers, new Predicate() {
+ @Override
+ public boolean apply(Card blocker) {
+ return !ComputerUtilCombat.isCombatDamagePrevented(blocker, attacker, blocker.getNetCombatDamage());
+ }
+ });
+ }
+
// Try to use safe blockers first
if (blockers.size() > 0) {
safeBlockers = getSafeBlockers(combat, attacker, blockers);
- for (final Card blocker : safeBlockers) {
- final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
- + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
- // Add an additional blocker if the current blockers are not
- // enough and the new one would deal additional damage
- if (damageNeeded > ComputerUtilCombat.totalDamageOfBlockers(attacker, combat.getBlockers(attacker))
- && ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker) > 0
- && CombatUtil.canBlock(attacker, blocker, combat)) {
- combat.addBlocker(attacker, blocker);
- }
- blockers.remove(blocker); // Don't check them again next
- }
+ for (final Card blocker : safeBlockers) {
+ final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
+ + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
+ // Add an additional blocker if the current blockers are not
+ // enough and the new one would deal additional damage
+ if (damageNeeded > ComputerUtilCombat.totalDamageOfBlockers(attacker, combat.getBlockers(attacker))
+ && ComputerUtilCombat.dealsDamageAsBlocker(attacker, blocker) > 0
+ && CombatUtil.canBlock(attacker, blocker, combat)) {
+ combat.addBlocker(attacker, blocker);
+ }
+ blockers.remove(blocker); // Don't check them again next
+ }
}
// don't try to kill what can't be killed
- if (attacker.hasKeyword("indestructible") || ComputerUtil.canRegenerate(ai, attacker)) {
- continue;
+ if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) {
+ continue;
}
// Try to add blockers that could be destroyed, but are worth less than the attacker
// Don't use blockers without First Strike or Double Strike if attacker has it
if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat)) {
- safeBlockers = CardLists.getKeyword(blockers, "First Strike");
- safeBlockers.addAll(CardLists.getKeyword(blockers, "Double Strike"));
+ safeBlockers = CardLists.getKeyword(blockers, Keyword.FIRST_STRIKE);
+ safeBlockers.addAll(CardLists.getKeyword(blockers, Keyword.DOUBLE_STRIKE));
} else {
safeBlockers = new ArrayList<>(blockers);
}
@@ -870,7 +880,7 @@ public class AiBlockController {
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains((Card) def)) {
- if (attacker.hasKeyword("Trample")) {
+ if (attacker.hasKeyword(Keyword.TRAMPLE)) {
// don't bother trying to chump a trampling creature
continue;
}
@@ -985,7 +995,7 @@ public class AiBlockController {
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
- diff = 0;
+ diff = 0;
}
// remove all attackers that can't be blocked anyway
@@ -1015,73 +1025,80 @@ public class AiBlockController {
// When the AI holds some Fog effect, don't bother about lifeInDanger
if (!ComputerUtil.hasAFogEffect(ai)) {
- lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
+ lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
makeTradeBlocks(combat); // choose necessary trade blocks
- // if life is still in danger
- if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
- makeChumpBlocks(combat); // choose necessary chump blocks
- } else {
- lifeInDanger = false;
+ // if life is still in danger
+ if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ makeChumpBlocks(combat); // choose necessary chump blocks
+ } else {
+ lifeInDanger = false;
}
- // if life is still in danger
- // Reinforce blockers blocking attackers with trample if life is still
- // in danger
- if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
- reinforceBlockersAgainstTrample(combat);
- } else {
- lifeInDanger = false;
+ // if life is still in danger
+ // Reinforce blockers blocking attackers with trample if life is
+ // still
+ // in danger
+ if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ reinforceBlockersAgainstTrample(combat);
+ } else {
+ lifeInDanger = false;
+ }
+ // Support blockers not destroying the attacker with more blockers
+ // to
+ // try to kill the attacker
+ if (!lifeInDanger) {
+ reinforceBlockersToKill(combat);
}
- // Support blockers not destroying the attacker with more blockers to
- // try to kill the attacker
- if (!lifeInDanger) {
- reinforceBlockersToKill(combat);
- }
- // == 2. If the AI life would still be in danger make a safer approach ==
- if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
- clearBlockers(combat, possibleBlockers); // reset every block assignment
- makeTradeBlocks(combat); // choose necessary trade blocks
- // if life is in danger
- makeGoodBlocks(combat);
- // choose necessary chump blocks if life is still in danger
- if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
- makeChumpBlocks(combat);
- } else {
- lifeInDanger = false;
- }
- // Reinforce blockers blocking attackers with trample if life is
- // still in danger
- if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
- reinforceBlockersAgainstTrample(combat);
- } else {
- lifeInDanger = false;
- }
- makeGangBlocks(combat);
- reinforceBlockersToKill(combat);
- }
+ // == 2. If the AI life would still be in danger make a safer
+ // approach ==
+ if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ clearBlockers(combat, possibleBlockers); // reset every block
+ // assignment
+ makeTradeBlocks(combat); // choose necessary trade blocks
+ // if life is in danger
+ makeGoodBlocks(combat);
+ // choose necessary chump blocks if life is still in danger
+ if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ makeChumpBlocks(combat);
+ } else {
+ lifeInDanger = false;
+ }
+ // Reinforce blockers blocking attackers with trample if life is
+ // still in danger
+ if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ reinforceBlockersAgainstTrample(combat);
+ } else {
+ lifeInDanger = false;
+ }
+ makeGangBlocks(combat);
+ reinforceBlockersToKill(combat);
+ }
- // == 3. If the AI life would be in serious danger make an even safer approach ==
- if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
- clearBlockers(combat, possibleBlockers); // reset every block assignment
- makeChumpBlocks(combat); // choose chump blocks
- if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
- makeTradeBlocks(combat); // choose necessary trade
- }
+ // == 3. If the AI life would be in serious danger make an even
+ // safer approach ==
+ if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
+ clearBlockers(combat, possibleBlockers); // reset every block
+ // assignment
+ makeChumpBlocks(combat); // choose chump blocks
+ if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ makeTradeBlocks(combat); // choose necessary trade
+ }
- if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
- makeGoodBlocks(combat);
- }
- // Reinforce blockers blocking attackers with trample if life is
- // still in danger
- else {
- reinforceBlockersAgainstTrample(combat);
- }
- makeGangBlocks(combat);
- // Support blockers not destroying the attacker with more blockers
- // to try to kill the attacker
- reinforceBlockersToKill(combat);
- }
+ if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
+ makeGoodBlocks(combat);
+ }
+ // Reinforce blockers blocking attackers with trample if life is
+ // still in danger
+ else {
+ reinforceBlockersAgainstTrample(combat);
+ }
+ makeGangBlocks(combat);
+ // Support blockers not destroying the attacker with more
+ // blockers
+ // to try to kill the attacker
+ reinforceBlockersToKill(combat);
+ }
}
// assign blockers that have to block
@@ -1167,10 +1184,11 @@ public class AiBlockController {
* Orders a blocker that put onto the battlefield blocking. Depends heavily
* on the implementation of orderBlockers().
*/
- public static CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) {
- // add blocker to existing ordering
- // sort by evaluate, then insert it appropriately
- // relies on current implementation of orderBlockers()
+ public static CardCollection orderBlocker(final Card attacker, final Card blocker,
+ final CardCollection oldBlockers) {
+ // add blocker to existing ordering
+ // sort by evaluate, then insert it appropriately
+ // relies on current implementation of orderBlockers()
final CardCollection allBlockers = new CardCollection(oldBlockers);
allBlockers.add(blocker);
ComputerUtilCard.sortByEvaluateCreature(allBlockers);
@@ -1182,24 +1200,28 @@ public class AiBlockController {
boolean newBlockerIsAdded = false;
// The new blocker comes right after this one
final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1));
- if (newBlockerRightAfter == null && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
- result.add(blocker);
- newBlockerIsAdded = true;
+ if (newBlockerRightAfter == null
+ && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
+ result.add(blocker);
+ newBlockerIsAdded = true;
}
- // Don't bother to keep damage up-to-date after the new blocker is added, as we can't modify the order of the other cards anyway
+ // Don't bother to keep damage up-to-date after the new blocker is
+ // added, as we can't modify the order of the other cards anyway
for (final Card c : oldBlockers) {
- final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true);
- damage -= lethal;
- result.add(c);
- if (!newBlockerIsAdded && c == newBlockerRightAfter && damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
- // If blocker is right after this card in priority and we have sufficient damage to kill it, add it here
- result.add(blocker);
- newBlockerIsAdded = true;
- }
+ final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true);
+ damage -= lethal;
+ result.add(c);
+ if (!newBlockerIsAdded && c == newBlockerRightAfter
+ && damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
+ // If blocker is right after this card in priority and we have
+ // sufficient damage to kill it, add it here
+ result.add(blocker);
+ newBlockerIsAdded = true;
+ }
}
// We don't have sufficient damage, just add it at the end!
if (!newBlockerIsAdded) {
- result.add(blocker);
+ result.add(blocker);
}
return result;
diff --git a/forge-ai/src/main/java/forge/ai/AiCardMemory.java b/forge-ai/src/main/java/forge/ai/AiCardMemory.java
index 510c6834c48..17a58d5eaf1 100644
--- a/forge-ai/src/main/java/forge/ai/AiCardMemory.java
+++ b/forge-ai/src/main/java/forge/ai/AiCardMemory.java
@@ -40,43 +40,55 @@ import java.util.Set;
*/
public class AiCardMemory {
- private final Set memMandatoryAttackers;
- private final Set memTrickAttackers;
- private final Set memHeldManaSources;
- private final Set memHeldManaSourcesForCombat;
- private final Set memAttachedThisTurn;
- private final Set memAnimatedThisTurn;
- private final Set memBouncedThisTurn;
- private final Set memActivatedThisTurn;
-
- public AiCardMemory() {
- this.memMandatoryAttackers = new HashSet<>();
- this.memHeldManaSources = new HashSet<>();
- this.memHeldManaSourcesForCombat = new HashSet<>();
- this.memAttachedThisTurn = new HashSet<>();
- this.memAnimatedThisTurn = new HashSet<>();
- this.memBouncedThisTurn = new HashSet<>();
- this.memActivatedThisTurn = new HashSet<>();
- this.memTrickAttackers = new HashSet<>();
- }
-
/**
* Defines the memory set in which the card is remembered
* (which, in its turn, defines how the AI utilizes the information
* about remembered cards).
*/
public enum MemorySet {
- MANDATORY_ATTACKERS,
- TRICK_ATTACKERS,
- HELD_MANA_SOURCES_FOR_MAIN2,
- HELD_MANA_SOURCES_FOR_DECLBLK,
- ATTACHED_THIS_TURN,
- ANIMATED_THIS_TURN,
- BOUNCED_THIS_TURN,
- ACTIVATED_THIS_TURN,
+ MANDATORY_ATTACKERS, // These creatures must attack this turn
+ TRICK_ATTACKERS, // These creatures will attack to try to provoke the opponent to block them into a combat trick
+ HELD_MANA_SOURCES_FOR_MAIN2, // These mana sources will not be used before Main 2
+ HELD_MANA_SOURCES_FOR_DECLBLK, // These mana sources will not be used before Combat - Declare Blockers
+ HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK, // These mana sources will not be used before the opponent's Combat - Declare Blockers
+ HELD_MANA_SOURCES_FOR_NEXT_SPELL, // These mana sources will not be used until the next time the AI chooses a spell to cast
+ ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
+ ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
+ BOUNCED_THIS_TURN, // These cards were bounced this turn
+ ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
+ CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
+ MARKED_TO_AVOID_REENTRY // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
//REVEALED_CARDS // stub, not linked to AI code yet
}
+ private final Set memMandatoryAttackers;
+ private final Set memTrickAttackers;
+ private final Set memHeldManaSources;
+ private final Set memHeldManaSourcesForCombat;
+ private final Set memHeldManaSourcesForEnemyCombat;
+ private final Set memHeldManaSourcesForNextSpell;
+ private final Set memAttachedThisTurn;
+ private final Set memAnimatedThisTurn;
+ private final Set memBouncedThisTurn;
+ private final Set memActivatedThisTurn;
+ private final Set memChosenFogEffect;
+ private final Set memMarkedToAvoidReentry;
+
+ public AiCardMemory() {
+ this.memMandatoryAttackers = new HashSet<>();
+ this.memHeldManaSources = new HashSet<>();
+ this.memHeldManaSourcesForCombat = new HashSet<>();
+ this.memHeldManaSourcesForEnemyCombat = new HashSet<>();
+ this.memAttachedThisTurn = new HashSet<>();
+ this.memAnimatedThisTurn = new HashSet<>();
+ this.memBouncedThisTurn = new HashSet<>();
+ this.memActivatedThisTurn = new HashSet<>();
+ this.memTrickAttackers = new HashSet<>();
+ this.memChosenFogEffect = new HashSet<>();
+ this.memMarkedToAvoidReentry = new HashSet<>();
+ this.memHeldManaSourcesForNextSpell = new HashSet<>();
+ }
+
private Set getMemorySet(MemorySet set) {
switch (set) {
case MANDATORY_ATTACKERS:
@@ -87,6 +99,10 @@ public class AiCardMemory {
return memHeldManaSources;
case HELD_MANA_SOURCES_FOR_DECLBLK:
return memHeldManaSourcesForCombat;
+ case HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
+ return memHeldManaSourcesForEnemyCombat;
+ case HELD_MANA_SOURCES_FOR_NEXT_SPELL:
+ return memHeldManaSourcesForNextSpell;
case ATTACHED_THIS_TURN:
return memAttachedThisTurn;
case ANIMATED_THIS_TURN:
@@ -95,6 +111,10 @@ public class AiCardMemory {
return memBouncedThisTurn;
case ACTIVATED_THIS_TURN:
return memActivatedThisTurn;
+ case CHOSEN_FOG_EFFECT:
+ return memChosenFogEffect;
+ case MARKED_TO_AVOID_REENTRY:
+ return memMarkedToAvoidReentry;
//case REVEALED_CARDS:
// return memRevealedCards;
default:
@@ -263,33 +283,66 @@ public class AiCardMemory {
* Clears all memory sets stored in this card memory for the given player.
*/
public void clearAllRemembered() {
- clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
- clearMemorySet(MemorySet.TRICK_ATTACKERS);
- clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
- clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
- clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
- clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
- clearMemorySet(MemorySet.BOUNCED_THIS_TURN);
- clearMemorySet(MemorySet.ACTIVATED_THIS_TURN);
+ for (MemorySet memSet : MemorySet.values()) {
+ clearMemorySet(memSet);
+ }
}
// Static functions to simplify access to AI card memory of a given AI player.
public static void rememberCard(Player ai, Card c, MemorySet set) {
+ if (!ai.getController().isAI()) {
+ return;
+ }
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().rememberCard(c, set);
}
+ public static void rememberCard(AiController aic, Card c, MemorySet set) {
+ aic.getCardMemory().rememberCard(c, set);
+ }
public static void forgetCard(Player ai, Card c, MemorySet set) {
+ if (!ai.getController().isAI()) {
+ return;
+ }
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().forgetCard(c, set);
}
+ public static void forgetCard(AiController aic, Card c, MemorySet set) {
+ aic.getCardMemory().forgetCard(c, set);
+ }
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
+ if (!ai.getController().isAI()) {
+ return false;
+ }
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
}
+ public static boolean isRememberedCard(AiController aic, Card c, MemorySet set) {
+ return aic.getCardMemory().isRememberedCard(c, set);
+ }
public static boolean isRememberedCardByName(Player ai, String name, MemorySet set) {
+ if (!ai.getController().isAI()) {
+ return false;
+ }
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCardByName(name, set);
}
+ public static boolean isRememberedCardByName(AiController aic, String name, MemorySet set) {
+ return aic.getCardMemory().isRememberedCardByName(name, set);
+ }
public static void clearMemorySet(Player ai, MemorySet set) {
+ if (!ai.getController().isAI()) {
+ return;
+ }
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
}
+ public static void clearMemorySet(AiController aic, MemorySet set) {
+ if (!isMemorySetEmpty(aic, set)) {
+ aic.getCardMemory().clearMemorySet(set);
+ }
+ }
public static boolean isMemorySetEmpty(Player ai, MemorySet set) {
+ if (!ai.getController().isAI()) {
+ return false;
+ }
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isMemorySetEmpty(set);
}
+ public static boolean isMemorySetEmpty(AiController aic, MemorySet set) {
+ return aic.getCardMemory().isMemorySetEmpty(set);
+ }
}
\ No newline at end of file
diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java
index 077b151457f..c4fc6be078f 100644
--- a/forge-ai/src/main/java/forge/ai/AiController.java
+++ b/forge-ai/src/main/java/forge/ai/AiController.java
@@ -38,10 +38,12 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased;
import forge.game.card.*;
+import forge.game.card.CardPredicates.Accessors;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
+import forge.game.keyword.Keyword;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -49,6 +51,7 @@ import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplaceMoved;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*;
+import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
@@ -58,6 +61,8 @@ import forge.util.Aggregates;
import forge.util.Expressions;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
+import io.sentry.Sentry;
+import io.sentry.event.BreadcrumbBuilder;
import java.security.InvalidParameterException;
import java.util.*;
@@ -88,6 +93,9 @@ public class AiController {
this.cheatShuffle = canCheatShuffle;
}
+ public boolean usesSimulation() {
+ return this.useSimulation;
+ }
public void setUseSimulation(boolean value) {
this.useSimulation = value;
}
@@ -126,10 +134,12 @@ public class AiController {
private List getPossibleETBCounters() {
CardCollection all = new CardCollection(player.getCardsIn(ZoneType.Hand));
+ CardCollectionView ccvPlayerLibrary = player.getCardsIn(ZoneType.Library);
+
all.addAll(player.getCardsIn(ZoneType.Exile));
all.addAll(player.getCardsIn(ZoneType.Graveyard));
- if (!player.getCardsIn(ZoneType.Library).isEmpty()) {
- all.add(player.getCardsIn(ZoneType.Library).get(0));
+ if (!ccvPlayerLibrary.isEmpty()) {
+ all.add(ccvPlayerLibrary.get(0));
}
for (final Player opp : player.getOpponents()) {
@@ -152,30 +162,34 @@ public class AiController {
// look for cards on the battlefield that should prevent the AI from using that spellability
private boolean checkCurseEffects(final SpellAbility sa) {
- for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
+ CardCollectionView ccvGameBattlefield = game.getCardsIn(ZoneType.Battlefield);
+ for (final Card c : ccvGameBattlefield) {
if (c.hasSVar("AICurseEffect")) {
final String curse = c.getSVar("AICurseEffect");
- final Card host = sa.getHostCard();
if ("NonActive".equals(curse) && !player.equals(game.getPhaseHandler().getPlayerTurn())) {
return true;
- } else if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
- && !sa.getHostCard().hasKeyword("Indestructible")) {
- return true;
- } else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
- && !sa.getHostCard().hasKeyword("CARDNAME can't be countered.")) {
- return true;
- } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")
- && host.getCMC() == c.getCounters(CounterType.CHARGE)) {
- return true;
- } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")) {
- for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
- if (!card.isToken() && card.getName().equals(host.getName())) {
- return true;
+ } else {
+ final Card host = sa.getHostCard();
+ if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
+ && !host.hasKeyword(Keyword.INDESTRUCTIBLE)) {
+ return true;
+ } else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
+ && CardFactoryUtil.isCounterable(host)) {
+ return true;
+ } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
+ && host.getCMC() == c.getCounters(CounterType.CHARGE)) {
+ return true;
+ } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
+ String hostName = host.getName();
+ for (Card card : ccvGameBattlefield) {
+ if (!card.isToken() && card.getName().equals(hostName)) {
+ return true;
+ }
}
- }
- for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
- if (card.getName().equals(host.getName())) {
- return true;
+ for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
+ if (card.getName().equals(hostName)) {
+ return true;
+ }
}
}
}
@@ -185,13 +199,14 @@ public class AiController {
}
public boolean checkETBEffects(final Card card, final SpellAbility sa, final ApiType api) {
- boolean rightapi = false;
-
if (card.isCreature()
&& game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers)) {
return api == null;
}
-
+ boolean rightapi = false;
+ String battlefield = ZoneType.Battlefield.toString();
+ Player activatingPlayer = sa.getActivatingPlayer();
+
// Trigger play improvements
for (final Trigger tr : card.getTriggers()) {
// These triggers all care for ETB effects
@@ -201,21 +216,22 @@ public class AiController {
continue;
}
- if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
+ if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (params.containsKey("ValidCard")) {
- if (!params.get("ValidCard").contains("Self")) {
+ String validCard = params.get("ValidCard");
+ if (!validCard.contains("Self")) {
continue;
}
- if (params.get("ValidCard").contains("notkicked")) {
+ if (validCard.contains("notkicked")) {
if (sa.isKicked()) {
continue;
}
- } else if (params.get("ValidCard").contains("kicked")) {
- if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
- String s = params.get("ValidCard").split("kicked ")[1];
+ } else if (validCard.contains("kicked")) {
+ if (validCard.contains("kicked ")) { // want a specific kicker
+ String s = validCard.split("kicked ")[1];
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
} else if (!sa.isKicked()) {
@@ -258,7 +274,7 @@ public class AiController {
}
if (sa != null) {
- exSA.setActivatingPlayer(sa.getActivatingPlayer());
+ exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
@@ -266,13 +282,11 @@ public class AiController {
exSA.setTrigger(true);
// for trigger test, need to ignore the conditions
- if (exSA.getConditions() != null) {
- SpellAbilityCondition cons = exSA.getConditions();
- if (cons.getIsPresent() != null) {
- String pres = cons.getIsPresent();
- if ("Card.Self".equals(pres) || "Card.StrictlySelf".equals(pres)) {
+ SpellAbilityCondition cons = exSA.getConditions();
+ if (cons != null) {
+ String pres = cons.getIsPresent();
+ if (pres != null && pres.matches("Card\\.(Strictly)?Self")) {
cons.setIsPresent(null);
- }
}
}
@@ -296,21 +310,22 @@ public class AiController {
continue;
}
- if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
+ if (!params.get("Destination").equals(battlefield)) {
continue;
}
if (params.containsKey("ValidCard")) {
- if (!params.get("ValidCard").contains("Self")) {
+ String validCard = params.get("ValidCard");
+ if (!validCard.contains("Self")) {
continue;
}
- if (params.get("ValidCard").contains("notkicked")) {
+ if (validCard.contains("notkicked")) {
if (sa.isKicked()) {
continue;
}
- } else if (params.get("ValidCard").contains("kicked")) {
- if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
- String s = params.get("ValidCard").split("kicked ")[1];
+ } else if (validCard.contains("kicked")) {
+ if (validCard.contains("kicked ")) { // want a specific kicker
+ String s = validCard.split("kicked ")[1];
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
} else if (!sa.isKicked()) { // otherwise just any must be present
@@ -326,7 +341,7 @@ public class AiController {
if (exSA != null) {
if (sa != null) {
- exSA.setActivatingPlayer(sa.getActivatingPlayer());
+ exSA.setActivatingPlayer(activatingPlayer);
}
else {
exSA.setActivatingPlayer(player);
@@ -374,8 +389,9 @@ public class AiController {
if (landsInPlay.size() + landList.size() > max) {
for (Card c : allCards) {
for (SpellAbility sa : c.getSpellAbilities()) {
- if (sa.getPayCosts() != null) {
- for (CostPart part : sa.getPayCosts().getCostParts()) {
+ Cost payCosts = sa.getPayCosts();
+ if (payCosts != null) {
+ for (CostPart part : payCosts.getCostParts()) {
if (part instanceof CostDiscard) {
return null;
}
@@ -389,10 +405,11 @@ public class AiController {
landList = CardLists.filter(landList, new Predicate() {
@Override
public boolean apply(final Card c) {
+ CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
canPlaySpellBasic(c, null);
- if (c.getType().isLegendary() && !c.getName().equals("Flagstones of Trokair")) {
- final CardCollectionView list = player.getCardsIn(ZoneType.Battlefield);
- if (Iterables.any(list, CardPredicates.nameEquals(c.getName()))) {
+ String name = c.getName();
+ if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
+ if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
return false;
}
}
@@ -401,7 +418,7 @@ public class AiController {
final FCollectionView spellAbilities = c.getSpellAbilities();
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
- CardCollection lands = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
+ CardCollection lands = new CardCollection(battlefield);
lands.addAll(hand);
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
@@ -412,7 +429,8 @@ public class AiController {
}
}
}
- return true;
+
+ return player.canPlayLand(c);
}
});
return landList;
@@ -422,6 +440,38 @@ public class AiController {
if (landList.isEmpty()) {
return null;
}
+
+ CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
+
+ // Some considerations for Momir/MoJhoSto
+ boolean hasMomir = !CardLists.filter(player.getCardsIn(ZoneType.Command),
+ CardPredicates.nameEquals("Momir Vig, Simic Visionary Avatar")).isEmpty();
+ if (hasMomir && nonLandsInHand.isEmpty()) {
+ // Only do this if we have an all-basic land hand, which covers both stock Momir and MoJhoSto modes
+ // and also a custom Vanguard setup with a customized basic land deck and Momir as the avatar.
+ String landStrategy = getProperty(AiProps.MOMIR_BASIC_LAND_STRATEGY);
+ if (landStrategy.equalsIgnoreCase("random")) {
+ // Pick a completely random basic land
+ return Aggregates.random(landList);
+ } else if (landStrategy.toLowerCase().startsWith("preforder:")) {
+ // Pick a basic land in order of preference, or play a random one if nothing is preferred
+ String order = landStrategy.substring(10);
+ for (char c : order.toCharArray()) {
+ byte color = MagicColor.fromName(c);
+ for (Card land : landList) {
+ for (final SpellAbility m : ComputerUtilMana.getAIPlayableMana(land)) {
+ AbilityManaPart mp = m.getManaPart();
+ if (mp.canProduce(MagicColor.toShortString(color), m)) {
+ return land;
+ }
+ }
+ }
+ }
+ return Aggregates.random(landList);
+ }
+ // If nothing is done here, proceeds to the default land picking strategy
+ }
+
//Skip reflected lands.
CardCollection unreflectedLands = new CardCollection(landList);
for (Card l : landList) {
@@ -433,7 +483,6 @@ public class AiController {
landList = unreflectedLands;
}
- CardCollection nonLandsInHand = CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
//try to skip lands that enter the battlefield tapped
if (!nonLandsInHand.isEmpty()) {
@@ -564,14 +613,17 @@ public class AiController {
Collections.sort(all, saComparator); // put best spells first
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
- if (sa.getApi() == ApiType.Counter || sa.getApi() == exceptSA) {
+ ApiType saApi = sa.getApi();
+
+ if (saApi == ApiType.Counter || saApi == exceptSA) {
continue;
}
sa.setActivatingPlayer(player);
// TODO: this currently only works as a limited prediction of permanent spells.
// Ideally this should cast canPlaySa to determine that the AI is truly able/willing to cast a spell,
// but that is currently difficult to implement due to various side effects leading to stack overflow.
- if (!ComputerUtil.castPermanentInMain1(player, sa) && sa.getHostCard() != null && !sa.getHostCard().isLand() && ComputerUtilCost.canPayCost(sa, player)) {
+ Card host = sa.getHostCard();
+ if (!ComputerUtil.castPermanentInMain1(player, sa) && host != null && !host.isLand() && ComputerUtilCost.canPayCost(sa, player)) {
if (sa instanceof SpellPermanent) {
return sa;
}
@@ -580,33 +632,61 @@ public class AiController {
return null;
}
- public void reserveManaSources(SpellAbility sa) {
- reserveManaSources(sa, PhaseType.MAIN2);
+ public boolean reserveManaSources(SpellAbility sa) {
+ return reserveManaSources(sa, PhaseType.MAIN2, false, false, null);
}
- public void reserveManaSources(SpellAbility sa, PhaseType phaseType) {
+ public boolean reserveManaSourcesForNextSpell(SpellAbility sa, SpellAbility exceptForSa) {
+ return reserveManaSources(sa, null, false, true, exceptForSa);
+ }
+
+ public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) {
+ return reserveManaSources(sa, phaseType, enemy, true, null);
+ }
+
+ public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) {
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
+ // used for chained spells where two spells need to be cast in succession
+ if (exceptForThisSa != null) {
+ manaSources.removeAll(ComputerUtilMana.getManaSourcesToPayCost(ComputerUtilMana.calculateManaCost(exceptForThisSa, true, 0), exceptForThisSa, player));
+ }
+
+ if (manaSources.isEmpty()) {
+ return false;
+ }
+
AiCardMemory.MemorySet memSet;
-
- switch (phaseType) {
- case MAIN2:
- memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
- break;
- case COMBAT_DECLARE_BLOCKERS:
- memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
- break;
- default:
- System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
- + phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
- memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
- break;
+ if (phaseType == null && forNextSpell) {
+ memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL;
+ } else {
+ switch (phaseType) {
+ case MAIN2:
+ memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
+ break;
+ case COMBAT_DECLARE_BLOCKERS:
+ memSet = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK
+ : AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
+ break;
+ default:
+ System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
+ + phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
+ memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
+ break;
+ }
}
- for (Card c : manaSources) {
- AiCardMemory.rememberCard(player, c, memSet);
+ // This is a simplification, since one mana source can produce more than one mana,
+ // but should work in most circumstances to ensure safety in whatever the AI is using this for.
+ if (manaSources.size() >= cost.getConvertedManaCost()) {
+ for (Card c : manaSources) {
+ AiCardMemory.rememberCard(player, c, memSet);
+ }
+ return true;
}
+
+ return false;
}
// This is for playing spells regularly (no Cascade/Ripple etc.)
@@ -615,11 +695,27 @@ public class AiController {
return AiPlayDecision.CantPlaySa;
}
- AiPlayDecision op = canPlaySa(sa);
- if (op != AiPlayDecision.WillPlay) {
- return op;
+ boolean xCost = sa.getPayCosts().hasXInAnyCostPart();
+ if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) {
+ // for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
+ // when the AI won't even be able to play the spell in the first place (even if it could afford it)
+ return AiPlayDecision.CantAfford;
}
- return ComputerUtilCost.canPayCost(sa, player) ? AiPlayDecision.WillPlay : AiPlayDecision.CantAfford;
+
+ AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
+ if (canPlay != AiPlayDecision.WillPlay) {
+ return canPlay;
+ }
+
+ if (xCost && !ComputerUtilCost.canPayCost(sa, player)) {
+ // for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
+ // on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
+ return AiPlayDecision.CantAfford;
+ }
+
+ // if we got here, looks like we can play the final cost and we could properly set up and target the API and
+ // are willing to play the SA
+ return AiPlayDecision.WillPlay;
}
public AiPlayDecision canPlaySa(SpellAbility sa) {
@@ -630,8 +726,37 @@ public class AiController {
if (sa instanceof WrappedAbility) {
return canPlaySa(((WrappedAbility) sa).getWrappedAbility());
}
+
+ // Trying to play a card that has Buyback without a Buyback cost, look for possible additional considerations
+ if (getBooleanProperty(AiProps.TRY_TO_PRESERVE_BUYBACK_SPELLS)) {
+ if (card.hasKeyword(Keyword.BUYBACK) && !sa.isBuyBackAbility() && !canPlaySpellWithoutBuyback(card, sa)) {
+ return AiPlayDecision.NeedsToPlayCriteriaNotMet;
+ }
+ }
+
+ // When processing a new SA, clear the previously remembered cards that have been marked to avoid re-entry
+ // which might potentially cause a stack overflow.
+ AiCardMemory.clearMemorySet(this, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY);
+
if (sa.getApi() != null) {
+
+ String msg = "AiController:canPlaySa: AI checks for if can PlaySa";
+ Sentry.getContext().recordBreadcrumb(
+ new BreadcrumbBuilder().setMessage(msg)
+ .withData("Api", sa.getApi().toString())
+ .withData("Card", card.getName()).withData("SA", sa.toString()).build()
+ );
+
+ // add Extra for debugging
+ Sentry.getContext().addExtra("Card", card);
+ Sentry.getContext().addExtra("SA", sa.toString());
+
boolean canPlay = SpellApiToAi.Converter.get(sa.getApi()).canPlayAIWithSubs(player, sa);
+
+ // remove added extra
+ Sentry.getContext().removeExtra("Card");
+ Sentry.getContext().removeExtra("SA");
+
if (!canPlay) {
return AiPlayDecision.CantPlayAi;
}
@@ -686,7 +811,7 @@ public class AiController {
// will need actual logic that determines if the enchantment is able
// to disable the permanent or it's still functional and a duplicate is unneeded.
boolean disabledByEnemy = false;
- for (Card card2 : card.getEnchantedBy(false)) {
+ for (Card card2 : card.getEnchantedBy()) {
if (card2.getOwner() != player) {
disabledByEnemy = true;
}
@@ -703,10 +828,71 @@ public class AiController {
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
}
+
// add any other necessary logic to play a basic spell here
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
}
+ private boolean canPlaySpellWithoutBuyback(Card card, SpellAbility sa) {
+ boolean wasteBuybackAllowed = false;
+
+ // About to lose game : allow
+ if (ComputerUtil.aiLifeInDanger(player, true, 0)) {
+ wasteBuybackAllowed = true;
+ }
+
+ int copies = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(card.getName())).size();
+ // Have two copies : allow
+ if (copies >= 2) {
+ wasteBuybackAllowed = true;
+ }
+
+ int neededMana = 0;
+ boolean dangerousRecurringCost = false;
+
+ Cost costWithBuyback = sa.getPayCosts() != null ? sa.getPayCosts().copy() : Cost.Zero;
+ for (OptionalCostValue opt : GameActionUtil.getOptionalCostValues(sa)) {
+ if (opt.getType() == OptionalCost.Buyback) {
+ costWithBuyback.add(opt.getCost());
+ }
+ }
+ CostAdjustment.adjust(costWithBuyback, sa);
+ if (costWithBuyback.getCostMana() != null) {
+ neededMana = costWithBuyback.getCostMana().getMana().getCMC();
+ }
+ if (costWithBuyback.hasSpecificCostType(CostPayLife.class)
+ || costWithBuyback.hasSpecificCostType(CostDiscard.class)
+ || costWithBuyback.hasSpecificCostType(CostSacrifice.class)) {
+ dangerousRecurringCost = true;
+ }
+
+ // won't be able to afford buyback any time soon
+ // if Buyback cost includes sacrifice, life, discard
+ if (dangerousRecurringCost) {
+ wasteBuybackAllowed = true;
+ }
+
+ // Memory Crystal-like effects need special handling
+ for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
+ for (StaticAbility s : c.getStaticAbilities()) {
+ if ("ReduceCost".equals(s.getParam("Mode"))
+ && "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
+ neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
+ }
+ }
+ }
+ if (neededMana < 0) {
+ neededMana = 0;
+ }
+
+ int hasMana = ComputerUtilMana.getAvailableManaEstimate(player, false);
+ if (hasMana < neededMana - 1) {
+ wasteBuybackAllowed = true;
+ }
+
+ return wasteBuybackAllowed;
+ }
+
// not sure "playing biggest spell" matters?
private final static Comparator saComparator = new Comparator() {
@Override
@@ -716,6 +902,13 @@ public class AiController {
int a1 = a.getPayCosts() == null ? 0 : a.getPayCosts().getTotalMana().getCMC();
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
+ // deprioritize SAs explicitly marked as preferred to be activated last compared to all other SAs
+ if (a.hasParam("AIActivateLast") && !b.hasParam("AIActivateLast")) {
+ return 1;
+ } else if (b.hasParam("AIActivateLast") && !a.hasParam("AIActivateLast")) {
+ return -1;
+ }
+
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
return 1;
@@ -755,6 +948,20 @@ public class AiController {
return 1;
}
+ if (a.getHostCard().equals(b.getHostCard()) && a.getApi() == b.getApi()
+ && a.getPayCosts() != null && b.getPayCosts() != null) {
+ // Cheaper Spectacle costs should be preferred
+ // FIXME: Any better way to identify that these are the same ability, one with Spectacle and one not?
+ // (looks like it's not a full-fledged alternative cost as such, and is not processed with other alt costs)
+ if (a.isSpectacle() && !b.isSpectacle()
+ && a.getPayCosts().getTotalMana().getCMC() < b.getPayCosts().getTotalMana().getCMC()) {
+ return 1;
+ } else if (b.isSpectacle() && !a.isSpectacle()
+ && b.getPayCosts().getTotalMana().getCMC() < a.getPayCosts().getTotalMana().getCMC()) {
+ return 1;
+ }
+ }
+
a1 += getSpellAbilityPriority(a);
b1 += getSpellAbilityPriority(b);
@@ -776,6 +983,9 @@ public class AiController {
if (source.isCreature()) {
p += 1;
}
+ if (source.hasSVar("AIPriorityModifier")) {
+ p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
+ }
// don't play equipments before having any creatures
if (source.isEquipment() && noCreatures) {
p -= 9;
@@ -792,14 +1002,13 @@ public class AiController {
}
}
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
- if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
+ if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) {
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
}
}
// use Surge and Prowl costs when able to
- if (sa.isSurged() ||
- (sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
+ if (sa.isSurged() || sa.isProwl()) {
p += 9;
}
// sort planeswalker abilities with most costly first
@@ -942,10 +1151,14 @@ public class AiController {
numLandsAvailable++;
}
- //Discard unplayable card
+ // Discard unplayable card (checks by CMC)
+ // But check if there is a card in play that allows casting spells for free!
+ // if yes, nothing is unplayable based on CMC alone
boolean discardedUnplayable = false;
+ boolean freeCastAllowed = ComputerUtilCost.isFreeCastAllowedByPermanent(player, null);
+
for (int j = 0; j < validCards.size(); j++) {
- if (validCards.get(j).getCMC() > numLandsAvailable && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
+ if ((validCards.get(j).getCMC() > numLandsAvailable || freeCastAllowed) && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
discardList.add(validCards.get(j));
validCards.remove(validCards.get(j));
discardedUnplayable = true;
@@ -998,7 +1211,7 @@ public class AiController {
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
ApiType api = sa.getApi();
- // Abilities without api may also use this routine, However they should provide a unique mode value
+ // Abilities without api may also use this routine, However they should provide a unique mode value ?? How could this work?
if (api == null) {
String exMsg = String.format("AI confirmAction does not know what to decide about %s mode (api is null).",
mode);
@@ -1140,6 +1353,9 @@ public class AiController {
// re-created if needed and used for any AI logic that needs it.
predictedCombat = null;
+ // Reset priority mana reservation that's meant to work for one spell only
+ AiCardMemory.clearMemorySet(player, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL);
+
if (useSimulation) {
return singleSpellAbilityList(simPicker.chooseSpellAbilityToPlay(null));
}
@@ -1161,7 +1377,7 @@ public class AiController {
if (landsWannaPlay != null) {
landsWannaPlay = filterLandsToPlay(landsWannaPlay);
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
- if (landsWannaPlay != null && !landsWannaPlay.isEmpty() && player.canPlayLand(null)) {
+ if (landsWannaPlay != null && !landsWannaPlay.isEmpty()) {
// TODO search for other land it might want to play?
Card land = chooseBestLandToPlay(landsWannaPlay);
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
@@ -1303,12 +1519,28 @@ public class AiController {
}
private final SpellAbility getSpellAbilityToPlay() {
- // if top of stack is owned by me
- if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
- // probably should let my stuff resolve
- return null;
- }
final CardCollection cards = ComputerUtilAbility.getAvailableCards(game, player);
+ List saList = Lists.newArrayList();
+
+ SpellAbility top = null;
+ if (!game.getStack().isEmpty()) {
+ top = game.getStack().peekAbility();
+ }
+ final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player);
+
+ if (topOwnedByAI) {
+ // AI's own spell: should probably let my stuff resolve first, but may want to copy the SA or respond to it
+ // in a scripted timed fashion.
+ final boolean mustRespond = top.hasParam("AIRespondsToOwnAbility");
+
+ if (!mustRespond) {
+ saList = ComputerUtilAbility.getSpellAbilities(cards, player); // get the SA list early to check for copy SAs
+ if (ComputerUtilAbility.getFirstCopySASpell(saList) == null) {
+ // Nothing to copy the spell with, so do nothing.
+ return null;
+ }
+ }
+ }
if (!game.getStack().isEmpty()) {
SpellAbility counter = chooseCounterSpell(getPlayableCounters(cards));
@@ -1319,7 +1551,13 @@ public class AiController {
return counterETB;
}
- return chooseSpellAbilityToPlayFromList(ComputerUtilAbility.getSpellAbilities(cards, player), true);
+ if (saList.isEmpty()) {
+ saList = ComputerUtilAbility.getSpellAbilities(cards, player);
+ }
+
+ SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true);
+
+ return chosenSa;
}
private SpellAbility chooseSpellAbilityToPlayFromList(final List all, boolean skipCounter) {
@@ -1334,7 +1572,7 @@ public class AiController {
continue;
}
- if (sa.getHostCard().hasKeyword("Storm")
+ if (sa.getHostCard().hasKeyword(Keyword.STORM)
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
&& CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm")))).size() > 0) {
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
@@ -1346,7 +1584,7 @@ public class AiController {
sa.setActivatingPlayer(player);
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
-
+
AiPlayDecision opinion = canPlayAndPayFor(sa);
// PhaseHandler ph = game.getPhaseHandler();
// System.out.printf("Ai thinks '%s' of %s -> %s @ %s %s >>> \n", opinion, sa.getHostCard(), sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());
@@ -1470,23 +1708,25 @@ public class AiController {
boolean hasLeyline1 = false;
SpellAbility saGemstones = null;
-
- for(int i = 0; i < result.size(); i++) {
- SpellAbility sa = result.get(i);
-
+
+ List toRemove = Lists.newArrayList();
+ for(SpellAbility sa : result) {
String srcName = sa.getHostCard().getName();
if ("Gemstone Caverns".equals(srcName)) {
if (saGemstones == null)
saGemstones = sa;
else
- result.remove(i--);
+ toRemove.add(sa);
} else if ("Leyline of Singularity".equals(srcName)) {
if (!hasLeyline1)
hasLeyline1 = true;
else
- result.remove(i--);
+ toRemove.add(sa);
}
}
+ for(SpellAbility sa : toRemove) {
+ result.remove(sa);
+ }
// Play them last
if (saGemstones != null) {
@@ -1562,11 +1802,11 @@ public class AiController {
// and exaclty one counter of the specifice type gets high priority to keep the card
if (allies.contains(crd.getController())) {
// except if its a Chronozoa, because it WANTS to be removed to make more
- if (crd.hasKeyword("Vanishing") && !"Chronozoa".equals(crd.getName())) {
+ if (crd.hasKeyword(Keyword.VANISHING) && !"Chronozoa".equals(crd.getName())) {
if (crd.getCounters(CounterType.TIME) == 1) {
return CounterType.TIME;
}
- } else if (crd.hasKeyword("Fading")) {
+ } else if (crd.hasKeyword(Keyword.FADING)) {
if (crd.getCounters(CounterType.FADE) == 1) {
return CounterType.FADE;
}
@@ -1688,7 +1928,7 @@ public class AiController {
if (!useSimulation) {
for (Entry ds : myDeck) {
for (Entry cp : ds.getValue()) {
- if (cp.getKey().getRules().getAiHints().getRemAIDecks())
+ if (cp.getKey().getRules().getAiHints().getRemAIDecks())
result.add(cp.getKey());
}
}
@@ -1788,6 +2028,12 @@ public class AiController {
return left.contains(ComputerUtilCard.getBestCreatureAI(all));
}
}
+ if ("Aminatou".equals(sa.getParam("AILogic")) && game.getPlayers().size() > 2) {
+ CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.NONLAND_PERMANENTS);
+ CardCollection left = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Left));
+ CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(player, Direction.Right));
+ return Aggregates.sum(left, Accessors.fnGetCmc) > Aggregates.sum(right, Accessors.fnGetCmc);
+ }
return MyRandom.getRandom().nextBoolean();
}
diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java
index 1d7179b017f..c5ae2738f94 100644
--- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java
+++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java
@@ -459,6 +459,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
String type = cost.getType();
boolean isVehicle = type.contains("+withTotalPowerGE");
+
+ CardCollection exclude = new CardCollection();
+ exclude.addAll(tapped);
+
if (c == null) {
final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) {
@@ -467,6 +471,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
ability.getActivatingPlayer(), ability.getHostCard(), ability);
typeList = CardLists.filter(typeList, Presets.UNTAPPED);
c = typeList.size();
+ // account for the fact that the activated card may be tapped in the process
+ if (ability.getPayCosts().hasTapCost() && typeList.contains(ability.getHostCard())) {
+ c--;
+ }
source.setSVar("ChosenX", "Number$" + Integer.toString(c));
} else {
if (!isVehicle) {
@@ -478,18 +486,36 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
+ if ("DontPayTapCostWithManaSources".equals(source.getSVar("AIPaymentPreference"))) {
+ CardCollectionView toExclude =
+ CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"),
+ ability.getActivatingPlayer(), ability.getHostCard(), ability);
+ toExclude = CardLists.filter(toExclude, new Predicate() {
+ @Override
+ public boolean apply(Card card) {
+ for (final SpellAbility sa : card.getSpellAbilities()) {
+ if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
+ return true;
+ }
+ }
+ return false;
+ }
+ });
+ exclude.addAll(toExclude);
+ }
+
String totalP = "";
CardCollectionView totap;
if (isVehicle) {
totalP = type.split("withTotalPowerGE")[1];
type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
- totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped);
+ totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), exclude);
} else {
- totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);
+ totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, exclude);
}
if (totap == null) {
-// System.out.println("Couldn't find a valid card(s) to tap for: " + source.getName());
+ //System.out.println("Couldn't find a valid card(s) to tap for: " + source.getName());
return null;
}
tapped.addAll(totap);
@@ -510,19 +536,26 @@ public class AiCostDecision extends CostDecisionMakerBase {
Integer c = cost.convertAmount();
if (c == null) {
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
- if ("SacToReduceCost".equals(ability.getParam("AILogic"))) {
+ String logic = ability.getParamOrDefault("AILogic", "");
+ if ("SacToReduceCost".equals(logic)) {
// e.g. Torgaar, Famine Incarnate
// TODO: currently returns an empty list, so the AI doesn't sacrifice anything. Trying to make
// the AI decide on creatures to sac makes the AI sacrifice them, but the cost is not reduced and the
// AI pays the full mana cost anyway (despite sacrificing creatures).
return PaymentDecision.card(new CardCollection());
+ } else if (!logic.isEmpty() && !logic.equals("Never")) {
+ // If at least some other AI logic is specified, assume that the AI for that API knows how
+ // to define ChosenX and thus honor that value.
+ // Cards which have no special logic for this yet but which do work in a simple/suboptimal way
+ // are currently conventionally flagged with AILogic$ DoSacrifice.
+ c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
+ } else {
+ // Other cards are assumed to be flagged AI:RemoveDeck:All for now
+ return null;
}
-
- // Other cards are assumed to be flagged RemAIDeck for now
- return null;
+ } else {
+ c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
-
- c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
}
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
@@ -776,8 +809,20 @@ public class AiCostDecision extends CostDecisionMakerBase {
final String sVar = ability.getSVar(amount);
if (sVar.equals("XChoice")) {
c = AbilityUtils.calculateAmount(source, "ChosenX", ability);
+ source.setSVar("ChosenX", "Number$" + String.valueOf(c));
} else if (amount.equals("All")) {
c = source.getCounters(cost.counter);
+ } else if (sVar.equals("Targeted$CardManaCost")) {
+ c = 0;
+ if (ability.getTargets().getNumTargeted() > 0) {
+ for (Card tgt : ability.getTargets().getTargetCards()) {
+ if (tgt.getManaCost() != null) {
+ c += tgt.getManaCost().getCMC();
+ }
+ }
+ }
+ } else if (sVar.equals("Count$xPaid")) {
+ c = AbilityUtils.calculateAmount(source, "PayX", null);
} else {
c = AbilityUtils.calculateAmount(source, amount, ability);
}
diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java
index 2f7857a5690..290744383e4 100644
--- a/forge-ai/src/main/java/forge/ai/AiProps.java
+++ b/forge-ai/src/main/java/forge/ai/AiProps.java
@@ -58,6 +58,7 @@ public enum AiProps { /** */
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
+ TRY_TO_PRESERVE_BUYBACK_SPELLS ("true"), /** */
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
@@ -69,11 +70,18 @@ public enum AiProps { /** */
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
ALWAYS_COUNTER_AURAS ("true"), /** */
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
+ CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK ("30"), /** */
+ ALWAYS_COPY_SPELL_IF_CMC_DIFF ("2"), /** */
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
+ AVOID_TARGETING_CREATS_THAT_WILL_DIE ("true"), /** */
+ DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION ("true"), /** */
+ CHANCE_TO_CHAIN_TWO_DAMAGE_SPELLS("50"), /** */
+ HOLD_X_DAMAGE_SPELLS_FOR_MORE_DAMAGE_CHANCE("100"),
+ HOLD_X_DAMAGE_SPELLS_THRESHOLD("5"), /** */
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
USE_BERSERK_AGGRESSIVELY ("false"), /** */
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
@@ -93,6 +101,8 @@ public enum AiProps { /** */
SCRY_EVALTHR_CMC_THRESHOLD ("3"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
+ SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL ("10"), /** */
+ SURVEIL_LIFEPERC_AFTER_PAYING_LIFE ("75"), /** */
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
@@ -104,13 +114,24 @@ public enum AiProps { /** */
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE("2"), /** */
+ MOMIR_BASIC_LAND_STRATEGY("default"), /** */
MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA("5"), /** */
MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR ("50"), /** */
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT ("20"), /** */
- // Experimental features, must be removed after extensive testing and, ideally, defaulting
+ AI_IN_DANGER_THRESHOLD("4"), /** */
+ AI_IN_DANGER_MAX_THRESHOLD("4"), /** */
+ FLASH_ENABLE_ADVANCED_LOGIC("true"), /** */
+ FLASH_CHANCE_TO_OBEY_AMBUSHAI("100"), /** */
+ FLASH_CHANCE_TO_CAST_DUE_TO_ETB_EFFECTS("100"), /** */
+ FLASH_CHANCE_TO_CAST_FOR_ETB_BEFORE_MAIN1("10"), /** */
+ FLASH_CHANCE_TO_RESPOND_TO_STACK_WITH_ETB("0"), /** */
+ FLASH_CHANCE_TO_CAST_AS_VALUABLE_BLOCKER("100"),
+ FLASH_USE_BUFF_AURAS_AS_COMBAT_TRICKS("true"),
+ FLASH_BUFF_AURA_CHANCE_TO_CAST_EARLY("1"),
+ FLASH_BUFF_AURA_CHANCE_CAST_AT_EOT("5"),
+ FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"); /** */
+ // Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->
- AI_IN_DANGER_THRESHOLD("4"),
- AI_IN_DANGER_MAX_THRESHOLD("4");
private final String strDefaultVal;
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
index faed8a063a9..bd28641348a 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
@@ -20,14 +20,14 @@ package forge.ai;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.*;
+import forge.ai.ability.ChooseGenericEffectAi;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
import forge.card.CardType;
+import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCostShard;
-import forge.game.CardTraitPredicates;
-import forge.game.Game;
-import forge.game.GameObject;
+import forge.game.*;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -37,6 +37,7 @@ import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.*;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -72,6 +73,7 @@ public class ComputerUtil {
public static boolean handlePlayingSpellAbility(final Player ai, SpellAbility sa, final Game game, Runnable chooseTargets) {
game.getStack().freezeStack();
final Card source = sa.getHostCard();
+ source.setSplitStateToPlayAbility(sa);
if (sa.isSpell() && !source.isCopiedSpell()) {
if (source.getType().hasStringType("Arcane")) {
@@ -94,7 +96,9 @@ public class ComputerUtil {
sa.setHostCard(game.getAction().moveToStack(source, sa));
}
- sa.resetPaidHash();
+ if (sa.isCopied()) {
+ sa.resetPaidHash();
+ }
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
CharmEffect.makeChoices(sa);
@@ -259,6 +263,10 @@ public class ComputerUtil {
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
newSA.setHostCard(game.getAction().moveToStack(source, sa));
+
+ if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) {
+ CharmEffect.makeChoices(newSA);
+ }
}
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
@@ -401,6 +409,35 @@ public class ComputerUtil {
}
}
+ if (ComputerUtilCost.isFreeCastAllowedByPermanent(ai, "Discard")) {
+ // Dream Halls allows to discard 1 worthless card to cast 1 expensive for free
+ // Do it even if nothing marked for discard in hand, if it's worth doing!
+ int mana = ComputerUtilMana.getAvailableManaEstimate(ai, false);
+
+ boolean cantAffordSoon = activate.getCMC() > mana + 1;
+ boolean wrongColor = !activate.determineColor().hasNoColorsExcept(ColorSet.fromNames(ComputerUtilCost.getAvailableManaColors(ai, ImmutableList.of())).getColor());
+
+ // Only do this for spells, not activated abilities
+ // We can't pay for this spell even if we play another land, or have wrong colors
+ if (!activate.isInPlay() && (cantAffordSoon || wrongColor)) {
+ CardCollection options = new CardCollection();
+ for (Card c : typeList) {
+ // Try to avoid stupidity by playing cheap spells and paying for them with expensive spells
+ // while the intention was to do things the other way around
+ if (c.isCreature() && activate.isCreature()) {
+ if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(activate)) {
+ options.add(c);
+ }
+ } else if (c.getCMC() <= activate.getCMC()) {
+ options.add(c);
+ }
+ }
+ if (!options.isEmpty()) {
+ return ComputerUtilCard.getWorstAI(options);
+ }
+ }
+ }
+
// Survival of the Fittest logic
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
@@ -789,7 +826,7 @@ public class ComputerUtil {
}
if (destroy) {
- final CardCollection indestructibles = CardLists.getKeyword(remaining, "Indestructible");
+ final CardCollection indestructibles = CardLists.getKeyword(remaining, Keyword.INDESTRUCTIBLE);
if (!indestructibles.isEmpty()) {
return indestructibles.get(0);
}
@@ -816,7 +853,7 @@ public class ComputerUtil {
if (c != null && c.isEnchanted()) {
// TODO: choose "worst" controlled enchanting Aura
- for (Card aura : c.getEnchantedBy(false)) {
+ for (Card aura : c.getEnchantedBy()) {
if (aura.getController().equals(c.getController()) && remaining.contains(aura)) {
return aura;
}
@@ -826,7 +863,7 @@ public class ComputerUtil {
}
public static boolean canRegenerate(Player ai, final Card card) {
- if (card.hasKeyword("CARDNAME can't be regenerated.")) {
+ if (!card.canBeShielded()) {
return false;
}
@@ -939,7 +976,7 @@ public class ComputerUtil {
}
// try not to cast Raid creatures in main 1 if an attack is likely
- if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword("Haste")) {
+ if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword(Keyword.HASTE)) {
for (Card potentialAtkr: ai.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
return false;
@@ -951,12 +988,33 @@ public class ComputerUtil {
return true;
}
- if (card.isCreature() && !card.hasKeyword("Defender")
- && (card.hasKeyword("Haste") || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
+ if (card.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) {
+ // Planning to choose Haste for Riot, so do this in Main 1
+ return true;
+ }
+
+ // if we have non-persistent mana in our pool, would be good to try to use it and not waste it
+ if (ai.getManaPool().willManaBeLostAtEndOfPhase()) {
+ boolean canUseToPayCost = false;
+ for (byte color : MagicColor.WUBRGC) {
+ if (ai.getManaPool().getAmountOfColor(color) > 0
+ && ((card.getManaCost().getColorProfile() & color) == color)) {
+ canUseToPayCost = true;
+ break;
+ }
+ }
+
+ if (canUseToPayCost) {
+ return true;
+ }
+ }
+
+ if (card.isCreature() && !card.hasKeyword(Keyword.DEFENDER)
+ && (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
return true;
}
- if (card.hasKeyword("Exalted")) {
+ if (card.hasKeyword(Keyword.EXALTED)) {
return true;
}
@@ -968,7 +1026,7 @@ public class ComputerUtil {
playNow = false;
break;
}
- if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c) && c.canBeEquippedBy(card)) {
+ if (!playNow && c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c) && c.canBeAttached(card)) {
playNow = true;
}
}
@@ -991,16 +1049,16 @@ public class ComputerUtil {
return true;
}
if (card.isCreature()) {
- if (buffedcard.hasKeyword("Soulbond") && !buffedcard.isPaired()) {
+ if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) {
return true;
}
- if (buffedcard.hasKeyword("Evolve")) {
+ if (buffedcard.hasKeyword(Keyword.EVOLVE)) {
if (buffedcard.getNetPower() < card.getNetPower() || buffedcard.getNetToughness() < card.getNetToughness()) {
return true;
}
}
}
- if (card.hasKeyword("Soulbond") && buffedcard.isCreature() && !buffedcard.isPaired()) {
+ if (card.hasKeyword(Keyword.SOULBOND) && buffedcard.isCreature() && !buffedcard.isPaired()) {
return true;
}
@@ -1018,6 +1076,20 @@ public class ComputerUtil {
}
} // AntiBuffedBy
+ // Plane cards that give Haste (e.g. Sokenzan)
+ if (ai.getGame().getRules().hasAppliedVariant(GameType.Planechase)) {
+ for (Card c : ai.getGame().getActivePlanes()) {
+ for (StaticAbility s : c.getStaticAbilities()) {
+ if (s.hasParam("AddKeyword")
+ && s.getParam("AddKeyword").contains("Haste")
+ && "Creature".equals(s.getParam("Affected"))
+ && card.isCreature()) {
+ return true;
+ }
+ }
+ }
+ }
+
final CardCollectionView vengevines = ai.getCardsIn(ZoneType.Graveyard, "Vengevine");
if (!vengevines.isEmpty()) {
final CardCollectionView creatures = ai.getCardsIn(ZoneType.Hand);
@@ -1248,7 +1320,7 @@ public class ComputerUtil {
// Special for Odric
if (ai.isCardInPlay("Odric, Lunarch Marshal")
- && !CardLists.getKeyword(all, "Haste").isEmpty()) {
+ && !CardLists.getKeyword(all, Keyword.HASTE).isEmpty()) {
return true;
}
@@ -1317,7 +1389,7 @@ public class ComputerUtil {
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
- final ArrayList affected = Lists.newArrayList(params.get("Affected").split(","));
+ final ArrayList affected = Lists.newArrayList(params.get("Affected").split(","));
if (affected.contains("Creature")) {
return true;
}
@@ -1340,6 +1412,16 @@ public class ComputerUtil {
if (sa.getApi() != ApiType.Fog) {
continue;
}
+
+ // Avoid re-entry for cards already being considered (e.g. in case the AI is considering
+ // Convoke or Improvise for a Fog-like effect)
+ if (c.hasKeyword("Convoke") || c.hasKeyword("Improvise")) {
+ if (AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY)) {
+ continue;
+ }
+ AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MARKED_TO_AVOID_REENTRY);
+ }
+
if (!ComputerUtilCost.canPayCost(sa, ai)) {
continue;
}
@@ -1376,6 +1458,9 @@ public class ComputerUtil {
if (!ComputerUtilCost.canPayCost(sa, ai)) {
continue;
}
+ if (!GameActionUtil.getOptionalCostValues(sa).isEmpty()) {
+ continue; // we can't rely on the AI being always willing and able to pay the optional cost to deal extra damage
+ }
damage = dmg;
}
@@ -1547,7 +1632,7 @@ public class ComputerUtil {
final Card c = (Card) o;
// indestructible
- if (c.hasKeyword("Indestructible")) {
+ if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1594,7 +1679,7 @@ public class ComputerUtil {
} else if (o instanceof Player) {
final Player p = (Player) o;
- if (source.hasKeyword("Infect")) {
+ if (source.hasKeyword(Keyword.INFECT)) {
if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getPoisonCounters()) {
threatened.add(p);
}
@@ -1615,14 +1700,14 @@ public class ComputerUtil {
if (o instanceof Card) {
final Card c = (Card) o;
final boolean canRemove = (c.getNetToughness() <= dmg)
- || (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
+ || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
if (!canRemove) {
continue;
}
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
final boolean cantSave = c.getNetToughness() + toughness <= dmg
- || (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && !grantIndestructible
+ || (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && !grantIndestructible
&& (dmg >= toughness + ComputerUtilCombat.getDamageToKill(c)));
if (cantSave && (tgt == null || !grantShroud)) {
continue;
@@ -1662,7 +1747,7 @@ public class ComputerUtil {
if (o instanceof Card) {
final Card c = (Card) o;
// indestructible
- if (c.hasKeyword("Indestructible")) {
+ if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
continue;
}
@@ -1753,7 +1838,69 @@ public class ComputerUtil {
Iterables.addAll(threatened, ComputerUtil.predictThreatenedObjects(aiPlayer, saviour, topStack.getSubAbility()));
return threatened;
}
-
+
+ /**
+ * Returns true if the specified creature will die this turn either from lethal damage in combat
+ * or from a killing spell on stack.
+ * TODO: This currently does not account for the fact that spells on stack can be countered, can be improved.
+ *
+ * @param creature
+ * A creature to check
+ * @return true if the creature dies according to current board position.
+ */
+ public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa) {
+ final Game game = creature.getGame();
+
+ // a creature will die as a result of combat
+ boolean willDieInCombat = game.getPhaseHandler().inCombat()
+ && ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat());
+
+ // a creature will [hopefully] die from a spell on stack
+ boolean willDieFromSpell = false;
+ boolean noStackCheck = false;
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+ if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
+ // See if permission is on stack and ignore this check if there is and the relevant AI flag is set
+ // TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
+ for (SpellAbilityStackInstance si : game.getStack()) {
+ SpellAbility sa = si.getSpellAbility(false);
+ if (sa.getApi() == ApiType.Counter) {
+ noStackCheck = true;
+ break;
+ }
+ }
+ }
+ willDieFromSpell = !noStackCheck && ComputerUtil.predictThreatenedObjects(creature.getController(), excludeSa).contains(creature);
+
+ return willDieInCombat || willDieFromSpell;
+ }
+
+ /**
+ * Returns a list of cards excluding any creatures that will die in active combat or from a spell on stack.
+ * Works only on AI profiles which have AVOID_TARGETING_CREATS_THAT_WILL_DIE enabled, otherwise returns
+ * the original list.
+ *
+ * @param ai
+ * The AI player performing this evaluation
+ * @param list
+ * The list of cards to work with
+ * @return a filtered list with no dying creatures in it
+ */
+ public static CardCollection filterCreaturesThatWillDieThisTurn(final Player ai, final CardCollection list, final SpellAbility excludeSa) {
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+ if (aic.getBooleanProperty(AiProps.AVOID_TARGETING_CREATS_THAT_WILL_DIE)) {
+ // Try to avoid targeting creatures that are dead on board
+ List willBeKilled = CardLists.filter(list, new Predicate() {
+ @Override
+ public boolean apply(Card card) {
+ return card.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, card, excludeSa);
+ }
+ });
+ list.removeAll(willBeKilled);
+ }
+ return list;
+ }
+
public static boolean playImmediately(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Zone zone = source.getZone();
@@ -2628,8 +2775,8 @@ public class ComputerUtil {
// and also on Chronozoa
|| (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|| type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA
- || type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
- || type == CounterType.SLEIGHT || type == CounterType.WAGE;
+ || type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
+ || type == CounterType.SLUMBER || type == CounterType.SLEIGHT || type == CounterType.WAGE;
}
// this countertypes has no effect
@@ -2701,7 +2848,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List list = player.getGame().getReplacementHandler().getReplacementList(repParams,
- ReplacementLayer.None);
+ ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
return false;
@@ -2732,7 +2879,7 @@ public class ComputerUtil {
repParams.put("Source", source);
List list = player.getGame().getReplacementHandler().getReplacementList(repParams,
- ReplacementLayer.None);
+ ReplacementLayer.Other);
if (Iterables.any(list, CardTraitPredicates.hasParam("AiLogic", "NoLife"))) {
// no life gain is not negative
@@ -2762,6 +2909,10 @@ public class ComputerUtil {
if (ab.getApi() == null) {
// only API-based SAs are supported, other things may lead to a NPE (e.g. Ancestral Vision Suspend SA)
continue;
+ } else if (ab.getApi() == ApiType.Mana && "ManaRitual".equals(ab.getParam("AILogic"))) {
+ // Mana Ritual cards are too complex for the AI to consider casting through a spell effect and will
+ // lead to a stack overflow. Consider improving.
+ continue;
}
SpellAbility abTest = withoutPayingManaCost ? ab.copyWithNoManaCost() : ab.copy();
// at this point, we're assuming that card will be castable from whichever zone it's in by the AI player.
@@ -2841,7 +2992,7 @@ public class ComputerUtil {
if (sa.getParam("AITgts").equals("BetterThanSource")) {
int value = ComputerUtilCard.evaluateCreature(source);
if (source.isEnchanted()) {
- for (Card enc : source.getEnchantedBy(false)) {
+ for (Card enc : source.getEnchantedBy()) {
if (enc.getController().equals(ai)) {
value += 100; // is 100 per AI's own aura enough?
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
index b4c88471be3..18687559682 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
@@ -1,11 +1,7 @@
package forge.ai;
-import java.util.Iterator;
-import java.util.List;
-
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
-
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GameActionUtil;
@@ -16,10 +12,14 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets;
import forge.game.player.Player;
+import forge.game.spellability.OptionalCostValue;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.zone.ZoneType;
+import java.util.Iterator;
+import java.util.List;
+
public class ComputerUtilAbility {
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
if (!game.getStack().isEmpty() || !game.getPhaseHandler().getPhase().isMain()) {
@@ -95,18 +95,58 @@ public class ComputerUtilAbility {
public static List getOriginalAndAltCostAbilities(final List originList, final Player player) {
final List newAbilities = Lists.newArrayList();
+
+ List originListWithAddCosts = Lists.newArrayList();
for (SpellAbility sa : originList) {
+ // If this spell has alternative additional costs, add them instead of the unmodified SA itself
sa.setActivatingPlayer(player);
- //add alternative costs as additional spell abilities
+ originListWithAddCosts.addAll(GameActionUtil.getAdditionalCostSpell(sa));
+ }
+
+ for (SpellAbility sa : originListWithAddCosts) {
+ // determine which alternative costs are cheaper than the original and prioritize them
+ List saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
+ List priorityAltSa = Lists.newArrayList();
+ List otherAltSa = Lists.newArrayList();
+ for (SpellAbility altSa : saAltCosts) {
+ if (altSa.getPayCosts() == null || sa.getPayCosts() == null) {
+ otherAltSa.add(altSa);
+ } else if (sa.getPayCosts().isOnlyManaCost()
+ && altSa.getPayCosts().isOnlyManaCost() && sa.getPayCosts().getTotalMana().compareTo(altSa.getPayCosts().getTotalMana()) == 1) {
+ // the alternative cost is strictly cheaper, so why not? (e.g. Omniscience etc.)
+ priorityAltSa.add(altSa);
+ } else {
+ otherAltSa.add(altSa);
+ }
+ }
+
+ // add alternative costs as additional spell abilities
+ newAbilities.addAll(priorityAltSa);
newAbilities.add(sa);
- newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
+ newAbilities.addAll(otherAltSa);
}
final List result = Lists.newArrayList();
for (SpellAbility sa : newAbilities) {
sa.setActivatingPlayer(player);
- result.addAll(GameActionUtil.getOptionalCosts(sa));
+
+ // Optional cost selection through the AI controller
+ boolean choseOptCost = false;
+ List list = GameActionUtil.getOptionalCostValues(sa);
+ if (!list.isEmpty()) {
+ list = player.getController().chooseOptionalCosts(sa, list);
+ if (!list.isEmpty()) {
+ choseOptCost = true;
+ result.add(GameActionUtil.addOptionalCosts(sa, list));
+ }
+ }
+
+ // Add only one ability: either the one with preferred optional costs, or the original one if there are none
+ if (!choseOptCost) {
+ result.add(sa);
+ }
}
+
return result;
}
@@ -128,6 +168,17 @@ public class ComputerUtilAbility {
return tgtSA;
}
+ public static SpellAbility getFirstCopySASpell(List spells) {
+ SpellAbility sa = null;
+ for (SpellAbility spell : spells) {
+ if (spell.getApi() == ApiType.CopySpellAbility) {
+ sa = spell;
+ break;
+ }
+ }
+ return sa;
+ }
+
public static Card getAbilitySource(SpellAbility sa) {
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
index 99b42a343f7..af09e275ed6 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
@@ -21,6 +21,7 @@ import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
+import forge.game.cost.Cost;
import forge.game.cost.CostPayEnergy;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordCollection;
@@ -585,7 +586,7 @@ public class ComputerUtilCard {
// Add all cost of all auras with the same controller
if (card.isEnchanted()) {
- final List auras = CardLists.filterControlledBy(card.getEnchantedBy(false), card.getController());
+ final List auras = CardLists.filterControlledBy(card.getEnchantedBy(), card.getController());
curCMC += Aggregates.sum(auras, CardPredicates.Accessors.fnGetCmc) + auras.size();
}
@@ -833,7 +834,7 @@ public class ComputerUtilCard {
int score = tmp.isTapped() ? 2 : 0;
score += tmp.isBasicLand() ? 1 : 0;
score -= tmp.isCreature() ? 4 : 0;
- for (Card aura : tmp.getEnchantedBy(false)) {
+ for (Card aura : tmp.getEnchantedBy()) {
if (aura.getController().isOpponentOf(tmp.getController())) {
score += 5;
} else {
@@ -857,7 +858,7 @@ public class ComputerUtilCard {
int score = tmp.isTapped() ? 0 : 2;
score += tmp.isBasicLand() ? 2 : 0;
score -= tmp.isCreature() ? 4 : 0;
- score -= 5 * tmp.getEnchantedBy(false).size();
+ score -= 5 * tmp.getEnchantedBy().size();
if (score >= maxScore) {
land = tmp;
@@ -1033,7 +1034,7 @@ public class ComputerUtilCard {
// interrupt 3: two for one = good
if (c.isEnchanted()) {
boolean myEnchants = false;
- for (Card enc : c.getEnchantedBy(false)) {
+ for (Card enc : c.getEnchantedBy()) {
if (enc.getOwner().equals(ai)) {
myEnchants = true;
break;
@@ -1081,7 +1082,7 @@ public class ComputerUtilCard {
valueTempo *= 2; //deal with annoying things
}
if (!destination.equals(ZoneType.Graveyard) && //TODO:boat-load of "when blah dies" triggers
- c.hasKeyword("Persist") || c.hasKeyword("Undying") || c.hasKeyword("Modular")) {
+ c.hasKeyword(Keyword.PERSIST) || c.hasKeyword(Keyword.UNDYING) || c.hasKeyword(Keyword.MODULAR)) {
valueTempo *= 2;
}
if (destination.equals(ZoneType.Hand) && !c.isToken()) {
@@ -1311,10 +1312,22 @@ public class ComputerUtilCard {
//2. grant haste
if (keywords.contains("Haste") && c.hasSickness() && !c.isTapped()) {
- chance += 0.5f;
- if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
- chance += 0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife();
+ double nonCombatChance = 0.0f;
+ double combatChance = 0.0f;
+ // non-combat Haste: has an activated ability with tap cost
+ for (SpellAbility ab : c.getSpellAbilities()) {
+ Cost abCost = ab.getPayCosts();
+ if (abCost != null && abCost.hasTapCost()
+ && (!abCost.hasManaCost() || ComputerUtilMana.canPayManaCost(ab, ai, 0))) {
+ nonCombatChance += 0.5f;
+ break;
+ }
}
+ // combat Haste: only grant it if the creature will attack
+ if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
+ combatChance += 0.5f + (0.5f * ComputerUtilCombat.damageIfUnblocked(pumped, opp, combat, true) / opp.getLife());
+ }
+ chance += nonCombatChance + combatChance;
}
//3. grant evasive
@@ -1358,8 +1371,9 @@ public class ComputerUtilCard {
//1. save combatant
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && !pumpedWillDie
- && !c.hasKeyword("Indestructible")) { // hack because attackerWouldBeDestroyed() does not
- // check for Indestructible when computing lethal damage
+ && !c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
+ // hack because attackerWouldBeDestroyed()
+ // does not check for Indestructible when computing lethal damage
return true;
}
@@ -1396,17 +1410,17 @@ public class ComputerUtilCard {
int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
// predict Infect
- if (pumpedDmg == 0 && c.hasKeyword("Infect")) {
+ if (pumpedDmg == 0 && c.hasKeyword(Keyword.INFECT)) {
if (poisonPumped > poisonOrig) {
pumpedDmg = poisonPumped;
}
}
if (combat.isBlocked(c)) {
- if (!c.hasKeyword("Trample")) {
+ if (!c.hasKeyword(Keyword.TRAMPLE)) {
dmg = 0;
}
- if (c.hasKeyword("Trample") || keywords.contains("Trample")) {
+ if (c.hasKeyword(Keyword.TRAMPLE) || keywords.contains("Trample")) {
for (Card b : combat.getBlockers(c)) {
pumpedDmg -= ComputerUtilCombat.getDamageToKill(b);
}
@@ -1415,8 +1429,8 @@ public class ComputerUtilCard {
}
}
if (pumpedDmg > dmg) {
- if ((!c.hasKeyword("Infect") && pumpedDmg >= opp.getLife())
- || (c.hasKeyword("Infect") && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
+ if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife())
+ || (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
return true;
}
}
@@ -1425,7 +1439,7 @@ public class ComputerUtilCard {
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
int totalPowerUnblocked = 0;
for (Card atk : combat.getAttackers()) {
- if (combat.isBlocked(atk) && !atk.hasKeyword("Trample")) {
+ if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) {
continue;
}
if (atk == c) {
@@ -1458,7 +1472,7 @@ public class ComputerUtilCard {
}
//4. lifelink
- if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword("Lifelink") && keywords.contains("Lifelink")
+ if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword(Keyword.LIFELINK) && keywords.contains("Lifelink")
&& (combat.isAttacking(c) || combat.isBlocking(c))) {
int dmg = pumped.getNetCombatDamage();
//The actual dmg inflicted should be the sum of ComputerUtilCombat.predictDamageTo() for opposing creature
@@ -1471,14 +1485,22 @@ public class ComputerUtilCard {
List blockedBy = combat.getAttackersBlockedBy(c);
boolean attackerHasTrample = false;
for (Card b : blockedBy) {
- attackerHasTrample |= b.hasKeyword("Trample");
+ attackerHasTrample |= b.hasKeyword(Keyword.TRAMPLE);
}
if (attackerHasTrample && (sa.isAbility() || ComputerUtilCombat.lifeInDanger(ai, combat))) {
return true;
}
}
}
-
+
+ if ("UntapCombatTrick".equals(sa.getParam("AILogic")) && c.isTapped()) {
+ if (phase.is(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.getPlayerTurn().isOpponentOf(ai)) {
+ chance += 0.5f; // this creature will untap to become a potential blocker
+ } else if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai)) {
+ chance += 1.0f; // untap after tapping for attack
+ }
+ }
+
if (isBerserk) {
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
@@ -1491,7 +1513,7 @@ public class ComputerUtilCard {
}
}
- boolean wantToHoldTrick = holdCombatTricks;
+ boolean wantToHoldTrick = holdCombatTricks && !ai.getCardsIn(ZoneType.Hand).isEmpty();
if (chanceToHoldCombatTricks >= 0) {
// Obey the chance specified in the AI profile for holding combat tricks
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
@@ -1507,14 +1529,18 @@ public class ComputerUtilCard {
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
// an army of opposing blockers with only one combat trick in hand)
- AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
- AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
// the combat trick
+ boolean reserved = false;
if (ai.getController().isAI()) {
- ((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS);
+ reserved = ((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, false);
+ // Only proceed with this if we could actually reserve mana
+ if (reserved) {
+ AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
+ AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
+ return false;
+ }
}
- return false;
} else {
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
// the AI overextending the attack
@@ -1572,10 +1598,10 @@ public class ComputerUtilCard {
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);
- pumped.addChangedCardKeywords(kws, new ArrayList(), false, timestamp);
+ pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
Set types = c.getCounters().keySet();
for(CounterType ct : types) {
- pumped.addCounterFireNoEvents(ct, c.getCounters(ct), c, true);
+ pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
}
//Copies tap-state and extra keywords (auras, equipment, etc.)
if (c.isTapped()) {
@@ -1595,7 +1621,7 @@ public class ComputerUtilCard {
}
}
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
- pumped.addChangedCardKeywordsInternal(toCopy, Lists.newArrayList(), false, timestamp2, true);
+ pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, true);
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped;
}
@@ -1615,6 +1641,7 @@ public class ComputerUtilCard {
if (exclude != null) {
list.removeAll(exclude);
}
+ list.add(vCard); // account for the static abilities that may be present on the card itself
for (final Card c : list) {
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map params = stAb.getMapParams();
@@ -1624,6 +1651,9 @@ public class ComputerUtilCard {
if (!params.containsKey("Affected")) {
continue;
}
+ if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
+ continue;
+ }
final String valid = params.get("Affected");
if (!vCard.isValid(valid, c.getController(), c, null)) {
continue;
@@ -1713,10 +1743,10 @@ public class ComputerUtilCard {
}
public static boolean hasActiveUndyingOrPersist(final Card c) {
- if (c.hasKeyword("Undying") && c.getCounters(CounterType.P1P1) == 0) {
+ if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) {
return true;
}
- if (c.hasKeyword("Persist") && c.getCounters(CounterType.M1M1) == 0) {
+ if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) {
return true;
}
return false;
@@ -1781,15 +1811,20 @@ public class ComputerUtilCard {
CardCollection priorityCards = new CardCollection();
for (Card atk : oppCards) {
+ boolean canBeBlocked = false;
if (isUselessCreature(atk.getController(), atk)) {
continue;
}
for (Card blk : aiCreats) {
- if (!CombatUtil.canBlock(atk, blk, true)) {
- boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
- if (!priorityRemovalOnlyInDanger || threat) {
- priorityCards.add(atk);
- }
+ if (CombatUtil.canBlock(atk, blk, true)) {
+ canBeBlocked = true;
+ break;
+ }
+ }
+ if (!canBeBlocked) {
+ boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
+ if (!priorityRemovalOnlyInDanger || threat) {
+ priorityCards.add(atk);
}
}
}
@@ -1840,4 +1875,13 @@ public class ComputerUtilCard {
return AiPlayDecision.WillPlay;
}
+
+ // Determine if the AI has an AI:RemoveDeck:All or an AI:RemoveDeck:Random hint specified.
+ // Includes a NPE guard on getRules() which might otherwise be tripped on some cards (e.g. tokens).
+ public static boolean isCardRemAIDeck(final Card card) {
+ return card.getRules() != null && card.getRules().getAiHints().getRemAIDecks();
+ }
+ public static boolean isCardRemRandomDeck(final Card card) {
+ return card.getRules() != null && card.getRules().getAiHints().getRemRandomDecks();
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
index e41429127e6..48172bc1635 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
@@ -36,6 +36,7 @@ import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
+import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.Untap;
import forge.game.player.Player;
@@ -201,10 +202,10 @@ public class ComputerUtilCombat {
}
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
- if (!attacker.hasKeyword("Infect")) {
+ if (!attacker.hasKeyword(Keyword.INFECT)) {
sum = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
- if (attacker.hasKeyword("Double Strike")) {
- sum += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
+ if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ sum *= 2;
}
}
return sum;
@@ -226,14 +227,15 @@ public class ComputerUtilCombat {
int damage = attacker.getNetCombatDamage();
int poison = 0;
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, null, false);
- if (attacker.hasKeyword("Infect")) {
- poison += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
- if (attacker.hasKeyword("Double Strike")) {
- poison += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
+ if (attacker.hasKeyword(Keyword.INFECT)) {
+ int pd = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
+ poison += pd;
+ if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ poison += pd;
}
}
- if (attacker.hasKeyword("Poisonous") && (damage > 0)) {
- poison += attacker.getKeywordMagnitude("Poisonous");
+ if (attacker.hasKeyword(Keyword.POISONOUS) && (damage > 0)) {
+ poison += attacker.getKeywordMagnitude(Keyword.POISONOUS);
}
return poison;
}
@@ -301,9 +303,9 @@ public class ComputerUtilCombat {
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage "
+ "as though it weren't blocked.")) {
unblocked.add(attacker);
- } else if (attacker.hasKeyword("Trample")
+ } else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
- if (!attacker.hasKeyword("Infect")) {
+ if (!attacker.hasKeyword(Keyword.INFECT)) {
damage += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
}
}
@@ -330,6 +332,11 @@ public class ComputerUtilCombat {
*/
public static int resultingPoison(final Player ai, final Combat combat) {
+ // ai can't get poision counters, so the value can't change
+ if (!ai.canReceiveCounters(CounterType.POISON)) {
+ return ai.getPoisonCounters();
+ }
+
int poison = 0;
final List attackers = combat.getAttackersOf(ai);
@@ -343,13 +350,13 @@ public class ComputerUtilCombat {
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage"
+ " as though it weren't blocked.")) {
unblocked.add(attacker);
- } else if (attacker.hasKeyword("Trample")
+ } else if (attacker.hasKeyword(Keyword.TRAMPLE)
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
- if (attacker.hasKeyword("Infect")) {
+ if (attacker.hasKeyword(Keyword.INFECT)) {
poison += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
}
- if (attacker.hasKeyword("Poisonous")) {
- poison += attacker.getKeywordMagnitude("Poisonous");
+ if (attacker.hasKeyword(Keyword.POISONOUS)) {
+ poison += attacker.getKeywordMagnitude(Keyword.POISONOUS);
}
}
}
@@ -576,7 +583,7 @@ public class ComputerUtilCombat {
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
- if (defender.hasKeyword("Double Strike")) {
+ if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true);
}
@@ -590,25 +597,26 @@ public class ComputerUtilCombat {
* @return
*/
private static int predictDamageByBlockerWithoutDoubleStrike(final Card attacker, final Card defender) {
- if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword("Indestructible")) {
+ if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
int flankingMagnitude = 0;
- if (attacker.hasKeyword("Flanking") && !defender.hasKeyword("Flanking")) {
+ if (attacker.hasKeyword(Keyword.FLANKING) && !defender.hasKeyword(Keyword.FLANKING)) {
- flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
+ flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= defender.getNetToughness()) {
return 0;
}
if ((flankingMagnitude >= (defender.getNetToughness() - defender.getDamage()))
- && !defender.hasKeyword("Indestructible")) {
+ && !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
} // flanking
- if (attacker.hasKeyword("Indestructible") && !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
+ if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE)
+ && !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
return 0;
}
@@ -667,21 +675,21 @@ public class ComputerUtilCombat {
}
int flankingMagnitude = 0;
- if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
+ if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
- flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
+ flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
return 0;
}
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
- && !blocker.hasKeyword("Indestructible")) {
+ && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return 0;
}
} // flanking
- final int defBushidoMagnitude = blocker.getKeywordMagnitude("Bushido");
+ final int defBushidoMagnitude = blocker.getKeywordMagnitude(Keyword.BUSHIDO);
final int defenderDefense = (blocker.getLethalDamage() - flankingMagnitude) + defBushidoMagnitude;
@@ -727,16 +735,16 @@ public class ComputerUtilCombat {
for (final Card defender : blockers) {
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
- && !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
+ && !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
return true;
}
- if (defender.hasKeyword("First Strike") || defender.hasKeyword("Double Strike")) {
+ if (defender.hasKeyword(Keyword.FIRST_STRIKE) || defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
firstStrikeBlockerDmg += defender.getNetCombatDamage();
}
}
// Consider first strike and double strike
- if (attacker.hasKeyword("First Strike") || attacker.hasKeyword("Double Strike")) {
+ if (attacker.hasKeyword(Keyword.FIRST_STRIKE) || attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
return firstStrikeBlockerDmg >= ComputerUtilCombat.getDamageToKill(attacker);
}
@@ -920,9 +928,9 @@ public class ComputerUtilCombat {
// if the attacker has first strike and wither the blocker will deal
// less damage than expected
if (dealsFirstStrikeDamage(attacker, withoutAbilities, null)
- && (attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect"))
+ && (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, null)
- && !blocker.hasKeyword("CARDNAME can't have counters put on it.")) {
+ && !blocker.canReceiveCounters(CounterType.M1M1)) {
power -= attacker.getNetCombatDamage();
}
@@ -967,12 +975,20 @@ public class ComputerUtilCombat {
final Map trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
- if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
- || !trigParams.containsKey("Execute")) {
+ if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
continue;
}
- final String ability = source.getSVar(trigParams.get("Execute"));
- final Map abilityParams = AbilityFactory.getMapParams(ability);
+
+ Map abilityParams = null;
+ if (trigger.getOverridingAbility() != null) {
+ abilityParams = trigger.getOverridingAbility().getMapParams();
+ } else if (trigParams.containsKey("Execute")) {
+ final String ability = source.getSVar(trigParams.get("Execute"));
+ abilityParams = AbilityFactory.getMapParams(ability);
+ } else {
+ continue;
+ }
+
if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) {
continue;
}
@@ -1036,11 +1052,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
-
+
if (ability.hasParam("Monstrosity") && blocker.isMonstrous()) {
- continue;
+ continue;
}
-
+
+ if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
+ continue;
+ }
+
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (pBonus > 0) {
@@ -1069,8 +1089,8 @@ public class ComputerUtilCombat {
public static int predictToughnessBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
int toughness = 0;
- if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
- toughness -= attacker.getAmountOfKeyword("Flanking");
+ if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
+ toughness -= attacker.getAmountOfKeyword(Keyword.FLANKING);
}
if (blocker.getName().equals("Shape Stealer")) {
@@ -1090,12 +1110,20 @@ public class ComputerUtilCombat {
final Map trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
- if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
- || !trigParams.containsKey("Execute")) {
+ if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
continue;
}
- final String ability = source.getSVar(trigParams.get("Execute"));
- final Map abilityParams = AbilityFactory.getMapParams(ability);
+
+ Map abilityParams = null;
+ if (trigger.getOverridingAbility() != null) {
+ abilityParams = trigger.getOverridingAbility().getMapParams();
+ } else if (trigParams.containsKey("Execute")) {
+ final String ability = source.getSVar(trigParams.get("Execute"));
+ abilityParams = AbilityFactory.getMapParams(ability);
+ } else {
+ continue;
+ }
+
String abType = "";
if (abilityParams.containsKey("AB")) {
abType = abilityParams.get("AB");
@@ -1200,11 +1228,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
-
+
if (ability.hasParam("Monstrosity") && blocker.isMonstrous()) {
- continue;
+ continue;
}
-
+
+ if (ability.hasParam("Adapt") && blocker.getCounters(CounterType.P1P1) > 0) {
+ continue;
+ }
+
if (ComputerUtilCost.canPayCost(ability, blocker.getController())) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (tBonus > 0) {
@@ -1239,9 +1271,7 @@ public class ComputerUtilCombat {
//check Exalted only for the first attacker
if (combat != null && combat.getAttackers().isEmpty()) {
- for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
- power += card.getAmountOfKeyword("Exalted");
- }
+ power += attacker.getController().countExaltedBonus();
}
// Serene Master switches power with attacker
@@ -1263,9 +1293,9 @@ public class ComputerUtilCombat {
// less damage than expected
if (null != blocker) {
if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
- && (blocker.hasKeyword("Wither") || blocker.hasKeyword("Infect"))
- && !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
- && !attacker.hasKeyword("CARDNAME can't have counters put on it.")) {
+ && (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))
+ && !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
+ && !attacker.canReceiveCounters(CounterType.M1M1)) {
power -= blocker.getNetCombatDamage();
}
theTriggers.addAll(blocker.getTriggers());
@@ -1305,12 +1335,20 @@ public class ComputerUtilCombat {
final Map trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
- if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
- || !trigParams.containsKey("Execute")) {
+ if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
continue;
}
- final String ability = source.getSVar(trigParams.get("Execute"));
- final Map abilityParams = AbilityFactory.getMapParams(ability);
+
+ Map abilityParams = null;
+ if (trigger.getOverridingAbility() != null) {
+ abilityParams = trigger.getOverridingAbility().getMapParams();
+ } else if (trigParams.containsKey("Execute")) {
+ final String ability = source.getSVar(trigParams.get("Execute"));
+ abilityParams = AbilityFactory.getMapParams(ability);
+ } else {
+ continue;
+ }
+
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
continue; // targeted pumping not supported
}
@@ -1324,7 +1362,14 @@ public class ComputerUtilCombat {
}
if (abilityParams.containsKey("Cost")) {
- final SpellAbility sa = AbilityFactory.getAbility(ability, source);
+ SpellAbility sa = null;
+ if (trigger.getOverridingAbility() != null) {
+ sa = trigger.getOverridingAbility();
+ } else {
+ final String ability = source.getSVar(trigParams.get("Execute"));
+ sa = AbilityFactory.getAbility(ability, source);
+ }
+
sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1405,11 +1450,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
-
+
if (ability.hasParam("Monstrosity") && attacker.isMonstrous()) {
- continue;
+ continue;
}
-
+
+ if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) {
+ continue;
+ }
+
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
int pBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (pBonus > 0) {
@@ -1446,9 +1495,7 @@ public class ComputerUtilCombat {
//check Exalted only for the first attacker
if (combat != null && combat.getAttackers().isEmpty()) {
- for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
- toughness += card.getAmountOfKeyword("Exalted");
- }
+ toughness += attacker.getController().countExaltedBonus();
}
if (blocker != null && attacker.getName().equals("Shape Stealer")) {
@@ -1494,7 +1541,7 @@ public class ComputerUtilCombat {
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
if (!attacker.isValid(valid, card.getController(), card, null)
- || attacker.hasKeyword("Vigilance")) {
+ || attacker.hasKeyword(Keyword.VIGILANCE)) {
continue;
}
// remove the bonus, because it will no longer be granted
@@ -1510,12 +1557,20 @@ public class ComputerUtilCombat {
final Map trigParams = trigger.getMapParams();
final Card source = trigger.getHostCard();
- if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
- || !trigParams.containsKey("Execute")) {
+ if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
continue;
}
- final String ability = source.getSVar(trigParams.get("Execute"));
- final Map abilityParams = AbilityFactory.getMapParams(ability);
+
+ Map abilityParams = null;
+ if (trigger.getOverridingAbility() != null) {
+ abilityParams = trigger.getOverridingAbility().getMapParams();
+ } else if (trigParams.containsKey("Execute")) {
+ final String ability = source.getSVar(trigParams.get("Execute"));
+ abilityParams = AbilityFactory.getMapParams(ability);
+ } else {
+ continue;
+ }
+
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
continue; // targeted pumping not supported
}
@@ -1548,7 +1603,14 @@ public class ComputerUtilCombat {
}
if (abilityParams.containsKey("Cost")) {
- final SpellAbility sa = AbilityFactory.getAbility(ability, source);
+ SpellAbility sa = null;
+ if (trigger.getOverridingAbility() != null) {
+ sa = trigger.getOverridingAbility();
+ } else {
+ final String ability = source.getSVar(trigParams.get("Execute"));
+ sa = AbilityFactory.getAbility(ability, source);
+ }
+
sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1625,11 +1687,15 @@ public class ComputerUtilCombat {
if (!ability.hasParam("CounterType") || !ability.getParam("CounterType").equals("P1P1")) {
continue;
}
-
+
if (ability.hasParam("Monstrosity") && attacker.isMonstrous()) {
- continue;
+ continue;
}
-
+
+ if (ability.hasParam("Adapt") && attacker.getCounters(CounterType.P1P1) > 0) {
+ continue;
+ }
+
if (!ability.getPayCosts().hasTapCost() && ComputerUtilCost.canPayCost(ability, attacker.getController())) {
int tBonus = AbilityUtils.calculateAmount(ability.getHostCard(), ability.getParam("CounterNum"), ability);
if (tBonus > 0) {
@@ -1647,7 +1713,7 @@ public class ComputerUtilCombat {
if (blocker.isEquippedBy("Godsend")) {
return true;
}
- if (attacker.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
+ if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
return false;
}
@@ -1712,12 +1778,12 @@ public class ComputerUtilCombat {
*/
public static boolean attackerCantBeDestroyedInCombat(Player ai, final Card attacker) {
// attacker is either indestructible or may regenerate
- if (attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker))) {
+ if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker))) {
return true;
}
// attacker will regenerate
- if (attacker.getShieldCount() > 0 && !attacker.hasKeyword("CARDNAME can't be regenerated.")) {
+ if (attacker.getShieldCount() > 0 && attacker.canBeShielded()) {
return true;
}
@@ -1766,24 +1832,24 @@ public class ComputerUtilCombat {
}
int flankingMagnitude = 0;
- if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
+ if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
- flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
+ flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
return false;
}
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
- && !blocker.hasKeyword("Indestructible")) {
+ && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return false;
}
} // flanking
- if (((attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
- && !(blocker.hasKeyword("Wither") || blocker.hasKeyword("Infect")))
- || (attacker.hasKeyword("Persist") && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker
+ if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
+ && !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)))
+ || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker
.getCounters(CounterType.M1M1) == 0))
- || (attacker.hasKeyword("Undying") && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker
+ || (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker
.getCounters(CounterType.P1P1) == 0))) {
return false;
}
@@ -1830,7 +1896,7 @@ public class ComputerUtilCombat {
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
- if (blocker.hasKeyword("Double Strike")) {
+ if (blocker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true;
}
@@ -1840,7 +1906,8 @@ public class ComputerUtilCombat {
// Attacker may kill the blocker before he can deal normal
// (secondary) damage
- if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat) && !blocker.hasKeyword("Indestructible")) {
+ if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
+ && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
if (attackerDamage >= defenderLife) {
return false;
}
@@ -1856,7 +1923,7 @@ public class ComputerUtilCombat {
else { // no double strike for defender
// Attacker may kill the blocker before he can deal any damage
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
- && !blocker.hasKeyword("Indestructible")
+ && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, combat)) {
if (attackerDamage >= defenderLife) {
@@ -1895,7 +1962,7 @@ public class ComputerUtilCombat {
for (Card attacker : attackers) {
if (ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, true)
- && !(attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect"))) {
+ && !(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) {
return true;
}
}
@@ -1913,19 +1980,21 @@ public class ComputerUtilCombat {
}
int flankingMagnitude = 0;
- if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
+ if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
- flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
+ flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
return true;
}
- if ((flankingMagnitude >= ComputerUtilCombat.getDamageToKill(blocker)) && !blocker.hasKeyword("Indestructible")) {
+ if ((flankingMagnitude >= ComputerUtilCombat.getDamageToKill(blocker))
+ && !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return true;
}
} // flanking
- if (blocker.hasKeyword("Indestructible") || dontTestRegen || ComputerUtil.canRegenerate(blocker.getController(), blocker)) {
+ if (blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || dontTestRegen
+ || ComputerUtil.canRegenerate(blocker.getController(), blocker)) {
return false;
}
@@ -2010,11 +2079,11 @@ public class ComputerUtilCombat {
return true;
}
- if (((blocker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
- .hasKeyword("Wither") || attacker.hasKeyword("Infect")))
- || (blocker.hasKeyword("Persist") && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker
+ if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
+ .hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)))
+ || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker
.getCounters(CounterType.M1M1) == 0))
- || (blocker.hasKeyword("Undying") && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker
+ || (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker
.getCounters(CounterType.P1P1) == 0))) {
return false;
}
@@ -2051,6 +2120,16 @@ public class ComputerUtilCombat {
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
+ // Damage prevention might come from a static effect
+ if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
+ if (isCombatDamagePrevented(attacker, blocker, attackerDamage)) {
+ attackerDamage = 0;
+ }
+ if (isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
+ defenderDamage = 0;
+ }
+ }
+
if (combat != null) {
for (Card atkr : combat.getAttackersBlockedBy(blocker)) {
if (!atkr.equals(attacker)) {
@@ -2064,7 +2143,7 @@ public class ComputerUtilCombat {
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
- if (attacker.hasKeyword("Double Strike")) {
+ if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
return true;
}
@@ -2074,7 +2153,8 @@ public class ComputerUtilCombat {
// Attacker may kill the blocker before he can deal normal
// (secondary) damage
- if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")) {
+ if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
+ && !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
if (defenderDamage >= attackerLife) {
return false;
}
@@ -2089,7 +2169,8 @@ public class ComputerUtilCombat {
else { // no double strike for attacker
// Defender may kill the attacker before he can deal any damage
- if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")
+ if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
+ && !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)
&& !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)) {
if (defenderDamage >= attackerLife) {
@@ -2136,7 +2217,7 @@ public class ComputerUtilCombat {
return damageMap;
}
- final boolean hasTrample = attacker.hasKeyword("Trample");
+ final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE);
if (block.size() == 1) {
final Card blocker = block.getFirst();
@@ -2238,11 +2319,11 @@ public class ComputerUtilCombat {
final boolean noPrevention) {
final int killDamage = c.isPlaneswalker() ? c.getCurrentLoyalty() : ComputerUtilCombat.getDamageToKill(c);
- if (c.hasKeyword("Indestructible") || c.getShieldCount() > 0) {
- if (!(source.hasKeyword("Wither") || source.hasKeyword("Infect"))) {
+ if (c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getShieldCount() > 0) {
+ if (!(source.hasKeyword(Keyword.WITHER) || source.hasKeyword(Keyword.INFECT))) {
return maxDamage + 1;
}
- } else if (source.hasKeyword("Deathtouch")) {
+ } else if (source.hasKeyword(Keyword.DEATHTOUCH)) {
for (int i = 1; i <= maxDamage; i++) {
if (noPrevention) {
if (c.staticReplaceDamage(i, source, isCombat) > 0) {
@@ -2405,7 +2486,7 @@ public class ComputerUtilCombat {
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
- if (combatant.hasKeyword("Double Strike") || combatant.hasKeyword("First Strike")) {
+ if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) {
return true;
}
@@ -2495,7 +2576,7 @@ public class ComputerUtilCombat {
return original;
}
- private static boolean isCombatDamagePrevented(final Card attacker, final GameEntity target, final int damage) {
+ public static boolean isCombatDamagePrevented(final Card attacker, final GameEntity target, final int damage) {
final Game game = attacker.getGame();
// first try to replace the damage
@@ -2509,14 +2590,14 @@ public class ComputerUtilCombat {
// repParams.put("PreventMap", preventMap);
List list = game.getReplacementHandler().getReplacementList(repParams,
- ReplacementLayer.None);
+ ReplacementLayer.Other);
return !list.isEmpty();
}
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
// TODO: expand this to account for more complex situations like the Wildfire Eternal unblocked trigger
- int afflictDmg = attacker.getKeywordMagnitude("Afflict");
+ int afflictDmg = attacker.getKeywordMagnitude(Keyword.AFFLICT);
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
}
@@ -2541,18 +2622,10 @@ public class ComputerUtilCombat {
CardCollection withoutEvasion = new CardCollection();
for (Card atk : attackers) {
- boolean hasProtection = false;
- for (KeywordInterface inst : atk.getKeywords()) {
- String kw = inst.getOriginal();
- if (kw.startsWith("Protection")) {
- hasProtection = true;
- break;
- }
- }
-
- if (atk.hasKeyword("Flying") || atk.hasKeyword("Shadow")
- || atk.hasKeyword("Horsemanship") || (atk.hasKeyword("Fear")
- || atk.hasKeyword("Intimidate") || atk.hasKeyword("Skulk") || hasProtection)) {
+ if (atk.hasKeyword(Keyword.FLYING) || atk.hasKeyword(Keyword.SHADOW)
+ || atk.hasKeyword(Keyword.HORSEMANSHIP) || (atk.hasKeyword(Keyword.FEAR)
+ || atk.hasKeyword(Keyword.INTIMIDATE) || atk.hasKeyword(Keyword.SKULK)
+ || atk.hasKeyword(Keyword.PROTECTION))) {
withEvasion.add(atk);
} else {
withoutEvasion.add(atk);
@@ -2608,6 +2681,30 @@ public class ComputerUtilCombat {
return attackerAfterTrigs;
}
+
+ public static boolean willKillAtLeastOne(final Player ai, final Card c, final Combat combat) {
+ // This method detects if the attacking or blocking group the card "c" belongs to will kill
+ // at least one creature it's in combat with (either profitably or as a trade),
+ if (combat == null) {
+ return false;
+ }
+
+ if (combat.isBlocked(c)) {
+ for (Card blk : combat.getBlockers(c)) {
+ if (ComputerUtilCombat.blockerWouldBeDestroyed(ai, blk, combat)) {
+ return true;
+ }
+ }
+ } else if (combat.isBlocking(c)) {
+ for (Card atk : combat.getAttackersBlockedBy(c)) {
+ if (ComputerUtilCombat.attackerWouldBeDestroyed(ai, atk, combat)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
index 73e4f20c520..fb44ff4e31c 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
@@ -5,15 +5,13 @@ import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
-import forge.game.GameActionUtil;
+import forge.game.Game;
import forge.game.ability.AbilityUtils;
-import forge.game.card.Card;
-import forge.game.card.CardCollection;
-import forge.game.card.CardLists;
+import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
-import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.cost.*;
+import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
@@ -31,7 +29,7 @@ public class ComputerUtilCost {
/**
* Check add m1 m1 counter cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -46,7 +44,7 @@ public class ComputerUtilCost {
if (part instanceof CostPutCounter) {
final CostPutCounter addCounter = (CostPutCounter) part;
final CounterType type = addCounter.getCounter();
-
+
if (type.equals(CounterType.M1M1)) {
return false;
}
@@ -60,7 +58,7 @@ public class ComputerUtilCost {
}
/**
* Check remove counter cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -74,10 +72,10 @@ public class ComputerUtilCost {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
-
+
final CounterType type = remCounter.counter;
if (!part.payCostFromSource()) {
- if (type.name().equals("P1P1")) {
+ if (CounterType.P1P1.equals(type)) {
return false;
}
continue;
@@ -92,7 +90,7 @@ public class ComputerUtilCost {
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount());
- if (sVar.equals("XChoice")) {
+ if (sVar.equals("XChoice") && !sa.hasSVar("ChosenX")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
}
}
@@ -108,7 +106,8 @@ public class ComputerUtilCost {
}
//don't kill the creature
- if (type.name().equals("P1P1") && source.getLethalDamage() <= 1) {
+ if (CounterType.P1P1.equals(type) && source.getLethalDamage() <= 1
+ && !source.hasKeyword(Keyword.UNDYING)) {
return false;
}
}
@@ -118,7 +117,7 @@ public class ComputerUtilCost {
/**
* Check discard cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -135,7 +134,7 @@ public class ComputerUtilCost {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) {
final CostDiscard disc = (CostDiscard) part;
-
+
final String type = disc.getType();
if (type.equals("CARDNAME") && source.getAbilityText().contains("Bloodrush")) {
continue;
@@ -145,6 +144,7 @@ public class ComputerUtilCost {
continue;
}
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
+
for (int i = 0; i < num; i++) {
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
if (pref == null) {
@@ -161,7 +161,7 @@ public class ComputerUtilCost {
/**
* Check life cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -193,7 +193,7 @@ public class ComputerUtilCost {
/**
* Check life cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -210,12 +210,12 @@ public class ComputerUtilCost {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
final CostPayLife payLife = (CostPayLife) part;
-
+
Integer amount = payLife.convertAmount();
if (amount == null) {
amount = AbilityUtils.calculateAmount(source, payLife.getAmount(), sourceAbility);
}
-
+
// check if there's override for the remainingLife threshold
if (sourceAbility != null && sourceAbility.hasParam("AILifeThreshold")) {
remainingLife = Integer.parseInt(sourceAbility.getParam("AILifeThreshold"));
@@ -231,7 +231,7 @@ public class ComputerUtilCost {
/**
* Check creature sacrifice cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -251,7 +251,7 @@ public class ComputerUtilCost {
return false;
}
final String type = sac.getType();
-
+
if (type.equals("CARDNAME")) {
continue;
}
@@ -276,7 +276,7 @@ public class ComputerUtilCost {
/**
* Check sacrifice cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -300,7 +300,7 @@ public class ComputerUtilCost {
if (!important) {
return false;
}
- if (!CardLists.filterControlledBy(source.getEnchantedBy(false), source.getController()).isEmpty()) {
+ if (!CardLists.filterControlledBy(source.getEnchantedBy(), source.getController()).isEmpty()) {
return false;
}
continue;
@@ -323,7 +323,7 @@ public class ComputerUtilCost {
}
return true;
}
-
+
public static boolean isSacrificeSelfCost(final Cost cost) {
if (cost == null) {
return false;
@@ -340,7 +340,7 @@ public class ComputerUtilCost {
/**
* Check creature sacrifice cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -386,7 +386,7 @@ public class ComputerUtilCost {
/**
* Check sacrifice cost.
- *
+ *
* @param cost
* the cost
* @param source
@@ -401,14 +401,14 @@ public class ComputerUtilCost {
*
* shouldPayCost.
*
- *
+ *
* @param hostCard
* a {@link forge.game.card.Card} object.
* @param cost
* @return a boolean.
*/
public static boolean shouldPayCost(final Player ai, final Card hostCard, final Cost cost) {
-
+
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostPayLife) {
if (!ai.cantLoseForZeroOrLessLife()) {
@@ -423,7 +423,7 @@ public class ComputerUtilCost {
}
}
}
-
+
return true;
} // shouldPayCost()
@@ -431,7 +431,7 @@ public class ComputerUtilCost {
*
* canPayCost.
*
- *
+ *
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param player
@@ -446,13 +446,13 @@ public class ComputerUtilCost {
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (sa instanceof Spell) {
- final boolean cannotBeCountered = sa.getHostCard().hasKeyword("CARDNAME can't be countered.");
+ final boolean cannotBeCountered = !CardFactoryUtil.isCounterable(sa.getHostCard());
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
- if (cannotBeCountered && c.getName().equals("Nether Void")) {
- continue;
- }
+ if (cannotBeCountered && c.getName().equals("Nether Void")) {
+ continue;
+ }
String[] parts = TextUtil.split(snem, ' ');
boolean meetsRestriction = parts.length == 1 || player.isValid(parts[1], c.getController(), c, sa);
if(!meetsRestriction)
@@ -467,7 +467,7 @@ public class ComputerUtilCost {
}
for (Card c : player.getCardsIn(ZoneType.Command)) {
if (cannotBeCountered) {
- continue;
+ continue;
}
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
if (!StringUtils.isBlank(snem)) {
@@ -479,7 +479,7 @@ public class ComputerUtilCost {
}
}
}
-
+
// Try not to lose Planeswalker if not threatened
if (sa.getRestrictions().isPwAbility()) {
for (final CostPart part : sa.getPayCosts().getCostParts()) {
@@ -495,6 +495,7 @@ public class ComputerUtilCost {
}
}
}
+
// KLD vehicle
if (sa.hasParam("Crew")) { // put under checkTapTypeCost?
for (final CostPart part : sa.getPayCosts().getCostParts()) {
@@ -504,7 +505,37 @@ public class ComputerUtilCost {
}
}
- return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
+ // TODO: Alternate costs which involve both paying mana and tapping a card, e.g. Zahid, Djinn of the Lamp
+ // Current AI decides on each part separately, thus making it possible for the AI to cheat by
+ // tapping a mana source for mana and for the tap cost at the same time. Until this is improved, AI
+ // will not consider mana sources valid for paying the tap cost to avoid this exact situation.
+ if ("DontPayTapCostWithManaSources".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
+ for (final CostPart part : sa.getPayCosts().getCostParts()) {
+ if (part instanceof CostTapType) {
+ CardCollectionView nonManaSources =
+ CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
+ sa.getActivatingPlayer(), sa.getHostCard(), sa);
+ nonManaSources = CardLists.filter(nonManaSources, new Predicate() {
+ @Override
+ public boolean apply(Card card) {
+ boolean hasManaSa = false;
+ for (final SpellAbility sa : card.getSpellAbilities()) {
+ if (sa.isManaAbility() && sa.getPayCosts() != null && sa.getPayCosts().hasTapCost()) {
+ hasManaSa = true;
+ break;
+ }
+ }
+ return !hasManaSa;
+ }
+ });
+ if (nonManaSources.size() < part.convertAmount()) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
} // canPayCost()
@@ -516,7 +547,7 @@ public class ComputerUtilCost {
boolean payNever = "Never".equals(aiLogic);
boolean shockland = "Shockland".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
-
+
if (payNever) { return false; }
if (payForOwnOnly && !isMine) { return false; }
if (payOwner) {
@@ -536,7 +567,7 @@ public class ComputerUtilCost {
// if the new land size would equal the CMC of a card in AIs hand, consider playing it untapped,
// otherwise don't bother running other checks
if (landsize != c.getCMC()) {
- continue;
+ continue;
}
// try to determine in the AI is actually planning to play a spell ability from the card
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
@@ -607,7 +638,6 @@ public class ComputerUtilCost {
Set colorsAvailable = Sets.newHashSet();
if (additionalLands != null) {
- GameActionUtil.grantBasicLandsManaAbilities(additionalLands);
cardsToConsider.addAll(additionalLands);
}
@@ -621,4 +651,15 @@ public class ComputerUtilCost {
return colorsAvailable;
}
+
+ public static boolean isFreeCastAllowedByPermanent(Player player, String altCost) {
+ Game game = player.getGame();
+ for (Card cardInPlay : game.getCardsIn(ZoneType.Battlefield)) {
+ if (cardInPlay.hasSVar("AllowFreeCast")) {
+ return altCost == null ? "Always".equals(cardInPlay.getSVar("AllowFreeCast"))
+ : altCost.equals(cardInPlay.getSVar("AllowFreeCast"));
+ }
+ }
+ return false;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
index 3011e56ba06..a672d60af0f 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
@@ -16,11 +16,7 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.CombatUtil;
-import forge.game.cost.Cost;
-import forge.game.cost.CostAdjustment;
-import forge.game.cost.CostPartMana;
-import forge.game.cost.CostPayEnergy;
-import forge.game.cost.CostPayment;
+import forge.game.cost.*;
import forge.game.mana.Mana;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.mana.ManaPool;
@@ -860,14 +856,22 @@ public class ComputerUtilMana {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
+ // Mana reserved for spell synchronization
+ if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_NEXT_SPELL)) {
+ return true;
+ }
+
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
// For combat tricks, always obey mana reservation
if (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
- }
- else {
- if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) {
+ } else if (!(ai.getGame().getPhaseHandler().isPlayerTurn(ai)) && (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP)) {
+ AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
+ AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
+ } else {
+ if ((AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) ||
+ (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK))) {
// This mana source is held elsewhere for a combat trick.
return true;
}
@@ -910,6 +914,10 @@ public class ComputerUtilMana {
// Make mana needed to avoid negative effect a mandatory cost for the AI
for (String manaPart : card.getSVar("ManaNeededToAvoidNegativeEffect").split(",")) {
// convert long color strings to short color strings
+ if (manaPart.isEmpty()) {
+ continue;
+ }
+
byte mask = ManaAtom.fromName(manaPart);
// make mana mandatory for AI
@@ -1506,7 +1514,7 @@ public class ComputerUtilMana {
final Card offering = sa.getSacrificedAsOffering();
offering.setUsedToPay(false);
if (costIsPaid && !test) {
- sa.getHostCard().getController().getGame().getAction().sacrifice(offering, sa);
+ sa.getHostCard().getGame().getAction().sacrifice(offering, sa, null);
}
sa.resetSacrificedAsOffering();
}
@@ -1514,7 +1522,7 @@ public class ComputerUtilMana {
final Card emerge = sa.getSacrificedAsEmerge();
emerge.setUsedToPay(false);
if (costIsPaid && !test) {
- sa.getHostCard().getController().getGame().getAction().sacrifice(emerge, sa);
+ sa.getHostCard().getGame().getAction().sacrifice(emerge, sa, null);
}
sa.resetSacrificedAsEmerge();
}
diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
index e9e2b7bf894..3b7a9c7890a 100644
--- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
@@ -7,6 +7,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.cost.CostPayEnergy;
+import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.spellability.SpellAbility;
@@ -53,10 +54,10 @@ public class CreatureEvaluator implements Function {
}
// Evasion keywords
- if (c.hasKeyword("Flying")) {
+ if (c.hasKeyword(Keyword.FLYING)) {
value += addValue(power * 10, "flying");
}
- if (c.hasKeyword("Horsemanship")) {
+ if (c.hasKeyword(Keyword.HORSEMANSHIP)) {
value += addValue(power * 10, "horses");
}
if (c.hasKeyword("Unblockable")) {
@@ -65,13 +66,13 @@ public class CreatureEvaluator implements Function {
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
value += addValue(power * 6, "thorns");
}
- if (c.hasKeyword("Fear")) {
+ if (c.hasKeyword(Keyword.FEAR)) {
value += addValue(power * 6, "fear");
}
- if (c.hasKeyword("Intimidate")) {
+ if (c.hasKeyword(Keyword.INTIMIDATE)) {
value += addValue(power * 6, "intimidate");
}
- if (c.hasStartOfKeyword("Menace")) {
+ if (c.hasKeyword(Keyword.MENACE)) {
value += addValue(power * 4, "menace");
}
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
@@ -81,49 +82,49 @@ public class CreatureEvaluator implements Function {
// Other good keywords
if (power > 0) {
- if (c.hasKeyword("Double Strike")) {
+ if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
value += addValue(10 + (power * 15), "ds");
- } else if (c.hasKeyword("First Strike")) {
+ } else if (c.hasKeyword(Keyword.FIRST_STRIKE)) {
value += addValue(10 + (power * 5), "fs");
}
- if (c.hasKeyword("Deathtouch")) {
+ if (c.hasKeyword(Keyword.DEATHTOUCH)) {
value += addValue(25, "dt");
}
- if (c.hasKeyword("Lifelink")) {
+ if (c.hasKeyword(Keyword.LIFELINK)) {
value += addValue(power * 10, "lifelink");
}
- if (power > 1 && c.hasKeyword("Trample")) {
+ if (power > 1 && c.hasKeyword(Keyword.TRAMPLE)) {
value += addValue((power - 1) * 5, "trample");
}
- if (c.hasKeyword("Vigilance")) {
+ if (c.hasKeyword(Keyword.VIGILANCE)) {
value += addValue((power * 5) + (toughness * 5), "vigilance");
}
- if (c.hasKeyword("Wither")) {
+ if (c.hasKeyword(Keyword.WITHER)) {
value += addValue(power * 10, "Wither");
}
- if (c.hasKeyword("Infect")) {
+ if (c.hasKeyword(Keyword.INFECT)) {
value += addValue(power * 15, "infect");
}
- value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
- value += addValue(c.getKeywordMagnitude("Afflict") * 5, "afflict");
+ value += addValue(c.getKeywordMagnitude(Keyword.RAMPAGE), "rampage");
+ value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict");
}
- value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
- value += addValue(c.getAmountOfKeyword("Flanking") * 15, "flanking");
- value += addValue(c.getAmountOfKeyword("Exalted") * 15, "exalted");
- value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
- value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
+ value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
+ value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
+ value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
+ value += addValue(c.getKeywordMagnitude(Keyword.ANNIHILATOR) * 50, "eldrazi");
+ value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb");
// Keywords that may produce temporary or permanent buffs over time
- if (c.hasKeyword("Prowess")) {
+ if (c.hasKeyword(Keyword.PROWESS)) {
value += addValue(5, "prowess");
}
- if (c.hasKeyword("Outlast")) {
+ if (c.hasKeyword(Keyword.OUTLAST)) {
value += addValue(10, "outlast");
}
// Defensive Keywords
- if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
+ if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) {
value += addValue(5, "reach");
}
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
@@ -131,7 +132,7 @@ public class CreatureEvaluator implements Function {
}
// Protection
- if (c.hasKeyword("Indestructible")) {
+ if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
value += addValue(70, "darksteel");
}
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
@@ -139,20 +140,17 @@ public class CreatureEvaluator implements Function {
} else if (c.hasKeyword("Prevent all combat damage that would be dealt to CARDNAME.")) {
value += addValue(50, "fogbank");
}
- if (c.hasKeyword("Hexproof")) {
+ if (c.hasKeyword(Keyword.HEXPROOF)) {
value += addValue(35, "hexproof");
- } else if (c.hasKeyword("Shroud")) {
+ } else if (c.hasKeyword(Keyword.SHROUD)) {
value += addValue(30, "shroud");
}
- if (c.hasStartOfKeyword("Protection")) {
+ if (c.hasKeyword(Keyword.PROTECTION)) {
value += addValue(20, "protection");
}
- if (c.hasStartOfKeyword("PreventAllDamageBy")) {
- value += addValue(10, "prevent-dmg");
- }
// Bad keywords
- if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
+ if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) {
value -= subValue((power * 9) + 40, "defender");
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
value -= subValue(40, "sac-end");
@@ -188,17 +186,17 @@ public class CreatureEvaluator implements Function {
value -= subValue(30, "cupkeep");
} else if (c.hasStartOfKeyword("UpkeepCost")) {
value -= subValue(20, "sac-unless");
- } else if (c.hasStartOfKeyword("Echo") && c.cameUnderControlSinceLastUpkeep()) {
+ } else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
value -= subValue(10, "echo-unpaid");
}
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
value -= subValue(20, "upkeep-dmg");
}
- if (c.hasStartOfKeyword("Fading")) {
+ if (c.hasKeyword(Keyword.FADING)) {
value -= subValue(20, "fading");
}
- if (c.hasStartOfKeyword("Vanishing")) {
+ if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "vanishing");
}
if (c.getSVar("Targeting").equals("Dies")) {
diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java
index 9a3712a5f51..9891b166767 100644
--- a/forge-ai/src/main/java/forge/ai/GameState.java
+++ b/forge-ai/src/main/java/forge/ai/GameState.java
@@ -6,6 +6,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName;
+import forge.card.MagicColor;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
@@ -19,8 +20,10 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.event.GameEventAttackersDeclared;
import forge.game.event.GameEventCombatChanged;
+import forge.game.mana.ManaPool;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
+import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
@@ -52,6 +55,14 @@ public abstract class GameState {
private int computerLife = -1;
private String humanCounters = "";
private String computerCounters = "";
+ private String humanManaPool = "";
+ private String computerManaPool = "";
+ private String humanPersistentMana = "";
+ private String computerPersistentMana = "";
+ private int humanLandsPlayed = 0;
+ private int computerLandsPlayed = 0;
+ private int humanLandsPlayedLastTurn = 0;
+ private int computerLandsPlayedLastTurn = 0;
private boolean puzzleCreatorState = false;
@@ -60,8 +71,10 @@ public abstract class GameState {
private final Map idToCard = new HashMap<>();
private final Map cardToAttachId = new HashMap<>();
+ private final Map cardToEnchantPlayerId = new HashMap<>();
private final Map markedDamage = new HashMap<>();
private final Map> cardToChosenClrs = new HashMap<>();
+ private final Map cardToChosenCards = new HashMap<>();
private final Map cardToChosenType = new HashMap<>();
private final Map> cardToRememberedId = new HashMap<>();
private final Map> cardToImprintedId = new HashMap<>();
@@ -79,11 +92,15 @@ public abstract class GameState {
private String tChangePlayer = "NONE";
private String tChangePhase = "NONE";
+ private String tAdvancePhase = "NONE";
+
private String precastHuman = null;
private String precastAI = null;
private int turn = 1;
+ private boolean removeSummoningSickness = false;
+
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
private final int TARGET_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
private final int TARGET_HUMAN = -2;
@@ -112,6 +129,10 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("humanlife=", String.valueOf(humanLife), "\n"));
sb.append(TextUtil.concatNoSpace("ailife=", String.valueOf(computerLife), "\n"));
+ sb.append(TextUtil.concatNoSpace("humanlandsplayed=", String.valueOf(humanLandsPlayed), "\n"));
+ sb.append(TextUtil.concatNoSpace("ailandsplayed=", String.valueOf(computerLandsPlayed), "\n"));
+ sb.append(TextUtil.concatNoSpace("humanlandsplayedlastturn=", String.valueOf(humanLandsPlayedLastTurn), "\n"));
+ sb.append(TextUtil.concatNoSpace("ailandsplayedlastturn=", String.valueOf(computerLandsPlayedLastTurn), "\n"));
sb.append(TextUtil.concatNoSpace("turn=", String.valueOf(turn), "\n"));
if (!humanCounters.isEmpty()) {
@@ -121,6 +142,13 @@ public abstract class GameState {
sb.append(TextUtil.concatNoSpace("aicounters=", computerCounters, "\n"));
}
+ if (!humanManaPool.isEmpty()) {
+ sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
+ }
+ if (!computerManaPool.isEmpty()) {
+ sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
+ }
+
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
appendCards(humanCardTexts, "human", sb);
@@ -147,8 +175,14 @@ public abstract class GameState {
}
humanLife = human.getLife();
computerLife = ai.getLife();
+ humanLandsPlayed = human.getLandsPlayedThisTurn();
+ computerLandsPlayed = ai.getLandsPlayedThisTurn();
+ humanLandsPlayedLastTurn = human.getLandsPlayedLastTurn();
+ computerLandsPlayedLastTurn = ai.getLandsPlayedLastTurn();
humanCounters = countersToString(human.getCounters());
computerCounters = countersToString(ai.getCounters());
+ humanManaPool = processManaPool(human.getManaPool());
+ computerManaPool = processManaPool(ai.getManaPool());
tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human";
tChangePhase = game.getPhaseHandler().getPhase().toString();
@@ -165,9 +199,7 @@ public abstract class GameState {
cardsReferencedByID.add(card.getExiledWith());
}
if (zone == ZoneType.Battlefield) {
- if (!card.getEnchantedBy(false).isEmpty()
- || !card.getEquippedBy(false).isEmpty()
- || !card.getFortifiedBy(false).isEmpty()) {
+ if (!card.getAttachedCards().isEmpty()) {
// Remember the ID of cards that have attachments
cardsReferencedByID.add(card);
}
@@ -182,6 +214,10 @@ public abstract class GameState {
// Remember the IDs of imprinted cards
cardsReferencedByID.add(i);
}
+ for (Card i : card.getChosenCards()) {
+ // Remember the IDs of chosen cards
+ cardsReferencedByID.add(i);
+ }
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
// Remember the IDs of attacked planeswalkers
GameEntity def = game.getCombat().getDefenderByAttacker(card);
@@ -241,8 +277,7 @@ public abstract class GameState {
newText.append("|Renowned");
}
if (c.isMonstrous()) {
- newText.append("|Monstrous:");
- newText.append(c.getMonstrosityNum());
+ newText.append("|Monstrous");
}
if (c.isPhasedOut()) {
newText.append("|PhasedOut");
@@ -260,12 +295,14 @@ public abstract class GameState {
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
}
- if (c.getEquipping() != null) {
- newText.append("|Attaching:").append(c.getEquipping().getId());
- } else if (c.getFortifying() != null) {
- newText.append("|Attaching:").append(c.getFortifying().getId());
- } else if (c.getEnchantingCard() != null) {
- newText.append("|Attaching:").append(c.getEnchantingCard().getId());
+ if (c.isAttachedToEntity()) {
+ newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
+ }
+ if (c.getPlayerAttachedTo() != null) {
+ // TODO: improve this for game states with more than two players
+ newText.append("|EnchantingPlayer:");
+ Player p = c.getPlayerAttachedTo();
+ newText.append(p.getController().isAI() ? "AI" : "HUMAN");
}
if (c.getDamage() > 0) {
@@ -282,6 +319,17 @@ public abstract class GameState {
newText.append("|NamedCard:").append(c.getNamedCard());
}
+ List chosenCardIds = Lists.newArrayList();
+ for (Object obj : c.getChosenCards()) {
+ if (obj instanceof Card) {
+ int id = ((Card)obj).getId();
+ chosenCardIds.add(String.valueOf(id));
+ }
+ }
+ if (!chosenCardIds.isEmpty()) {
+ newText.append("|ChosenCards:").append(TextUtil.join(chosenCardIds, ","));
+ }
+
List rememberedCardIds = Lists.newArrayList();
for (Object obj : c.getRemembered()) {
if (obj instanceof Card) {
@@ -389,8 +437,10 @@ public abstract class GameState {
if (categoryName.startsWith("active")) {
if (categoryName.endsWith("player"))
tChangePlayer = categoryValue.trim().toLowerCase();
- if (categoryName.endsWith("phase"))
+ else if (categoryName.endsWith("phase"))
tChangePhase = categoryValue.trim().toUpperCase();
+ else if (categoryName.endsWith("phaseadvance"))
+ tAdvancePhase = categoryValue.trim().toUpperCase();
return;
}
@@ -400,6 +450,10 @@ public abstract class GameState {
turn = Integer.parseInt(categoryValue);
}
+ else if (categoryName.equals("removesummoningsickness")) {
+ removeSummoningSickness = categoryValue.equalsIgnoreCase("true");
+ }
+
else if (categoryName.endsWith("life")) {
if (isHuman)
humanLife = Integer.parseInt(categoryValue);
@@ -414,6 +468,20 @@ public abstract class GameState {
computerCounters = categoryValue;
}
+ else if (categoryName.endsWith("landsplayed")) {
+ if (isHuman)
+ humanLandsPlayed = Integer.parseInt(categoryValue);
+ else
+ computerLandsPlayed = Integer.parseInt(categoryValue);
+ }
+
+ else if (categoryName.endsWith("landsplayedlastturn")) {
+ if (isHuman)
+ humanLandsPlayedLastTurn = Integer.parseInt(categoryValue);
+ else
+ computerLandsPlayedLastTurn = Integer.parseInt(categoryValue);
+ }
+
else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
if (isHuman)
humanCardTexts.put(ZoneType.Battlefield, categoryValue);
@@ -466,6 +534,21 @@ public abstract class GameState {
else
precastAI = categoryValue;
}
+
+ else if (categoryName.endsWith("manapool")) {
+ if (isHuman)
+ humanManaPool = categoryValue;
+ else
+ computerManaPool = categoryValue;
+ }
+
+ else if (categoryName.endsWith("persistentmana")) {
+ if (isHuman)
+ humanPersistentMana = categoryValue;
+ else
+ computerPersistentMana = categoryValue;
+ }
+
else {
System.out.println("Unknown key: " + categoryName);
}
@@ -486,20 +569,28 @@ public abstract class GameState {
idToCard.clear();
cardToAttachId.clear();
+ cardToEnchantPlayerId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
markedDamage.clear();
cardToChosenClrs.clear();
+ cardToChosenCards.clear();
cardToChosenType.clear();
cardToScript.clear();
cardAttackMap.clear();
- Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
- PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
+ Player newPlayerTurn = tChangePlayer.equalsIgnoreCase("human") ? human : tChangePlayer.equalsIgnoreCase("ai") ? ai : null;
+ PhaseType newPhase = tChangePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tChangePhase);
+ PhaseType advPhase = tAdvancePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tAdvancePhase);
// Set stack to resolving so things won't trigger/effects be checked right away
game.getStack().setResolving(true);
+ updateManaPool(human, humanManaPool, true, false);
+ updateManaPool(ai, computerManaPool, true, false);
+ updateManaPool(human, humanPersistentMana, false, true);
+ updateManaPool(ai, computerPersistentMana, false, true);
+
if (!humanCounters.isEmpty()) {
applyCountersToGameEntity(human, humanCounters);
}
@@ -511,8 +602,8 @@ public abstract class GameState {
game.getTriggerHandler().setSuppressAllTriggers(true);
- setupPlayerState(humanLife, humanCardTexts, human);
- setupPlayerState(computerLife, aiCardTexts, ai);
+ setupPlayerState(humanLife, humanCardTexts, human, humanLandsPlayed, humanLandsPlayedLastTurn);
+ setupPlayerState(computerLife, aiCardTexts, ai, computerLandsPlayed, computerLandsPlayedLastTurn);
handleCardAttachments();
handleChosenEntities();
@@ -532,9 +623,56 @@ public abstract class GameState {
game.getStack().setResolving(false);
+ // Advance to a certain phase, activating all triggered abilities
+ if (advPhase != null) {
+ game.getPhaseHandler().devAdvanceToPhase(advPhase);
+ }
+
+ if (removeSummoningSickness) {
+ for (Card card : game.getCardsInGame()) {
+ card.setSickness(false);
+ }
+ }
+
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
}
+ private String processManaPool(ManaPool manaPool) {
+ String mana = "";
+ for (final byte c : MagicColor.WUBRGC) {
+ int amount = manaPool.getAmountOfColor(c);
+ for (int i = 0; i < amount; i++) {
+ mana += MagicColor.toShortString(c) + " ";
+ }
+ }
+
+ return mana.trim();
+ }
+
+ private void updateManaPool(Player p, String manaDef, boolean clearPool, boolean persistent) {
+ Game game = p.getGame();
+ if (clearPool) {
+ p.getManaPool().clearPool(false);
+ }
+
+ if (!manaDef.isEmpty()) {
+ final Card dummy = new Card(-777777, game);
+ dummy.setOwner(p);
+ final Map produced = Maps.newHashMap();
+ produced.put("Produced", manaDef);
+ if (persistent) {
+ produced.put("PersistentMana", "True");
+ }
+ final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
+ game.getAction().invoke(new Runnable() {
+ @Override
+ public void run() {
+ abMana.produceMana(null);
+ }
+ });
+ }
+ }
+
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
// First we need to ensure that all attackers are declared in the Declare Attackers step,
// even if proceeding straight to Declare Blockers
@@ -838,33 +976,39 @@ public abstract class GameState {
Card c = entry.getKey();
c.setNamedCard(entry.getValue());
}
+
+ // Chosen cards
+ for (Entry entry : cardToChosenCards.entrySet()) {
+ Card c = entry.getKey();
+ c.setChosenCards(entry.getValue());
+ }
}
private void handleCardAttachments() {
// Unattach all permanents first
for(Entry entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
-
- attachedTo.unEnchantAllCards();
- attachedTo.unEquipAllCards();
- for (Card c : attachedTo.getFortifiedBy(true)) {
- attachedTo.unFortifyCard(c);
- }
+ attachedTo.unAttachAllCards();
}
// Attach permanents by ID
for(Entry entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
-
- if (attacher.isEquipment()) {
- attacher.equipCard(attachedTo);
- } else if (attacher.isAura()) {
- attacher.enchantEntity(attachedTo);
- } else if (attacher.isFortified()) {
- attacher.fortifyCard(attachedTo);
+ if (attacher.isAttachment()) {
+ attacher.attachToEntity(attachedTo);
}
}
+
+ // Enchant players by ID
+ for(Entry entry : cardToEnchantPlayerId.entrySet()) {
+ // TODO: improve this for game states with more than two players
+ Card attacher = entry.getKey();
+ Game game = attacher.getGame();
+ Player attachedTo = entry.getValue() == TARGET_AI ? game.getPlayers().get(1) : game.getPlayers().get(0);
+
+ attacher.attachToEntity(attachedTo);
+ }
}
private void applyCountersToGameEntity(GameEntity entity, String counterString) {
@@ -876,9 +1020,15 @@ public abstract class GameState {
}
}
- private void setupPlayerState(int life, Map cardTexts, final Player p) {
+ private void setupPlayerState(int life, Map cardTexts, final Player p, final int landsPlayed, final int landsPlayedLastTurn) {
// Lock check static as we setup player state
+ // Clear all zones first, this ensures that any lingering cards and effects (e.g. in command zone) get cleared up
+ // before setting up a new state
+ for (ZoneType zt : ZONES.keySet()) {
+ p.getZone(zt).removeAllCards(true);
+ }
+
Map playerCards = new EnumMap(ZoneType.class);
for (Entry kv : cardTexts.entrySet()) {
String value = kv.getValue();
@@ -886,6 +1036,9 @@ public abstract class GameState {
}
if (life >= 0) p.setLife(life, null);
+ p.setLandsPlayedThisTurn(landsPlayed);
+ p.setLandsPlayedLastTurn(landsPlayedLastTurn);
+
for (Entry kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
@@ -909,7 +1062,9 @@ public abstract class GameState {
if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
- c.setEnchanting(c);
+
+ //FIXME it shouldn't be able to attach itself
+ c.setEntityAttachedTo(c);
}
if (cardsWithoutETBTrigs.contains(c)) {
@@ -927,7 +1082,6 @@ public abstract class GameState {
zone.setCards(kv.getValue());
}
}
-
}
/**
@@ -979,9 +1133,8 @@ public abstract class GameState {
c.tap();
} else if (info.startsWith("Renowned")) {
c.setRenowned(true);
- } else if (info.startsWith("Monstrous:")) {
+ } else if (info.startsWith("Monstrous")) {
c.setMonstrous(true);
- c.setMonstrosityNum(Integer.parseInt(info.substring((info.indexOf(':') + 1))));
} else if (info.startsWith("PhasedOut")) {
c.setPhasedOut(true);
} else if (info.startsWith("Counters:")) {
@@ -1007,9 +1160,13 @@ public abstract class GameState {
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c);
- } else if (info.startsWith("Attaching:")) {
+ } else if (info.startsWith("Attaching:") /*deprecated*/ || info.startsWith("AttachedTo:")) {
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
cardToAttachId.put(c, id);
+ } else if (info.startsWith("EnchantingPlayer:")) {
+ // TODO: improve this for game states with more than two players
+ String tgt = info.substring(info.indexOf(':') + 1);
+ cardToEnchantPlayerId.put(c, tgt.equalsIgnoreCase("AI") ? TARGET_AI : TARGET_HUMAN);
} else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
@@ -1020,6 +1177,13 @@ public abstract class GameState {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
+ } else if (info.startsWith("ChosenCards:")) {
+ CardCollection chosen = new CardCollection();
+ String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
+ for (String id : idlist) {
+ chosen.add(idToCard.get(Integer.parseInt(id)));
+ }
+ cardToChosenCards.put(c, chosen);
} else if (info.startsWith("NamedCard:")) {
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ExecuteScript:")) {
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index 8fa66175905..3ce2d7333da 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -26,6 +26,7 @@ import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.*;
import forge.game.mana.Mana;
+import forge.game.mana.ManaConversionMatrix;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -161,19 +162,46 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List chooseEntitiesForEffect(
- FCollectionView optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
+ FCollectionView optionList, int min, int max, DelayedReveal delayedReveal, SpellAbility sa, String title,
Player targetedPlayer) {
- // this isn't used
- return null;
+ if (delayedReveal != null) {
+ reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
+ }
+ FCollection remaining = new FCollection(optionList);
+ List selecteds = new ArrayList();
+ T selected;
+ do {
+ selected = chooseSingleEntityForEffect(remaining, null, sa, title, selecteds.size()>=min, targetedPlayer);
+ if ( selected != null ) {
+ remaining.remove(selected);
+ selecteds.add(selected);
+ }
+ } while ( (selected != null ) && (selecteds.size() < max) );
+ return selecteds;
}
@Override
- public SpellAbility chooseSingleSpellForEffect(java.util.List spells, SpellAbility sa, String title) {
+ public List chooseFromTwoListsForEffect(FCollectionView optionList1, FCollectionView optionList2,
+ boolean optional, DelayedReveal delayedReveal, SpellAbility sa, String title, Player targetedPlayer) {
+ if (delayedReveal != null) {
+ reveal(delayedReveal.getCards(), delayedReveal.getZone(), delayedReveal.getOwner(), delayedReveal.getMessagePrefix());
+ }
+ T selected1 = chooseSingleEntityForEffect(optionList1, null, sa, title, optional, targetedPlayer);
+ T selected2 = chooseSingleEntityForEffect(optionList2, null, sa, title, optional || selected1!=null, targetedPlayer);
+ List selecteds = new ArrayList();
+ if ( selected1 != null ) { selecteds.add(selected1); }
+ if ( selected2 != null ) { selecteds.add(selected2); }
+ return selecteds;
+ }
+
+ @Override
+ public SpellAbility chooseSingleSpellForEffect(java.util.List spells, SpellAbility sa, String title,
+ Map params) {
ApiType api = sa.getApi();
if (null == api) {
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
}
- return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells);
+ return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
}
@Override
@@ -287,21 +315,54 @@ public class PlayerControllerAi extends PlayerController {
}
// put the rest on top in random order
- Collections.shuffle(toTop);
+ Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toBottom);
}
+ /* (non-Javadoc)
+ * @see forge.game.player.PlayerController#arrangeForSurveil(forge.game.card.CardCollection)
+ */
+ @Override
+ public ImmutablePair arrangeForSurveil(CardCollection topN) {
+ CardCollection toGraveyard = new CardCollection();
+ CardCollection toTop = new CardCollection();
+
+ // TODO: Currently this logic uses the same routine as Scry. Possibly differentiate this and implement
+ // a specific logic for Surveil (e.g. maybe to interact better with Reanimator strategies etc.).
+ if (getPlayer().getCardsIn(ZoneType.Library).size() <= getAi().getIntProperty(AiProps.SURVEIL_NUM_CARDS_IN_LIBRARY_TO_BAIL)) {
+ toTop.addAll(topN);
+ } else {
+ for (Card c : topN) {
+ if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c)) {
+ toGraveyard.add(c);
+ } else {
+ toTop.add(c);
+ }
+ }
+ }
+
+ Collections.shuffle(toTop, MyRandom.getRandom());
+ return ImmutablePair.of(toTop, toGraveyard);
+ }
+
@Override
public boolean willPutCardOnTop(Card c) {
- return true; // AI does not know what will happen next (another clash or that would become his topdeck)
+ // This is used for Clash. Currently uses Scry logic to determine whether the card should be put on top.
+ // Note that the AI does not know what will happen next (another clash or that would become his topdeck)
+
+ return !ComputerUtil.scryWillMoveCardToBottomOfLibrary(player, c);
}
@Override
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
//TODO Add more logic for AI ordering here
- // In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
+ if (cards.isEmpty()) {
+ return cards;
+ }
+
if (destinationZone == ZoneType.Graveyard) {
+ // In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (!CardLists.filter(game.getCardsInGame(), new Predicate() {
@Override
public boolean apply(Card card) {
@@ -331,6 +392,61 @@ public class PlayerControllerAi extends PlayerController {
return reordered;
}
}
+ } else if (destinationZone == ZoneType.Library) {
+ // Ponder and similar cards
+ Player p = cards.getFirst().getController(); // whose library are we reordering?
+ CardCollection reordered = new CardCollection();
+
+ // Try to use the Scry logic to figure out what should be closer to the top and what should be closer to the bottom
+ CardCollection topLands = new CardCollection(), topNonLands = new CardCollection(), bottom = new CardCollection();
+ for (Card c : cards) {
+ if (ComputerUtil.scryWillMoveCardToBottomOfLibrary(p, c)) {
+ bottom.add(c);
+ } else {
+ if (c.isLand()) {
+ topLands.add(c);
+ } else {
+ topNonLands.add(c);
+ }
+ }
+ }
+
+ int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
+
+ if (!p.isOpponentOf(player)) {
+ if (landsOTB <= 2) {
+ // too few lands, add all the lands from the "top" category first
+ reordered.addAll(topLands);
+ topLands.clear();
+ } else {
+ // we would have scried a land to top, so add one land from the "top" category if it's available there, but not more
+ if (!topLands.isEmpty()) {
+ Card first = topLands.getFirst();
+ reordered.add(first);
+ topLands.remove(first);
+ }
+ }
+ // add everything that was deemed playable
+ reordered.addAll(topNonLands);
+ // then all the land extras that may be there
+ reordered.addAll(topLands);
+ // and then everything else that was deemed unplayable and thus scriable to the bottom
+ reordered.addAll(bottom);
+ } else {
+ // try to screw the opponent up as much as possible by placing the uncastables first
+ reordered.addAll(bottom);
+ if (landsOTB <= 5) {
+ reordered.addAll(topNonLands);
+ reordered.addAll(topLands);
+ } else {
+ reordered.addAll(topLands);
+ reordered.addAll(topNonLands);
+ }
+ }
+
+ assert(reordered.size() == cards.size());
+
+ return reordered;
}
// Default: return with the same order as was passed into this method
@@ -891,7 +1007,7 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, boolean isActivatedSa) {
+ public boolean payManaCost(ManaCost toPay, CostPartMana costPartMana, SpellAbility sa, String prompt /* ai needs hints as well */, ManaConversionMatrix matrix, boolean isActivatedSa) {
// TODO Auto-generated method stub
ManaCostBeingPaid cost = isActivatedSa ? ComputerUtilMana.calculateManaCost(sa, false, 0) : new ManaCostBeingPaid(toPay);
return ComputerUtilMana.payManaCost(cost, sa, player);
@@ -1000,7 +1116,7 @@ public class PlayerControllerAi extends PlayerController {
@Override
public List chooseCardsForZoneChange(
- ZoneType destination, List origin, SpellAbility sa, CardCollection fetchList,
+ ZoneType destination, List origin, SpellAbility sa, CardCollection fetchList, int min, int max,
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
// this isn't used
return null;
@@ -1057,9 +1173,27 @@ public class PlayerControllerAi extends PlayerController {
}
@Override
- public List chooseOptionalCosts(SpellAbility choosen,
+ public List chooseOptionalCosts(SpellAbility chosen,
List optionalCostValues) {
- // TODO Auto-generated method stub
- return null;
+ List chosenOptCosts = Lists.newArrayList();
+ Cost costSoFar = chosen.getPayCosts() != null ? chosen.getPayCosts().copy() : Cost.Zero;
+
+ for (OptionalCostValue opt : optionalCostValues) {
+ // Choose the optional cost if it can be paid (to be improved later, check for playability and other conditions perhaps)
+ Cost fullCost = opt.getCost().copy().add(costSoFar);
+ SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
+ if (ComputerUtilCost.canPayCost(fullCostSa, player)) {
+ chosenOptCosts.add(opt);
+ costSoFar.add(opt.getCost());
+ }
+ }
+
+ return chosenOptCosts;
+ }
+
+ @Override
+ public boolean confirmMulliganScry(Player p) {
+ // Always true?
+ return true;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
index d69ddd0c7c8..e091a2462e9 100644
--- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
@@ -33,6 +33,7 @@ import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPart;
+import forge.game.keyword.Keyword;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -206,7 +207,9 @@ public class SpecialCardAi {
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
- CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"))));
+ CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
+ Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(
+ CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH))));
boolean hasUsefulBlocker = false;
for (Card c : flyingCreatures) {
@@ -329,7 +332,7 @@ public class SpecialCardAi {
boolean oppHasFirstStrike = false;
boolean oppCantDie = true;
boolean unblocked = opposition.isEmpty();
- boolean canTrample = source.hasKeyword("Trample");
+ boolean canTrample = source.hasKeyword(Keyword.TRAMPLE);
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = ((Card)combat.getDefenderByAttacker(source)).getCounters(CounterType.LOYALTY);
@@ -351,7 +354,7 @@ public class SpecialCardAi {
}
for (Card c : opposition) {
- if (c.hasKeyword("First Strike") || c.hasKeyword("Double Strike")) {
+ if (c.hasKeyword(Keyword.FIRST_STRIKE) || c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
oppHasFirstStrike = true;
}
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
@@ -382,8 +385,8 @@ public class SpecialCardAi {
// Already enough to kill the blockers and survive, don't overpump
return false;
}
- if (oppCantDie && !source.hasKeyword("Trample") && !source.hasKeyword("Wither")
- && !source.hasKeyword("Infect") && predictedPT.getLeft() <= oppT) {
+ if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.hasKeyword(Keyword.WITHER)
+ && !source.hasKeyword(Keyword.INFECT) && predictedPT.getLeft() <= oppT) {
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
return false;
}
@@ -400,7 +403,7 @@ public class SpecialCardAi {
CardCollection potentialBlockers = new CardCollection();
for (Card b : oppInPlay) {
- if (CombatUtil.canBlock(sa.getHostCard(), b)) {
+ if (CombatUtil.canBlock(source, b)) {
potentialBlockers.add(b);
}
}
@@ -408,7 +411,7 @@ public class SpecialCardAi {
Pair predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
int oppT = Aggregates.sum(potentialBlockers, CardPredicates.Accessors.fnGetNetToughness);
- if (potentialBlockers.isEmpty() || (sa.getHostCard().hasKeyword("Trample") && predictedPT.getLeft() - oppT >= oppLife)) {
+ if (potentialBlockers.isEmpty() || (source.hasKeyword(Keyword.TRAMPLE) && predictedPT.getLeft() - oppT >= oppLife)) {
return true;
}
@@ -674,6 +677,14 @@ 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) {
+ // 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(CounterType.TIME) > 0) {
+ return false;
+ }
+ }
+
int aiBattlefieldPower = 0, aiGraveyardPower = 0;
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value
@@ -818,6 +829,10 @@ public class SpecialCardAi {
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
+ }
+
if (!CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Yawgmoth's Bargain")).isEmpty()) {
// Prefer Yawgmoth's Bargain because AI is generally better with it
@@ -1334,6 +1349,10 @@ public class SpecialCardAi {
Game game = ai.getGame();
PhaseHandler ph = game.getPhaseHandler();
+ if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
+ return false; // nothing to draw from the library
+ }
+
int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize();
diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
index be3d906ec92..03c96209a0c 100644
--- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
@@ -76,6 +76,13 @@ public abstract class SpellAbilityAi {
return false;
}
}
+
+ if (sa.hasParam("AITgtBeforeCostEval")) {
+ // Cost payment requires a valid target to be specified, e.g. Quillmane Baku, so run the API logic first
+ // to set the target, then decide on paying costs (slower, so only use for cards where it matters)
+ return checkApiLogic(ai, sa) && (cost == null || willPayCosts(ai, sa, cost, source));
+ }
+
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return false;
}
@@ -169,7 +176,7 @@ public abstract class SpellAbilityAi {
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory)
{
- if (!doTriggerAINoCost(aiPlayer, sa, mandatory)) {
+ if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
return false;
}
final AbilitySub subAb = sa.getSubAbility();
@@ -238,8 +245,8 @@ public abstract class SpellAbilityAi {
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa) {
- return (sa.isSpell() && sa.getHostCard().isSorcery())
- || (sa.isAbility() && sa.getRestrictions().isSorcerySpeed())
+ return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
+ || (sa.getRootAbility().isAbility() && sa.getRestrictions().isSorcerySpeed())
|| (sa.getRestrictions().isPwAbility() && !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed."));
}
@@ -324,7 +331,7 @@ public abstract class SpellAbilityAi {
return null;
}
- public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells) {
+ public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells, Map params) {
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleSpellAbility is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return spells.get(0);
}
diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
index c11bd25aad6..611161a59fb 100644
--- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
@@ -25,6 +25,7 @@ public enum SpellApiToAi {
.put(ApiType.AnimateAll, AnimateAllAi.class)
.put(ApiType.Attach, AttachAi.class)
.put(ApiType.Ascend, AlwaysPlayAi.class)
+ .put(ApiType.AssignGroup, AssignGroupAi.class)
.put(ApiType.Balance, BalanceAi.class)
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
@@ -78,12 +79,14 @@ public enum SpellApiToAi {
.put(ApiType.FlipACoin, FlipACoinAi.class)
.put(ApiType.Fog, FogAi.class)
.put(ApiType.GainControl, ControlGainAi.class)
+ .put(ApiType.GainControlVariant, AlwaysPlayAi.class)
.put(ApiType.GainLife, LifeGainAi.class)
.put(ApiType.GainOwnership, CannotPlayAi.class)
.put(ApiType.GameDrawn, CannotPlayAi.class)
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
.put(ApiType.Goad, GoadAi.class)
.put(ApiType.Haunt, HauntAi.class)
+ .put(ApiType.ImmediateTrigger, AlwaysPlayAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class)
.put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.Mana, ManaEffectAi.class)
@@ -144,6 +147,7 @@ public enum SpellApiToAi {
.put(ApiType.SkipTurn, SkipTurnAi.class)
.put(ApiType.StoreMap, StoreMapAi.class)
.put(ApiType.StoreSVar, StoreSVarAi.class)
+ .put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)
.put(ApiType.TapOrUntap, TapOrUntapAi.class)
diff --git a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java
index b71f07080f7..7eba97960da 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ActivateAbilityAi.java
@@ -12,6 +12,7 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
+import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@@ -93,7 +94,8 @@ public class ActivateAbilityAi extends SpellAbilityAi {
}
@Override
- public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells) {
+ public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells,
+ Map params) {
return spells.get(0);
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
index 100a46d40d1..0537a2cf21f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
@@ -23,12 +23,12 @@ import forge.game.staticability.StaticAbilityLayer;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType;
-import forge.util.collect.FCollectionView;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import forge.game.ability.effects.AnimateEffectBase;
/**
*
@@ -149,15 +149,15 @@ public class AnimateAi extends SpellAbilityAi {
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
int power = -5;
if (sa.hasParam("Power")) {
- power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
+ power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
}
int toughness = -5;
if (sa.hasParam("Toughness")) {
- toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
+ toughness = AbilityUtils.calculateAmount(c, sa.getParam("Toughness"), sa);
}
if (sa.hasParam("Keywords")) {
for (String keyword : sa.getParam("Keywords").split(" & ")) {
- if (!source.hasKeyword(keyword)) {
+ if (!c.hasKeyword(keyword)) {
bFlag = true;
}
}
@@ -188,7 +188,7 @@ public class AnimateAi extends SpellAbilityAi {
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
- if (!sa.getHostCard().isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(sa.getHostCard()))) {
+ if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
bFlag = true;
}
}
@@ -342,7 +342,7 @@ public class AnimateAi extends SpellAbilityAi {
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
- // that animate a target. Those can just use SVar:RemAIDeck:True until
+ // that animate a target. Those can just use AI:RemoveDeck:All until
// this can do a reasonably
// good job of picking a good target
return false;
@@ -363,11 +363,11 @@ public class AnimateAi extends SpellAbilityAi {
card.setSickness(hasOriginalCardSickness);
// AF specific sa
- int power = -1;
+ Integer power = null;
if (sa.hasParam("Power")) {
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
}
- int toughness = -1;
+ Integer toughness = null;
if (sa.hasParam("Toughness")) {
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
}
@@ -453,59 +453,7 @@ public class AnimateAi extends SpellAbilityAi {
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
}
- // duplicating AnimateEffectBase.doAnimate
- boolean removeSuperTypes = false;
- boolean removeCardTypes = false;
- boolean removeSubTypes = false;
- boolean removeCreatureTypes = false;
-
- if (sa.hasParam("OverwriteTypes")) {
- removeSuperTypes = true;
- removeCardTypes = true;
- removeSubTypes = true;
- removeCreatureTypes = true;
- }
-
- if (sa.hasParam("KeepSupertypes")) {
- removeSuperTypes = false;
- }
-
- if (sa.hasParam("KeepCardTypes")) {
- removeCardTypes = false;
- }
-
- if (sa.hasParam("RemoveSuperTypes")) {
- removeSuperTypes = true;
- }
-
- if (sa.hasParam("RemoveCardTypes")) {
- removeCardTypes = true;
- }
-
- if (sa.hasParam("RemoveSubTypes")) {
- removeSubTypes = true;
- }
-
- if (sa.hasParam("RemoveCreatureTypes")) {
- removeCreatureTypes = true;
- }
-
- if ((power != -1) || (toughness != -1)) {
- card.addNewPT(power, toughness, timestamp);
- }
-
- if (!types.isEmpty() || !removeTypes.isEmpty() || removeCreatureTypes) {
- card.addChangedCardTypes(types, removeTypes, removeSuperTypes, removeCardTypes, removeSubTypes,
- removeCreatureTypes, timestamp);
- }
-
- card.addChangedCardKeywords(keywords, removeKeywords, sa.hasParam("RemoveAllAbilities"), timestamp);
-
- for (final String k : hiddenKeywords) {
- card.addHiddenExtrinsicKeyword(k);
- }
-
- card.addColor(finalDesc, !sa.hasParam("OverwriteColors"), timestamp);
+ AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp);
// back to duplicating AnimateEffect.resolve
// TODO will all these abilities/triggers/replacements/etc. lead to
@@ -515,10 +463,14 @@ public class AnimateAi extends SpellAbilityAi {
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
boolean clearSpells = sa.hasParam("OverwriteSpells");
boolean removeAll = sa.hasParam("RemoveAllAbilities");
+ boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
if (clearAbilities || clearSpells || removeAll) {
for (final SpellAbility ab : card.getSpellAbilities()) {
- if (removeAll || (ab.isAbility() && clearAbilities) || (ab.isSpell() && clearSpells)) {
+ if (removeAll
+ || (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())
+ || (ab.isAbility() && clearAbilities)
+ || (ab.isSpell() && clearSpells)) {
card.removeSpellAbility(ab);
removedAbilities.add(ab);
}
@@ -559,9 +511,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress triggers from the animated card
final List removedTriggers = Lists.newArrayList();
- if (sa.hasParam("OverwriteTriggers") || removeAll) {
- final FCollectionView triggersToRemove = card.getTriggers();
- for (final Trigger trigger : triggersToRemove) {
+ if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
+ for (final Trigger trigger : card.getTriggers()) {
+ if (removeIntrinsic && !trigger.isIntrinsic()) {
+ continue;
+ }
trigger.setSuppressed(true);
removedTriggers.add(trigger);
}
@@ -597,9 +551,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress static abilities from the animated card
final List removedStatics = Lists.newArrayList();
- if (sa.hasParam("OverwriteStatics") || removeAll) {
- final FCollectionView staticsToRemove = card.getStaticAbilities();
- for (final StaticAbility stAb : staticsToRemove) {
+ if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
+ for (final StaticAbility stAb : card.getStaticAbilities()) {
+ if (removeIntrinsic && !stAb.isIntrinsic()) {
+ continue;
+ }
stAb.setTemporarilySuppressed(true);
removedStatics.add(stAb);
}
@@ -607,8 +563,11 @@ public class AnimateAi extends SpellAbilityAi {
// suppress static abilities from the animated card
final List removedReplacements = Lists.newArrayList();
- if (sa.hasParam("OverwriteReplacements") || removeAll) {
+ if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
for (final ReplacementEffect re : card.getReplacementEffects()) {
+ if (removeIntrinsic && !re.isIntrinsic()) {
+ continue;
+ }
re.setTemporarilySuppressed(true);
removedReplacements.add(re);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java b/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java
new file mode 100644
index 00000000000..691a21904ce
--- /dev/null
+++ b/forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java
@@ -0,0 +1,33 @@
+package forge.ai.ability;
+
+import java.util.List;
+import java.util.Map;
+
+import com.google.common.collect.Iterables;
+
+import forge.ai.SpellAbilityAi;
+import forge.game.player.Player;
+import forge.game.spellability.SpellAbility;
+
+public class AssignGroupAi extends SpellAbilityAi {
+
+ protected boolean canPlayAI(Player ai, SpellAbility sa) {
+ // TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
+ // otherwise the AI considers the card playable.
+
+ return true;
+ }
+
+ public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells, Map params) {
+ final String logic = sa.getParamOrDefault("AILogic", "");
+
+ if (logic.equals("FriendOrFoe")) {
+ if (params.containsKey("Affected") && spells.size() >= 2) {
+ Player t = (Player) params.get("Affected");
+ return spells.get(player.isOpponentOf(t) ? 1 : 0);
+ }
+ }
+
+ return Iterables.getFirst(spells, null);
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
index 2b71a67653e..ae728a26dc7 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
@@ -2,8 +2,12 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
import forge.ai.*;
+import forge.game.Game;
import forge.game.GameObject;
+import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -13,16 +17,19 @@ import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
+import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
+import forge.util.MyRandom;
import java.util.ArrayList;
import java.util.Iterator;
@@ -39,6 +46,12 @@ public class AttachAi extends SpellAbilityAi {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
+ // TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
+ // and gaining card advantage
+ if (source.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
+ return false;
+ }
+
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
@@ -49,6 +62,16 @@ public class AttachAi extends SpellAbilityAi {
}
}
+ if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)
+ && source.getType().isLegendary() && sa instanceof SpellPermanent
+ && ai.isCardInPlay(source.getName())) {
+ // Don't play the second copy of a legendary enchantment already in play
+
+ // TODO: Add some extra checks for where the AI may want to cast a replacement aura
+ // on another creature and keep it when the original enchanted creature is useless
+ return false;
+ }
+
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !"Curse".equals(sa.getParam("AILogic"))) {
return false;
@@ -68,6 +91,15 @@ public class AttachAi extends SpellAbilityAi {
}
}
+ // Flash logic
+ boolean advancedFlash = false;
+ if (ai.getController().isAI()) {
+ advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC);
+ }
+ if (source.withFlash(ai) && source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
+ return false;
+ }
+
if (abCost.getTotalMana().countX() > 0 && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. (Endless Scream and Venarian
// Gold)
@@ -88,20 +120,136 @@ public class AttachAi extends SpellAbilityAi {
final CardCollection targets = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof"));
+ return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
}
});
if (targets.isEmpty()) {
return false;
}
}
-
+
+ return true;
+ }
+
+ private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) {
+ Card source = sa.getHostCard();
+ Game game = ai.getGame();
+ Combat combat = game.getCombat();
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+
+ if (!aic.getBooleanProperty(AiProps.FLASH_USE_BUFF_AURAS_AS_COMBAT_TRICKS)) {
+ // Currently this only works with buff auras, so if the relevant toggle is disabled, just return true
+ // for instant speed use. To be improved later.
+ return true;
+ }
+
+ int power = 0, toughness = 0;
+ List keywords = Lists.newArrayList();
+ for (StaticAbility stAb : source.getStaticAbilities()) {
+ if ("Continuous".equals(stAb.getParam("Mode"))) {
+ if (stAb.hasParam("AddPower")) {
+ power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
+ }
+ if (stAb.hasParam("AddToughness")) {
+ toughness += AbilityUtils.calculateAmount(source, stAb.getParam("AddToughness"), stAb);
+ }
+ if (stAb.hasParam("AddKeyword")) {
+ keywords.addAll(Lists.newArrayList(stAb.getParam("AddKeyword").split(" & ")));
+ }
+ }
+ }
+
+ boolean isBuffAura = !sa.isCurse() && (power > 0 || toughness > 0 || !keywords.isEmpty());
+ if (!isBuffAura) {
+ // Currently only works with buff auras, otherwise returns true for instant speed use. To be improved later.
+ return true;
+ }
+
+ boolean canRespondToStack = false;
+ if (!game.getStack().isEmpty()) {
+ SpellAbility peekSa = game.getStack().peekAbility();
+ Player activator = peekSa.getActivatingPlayer();
+ if (activator != null && activator.isOpponentOf(ai)
+ && (!peekSa.usesTargeting() || peekSa.getTargets().getTargetCards().contains(attachTarget))) {
+ if (peekSa.getApi() == ApiType.DealDamage || peekSa.getApi() == ApiType.DamageAll) {
+ int dmg = AbilityUtils.calculateAmount(peekSa.getHostCard(), peekSa.getParam("NumDmg"), peekSa);
+ if (dmg < toughness + attachTarget.getNetToughness()) {
+ canRespondToStack = true;
+ }
+ } else if (peekSa.getApi() == ApiType.Destroy || peekSa.getApi() == ApiType.DestroyAll) {
+ if (!attachTarget.hasKeyword(Keyword.INDESTRUCTIBLE) && !ComputerUtil.canRegenerate(ai, attachTarget)
+ && keywords.contains("Indestructible")) {
+ canRespondToStack = true;
+ }
+ } else if (peekSa.getApi() == ApiType.Pump || peekSa.getApi() == ApiType.PumpAll) {
+ int p = AbilityUtils.calculateAmount(peekSa.getHostCard(), peekSa.getParam("NumAtt"), peekSa);
+ int t = AbilityUtils.calculateAmount(peekSa.getHostCard(), peekSa.getParam("NumDef"), peekSa);
+ if (t < 0 && toughness > 0 && attachTarget.getNetToughness() + t + toughness > 0) {
+ canRespondToStack = true;
+ } else if (p < 0 && power > 0 && attachTarget.getNetPower() + p + power > 0
+ && attachTarget.getNetToughness() + t + toughness > 0) {
+ // Yep, still need to ensure that the net toughness will be positive here even if buffing for power
+ canRespondToStack = true;
+ }
+ }
+ }
+ }
+
+ boolean canSurviveCombat = true;
+ if (combat != null && combat.isBlocked(attachTarget)) {
+ if (!attachTarget.hasKeyword(Keyword.INDESTRUCTIBLE) && !ComputerUtil.canRegenerate(ai, attachTarget)) {
+ boolean dangerous = false;
+ int totalAtkPower = 0;
+ for (Card attacker : combat.getBlockers(attachTarget)) {
+ if (attacker.hasKeyword(Keyword.DEATHTOUCH) || attacker.hasKeyword(Keyword.INFECT)
+ || attacker.hasKeyword(Keyword.WITHER)) {
+ dangerous = true;
+ }
+ totalAtkPower += attacker.getNetPower();
+ }
+ if (totalAtkPower > attachTarget.getNetToughness() + toughness || dangerous) {
+ canSurviveCombat = false;
+ }
+ }
+ }
+
+ if (!canSurviveCombat || (attachTarget.isCreature() && ComputerUtilCard.isUselessCreature(ai, attachTarget))) {
+ // don't buff anything that will die or get seriously crippled in combat, it's pointless anyway
+ return false;
+ }
+
+ int chanceToCastAtEOT = aic.getIntProperty(AiProps.FLASH_BUFF_AURA_CHANCE_CAST_AT_EOT);
+ int chanceToCastEarly = aic.getIntProperty(AiProps.FLASH_BUFF_AURA_CHANCE_TO_CAST_EARLY);
+ int chanceToRespondToStack = aic.getIntProperty(AiProps.FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK);
+
+ boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
+ boolean willDiscardNow = game.getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
+ && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
+ boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
+ boolean willRespondToStack = canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack);
+ boolean willCastEarly = MyRandom.percentTrue(chanceToCastEarly);
+ boolean willCastAtEOT = game.getPhaseHandler().is(PhaseType.END_OF_TURN)
+ && game.getPhaseHandler().getNextTurn().equals(ai) && MyRandom.percentTrue(chanceToCastAtEOT);
+
+ boolean alternativeConsiderations = hasFloatMana || willDiscardNow || willDieNow || willRespondToStack || willCastAtEOT || willCastEarly;
+
+ if (!alternativeConsiderations) {
+ if (combat == null ||
+ game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
+ return false;
+ }
+
+ if (!(combat.isAttacking(attachTarget) || combat.isBlocking(attachTarget))) {
+ return false;
+ }
+ }
+
return true;
}
/**
* Acceptable choice.
- *
+ *
* @param c
* the c
* @param mandatory
@@ -126,7 +274,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Choose unpreferred.
- *
+ *
* @param mandatory
* the mandatory
* @param list
@@ -143,7 +291,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Choose less preferred.
- *
+ *
* @param mandatory
* the mandatory
* @param list
@@ -160,7 +308,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai change type preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -191,7 +339,7 @@ public class AttachAi extends SpellAbilityAi {
}
list = CardLists.getNotType(list, type); // Filter out Basic Lands that have the same type as the changing type
-
+
// Don't target fetchlands
list = CardLists.filter(list, new Predicate() {
@Override
@@ -232,7 +380,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai keep tapped preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -249,7 +397,7 @@ public class AttachAi extends SpellAbilityAi {
@Override
public boolean apply(final Card c) {
// Don't do Untapped Vigilance cards
- if (c.isCreature() && c.hasKeyword("Vigilance") && c.isUntapped()) {
+ if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
return false;
}
@@ -269,7 +417,7 @@ public class AttachAi extends SpellAbilityAi {
return true;
}
- final Iterable auras = c.getEnchantedBy(false);
+ final Iterable auras = c.getEnchantedBy();
final Iterator itr = auras.iterator();
while (itr.hasNext()) {
final Card aura = itr.next();
@@ -360,7 +508,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai control preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -391,7 +539,7 @@ public class AttachAi extends SpellAbilityAi {
List evenBetterList = CardLists.filter(betterList, new Predicate() {
@Override
public boolean apply(final Card c) {
- return c.hasKeyword("Indestructible") || c.hasKeyword("Hexproof");
+ return c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF);
}
});
if (!evenBetterList.isEmpty()) {
@@ -431,7 +579,7 @@ public class AttachAi extends SpellAbilityAi {
}
c = ComputerUtilCard.getWorstAI(betterList);
}
-
+
// If Mandatory (brought directly into play without casting) gotta
// choose something
@@ -444,7 +592,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai reanimate preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -458,8 +606,40 @@ public class AttachAi extends SpellAbilityAi {
private static Card attachAIReanimatePreference(final SpellAbility sa, final List list, final boolean mandatory,
final Card attachSource) {
// AI For choosing a Card to Animate.
- // TODO Add some more restrictions for Reanimation Auras
- final Card c = ComputerUtilCard.getBestCreatureAI(list);
+ final Player ai = sa.getActivatingPlayer();
+ final Card attachSourceLki = CardUtil.getLKICopy(attachSource);
+ attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
+ // Suppress original attach Spell to replace it with another
+ attachSourceLki.getFirstAttachSpell().setSuppressed(true);
+
+ //TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
+ attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
+ List betterList = CardLists.filter(list, new Predicate() {
+ @Override
+ public boolean apply(final Card c) {
+ final Card lki = CardUtil.getLKICopy(c);
+ // need to fake it as if lki would be on the battlefield
+ lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
+
+ // Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
+ attachSourceLki.clearRemembered();
+ attachSourceLki.addRemembered(lki);
+
+ // need to check what the cards would be on the battlefield
+ // do not attach yet, that would cause Events
+ CardCollection preList = new CardCollection(lki);
+ preList.add(attachSourceLki);
+ c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
+ boolean result = lki.canBeAttached(attachSourceLki);
+
+ //reset static abilities
+ c.getGame().getAction().checkStaticAbilities(false);
+
+ return result;
+ }
+ });
+
+ final Card c = ComputerUtilCard.getBestCreatureAI(betterList);
// If Mandatory (brought directly into play without casting) gotta
// choose something
@@ -478,23 +658,23 @@ public class AttachAi extends SpellAbilityAi {
for (Card card : list) {
int cardPriority = 0;
// Prefer Evasion
- if (card.hasKeyword("Trample")) {
+ if (card.hasKeyword(Keyword.TRAMPLE)) {
cardPriority += 10;
}
- if (card.hasKeyword("Menace")) {
+ if (card.hasKeyword(Keyword.MENACE)) {
cardPriority += 10;
}
// Avoid this for Sleepers Robe?
- if (card.hasKeyword("Fear")) {
+ if (card.hasKeyword(Keyword.FEAR)) {
cardPriority += 15;
}
- if (card.hasKeyword("Flying")) {
+ if (card.hasKeyword(Keyword.FLYING)) {
cardPriority += 20;
}
- if (card.hasKeyword("Shadow")) {
+ if (card.hasKeyword(Keyword.SHADOW)) {
cardPriority += 30;
}
- if (card.hasKeyword("Horsemanship")) {
+ if (card.hasKeyword(Keyword.HORSEMANSHIP)) {
cardPriority += 40;
}
if (card.hasKeyword("Unblockable")) {
@@ -503,7 +683,7 @@ public class AttachAi extends SpellAbilityAi {
// Prefer "tap to deal damage"
// TODO : Skip this one if triggers on combat damage only?
for (SpellAbility sa2 : card.getSpellAbilities()) {
- if ((sa2.getApi().equals(ApiType.DealDamage))
+ if (ApiType.DealDamage.equals(sa2.getApi())
&& (sa2.getTargetRestrictions().canTgtPlayer())) {
cardPriority += 300;
}
@@ -514,10 +694,10 @@ public class AttachAi extends SpellAbilityAi {
if (card.getCurrentPower() <= 0) {
cardPriority = -100;
}
- if (card.hasKeyword("Defender")) {
+ if (card.hasKeyword(Keyword.DEFENDER)) {
cardPriority = -100;
}
- if (card.hasKeyword("Indestructible")) {
+ if (card.hasKeyword(Keyword.INDESTRUCTIBLE)) {
cardPriority += 15;
}
if (cardPriority > priority) {
@@ -531,7 +711,7 @@ public class AttachAi extends SpellAbilityAi {
}
/**
* Attach ai specific card preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -548,7 +728,7 @@ public class AttachAi extends SpellAbilityAi {
final Player ai = sa.getActivatingPlayer();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
Card chosen = null;
-
+
if ("Guilty Conscience".equals(sourceName)) {
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
} else if ("Bonds of Faith".equals(sourceName)) {
@@ -607,7 +787,7 @@ public class AttachAi extends SpellAbilityAi {
// Should generalize this code a bit since they all have similar structures
/**
* Attach ai control preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -643,7 +823,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai highest evaluated preference.
- *
+ *
* @param list the initial valid list
* @return the card
*/
@@ -653,7 +833,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai curse preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -719,7 +899,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- if (!c.hasKeyword("Indestructible") && (c.getLethalDamage() <= Math.abs(tgh))) {
+ if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && (c.getLethalDamage() <= Math.abs(tgh))) {
return true;
}
@@ -777,7 +957,7 @@ public class AttachAi extends SpellAbilityAi {
* the sa
* @param mandatory
* the mandatory
- *
+ *
* @return true, if successful
*/
@Override
@@ -847,7 +1027,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach ai pump preference.
- *
+ *
* @param sa
* the sa
* @param list
@@ -898,8 +1078,36 @@ public class AttachAi extends SpellAbilityAi {
});
}
+ // Look for triggers that will damage the creature and remove AI-owned creatures that will die
+ CardCollection toRemove = new CardCollection();
+ for (Trigger t : attachSource.getTriggers()) {
+ if (t.getMode() == TriggerType.ChangesZone) {
+ final Map params = t.getMapParams();
+ if ("Card.Self".equals(params.get("ValidCard"))
+ && "Battlefield".equals(params.get("Destination"))) {
+ SpellAbility trigSa = null;
+ if (t.hasParam("Execute") && attachSource.hasSVar(t.getParam("Execute"))) {
+ trigSa = AbilityFactory.getAbility(attachSource.getSVar(params.get("Execute")), attachSource);
+ } else if (t.getOverridingAbility() != null) {
+ trigSa = t.getOverridingAbility();
+ }
+ if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
+ for (Card target : list) {
+ if (!target.getController().isOpponentOf(ai)) {
+ int numDmg = AbilityUtils.calculateAmount(target, trigSa.getParam("NumDmg"), trigSa);
+ if (target.getNetToughness() - target.getDamage() <= numDmg && !target.hasKeyword(Keyword.INDESTRUCTIBLE)) {
+ toRemove.add(target);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ list.removeAll(toRemove);
+
if (magnetList != null) {
-
+
// Look for Heroic triggers
if (magnetList.isEmpty() && sa.isSpell()) {
for (Card target : list) {
@@ -914,7 +1122,7 @@ public class AttachAi extends SpellAbilityAi {
}
}
}
-
+
if (!magnetList.isEmpty()) {
// Always choose something from the Magnet List.
// Probably want to "weight" the list by amount of Enchantments and
@@ -964,8 +1172,8 @@ public class AttachAi extends SpellAbilityAi {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
- totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
- totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
+ totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
+ totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");
@@ -985,6 +1193,10 @@ public class AttachAi extends SpellAbilityAi {
}
CardCollection prefList = new CardCollection(list);
+
+ // Filter AI-specific targets if provided
+ prefList = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
+
if (totToughness < 0) {
// Don't kill my own stuff with Negative toughness Auras
final int tgh = totToughness;
@@ -1006,7 +1218,7 @@ public class AttachAi extends SpellAbilityAi {
if (isUsefulAttachKeyword(keyword, c, sa, pow)) {
return true;
}
- if (c.hasKeyword("Infect") && pow >= 2) {
+ if (c.hasKeyword(Keyword.INFECT) && pow >= 2) {
// consider +2 power a significant bonus on Infect creatures
return true;
}
@@ -1034,7 +1246,7 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, new Predicate() {
@Override
public boolean apply(final Card c) {
- return !c.isEnchanted() || c.hasKeyword("Hexproof");
+ return !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF);
}
});
}
@@ -1085,7 +1297,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach to card ai preferences.
- *
+ *
* @param sa
* the sa
* @param sa
@@ -1104,8 +1316,15 @@ public class AttachAi extends SpellAbilityAi {
if (attachSource.hasSVar("DontEquip")) {
return null;
}
+
+ // is no attachment so no using attach
+ if (!attachSource.isAttachment()) {
+ return null;
+ }
+
// Don't fortify if already fortifying
- if (attachSource.getFortifying() != null && attachSource.getFortifying().getController() == aiPlayer) {
+ if (attachSource.isFortification() && attachSource.getAttachedTo() != null
+ && attachSource.getAttachedTo().getController() == aiPlayer) {
return null;
}
@@ -1115,11 +1334,7 @@ public class AttachAi extends SpellAbilityAi {
} else {
list = CardLists.getValidCards(aiPlayer.getGame().getCardsIn(tgt.getZone()), tgt.getValidTgts(), sa.getActivatingPlayer(), attachSource, sa);
- if (attachSource.isAura()) {
- list = CardLists.filter(list, CardPredicates.canBeEnchantedBy(attachSource));
- } else if (attachSource.isEquipment()) {
- list = CardLists.filter(list, CardPredicates.canBeEquippedBy(attachSource));
- }
+ list = CardLists.filter(list, CardPredicates.canBeAttached(attachSource));
// TODO If Attaching without casting, don't need to actually target.
// I believe this is the only case where mandatory will be true, so just
@@ -1143,7 +1358,7 @@ public class AttachAi extends SpellAbilityAi {
Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic"));
AiController aic = ((PlayerControllerAi)aiPlayer.getController()).getAi();
- if (c != null && attachSource.isEquipment()
+ if (c != null && attachSource.isEquipment()
&& attachSource.isEquipping()
&& attachSource.getEquipping().getController() == aiPlayer) {
if (c.equals(attachSource.getEquipping())) {
@@ -1167,7 +1382,7 @@ public class AttachAi extends SpellAbilityAi {
return null;
}
}
-
+
// make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around
boolean decideMoveFromUseless = uselessCreature && aic.getBooleanProperty(AiProps.PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS);
@@ -1181,7 +1396,7 @@ public class AttachAi extends SpellAbilityAi {
// avoid randomly moving the equipment back and forth between several creatures in one turn
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN)) {
return null;
- }
+ }
// do not equip if the new creature is not significantly better than the previous one (evaluates at least better by evalT)
int evalT = aic.getIntProperty(AiProps.MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD);
@@ -1189,7 +1404,7 @@ public class AttachAi extends SpellAbilityAi {
return null;
}
}
-
+
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ATTACHED_THIS_TURN);
if (c == null && mandatory) {
@@ -1201,7 +1416,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Attach general ai.
- *
+ *
* @param sa
* the sa
* @param list
@@ -1277,7 +1492,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Contains useful curse keyword.
- *
+ *
* @param keywords
* the keywords
* @param card
@@ -1296,7 +1511,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Checks if is useful keyword.
- *
+ *
* @param keyword
* the keyword
* @param card
@@ -1307,10 +1522,26 @@ public class AttachAi extends SpellAbilityAi {
private static boolean isUsefulAttachKeyword(final String keyword, final Card card, final SpellAbility sa, final int powerBonus) {
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
-
+
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
return false;
}
+
+ // Don't play if would choose a color the target is already protected from
+ if (sa.getHostCard().hasSVar("ChosenProtection")) {
+ CardCollectionView oppAllCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
+ String cc = ComputerUtilCard.getMostProminentColor(oppAllCards);
+ if (card.hasKeyword("Protection from " + cc.toLowerCase())) {
+ return false;
+ }
+ // Also don't play if it would destroy own Aura
+ for (Card c : card.getEnchantedBy()) {
+ if ((c.getController().equals(ai)) && (c.isOfColor(cc))) {
+ return false;
+ }
+ }
+ }
+
final boolean evasive = (keyword.equals("Unblockable") || keyword.equals("Fear")
|| keyword.equals("Intimidate") || keyword.equals("Shadow")
|| keyword.equals("Flying") || keyword.equals("Horsemanship")
@@ -1353,7 +1584,7 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("First Strike")) {
- if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword("Double Strike")
+ if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword(Keyword.DOUBLE_STRIKE)
|| (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) {
return false;
}
@@ -1386,7 +1617,7 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("Reach")) {
- if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) {
+ if (card.hasKeyword(Keyword.FLYING) || !CombatUtil.canBlock(card, true)) {
return false;
}
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
@@ -1395,11 +1626,11 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
- if (!card.hasKeyword("Defender") || card.getNetCombatDamage() + powerBonus <= 0) {
+ if (!card.hasKeyword(Keyword.DEFENDER) || card.getNetCombatDamage() + powerBonus <= 0) {
return false;
}
} else if (keyword.equals("Shroud") || keyword.equals("Hexproof")) {
- if (card.hasKeyword("Shroud") || card.hasKeyword("Hexproof")) {
+ if (card.hasKeyword(Keyword.SHROUD) || card.hasKeyword(Keyword.HEXPROOF)) {
return false;
}
} else if (keyword.equals("Defender")) {
@@ -1410,7 +1641,7 @@ public class AttachAi extends SpellAbilityAi {
/**
* Checks if is useful curse keyword.
- *
+ *
* @param keyword
* the keyword
* @param card
@@ -1463,15 +1694,15 @@ public class AttachAi extends SpellAbilityAi {
/**
* Checks if it is useful to execute the attach action given the current context.
- *
+ *
* @param c
* the card
* @param sa SpellAbility
- * @return true, if the action is useful (beneficial) in the current minimal context (Card vs. Attach SpellAbility)
+ * @return true, if the action is useful (beneficial) in the current minimal context (Card vs. Attach SpellAbility)
*/
private static boolean isUsefulAttachAction(Player ai, Card c, SpellAbility sa) {
if (c == null) {
- return false;
+ return false;
}
if (sa.getHostCard() == null) {
// FIXME: Not sure what should the resolution be if a SpellAbility has no host card. This should
@@ -1529,12 +1760,12 @@ public class AttachAi extends SpellAbilityAi {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
-
+
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer) {
return attachToCardAIPreferences(ai, sa, true);
}
-
+
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) {
return attachToPlayerAIPreferences(ai, sa, true);
diff --git a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java
index 2d42c358ece..f3c44f1caa0 100644
--- a/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/BecomesBlockedAi.java
@@ -7,6 +7,7 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -30,7 +31,7 @@ public class BecomesBlockedAi extends SpellAbilityAi {
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.getTargetableCards(list, sa);
- list = CardLists.getNotKeyword(list, "Trample");
+ list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) {
Card choice = null;
diff --git a/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java b/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java
index 0b4cf77f7c9..96876913dd9 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java
@@ -6,6 +6,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import java.util.List;
+import java.util.Map;
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
@@ -37,7 +38,8 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
@Override
- public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells) {
+ public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells,
+ Map params) {
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
return spells.get(0);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
index 2eb6f30a6f2..e5c52128bef 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
@@ -152,12 +152,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
return doReturnCommanderLogic(sa, aiPlayer);
}
- if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
+ if ("Always".equals(sa.getParam("AILogic"))) {
+ return true;
+ } else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return true; // debuffed by opponent's auras to the level that it becomes useless
}
int delta = 0;
- for (Card enc : sa.getHostCard().getEnchantedBy(false)) {
+ for (Card enc : sa.getHostCard().getEnchantedBy()) {
if (enc.getController().isOpponentOf(aiPlayer)) {
delta--;
} else {
@@ -249,6 +251,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return false;
}
+
+ if (ai.getGame().getCombat() == null) {
+ return false;
+ }
List attackers = ai.getGame().getCombat().getUnblockedAttackers();
boolean lowerCMC = false;
for (Card attacker : attackers) {
@@ -955,7 +961,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- for (Card aura : c.getEnchantedBy(false)) {
+ for (Card aura : c.getEnchantedBy()) {
if (aura.getController().isOpponentOf(ai)) {
return true;
} else {
@@ -1048,7 +1054,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- for (Card aura : c.getEnchantedBy(false)) {
+ for (Card aura : c.getEnchantedBy()) {
if (c.getOwner().isOpponentOf(ai) && aura.getController().equals(ai)) {
return false;
}
@@ -1457,7 +1463,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
fetchList = CardLists.filter(fetchList, new Predicate() {
@Override
public boolean apply(final Card c) {
- if (c.hasSVar("RemAIDeck") || c.hasSVar("RemRandomDeck")) {
+ if (ComputerUtilCard.isCardRemAIDeck(c) || ComputerUtilCard.isCardRemRandomDeck(c)) {
return false;
}
return true;
@@ -1727,7 +1733,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
Map originalParams = (Map)sa.getReplacingObject("OriginalParams");
SpellAbility causeSa = (SpellAbility)originalParams.get("Cause");
SpellAbility causeSub = null;
-
+
+ // Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
+ if (sa.getHostCard().getName().contains("Squee, the Immortal")) {
+ return false;
+ }
+
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
ApiType subApi = causeSub.getApi();
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java
index ad5de0b33b1..87f87ccf8a8 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java
@@ -114,7 +114,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// spBounceAll has some AI we can compare to.
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (!sa.usesTargeting()) {
- // TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar)
+ // TODO: improve logic for non-targeted SAs of this type (most are currently AI:RemoveDeck:All, e.g. Memory Jar)
return true;
} else {
// search targetable Opponents
@@ -220,6 +220,9 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} else {
return false;
}
+ } else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) {
+ return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
+ && !ComputerUtil.isPlayingReanimator(ai);
}
} else if (origin.equals(ZoneType.Exile)) {
String logic = sa.getParam("AILogic");
@@ -344,8 +347,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
- // Profaner from exile without paying its mana cost. Otherwise the card is marked RemAIDeck and there is no
- // specific AI to support playing it in a smarter way. Feel free to expand.
+ // Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
+ // there is no specific AI to support playing it in a smarter way. Feel free to expand.
return !CardLists.filter(ai.getOpponents().getCardsIn(origin), CardPredicates.Presets.CREATURES).isEmpty();
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java
index 8278642c9c3..db861634ebe 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardAi.java
@@ -4,6 +4,7 @@ import java.util.Collections;
import java.util.List;
import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
@@ -22,6 +23,7 @@ import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
@@ -126,8 +128,8 @@ public class ChooseCardAi extends SpellAbilityAi {
} else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
- aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
- oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
+ aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
+ oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
// Use it as a wrath, when the human creatures threat the ai's life
if (aiCreatures.isEmpty() && ComputerUtilCombat.sumDamageIfUnblocked(oppCreatures, ai) >= ai.getLife()) {
@@ -261,7 +263,7 @@ public class ChooseCardAi extends SpellAbilityAi {
}
} else if (logic.equals("Duneblast")) {
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
- aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
+ aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
if (aiCreatures.isEmpty()) {
return null;
@@ -273,6 +275,22 @@ public class ChooseCardAi extends SpellAbilityAi {
if (ai.equals(sa.getActivatingPlayer())) {
choice = ComputerUtilCard.getBestAI(options);
} // TODO: improve ai
+ } else if (logic.equals("Phylactery")) {
+ CardCollection aiArtifacts = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.ARTIFACTS);
+ CardCollection indestructibles = CardLists.filter(aiArtifacts, CardPredicates.hasKeyword(Keyword.INDESTRUCTIBLE));
+ CardCollection nonCreatures = CardLists.filter(aiArtifacts, Predicates.not(Presets.CREATURES));
+ CardCollection creatures = CardLists.filter(aiArtifacts, Presets.CREATURES);
+ if (!indestructibles.isEmpty()) {
+ // Choose the worst (smallest) indestructible artifact so that the opponent would have to waste
+ // removal on something unpreferred
+ choice = ComputerUtilCard.getWorstAI(indestructibles);
+ } else if (!nonCreatures.isEmpty()) {
+ // The same as above, but for non-indestructible non-creature artifacts (they can't die in combat)
+ choice = ComputerUtilCard.getWorstAI(nonCreatures);
+ } else if (!creatures.isEmpty()) {
+ // Choose the best (hopefully the fattest, whatever) creature so that hopefully it won't die too easily
+ choice = ComputerUtilCard.getBestAI(creatures);
+ }
} else {
choice = ComputerUtilCard.getBestAI(options);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java
index 8e5f756af3c..3480f561d70 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseDirectionAi.java
@@ -1,8 +1,16 @@
package forge.ai.ability;
import forge.ai.SpellAbilityAi;
+import forge.game.Direction;
+import forge.game.Game;
+import forge.game.card.CardCollection;
+import forge.game.card.CardLists;
+import forge.game.card.CardPredicates;
+import forge.game.card.CardPredicates.Presets;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
+import forge.game.zone.ZoneType;
+import forge.util.Aggregates;
public class ChooseDirectionAi extends SpellAbilityAi {
@@ -12,10 +20,23 @@ public class ChooseDirectionAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final String logic = sa.getParam("AILogic");
+ final Game game = sa.getActivatingPlayer().getGame();
if (logic == null) {
return false;
} else {
- // TODO: default ai
+ if ("Aminatou".equals(logic)) {
+ CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.NONLAND_PERMANENTS);
+ CardCollection aiPermanent = CardLists.filterControlledBy(all, ai);
+ aiPermanent.remove(sa.getHostCard());
+ int aiValue = Aggregates.sum(aiPermanent, CardPredicates.Accessors.fnGetCmc);
+ CardCollection left = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Left));
+ CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right));
+ int leftValue = Aggregates.sum(left, CardPredicates.Accessors.fnGetCmc);
+ int rightValue = Aggregates.sum(right, CardPredicates.Accessors.fnGetCmc);
+ if (aiValue > leftValue || aiValue > rightValue) {
+ return false;
+ }
+ }
}
return true;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
index 05ba5338e55..39541df0f28 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
@@ -1,10 +1,13 @@
package forge.ai.ability;
import java.util.List;
+import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
@@ -23,6 +26,8 @@ import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
+import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -37,7 +42,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
return true;
- } else if (aiLogic.startsWith("Fabricate")) {
+ } else if (aiLogic.startsWith("Fabricate") || "Riot".equals(aiLogic)) {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
@@ -79,7 +84,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
}
@Override
- public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells) {
+ public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells,
+ Map params) {
Card host = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = host.getGame();
@@ -342,7 +348,56 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
if (!filtered.isEmpty()) {
return filtered.get(0);
}
+ } else if ("Riot".equals(logic)) {
+ SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
+ return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
}
return spells.get(0); // return first choice if no logic found
}
-}
\ No newline at end of file
+
+ public static boolean preferHasteForRiot(SpellAbility sa, Player player) {
+ // returning true means preferring Haste, returning false means preferring a +1/+1 counter
+ final Card host = sa.getHostCard();
+ final Game game = host.getGame();
+ final Card copy = CardUtil.getLKICopy(host);
+ copy.setLastKnownZone(player.getZone(ZoneType.Battlefield));
+
+ // check state it would have on the battlefield
+ CardCollection preList = new CardCollection(copy);
+ game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
+ // reset again?
+ game.getAction().checkStaticAbilities(false);
+
+ // can't gain counters, use Haste
+ if (!copy.canReceiveCounters(CounterType.P1P1)) {
+ return true;
+ }
+
+ // already has Haste, use counter
+ if (copy.hasKeyword(Keyword.HASTE)) {
+ return false;
+ }
+
+ // not AI turn
+ if (!game.getPhaseHandler().isPlayerTurn(player)) {
+ return false;
+ }
+
+ // not before Combat
+ if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
+ return false;
+ }
+
+ // TODO check other opponents too if able
+ final Player opp = player.getWeakestOpponent();
+ if (opp != null) {
+ // TODO add predict Combat Damage?
+ if (opp.getLife() < copy.getNetPower()) {
+ return true;
+ }
+ }
+
+ // haste might not be good enough?
+ return false;
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java
index 8479bde2c85..6d2618ee7f0 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CloneAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CloneAi.java
@@ -142,14 +142,17 @@ public class CloneAi extends SpellAbilityAi {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
+ } else if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {
+ CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getGame().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
+ sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
+ return true;
}
// Default:
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
- // two are the only things
- // that clone a target. Those can just use SVar:RemAIDeck:True until
- // this can do a reasonably
- // good job of picking a good target
+ // two are the only things that clone a target. Those can just use
+ // AI:RemoveDeck:All until this can do a reasonably good job of picking
+ // a good target
return false;
}
@@ -158,7 +161,18 @@ public class CloneAi extends SpellAbilityAi {
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
- // Didn't confirm in the original code
+ if (sa.hasParam("AILogic") && (!sa.usesTargeting() || sa.isTargetNumberValid())) {
+ // Had a special logic for it and managed to target, so confirm if viable
+ if ("CloneBestCreature".equals(sa.getParam("AILogic"))) {
+ return ComputerUtilCard.evaluateCreature(sa.getTargets().getFirstTargetedCard()) > ComputerUtilCard.evaluateCreature(sa.getHostCard());
+ } else if ("IfDefinedCreatureIsBetter".equals(sa.getParam("AILogic"))) {
+ List defined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
+ Card bestDefined = ComputerUtilCard.getBestCreatureAI(defined);
+ return ComputerUtilCard.evaluateCreature(bestDefined) > ComputerUtilCard.evaluateCreature(sa.getHostCard());
+ }
+ }
+
+ // Currently doesn't confirm anything that's not defined by AI logic
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java
index 8f38b905c4c..ab3c3465127 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ControlExchangeAi.java
@@ -36,7 +36,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- return !c.hasSVar("RemAIDeck") && c.canBeTargetedBy(sa);
+ return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
}
});
object1 = ComputerUtilCard.getBestAI(list);
diff --git a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java
index 50bdc533844..eb6e3ed5d9b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ControlGainAi.java
@@ -171,7 +171,7 @@ public class ControlGainAi extends SpellAbilityAi {
}
// do not take control on something it doesn't know how to use
- return !c.hasSVar("RemAIDeck");
+ return !ComputerUtilCard.isCardRemAIDeck(c);
}
});
diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
index a630836d38e..f543b662b3e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
@@ -83,7 +83,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
CardCollection list = new CardCollection(CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa));
- list = CardLists.filter(list, Predicates.not(CardPredicates.hasSVar("RemAIDeck")));
+ list = CardLists.filter(list, Predicates.not(CardPredicates.isRemAIDeck()));
//Nothing to target
if (list.isEmpty()) {
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java
index 4471074c3f5..d130c18a666 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java
@@ -1,42 +1,133 @@
package forge.ai.ability;
-import forge.ai.SpecialCardAi;
-import forge.ai.SpellAbilityAi;
+import forge.ai.*;
+import forge.game.Game;
+import forge.game.ability.ApiType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
+import forge.game.spellability.AbilityActivated;
+import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
+import forge.game.spellability.TargetRestrictions;
+import forge.util.MyRandom;
import java.util.List;
+import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
- // the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
- return sa.isMandatory() || "Always".equals(sa.getParam("AILogic"));
+ Game game = aiPlayer.getGame();
+ int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
+ int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
+ String logic = sa.getParamOrDefault("AILogic", "");
+
+ if (game.getStack().isEmpty()) {
+ return sa.isMandatory();
+ }
+
+ final SpellAbility top = game.getStack().peekAbility();
+ if (top != null
+ && top.getPayCosts() != null && top.getPayCosts().getCostMana() != null
+ && sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null
+ && top.getPayCosts().getCostMana().getMana().getCMC() >= sa.getPayCosts().getCostMana().getMana().getCMC() + diff) {
+ // The copied spell has a significantly higher CMC than the copy spell, consider copying
+ chance = 100;
+ }
+
+ if (top.getActivatingPlayer().isOpponentOf(aiPlayer)) {
+ chance = 100; // currently the AI will always copy the opponent's spell if viable
+ }
+
+ if (!MyRandom.percentTrue(chance)
+ && !"AlwaysIfViable".equals(logic)
+ && !"OnceIfViable".equals(logic)
+ && !"AlwaysCopyActivatedAbilities".equals(logic)) {
+ return false;
+ }
+
+ if ("OnceIfViable".equals(logic)) {
+ if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
+ return false;
+ }
+ }
+
+ final TargetRestrictions tgt = sa.getTargetRestrictions();
+ if (tgt != null) {
+
+ // Filter AI-specific targets if provided
+ if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
+ if (!top.getActivatingPlayer().equals(aiPlayer)) {
+ return false;
+ }
+ }
+
+ if (top.isWrapper() || !(top instanceof SpellAbility || top instanceof AbilityActivated)) {
+ // Shouldn't even try with triggered or wrapped abilities at this time, will crash
+ return false;
+ } else if (top.getApi() == ApiType.CopySpellAbility) {
+ // Don't try to copy a copy ability, too complex for the AI to handle
+ return false;
+ } else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
+ if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
+ // If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
+ // it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
+ return false;
+ }
+ } else if (top.hasParam("ConditionManaSpent")) {
+ // Mana spent is not copied, so these spells generally do nothing when copied.
+ return false;
+ } else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
+ // Don't try to copy anything you can't understand how to handle
+ return false;
+ }
+
+ // A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle.
+ final SpellAbility topCopy = top.copy(aiPlayer);
+ topCopy.resetTargets();
+
+ if (top.canBeTargetedBy(sa)) {
+ AiPlayDecision decision = AiPlayDecision.CantPlaySa;
+ if (top instanceof Spell) {
+ decision = ((PlayerControllerAi) aiPlayer.getController()).getAi().canPlayFromEffectAI((Spell) topCopy, true, true);
+ } else if (top instanceof AbilityActivated && top.getActivatingPlayer().equals(aiPlayer)
+ && logic.contains("CopyActivatedAbilities")) {
+ decision = AiPlayDecision.WillPlay; // FIXME: we activated it once, why not again? Or bad idea?
+ }
+ if (decision == AiPlayDecision.WillPlay) {
+ sa.getTargets().add(top);
+ AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
+ return true;
+ }
+ }
+ }
+
+ // the AI should not miss mandatory activations
+ return sa.isMandatory() || "Always".equals(logic);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
- return mandatory || "Always".equals(sa.getParam("AILogic"));
+ String logic = sa.getParamOrDefault("AILogic", "");
+ return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
- // NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
- // generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
- return super.chkAIDrawback(sa, aiPlayer);
+ return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
}
@Override
- public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells) {
+ public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells,
+ Map params) {
return spells.get(0);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
index 33088e22ac4..41edaa0d49a 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
@@ -105,7 +105,7 @@ public class CounterAi extends SpellAbilityAi {
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
- toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
+ toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java
index 8dd6367db26..2535c29505d 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersAi.java
@@ -27,6 +27,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
+import forge.game.keyword.Keyword;
import forge.util.Aggregates;
@@ -59,7 +60,7 @@ public abstract class CountersAi {
if (type.equals("M1M1")) {
// try to kill the best killable creature, or reduce the best one
// but try not to target a Undying Creature
- final List killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), "Undying");
+ final List killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
if (killable.size() > 0) {
choice = ComputerUtilCard.getBestCreatureAI(killable);
} else {
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java
index 42e66c01d0a..34827a4a273 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java
@@ -8,6 +8,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -191,7 +192,7 @@ public class CountersMoveAi extends SpellAbilityAi {
}
// check for some specific AI preferences
- if (src.hasStartOfKeyword("Graft") && "DontMoveCounterIfLethal".equals(src.getSVar("AIGraftPreference"))) {
+ if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
return false;
}
@@ -286,7 +287,7 @@ public class CountersMoveAi extends SpellAbilityAi {
// do not steal a P1P1 from Undying if it would die
// this way
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
- if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") || card.isToken()) {
+ if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) {
return true;
}
return false;
@@ -332,11 +333,12 @@ public class CountersMoveAi extends SpellAbilityAi {
// try to remove P1P1 from undying or evolve
if (CounterType.P1P1.equals(cType)) {
- if (card.hasKeyword("Undying") || card.hasKeyword("Evolve")) {
+ if (card.hasKeyword(Keyword.UNDYING) || card.hasKeyword(Keyword.EVOLVE)
+ || card.hasKeyword(Keyword.ADAPT)) {
return true;
}
}
- if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
+ if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return true;
}
@@ -391,10 +393,10 @@ public class CountersMoveAi extends SpellAbilityAi {
}
if (cType != null) {
- if (CounterType.P1P1.equals(cType) && card.hasKeyword("Undying")) {
+ if (CounterType.P1P1.equals(cType) && card.hasKeyword(Keyword.UNDYING)) {
return false;
}
- if (CounterType.M1M1.equals(cType) && card.hasKeyword("Persist")) {
+ if (CounterType.M1M1.equals(cType) && card.hasKeyword(Keyword.PERSIST)) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
index 3c38cfd40fa..c95502f6846 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
@@ -1,6 +1,7 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.*;
@@ -10,11 +11,10 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.*;
+import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
-import forge.game.cost.Cost;
-import forge.game.cost.CostPart;
-import forge.game.cost.CostRemoveCounter;
-import forge.game.cost.CostSacrifice;
+import forge.game.cost.*;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -44,17 +44,19 @@ public class CountersPutAi extends SpellAbilityAi {
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
final String type = sa.getParam("CounterType");
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
+
// TODO Auto-generated method stub
if (!super.willPayCosts(ai, sa, cost, source)) {
return false;
}
- // disable moving counters
+ // disable moving counters (unless a specialized AI logic supports it)
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
final CounterType counterType = remCounter.counter;
- if (counterType.name().equals(type)) {
+ if (counterType.name().equals(type) && !aiLogic.startsWith("MoveCounter")) {
return false;
}
if (!part.payCostFromSource()) {
@@ -100,7 +102,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (sa.hasParam("LevelUp")) {
// creatures enchanted by curse auras have low priority
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
- for (Card aura : source.getEnchantedBy(false)) {
+ for (Card aura : source.getEnchantedBy()) {
if (aura.getController().isOpponentOf(ai)) {
return false;
}
@@ -123,8 +125,10 @@ public class CountersPutAi extends SpellAbilityAi {
CardCollection list;
Card choice = null;
final String type = sa.getParam("CounterType");
- final String amountStr = sa.getParam("CounterNum");
+ final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
+ final String logic = sa.getParamOrDefault("AILogic", "");
+ PhaseHandler ph = ai.getGame().getPhaseHandler();
final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined"))
&& "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X"))
@@ -154,7 +158,7 @@ public class CountersPutAi extends SpellAbilityAi {
// receive counters, execpt it has undying
CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1));
- oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, "Undying");
+ oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate() {
@Override
@@ -214,15 +218,11 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
- if ("Never".equals(sa.getParam("AILogic"))) {
+ if ("Never".equals(logic)) {
return false;
- }
-
- if ("PayEnergy".equals(sa.getParam("AILogic"))) {
+ } else if ("PayEnergy".equals(logic)) {
return true;
- }
-
- if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
+ } else if ("PayEnergyConservatively".equals(logic)) {
boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
boolean onlyDefensive = ai.getController().isAI()
@@ -262,6 +262,28 @@ public class CountersPutAi extends SpellAbilityAi {
return true;
}
}
+ } else if (logic.equals("MarkOppCreature")) {
+ if (!ph.is(PhaseType.END_OF_TURN)) {
+ return false;
+ }
+
+ CardCollection oppCreats = CardLists.filter(ai.getOpponents().getCreaturesInPlay(),
+ Predicates.and(Predicates.not(CardPredicates.hasCounter(CounterType.getType(type))),
+ CardPredicates.isTargetableBy(sa)));
+
+ if (!oppCreats.isEmpty()) {
+ Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
+ sa.resetTargets();
+ sa.getTargets().add(bestCreat);
+ return true;
+ }
+ } else if (logic.equals("CheckDFC")) {
+ // for cards like Ludevic's Test Subject
+ if (!source.canTransform()) {
+ return false;
+ }
+ } else if (logic.startsWith("MoveCounter")) {
+ return doMoveCounterLogic(ai, sa, ph);
}
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
@@ -292,7 +314,18 @@ public class CountersPutAi extends SpellAbilityAi {
// TODO handle proper calculation of X values based on Cost
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
- if ("Fight".equals(sa.getParam("AILogic"))) {
+ if (sa.hasParam("Adapt")) {
+ Game game = ai.getGame();
+ Combat combat = game.getCombat();
+
+ if (!source.canReceiveCounters(CounterType.P1P1) || source.getCounters(CounterType.P1P1) > 0) {
+ return false;
+ } else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
+ return doCombatAdaptLogic(source, amount, combat);
+ }
+ }
+
+ if ("Fight".equals(logic)) {
int nPump = 0;
if (type.equals("P1P1")) {
nPump = amount;
@@ -323,7 +356,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
source.setSVar("PayX", Integer.toString(amount));
- } else if ("ExiledCreatureFromGraveCMC".equals(sa.getParam("AILogic"))) {
+ } else if ("ExiledCreatureFromGraveCMC".equals(logic)) {
// e.g. Necropolis
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
@@ -337,16 +370,10 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
- if ("Polukranos".equals(sa.getParam("AILogic"))) {
+ if ("Polukranos".equals(logic)) {
- CardCollection humCreatures = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
+ CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
- final CardCollection targets = CardLists.filter(humCreatures, new Predicate() {
- @Override
- public boolean apply(final Card c) {
- return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof"));
- }
- });
if (!targets.isEmpty()){
boolean canSurvive = false;
for (Card humanCreature : targets) {
@@ -360,9 +387,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
}
- PhaseHandler ph = ai.getGame().getPhaseHandler();
-
- if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
+ if ("AlwaysAtOppEOT".equals(logic)) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
return true;
}
@@ -421,6 +446,18 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
+ // Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
+ // but try to do it in Main 2 then so that the AI has a chance to play creatures first.
+ if (list.isEmpty()
+ && sa.hasParam("Planeswalker")
+ && sa.getPayCosts() != null
+ && sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
+ && sa.isTargetNumberValid()
+ && sa.getTargets().getNumTargeted() == 0
+ && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
+ return true;
+ }
+
if (sourceName.equals("Abzan Charm")) {
final TargetRestrictions abTgt = sa.getTargetRestrictions();
// specific AI for instant with distribute two +1/+1 counters
@@ -566,8 +603,9 @@ public class CountersPutAi extends SpellAbilityAi {
final Game game = ai.getGame();
Card choice = null;
final String type = sa.getParam("CounterType");
+ final String logic = sa.getParamOrDefault("AILogic", "");
- final String amountStr = sa.getParam("CounterNum");
+ final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
@@ -611,7 +649,7 @@ public class CountersPutAi extends SpellAbilityAi {
SpellAbility animate = sa.findSubAbilityByType(ApiType.Animate);
if (!lands.isEmpty() && animate != null) {
choice = ComputerUtilCard.getWorstLand(lands);
- } else if ("BoonCounterOnOppCreature".equals(sa.getParam("AILogic"))) {
+ } else if ("BoonCounterOnOppCreature".equals(logic)) {
choice = ComputerUtilCard.getWorstCreatureAI(list);
} else {
choice = CountersAi.chooseBoonTarget(list, type);
@@ -647,7 +685,7 @@ public class CountersPutAi extends SpellAbilityAi {
boolean preferred = true;
CardCollection list;
final String type = sa.getParam("CounterType");
- final String amountStr = sa.getParam("CounterNum");
+ final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
int left = amount;
@@ -702,6 +740,7 @@ public class CountersPutAi extends SpellAbilityAi {
int totalTargets = list.size();
+ sa.resetTargets();
while (sa.canAddMoreTarget()) {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
@@ -790,14 +829,15 @@ public class CountersPutAi extends SpellAbilityAi {
if (mode == PlayerActionConfirmMode.Tribute) {
// add counter if that opponent has a giant creature
final List creats = player.getCreaturesInPlay();
- final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
+ final String amountStr = sa.getParamOrDefault("CounterNum", "1");
+ final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
- final boolean isHaste = source.hasKeyword("Haste");
+ final boolean isHaste = source.hasKeyword(Keyword.HASTE);
List threatening = CardLists.filter(creats, new Predicate() {
@Override
public boolean apply(Card c) {
return CombatUtil.canBlock(source, c, !isHaste)
- && (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword("DeathTouch"));
+ && (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword(Keyword.DEATHTOUCH));
}
});
if (!threatening.isEmpty()) {
@@ -814,7 +854,7 @@ public class CountersPutAi extends SpellAbilityAi {
List canBlock = CardLists.filter(creats, new Predicate() {
@Override
public boolean apply(Card c) {
- return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword("DeathTouch"));
+ return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword(Keyword.DEATHTOUCH));
}
});
if (!canBlock.isEmpty()) {
@@ -849,7 +889,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
- final String amountStr = sa.getParam("CounterNum");
+ final String amountStr = sa.getParamOrDefault("CounterNum", "1");
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
final boolean isCurse = sa.isCurse();
@@ -907,7 +947,7 @@ public class CountersPutAi extends SpellAbilityAi {
final CardCollection persist = CardLists.filter(filtered, new Predicate() {
@Override
public boolean apply(Card input) {
- if (!input.hasKeyword("Persist"))
+ if (!input.hasKeyword(Keyword.PERSIST))
return false;
return input.getCounters(CounterType.M1M1) <= amount;
}
@@ -920,7 +960,7 @@ public class CountersPutAi extends SpellAbilityAi {
final CardCollection undying = CardLists.filter(filtered, new Predicate() {
@Override
public boolean apply(Card input) {
- if (!input.hasKeyword("Undying"))
+ if (!input.hasKeyword(Keyword.UNDYING))
return false;
return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount;
}
@@ -945,7 +985,7 @@ public class CountersPutAi extends SpellAbilityAi {
if (e instanceof Card) {
Card c = (Card) e;
if (c.getController().isOpponentOf(ai)) {
- if (options.contains(CounterType.M1M1) && !c.hasKeyword("Undying")) {
+ if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {
@@ -975,4 +1015,80 @@ public class CountersPutAi extends SpellAbilityAi {
}
return Iterables.getFirst(options, null);
}
+
+ private boolean 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
+ final int creatDiff = sa.getParam("AILogic").contains("IsCounterUser") ? 450 : 1;
+ final Combat combat = ai.getGame().getCombat();
+ final Card source = sa.getHostCard();
+
+ final boolean threatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source)
+ || (combat != null && (((combat.isBlocked(source) && ComputerUtilCombat.attackerWouldBeDestroyed(ai, source, combat)) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))
+ || (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
+
+ if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
+ return false;
+ }
+
+ CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
+ targets.remove(source);
+
+ targets = CardLists.filter(targets, new Predicate() {
+ @Override
+ public boolean apply(Card card) {
+ boolean tgtThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
+ || (combat != null && ((combat.isBlocked(card) && ComputerUtilCombat.attackerWouldBeDestroyed(ai, card, combat))
+ || (combat.isBlocking(card) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, card, combat))));
+ // when threatened, any non-threatened target is good to preserve the counter
+ return !tgtThreatened && (threatened || ComputerUtilCard.evaluateCreature(card, false, false) > ComputerUtilCard.evaluateCreature(source, false, false) + creatDiff);
+ }
+ });
+
+ Card bestTgt = ComputerUtilCard.getBestCreatureAI(targets);
+
+ if (bestTgt != null) {
+ sa.getTargets().add(bestTgt);
+ return true;
+ }
+
+ return false;
+ }
+
+ private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) {
+ if (combat.isAttacking(source)) {
+ if (!combat.isBlocked(source)) {
+ return true;
+ } else {
+ for (Card blockedBy : combat.getBlockers(source)) {
+ if (blockedBy.getNetToughness() > source.getNetPower()
+ && blockedBy.getNetToughness() <= source.getNetPower() + amount) {
+ return true;
+ }
+ }
+
+ int totBlkPower = Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
+ if (source.getNetToughness() <= totBlkPower
+ && source.getNetToughness() + amount > totBlkPower) {
+ return true;
+ }
+ }
+ } else if (combat.isBlocking(source)) {
+ for (Card blocked : combat.getAttackersBlockedBy(source)) {
+ if (blocked.getNetToughness() > source.getNetPower()
+ && blocked.getNetToughness() <= source.getNetPower() + amount) {
+ return true;
+ }
+ }
+
+ int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), CardPredicates.Accessors.fnGetNetPower);
+ if (source.getNetToughness() <= totAtkPower
+ && source.getNetToughness() + amount > totAtkPower) {
+ return true;
+ }
+ }
+ return false;
+ }
+
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java
index 6df50568446..4195a7924b7 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutOrRemoveAi.java
@@ -23,6 +23,7 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.*;
+import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility;
@@ -56,7 +57,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
- final Card source = sa.getHostCard();
final Game game = ai.getGame();
final int amount = Integer.valueOf(sa.getParam("CounterNum"));
@@ -71,7 +71,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
// Filter AI-specific targets if provided
- list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
+ list = ComputerUtil.filterAITgts(sa, ai, list, false);
if (sa.hasParam("CounterType")) {
// currently only Jhoira's Timebug
@@ -125,7 +125,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
- CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist");
+ CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
if (!aiPersistList.isEmpty()) {
aiM1M1List = aiPersistList;
}
@@ -137,7 +137,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
// do as P1P1 part
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
- CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying");
+ CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
if (!aiUndyingList.isEmpty()) {
aiP1P1List = aiUndyingList;
@@ -226,9 +226,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
return CounterType.ICE;
}
- } else if (tgt.hasKeyword("Undying") && options.contains(CounterType.P1P1)) {
+ } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) {
return CounterType.P1P1;
- } else if (tgt.hasKeyword("Persist") && options.contains(CounterType.M1M1)) {
+ } else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) {
return CounterType.M1M1;
}
@@ -272,9 +272,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
return false;
}
- } else if (type.equals(CounterType.M1M1) && tgt.hasKeyword("Persist")) {
+ } else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
return false;
- } else if (type.equals(CounterType.P1P1) && tgt.hasKeyword("Undying")) {
+ } else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java
index ba2043efa14..67b835ffc8a 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersRemoveAi.java
@@ -9,6 +9,7 @@ import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -188,7 +189,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
- CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist");
+ CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
if (!aiPersistList.isEmpty()) {
aiM1M1List = aiPersistList;
}
@@ -200,7 +201,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// do as P1P1 part
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
- CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying");
+ CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
if (!aiUndyingList.isEmpty()) {
aiP1P1List = aiUndyingList;
@@ -230,7 +231,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection aiList = CardLists.filterControlledBy(list, ai);
aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount));
- CardCollection aiPersist = CardLists.getKeyword(aiList, "Persist");
+ CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST);
if (!aiPersist.isEmpty()) {
aiList = aiPersist;
}
@@ -253,7 +254,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// targeting ai creatures too
CardCollection aiList = CardLists.filterControlledBy(list, ai);
if (!aiList.isEmpty()) {
- CardCollection aiListUndying = CardLists.getKeyword(aiList, "Undying");
+ CardCollection aiListUndying = CardLists.getKeyword(aiList, Keyword.UNDYING);
if (!aiListUndying.isEmpty()) {
aiList = aiListUndying;
}
@@ -266,7 +267,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
// need to target opponent creatures
CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents());
if (!oppList.isEmpty()) {
- CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, "Undying");
+ CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, Keyword.UNDYING);
if (!oppListNotUndying.isEmpty()) {
oppList = oppListNotUndying;
}
@@ -307,6 +308,30 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
if (mandatory) {
+ if (type.equals("P1P1")) {
+ // Try to target creatures with Adapt or similar
+ CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
+ if (!adaptCreats.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
+ return true;
+ }
+
+ // Outlast nice target
+ CardCollection outlastCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.OUTLAST));
+ if (!outlastCreats.isEmpty()) {
+ // outlast cards often benefit from having +1/+1 counters, try not to remove last one
+ CardCollection betterTargets = CardLists.filter(outlastCreats, CardPredicates.hasCounter(CounterType.P1P1, 2));
+
+ if (!betterTargets.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
+ return true;
+ }
+
+ sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
+ return true;
+ }
+ }
+
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
return true;
}
@@ -358,9 +383,9 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
} else {
- if (options.contains(CounterType.M1M1) && target.hasKeyword("Persist")) {
+ if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) {
return CounterType.M1M1;
- } else if (options.contains(CounterType.P1P1) && target.hasKeyword("Undying")) {
+ } else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) {
return CounterType.M1M1;
}
for (CounterType type : options) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java
index b252db592b1..1b2dc7ee7fd 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageAiBase.java
@@ -8,6 +8,7 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardPredicates;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -24,12 +25,14 @@ public abstract class DamageAiBase extends SpellAbilityAi {
// Do not target a player if they aren't below 75% of our health.
// Unless Lifelink will cancel the damage to us
Card hostcard = sa.getHostCard();
- boolean lifelink = hostcard.hasKeyword("Lifelink");
- for (Card ench : hostcard.getEnchantedBy(false)) {
- // Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
- if (ench.hasSVar("LikeLifeLink")) {
- if ("True".equals(ench.getSVar("LikeLifeLink"))) {
- lifelink = true;
+ boolean lifelink = hostcard.hasKeyword(Keyword.LIFELINK);
+ if (!lifelink) {
+ for (Card ench : hostcard.getEnchantedBy()) {
+ // Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
+ if (ench.hasSVar("LikeLifeLink")) {
+ if ("True".equals(ench.getSVar("LikeLifeLink"))) {
+ lifelink = true;
+ }
}
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java
index 8f2e82a901f..e43dfac49ab 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java
@@ -8,6 +8,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -268,7 +269,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
};
- list = CardLists.getNotKeyword(list, "Indestructible");
+ list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
list = CardLists.filter(list, filterKillable);
return list;
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
index e62b2f230d7..1d088464afb 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
@@ -4,12 +4,17 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.*;
+import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
+import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.cost.Cost;
+import forge.game.cost.CostPart;
+import forge.game.cost.CostPartMana;
import forge.game.cost.CostRemoveCounter;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -20,7 +25,11 @@ import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
+import forge.util.MyRandom;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
@@ -89,11 +98,26 @@ public class DamageDealAi extends DamageAiBase {
if (damage.equals("X")) {
if (sa.getSVar(damage).equals("Count$xPaid") || sourceName.equals("Crater's Claws")) {
- // Set PayX here to maximum value.
dmg = ComputerUtilMana.determineLeftoverMana(sa, ai);
+
+ // Try not to waste spells like Blaze or Fireball on early targets, try to do more damage with them if possible
+ if (ai.getController().isAI()) {
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+ int holdChance = aic.getIntProperty(AiProps.HOLD_X_DAMAGE_SPELLS_FOR_MORE_DAMAGE_CHANCE);
+ if (MyRandom.percentTrue(holdChance)) {
+ int threshold = aic.getIntProperty(AiProps.HOLD_X_DAMAGE_SPELLS_THRESHOLD);
+ boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
+ boolean isLethal = sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
+ if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
+ return false;
+ }
+ }
+ }
+
+ // Set PayX here to maximum value. It will be adjusted later depending on the target.
source.setSVar("PayX", Integer.toString(dmg));
- } else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
- dmg--; // the card will be spent casting the spell, so actual damage is 1 less
+ } else if (sa.getSVar(damage).contains("InYourHand") && source.getZone().is(ZoneType.Hand)) {
+ dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
if (sa.getTargetRestrictions().canTgtPlayer()) {
@@ -215,8 +239,33 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
- if (!this.damageTargetAI(ai, sa, dmg, false)) {
- return false;
+ // Try to chain damage/debuff effects
+ Pair chainDmg = getDamagingSAToChain(ai, sa, damage);
+
+ // test what happens if we chain this to another damaging spell
+ if (chainDmg != null) {
+ int extraDmg = chainDmg.getValue();
+ boolean willTargetIfChained = this.damageTargetAI(ai, sa, dmg + extraDmg, false);
+ if (!willTargetIfChained) {
+ return false; // won't play it even in chain
+ } else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) {
+ // we're trying to chain a pump spell to a damage spell targeting a player, that won't work
+ // so run an additional check to ensure that we want to cast the current spell separately
+ sa.resetTargets();
+ if (!this.damageTargetAI(ai, sa, dmg, false)) {
+ return false;
+ }
+ } else {
+ // we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
+ // the second spell so we don't misplay
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+ aic.reserveManaSourcesForNextSpell(chainDmg.getKey(), sa);
+ }
+ } else {
+ // simple targeting when there is no spell chaining plan
+ if (!this.damageTargetAI(ai, sa, dmg, false)) {
+ return false;
+ }
}
if ((damage.equals("X") && source.getSVar(damage).equals("Count$xPaid")) ||
@@ -237,6 +286,19 @@ public class DamageDealAi extends DamageAiBase {
source.setSVar("PayX", Integer.toString(actualPay));
}
}
+
+ if ("XCountersDamage".equals(logic) && sa.getPayCosts() != null) {
+ // Check to ensure that we have enough counters to remove per the defined PayX
+ for (CostPart part : sa.getPayCosts().getCostParts()) {
+ if (part instanceof CostRemoveCounter) {
+ if (source.getCounters(((CostRemoveCounter) part).counter) < Integer.valueOf(source.getSVar("PayX"))) {
+ return false;
+ }
+ break;
+ }
+ }
+ }
+
return true;
}
@@ -274,7 +336,7 @@ public class DamageDealAi extends DamageAiBase {
final Game game = source.getGame();
List hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game);
- List killables = CardLists.filter(hPlay, new Predicate() {
+ CardCollection killables = CardLists.filter(hPlay, new Predicate() {
@Override
public boolean apply(final Card c) {
return c.getSVar("Targeting").equals("Dies")
@@ -285,7 +347,10 @@ public class DamageDealAi extends DamageAiBase {
});
// Filter AI-specific targets if provided
- killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
+ killables = ComputerUtil.filterAITgts(sa, ai, killables, true);
+
+ // Try not to target anything which will already be dead by the time the spell resolves
+ killables = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, killables, sa);
Card targetCard = null;
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
@@ -480,10 +545,11 @@ public class DamageDealAi extends DamageAiBase {
final PhaseHandler phase = game.getPhaseHandler();
final boolean divided = sa.hasParam("DividedAsYouChoose");
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
+ final String logic = sa.getParamOrDefault("AILogic", "");
Player enemy = ComputerUtil.getOpponentFor(ai);
- if ("PowerDmg".equals(sa.getParam("AILogic"))) {
+ if ("PowerDmg".equals(logic)) {
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
if (tgt.canTgtCreatureAndPlayer() && this.shouldTgtP(ai, sa, dmg, noPrevention)){
sa.resetTargets();
@@ -492,7 +558,9 @@ public class DamageDealAi extends DamageAiBase {
return true;
}
- if (tgt.getMaxTargets(source, sa) <= 0) {
+ // AssumeAtLeastOneTarget is used for cards with funky targeting implementation like Fight with Fire which would
+ // otherwise confuse the AI by returning 0 unexpectedly during SA "AI can play" tests.
+ if (tgt.getMaxTargets(source, sa) <= 0 && !logic.equals("AssumeAtLeastOneTarget")) {
return false;
}
@@ -503,11 +571,11 @@ public class DamageDealAi extends DamageAiBase {
TargetChoices tcs = sa.getTargets();
// Do not use if would kill self
- if (("SelfDamage".equals(sa.getParam("AILogic"))) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
+ if (("SelfDamage".equals(logic)) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
return false;
}
- if ("ChoiceBurn".equals(sa.getParam("AILogic"))) {
+ if ("ChoiceBurn".equals(logic)) {
// do not waste burns on player if other choices are present
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(enemy);
@@ -516,7 +584,7 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
}
- if ("Polukranos".equals(sa.getParam("AILogic"))) {
+ if ("Polukranos".equals(logic)) {
int dmgTaken = 0;
CardCollection humCreatures = enemy.getCreaturesInPlay();
Card lastTgt = null;
@@ -558,7 +626,15 @@ public class DamageDealAi extends DamageAiBase {
return true;
}
}
+
+ int totalTargetedSoFar = -1;
while (tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
+ if (totalTargetedSoFar == tcs.getNumTargeted()) {
+ // Avoid looping endlessly when choosing targets for cards with variable target number and type
+ // like Jaya's Immolating Inferno
+ break;
+ }
+ totalTargetedSoFar = tcs.getNumTargeted();
if (oppTargetsChoice && sa.getActivatingPlayer().equals(ai) && !sa.isTrigger()) {
// canPlayAI (sa activated by ai)
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
@@ -572,10 +648,9 @@ public class DamageDealAi extends DamageAiBase {
if (c != null && !this.shouldTgtP(ai, sa, dmg, noPrevention, true)) {
tcs.add(c);
if (divided) {
- final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
- if (assignedDamage <= dmg) {
- tgt.addDividedAllocation(c, assignedDamage);
- }
+ int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
+ assignedDamage = Math.min(dmg, assignedDamage);
+ tgt.addDividedAllocation(c, assignedDamage);
dmg = dmg - assignedDamage;
if (dmg <= 0) {
break;
@@ -680,7 +755,7 @@ public class DamageDealAi extends DamageAiBase {
}
continue;
}
- } else if ("OppAtTenLife".equals(sa.getParam("AILogic"))) {
+ } else if ("OppAtTenLife".equals(logic)) {
for (final Player p : ai.getOpponents()) {
if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
tcs.add(p);
@@ -689,9 +764,10 @@ public class DamageDealAi extends DamageAiBase {
}
// TODO: Improve Damage, we shouldn't just target the player just
// because we can
- else if (sa.canTarget(enemy)) {
+ if (sa.canTarget(enemy) && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
+ || ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai))
|| sa.getPayCosts() == null || immediately
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) &&
(!avoidTargetP(ai, sa))) {
@@ -742,7 +818,7 @@ public class DamageDealAi extends DamageAiBase {
if (o instanceof Card) {
Card c = (Card) o;
final int restDamage = ComputerUtilCombat.predictDamageTo(c, dmg, saMe.getHostCard(), false);
- if (!c.hasKeyword("Indestructible") && ComputerUtilCombat.getDamageToKill(c) <= restDamage) {
+ if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && ComputerUtilCombat.getDamageToKill(c) <= restDamage) {
if (c.getController().equals(ai)) {
return false;
} else {
@@ -942,7 +1018,7 @@ public class DamageDealAi extends DamageAiBase {
for (Card c : creatures) {
int power = c.getNetPower();
int toughness = c.getNetToughness();
- boolean canDie = !(c.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(c.getController(), c));
+ boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c));
// Currently will target creatures with toughness 3+ (or power 5+)
// and only if the creature can actually die, do not "underdrain"
@@ -959,4 +1035,90 @@ public class DamageDealAi extends DamageAiBase {
source.setSVar("PayX", Integer.toString(dmg));
return true;
}
+
+ // Returns a pair of a SpellAbility (APIType DealDamage or Pump) and damage/debuff amount
+ // The returned spell ability can be chained to "sa" to deal more damage (enough mana is available to cast both
+ // and can be properly reserved).
+ public static Pair getDamagingSAToChain(Player ai, SpellAbility sa, String damage) {
+ if (!ai.getController().isAI()) {
+ return null; // should only work for the actual AI player
+ } else if (((PlayerControllerAi)ai.getController()).getAi().usesSimulation()) {
+ // simulated AI shouldn't use paired decisions, it tries to find complex decisions on its own
+ return null;
+ }
+
+ Game game = ai.getGame();
+ int chance = ((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_CHAIN_TWO_DAMAGE_SPELLS);
+
+ if (chance > 0 && (ComputerUtilCombat.lifeInDanger(ai, game.getCombat()) || ComputerUtil.aiLifeInDanger(ai, true, 0))) {
+ chance = 100; // in danger, do it even if normally the chance is low (unless chaining is completely disabled)
+ }
+
+ if (!MyRandom.percentTrue(chance)) {
+ return null;
+ }
+
+ if (sa.getSubAbility() != null || sa.getParent() != null) {
+ // Doesn't work yet for complex decisions where damage is only a part of the decision process
+ return null;
+ }
+
+ // Try to chain damage/debuff effects
+ if (StringUtils.isNumeric(damage) || (damage.startsWith("-") && StringUtils.isNumeric(damage.substring(1)))) {
+ // currently only works for predictable numeric damage
+ CardCollection cards = new CardCollection();
+ cards.addAll(ai.getCardsIn(ZoneType.Hand));
+ cards.addAll(ai.getCardsIn(ZoneType.Battlefield));
+ cards.addAll(ai.getCardsActivableInExternalZones(true));
+ for (Card c : cards) {
+ for (SpellAbility ab : c.getSpellAbilities()) {
+ if (ab.equals(sa) || ab.getSubAbility() != null) { // decisions for complex SAs with subs are not supported yet
+ continue;
+ }
+ if (!ab.canPlay()) {
+ continue;
+ }
+ // currently works only with cards that don't have additional costs (only mana is supported)
+ if (ab.getPayCosts() != null
+ && (ab.getPayCosts().hasNoManaCost() || ab.getPayCosts().hasOnlySpecificCostType(CostPartMana.class))) {
+ String dmgDef = "0";
+ if (ab.getApi() == ApiType.DealDamage) {
+ dmgDef = ab.getParamOrDefault("NumDmg", "0");
+ } else if (ab.getApi() == ApiType.Pump) {
+ dmgDef = ab.getParamOrDefault("NumDef", "0");
+ if (dmgDef.startsWith("-")) {
+ dmgDef = dmgDef.substring(1);
+ } else {
+ continue; // not a toughness debuff
+ }
+ }
+ if (StringUtils.isNumeric(dmgDef)) { // currently doesn't work for X and other dependent costs
+ if (sa.usesTargeting() && ab.usesTargeting()) {
+ // Ensure that the chained spell can target at least the same things (or more) as the current one
+ TargetRestrictions tgtSa = sa.getTargetRestrictions();
+ TargetRestrictions tgtAb = sa.getTargetRestrictions();
+ String[] validTgtsSa = tgtSa.getValidTgts();
+ String[] validTgtsAb = tgtAb.getValidTgts();
+ if (!Arrays.asList(validTgtsSa).containsAll(Arrays.asList(validTgtsAb))) {
+ continue;
+ }
+
+ // FIXME: should it also check restrictions for targeting players?
+ ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST;
+ ManaCost costAb = ab.getPayCosts().getTotalMana(); // checked for null above
+ ManaCost total = ManaCost.combine(costSa, costAb);
+ SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false));
+ // can we pay both costs?
+ if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) {
+ return Pair.of(ab, Integer.parseInt(dmgDef));
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return null;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
index e7e89fb8b95..06705baf8bf 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
@@ -8,6 +8,7 @@ import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -109,7 +110,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
for (Card c : list) {
- if (c.hasKeyword("Indestructible")) {
+ if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
sa.getTargets().add(c);
return true;
}
@@ -133,7 +134,7 @@ public class DestroyAi extends SpellAbilityAi {
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
- list = CardLists.getNotKeyword(list, "Indestructible");
+ list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
if (CardLists.getNotType(list, "Creature").isEmpty()) {
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
}
@@ -160,7 +161,7 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
//Check for undying
- return (!c.hasKeyword("Undying") || c.getCounters(CounterType.P1P1) > 0);
+ return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0);
}
});
}
@@ -177,6 +178,8 @@ 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;
}
@@ -270,7 +273,7 @@ public class DestroyAi extends SpellAbilityAi {
} else {
// Don't destroy stolen permanents when the stealing aura can be destroyed
if (choice.getOwner() == ai) {
- for (Card aura : choice.getEnchantedBy(false)) {
+ for (Card aura : choice.getEnchantedBy()) {
SpellAbility sp = aura.getFirstSpellAbility();
if (sp != null && "GainControl".equals(sp.getParam("AILogic"))
&& aura.getController() != ai && sa.canTarget(aura)) {
@@ -294,7 +297,7 @@ public class DestroyAi extends SpellAbilityAi {
if (list.isEmpty()
|| !CardLists.filterControlledBy(list, ai).isEmpty()
- || CardLists.getNotKeyword(list, "Indestructible").isEmpty()) {
+ || CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
return false;
}
}
@@ -312,11 +315,14 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
+ // Try to avoid targeting creatures that are dead on board
+ list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
+
if (list.isEmpty() || list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false;
}
- CardCollection preferred = CardLists.getNotKeyword(list, "Indestructible");
+ CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java
index 81e26eb32d4..b2b06161f29 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAllAi.java
@@ -7,6 +7,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -18,7 +19,7 @@ public class DestroyAllAi extends SpellAbilityAi {
private static final Predicate predicate = new Predicate() {
@Override
public boolean apply(final Card c) {
- return !(c.hasKeyword("Indestructible") || c.getSVar("SacMe").length() > 0);
+ return !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getSVar("SacMe").length() > 0);
}
};
@@ -64,10 +65,15 @@ public class DestroyAllAi extends SpellAbilityAi {
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
+ final String logic = sa.getParamOrDefault("AILogic", "");
Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
final int CREATURE_EVAL_THRESHOLD = 200;
+ if (logic.equals("Always")) {
+ return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
+ }
+
String valid = "";
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");
diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
index 0c020491679..3fd77edf9fc 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
@@ -26,10 +26,7 @@ import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
-import forge.game.cost.Cost;
-import forge.game.cost.CostDiscard;
-import forge.game.cost.CostPart;
-import forge.game.cost.PaymentDecision;
+import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -134,6 +131,8 @@ public class DrawAi extends SpellAbilityAi {
return true;
} else if (logic.equals("AlwaysAtOppEOT")) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
+ } else if (logic.equals("RespondToOwnActivation")) {
+ return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
}
// Don't use draw abilities before main 2 if possible
@@ -252,19 +251,34 @@ public class DrawAi extends SpellAbilityAi {
}
if (num != null && num.equals("ChosenX")) {
- // Necrologia, Pay X Life : Draw X Cards
if (sa.getSVar("X").equals("XChoice")) {
// Draw up to max hand size but leave at least 3 in library
numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
- // But no more than what's "safe" and doesn't risk a near death experience
- // Maybe would be better to check for "serious danger" and take more risk?
- while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
- numCards--;
+
+ if (sa.getPayCosts() != null) {
+ if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
+ // [Necrologia, Pay X Life : Draw X Cards]
+ // Don't draw more than what's "safe" and don't risk a near death experience
+ // Maybe would be better to check for "serious danger" and take more risk?
+ while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
+ numCards--;
+ }
+ } else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
+ // [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards]
+ // TODO: Add special logic to limit/otherwise modify the ChosenX value here
+
+ // Skip this ability if nothing is to be chosen for sacrifice
+ if (numCards <= 0) {
+ return false;
+ }
+ }
}
+
sa.setSVar("ChosenX", Integer.toString(numCards));
source.setSVar("ChosenX", Integer.toString(numCards));
}
}
+
// Logic for cards that require special handling
if ("YawgmothsBargain".equals(logic)) {
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
@@ -351,6 +365,10 @@ public class DrawAi extends SpellAbilityAi {
if (numCards >= computerLibrarySize) {
if (xPaid) {
numCards = computerLibrarySize - 1;
+ if (numCards <= 0 && !mandatory) {
+ // not drawing anything, so don't do it
+ return false;
+ }
} else if (!ai.isCardInPlay("Laboratory Maniac")) {
aiTarget = false;
}
@@ -384,6 +402,9 @@ public class DrawAi extends SpellAbilityAi {
if (computerHandSize + numCards > computerMaxHandSize && game.getPhaseHandler().isPlayerTurn(ai)) {
if (xPaid) {
numCards = computerMaxHandSize - computerHandSize;
+ if (sa.getHostCard().getZone().is(ZoneType.Hand)) {
+ numCards++; // the card will be spent
+ }
source.setSVar("PayX", Integer.toString(numCards));
} else {
// Don't draw too many cards and then risk discarding
diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
index f79a824d169..4b83190cc6f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
@@ -6,17 +6,15 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCard;
-import forge.ai.ComputerUtilCombat;
-import forge.ai.SpecialCardAi;
-import forge.ai.SpellAbilityAi;
-import forge.ai.SpellApiToAi;
+import forge.ai.*;
+import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.ApiType;
import forge.game.card.*;
import forge.game.combat.CombatUtil;
+import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -115,10 +113,39 @@ public class EffectAi extends SpellAbilityAi {
} else if (logic.equals("SpellCopy")) {
// fetch Instant or Sorcery and AI has reason to play this turn
// does not try to get itself
+ final ManaCost costSa = sa.getPayCosts() != null ? sa.getPayCosts().getTotalMana() : ManaCost.NO_COST;
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() {
@Override
public boolean apply(final Card c) {
- return (c.isInstant() || c.isSorcery()) && c != sa.getHostCard() && ComputerUtil.hasReasonToPlayCardThisTurn(ai, c);
+ if (!(c.isInstant() || c.isSorcery()) || c.equals(sa.getHostCard())) {
+ return false;
+ }
+ for (SpellAbility ab : c.getSpellAbilities()) {
+ if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
+ || ab.hasParam("AINoRecursiveCheck")) {
+ // prevent infinitely recursing mana ritual and other abilities with reentry
+ continue;
+ } else if ("SpellCopy".equals(ab.getParam("AILogic")) && ab.getApi() == ApiType.Effect) {
+ // don't copy another copy spell, too complex for the AI
+ continue;
+ }
+ if (!ab.canPlay()) {
+ continue;
+ }
+ AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab);
+ // see if we can pay both for this spell and for the Effect spell we're considering
+ if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
+ ManaCost costAb = ab.getPayCosts() != null ? ab.getPayCosts().getTotalMana() : ManaCost.NO_COST;
+ ManaCost total = ManaCost.combine(costSa, costAb);
+ SpellAbility combinedAb = ab.copyWithDefinedCost(new Cost(total, false));
+ // can we pay both costs?
+ if (ComputerUtilMana.canPayManaCost(combinedAb, ai, 0)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
}
});
@@ -138,7 +165,26 @@ public class EffectAi extends SpellAbilityAi {
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate() {
@Override
public boolean apply(final Card c) {
- return (c.isInstant() || c.isSorcery()) && !c.hasKeyword("Rebound") && ComputerUtil.hasReasonToPlayCardThisTurn(ai, c);
+ if (!(c.isInstant() || c.isSorcery()) || c.hasKeyword(Keyword.REBOUND)) {
+ return false;
+ }
+ for (SpellAbility ab : c.getSpellAbilities()) {
+ if (ComputerUtilAbility.getAbilitySourceName(sa).equals(ComputerUtilAbility.getAbilitySourceName(ab))
+ || ab.hasParam("AINoRecursiveCheck")) {
+ // prevent infinitely recursing mana ritual and other abilities with reentry
+ continue;
+ }
+ if (!ab.canPlay()) {
+ continue;
+ }
+ AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(ab);
+ if (decision == AiPlayDecision.WillPlay || decision == AiPlayDecision.WaitForMain2) {
+ if (ComputerUtilMana.canPayManaCost(ab, ai, 0)) {
+ return true;
+ }
+ }
+ }
+ return false;
}
});
@@ -149,6 +195,11 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
} else if (logic.equals("Always")) {
randomReturn = true;
+ } else if (logic.equals("Main1")) {
+ if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
+ return false;
+ }
+ randomReturn = true;
} else if (logic.equals("Main2")) {
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java
index d597b5cfa6d..3a1ad5b5528 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java
@@ -7,6 +7,7 @@ import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
+import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -32,6 +33,12 @@ public class FightAi extends SpellAbilityAi {
sa.resetTargets();
final Card source = sa.getHostCard();
+ // everything is defined or targeted above, can't do anything there?
+ if (sa.hasParam("Defined") && !sa.usesTargeting()) {
+ // TODO extend Logic for cards like Arena or Grothama
+ return true;
+ }
+
// Get creature lists
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);
@@ -162,15 +169,15 @@ public class FightAi extends SpellAbilityAi {
// Get sorted creature lists
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection humCreatures = ai.getOpponents().getCreaturesInPlay();
- if ("Time to Feed".equals(sourceName)) { // flip sa
- aiCreatures = CardLists.getTargetableCards(aiCreatures, tgtFight);
- aiCreatures = ComputerUtil.getSafeTargets(ai, tgtFight, aiCreatures);
- humCreatures = CardLists.getTargetableCards(humCreatures, sa);
- } else {
- aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);
- aiCreatures = ComputerUtil.getSafeTargets(ai, sa, aiCreatures);
- humCreatures = CardLists.getTargetableCards(humCreatures, tgtFight);
- }
+ if ("Time to Feed".equals(sourceName)) { // flip sa
+ aiCreatures = CardLists.getTargetableCards(aiCreatures, tgtFight);
+ aiCreatures = ComputerUtil.getSafeTargets(ai, tgtFight, aiCreatures);
+ humCreatures = CardLists.getTargetableCards(humCreatures, sa);
+ } else {
+ aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);
+ aiCreatures = ComputerUtil.getSafeTargets(ai, sa, aiCreatures);
+ humCreatures = CardLists.getTargetableCards(humCreatures, tgtFight);
+ }
ComputerUtilCard.sortByEvaluateCreature(aiCreatures);
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
if (humCreatures.isEmpty() || aiCreatures.isEmpty()) {
@@ -246,17 +253,19 @@ public class FightAi extends SpellAbilityAi {
}
return false;
}
+
public static boolean canKill(Card fighter, Card opponent, int pumpAttack) {
- if (opponent.getSVar("Targeting").equals("Dies")) {
- return true;
- }
- if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed()
- || opponent.getShieldCount() > 0 || ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
- return false;
- }
- if (fighter.hasKeyword("Deathtouch") || ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) {
- return true;
- }
- return false;
+ if (opponent.getSVar("Targeting").equals("Dies")) {
+ return true;
+ }
+ if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() || opponent.getShieldCount() > 0
+ || ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
+ return false;
+ }
+ if (fighter.hasKeyword(Keyword.DEATHTOUCH)
+ || ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) {
+ return true;
+ }
+ return false;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/FogAi.java b/forge-ai/src/main/java/forge/ai/ability/FogAi.java
index 66f34652f0b..ecdb0c03b4f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java
@@ -1,17 +1,19 @@
package forge.ai.ability;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCombat;
-import forge.ai.SpellAbilityAi;
+import forge.ai.*;
import forge.game.Game;
+import forge.game.GameObject;
import forge.game.card.Card;
import forge.game.card.CardPredicates;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
+import java.util.List;
+
public class FogAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -20,6 +22,35 @@ public class FogAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
+ final Card hostCard = sa.getHostCard();
+
+ // Don't cast it, if the effect is already in place
+ if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
+ return false;
+ }
+
+ // if card would be destroyed, react and use immediately if it's not own turn
+ if ((AiCardMemory.isRememberedCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
+ && (!game.getStack().isEmpty())
+ && (!game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()))) {
+ final List objects = ComputerUtil.predictThreatenedObjects(ai, null);
+ if (objects.contains(hostCard)) {
+ AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
+ return true;
+ }
+ }
+
+ // Reserve mana to cast this card if it will be likely needed
+ if (((game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()))
+ || (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)))
+ && (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
+ && (ComputerUtil.aiLifeInDanger(ai, false, 0))) {
+ boolean reserved = ((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, true);
+ if (reserved) {
+ AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
+ }
+ }
+
// AI should only activate this during Human's Declare Blockers phase
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return false;
@@ -33,17 +64,12 @@ public class FogAi extends SpellAbilityAi {
return false;
}
- // Don't cast it, if the effect is already in place
- if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
- return false;
- }
-
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
int dmg = 0;
for (Card atk : game.getCombat().getAttackersOf(ai)) {
if (game.getCombat().isUnblocked(atk)) {
dmg += atk.getNetCombatDamage();
- } else if (atk.hasKeyword("Trample")) {
+ } else if (atk.hasKeyword(Keyword.TRAMPLE)) {
dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness);
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java
index a34f5e0d801..144e91ff726 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeVariantAi.java
@@ -1,14 +1,15 @@
package forge.ai.ability;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilAbility;
-import forge.ai.ComputerUtilCombat;
-import forge.ai.SpellAbilityAi;
+import forge.ai.*;
import forge.game.Game;
import forge.game.card.Card;
+import forge.game.keyword.Keyword;
+import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
+import forge.game.zone.MagicStack;
+import forge.util.MyRandom;
public class LifeExchangeVariantAi extends SpellAbilityAi {
@@ -83,7 +84,48 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
return shouldDo;
}
else if ("Evra, Halcyon Witness".equals(sourceName)) {
- // TODO add logic
+ int aiLife = ai.getLife();
+
+ // Offensive use of Evra, try to kill the opponent or deal a lot of damage, and hopefully gain a lot of life too
+ if (game.getCombat() != null && game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
+ && game.getCombat().isAttacking(source) && source.getNetPower() > 0
+ && source.getNetPower() < aiLife) {
+ Player def = game.getCombat().getDefenderPlayerByAttacker(source);
+ if (game.getCombat().isUnblocked(source) && def.canLoseLife() && aiLife >= def.getLife() && source.getNetPower() < def.getLife()) {
+ // Unblocked Evra which can deal lethal damage
+ return true;
+ } else if (ai.getController().isAI() && aiLife > source.getNetPower() && source.hasKeyword(Keyword.LIFELINK)) {
+ int dangerMin = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
+ int dangerMax = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD));
+ int dangerDiff = dangerMax - dangerMin;
+ int lifeInDanger = dangerDiff <= 0 ? dangerMin : MyRandom.getRandom().nextInt(dangerDiff) + dangerMin;
+ if (source.getNetPower() >= lifeInDanger && ai.canGainLife() && ComputerUtil.lifegainPositive(ai, source)) {
+ // Blocked or unblocked Evra which will get bigger *and* we're getting our life back through Lifelink
+ return true;
+ }
+ }
+ }
+
+ // Defensive use of Evra, try to debuff Evra to try to gain some life
+ if (source.getNetPower() > aiLife) {
+ // Only makes sense if the AI can actually gain life from this
+ if (!ai.canGainLife())
+ return false;
+
+ if (ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
+ return true;
+ }
+
+ // check the top of stack
+ MagicStack stack = game.getStack();
+ if (!stack.isEmpty()) {
+ SpellAbility saTop = stack.peekAbility();
+ if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) {
+ return true;
+ }
+ }
+ }
+
}
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
index 8966067b3aa..96fc5d4f195 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
@@ -51,6 +51,13 @@ public class LifeLoseAi extends SpellAbilityAi {
if (tgtPlayers.contains(ai) && amount > 0 && amount + 3 > ai.getLife()) {
return false;
}
+
+ if (sa.usesTargeting()) {
+ if (!doTgt(ai, sa, false)) {
+ return false;
+ }
+ }
+
return true;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
index 6c7bf2dc1aa..868f69fd185 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
@@ -9,11 +9,13 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
+import forge.util.Aggregates;
import java.util.Arrays;
import java.util.List;
@@ -28,8 +30,10 @@ public class ManaEffectAi extends SpellAbilityAi {
*/
@Override
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
- if ("ManaRitual".equals(aiLogic)) {
+ if (aiLogic.startsWith("ManaRitual")) {
return doManaRitualLogic(ai, sa);
+ } else if ("Always".equals(aiLogic)) {
+ return true;
}
return super.checkAiLogic(ai, sa, aiLogic);
}
@@ -103,6 +107,7 @@ public class ManaEffectAi extends SpellAbilityAi {
// Dark Ritual and other similar instants/sorceries that add mana to mana pool
private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
final Card host = sa.getHostCard();
+ final String logic = sa.getParamOrDefault("AILogic", "");
CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
int numManaSrcs = manaSources.size();
@@ -114,7 +119,9 @@ public class ManaEffectAi extends SpellAbilityAi {
String produced = sa.getParam("Produced");
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
- if ("ChosenX".equals(sa.getParam("Amount"))
+ int numCounters = 0;
+ int manaSurplus = 0;
+ if ("XChoice".equals(host.getSVar("X"))
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterType.KI; // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) {
@@ -123,7 +130,12 @@ public class ManaEffectAi extends SpellAbilityAi {
break;
}
}
- manaReceived = host.getCounters(ctrType);
+ numCounters = host.getCounters(ctrType);
+ manaReceived = numCounters;
+ if (logic.startsWith("ManaRitualBattery.")) {
+ manaSurplus = Integer.valueOf(logic.substring(18)); // adds an extra mana even if no counters removed
+ manaReceived += manaSurplus;
+ }
}
int searchCMC = numManaSrcs - selfCost + manaReceived;
@@ -170,7 +182,7 @@ public class ManaEffectAi extends SpellAbilityAi {
}
testSaNoCost.setActivatingPlayer(ai);
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) {
- if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword("Haste")
+ if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword(Keyword.HASTE)
&& !ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
// AI will waste a ritual in Main 1 unless the casted permanent is a haste creature
continue;
@@ -193,6 +205,13 @@ public class ManaEffectAi extends SpellAbilityAi {
CardPredicates.lessCMC(searchCMC),
Predicates.or(CardPredicates.isColorless(), CardPredicates.isColor(producedColor))));
+ if (logic.startsWith("ManaRitualBattery")) {
+ // Don't remove more counters than would be needed to cast the more expensive thing we want to cast,
+ // otherwise the AI grabs too many counters at once.
+ int maxCtrs = Aggregates.max(castableSpells, CardPredicates.Accessors.fnGetCmc) - manaSurplus;
+ sa.setSVar("ChosenX", "Number$" + Math.min(numCounters, maxCtrs));
+ }
+
// TODO: this will probably still waste the card from time to time. Somehow improve detection of castable material.
return castableSpells.size() > 0;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java
index 509ff22f3a0..94a878c8afe 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java
@@ -113,7 +113,7 @@ public class ManifestAi extends SpellAbilityAi {
repParams.put("Origin", ZoneType.Library);
repParams.put("Destination", ZoneType.Battlefield);
repParams.put("Source", sa.getHostCard());
- List list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.None);
+ List list = game.getReplacementHandler().getReplacementList(repParams, ReplacementLayer.Other);
if (!list.isEmpty()) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java
index 5250f543a82..c975d894c29 100644
--- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java
@@ -55,9 +55,11 @@ public class MillAi extends SpellAbilityAi {
} else if ("ExileAndPlayOrDealDamage".equals(sa.getParam("AILogic"))) {
return (ph.is(PhaseType.MAIN1) || ph.is(PhaseType.MAIN2)) && ph.isPlayerTurn(ai); // Chandra, Torch of Defiance and similar
}
- if ("You".equals(sa.getParam("Defined")) && !(!SpellAbilityAi.isSorcerySpeed(sa) && ph.is(PhaseType.END_OF_TURN)
- && ph.getNextTurn().equals(ai))) {
- return false; // only self-mill at opponent EOT
+ if (!sa.hasParam("Planeswalker")) { // Planeswalker abilities are only activated at sorcery speed
+ if ("You".equals(sa.getParam("Defined")) && !(!SpellAbilityAi.isSorcerySpeed(sa) && ph.is(PhaseType.END_OF_TURN)
+ && ph.getNextTurn().equals(ai))) {
+ return false; // only self-mill at opponent EOT
+ }
}
if (sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) {
if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai))) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java
index 79c3cbbad0e..e01d0b38943 100644
--- a/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/MustBlockAi.java
@@ -2,15 +2,17 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCard;
-import forge.ai.ComputerUtilCombat;
-import forge.ai.SpellAbilityAi;
+import com.google.common.collect.Lists;
+import forge.ai.*;
+import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
+import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
+import forge.game.keyword.Keyword;
+import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -23,14 +25,45 @@ public class MustBlockAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
- // disabled for the AI until he/she can make decisions about who to make
- // block
+ final Card source = sa.getHostCard();
+ final Game game = aiPlayer.getGame();
+ final Combat combat = game.getCombat();
+ final PhaseHandler ph = game.getPhaseHandler();
+ final boolean onlyLethal = !"AllowNonLethal".equals(sa.getParam("AILogic"));
+
+ if (combat == null || !combat.isAttacking(source)) {
+ return false;
+ } else if (AiCardMemory.isRememberedCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
+ // The AI can meaningfully do it only to one creature per card yet, trying to do it to multiple cards
+ // may result in overextending and losing the attacker
+ return false;
+ }
+
+ final TargetRestrictions abTgt = sa.getTargetRestrictions();
+ final List list = determineGoodBlockers(source, aiPlayer, combat.getDefenderPlayerByAttacker(source), sa, onlyLethal,false);
+
+ if (!list.isEmpty()) {
+ final Card blocker = ComputerUtilCard.getBestCreatureAI(list);
+ if (blocker == null) {
+ return false;
+ }
+ sa.getTargets().add(blocker);
+ AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
+ return true;
+ }
+
return false;
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
- return false;
+ if (sa.hasParam("DefinedAttacker")) {
+ // The AI can't handle "target creature blocks another target creature" abilities yet
+ return false;
+ }
+
+ // Otherwise it's a standard targeted "target creature blocks CARDNAME" ability, so use the main canPlayAI code path
+ return canPlayAI(aiPlayer, sa);
}
@Override
@@ -62,27 +95,7 @@ public class MustBlockAi extends SpellAbilityAi {
boolean chance = false;
if (abTgt != null) {
- List list = CardLists.filter(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
- list = CardLists.getTargetableCards(list, sa);
- list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
- list = CardLists.filter(list, new Predicate() {
- @Override
- public boolean apply(final Card c) {
- boolean tapped = c.isTapped();
- c.setTapped(false);
- if (!CombatUtil.canBlock(definedAttacker, c)) {
- return false;
- }
- if (ComputerUtilCombat.canDestroyAttacker(ai, definedAttacker, c, null, false)) {
- return false;
- }
- if (!ComputerUtilCombat.canDestroyBlocker(ai, c, definedAttacker, null, false)) {
- return false;
- }
- c.setTapped(tapped);
- return true;
- }
- });
+ final List list = determineGoodBlockers(definedAttacker, ai, ComputerUtil.getOpponentFor(ai), sa, true,true);
if (list.isEmpty()) {
return false;
}
@@ -90,6 +103,20 @@ public class MustBlockAi extends SpellAbilityAi {
if (blocker == null) {
return false;
}
+
+ if (source.hasKeyword(Keyword.PROVOKE) && blocker.isTapped()) {
+ // Don't provoke if the attack is potentially lethal
+ Combat combat = ai.getGame().getCombat();
+ if (combat != null) {
+ Player defender = combat.getDefenderPlayerByAttacker(source);
+ if (defender != null && combat.getAttackingPlayer().equals(ai)
+ && defender.canLoseLife() && !defender.cantLoseForZeroOrLessLife()
+ && ComputerUtilCombat.lifeThatWouldRemain(defender, combat) <= 0) {
+ return false;
+ }
+ }
+ }
+
sa.getTargets().add(blocker);
chance = true;
} else {
@@ -98,4 +125,40 @@ public class MustBlockAi extends SpellAbilityAi {
return chance;
}
+
+ private List determineGoodBlockers(final Card attacker, final Player ai, Player defender, SpellAbility sa,
+ final boolean onlyLethal, final boolean testTapped) {
+ final Card source = sa.getHostCard();
+ final TargetRestrictions abTgt = sa.getTargetRestrictions();
+
+ List list = Lists.newArrayList();
+ list = CardLists.filter(defender.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
+ list = CardLists.getTargetableCards(list, sa);
+ list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
+ list = CardLists.filter(list, new Predicate() {
+ @Override
+ public boolean apply(final Card c) {
+ boolean tapped = c.isTapped();
+ if (testTapped) {
+ c.setTapped(false);
+ }
+ if (!CombatUtil.canBlock(attacker, c)) {
+ return false;
+ }
+ if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, c, null, false)) {
+ return false;
+ }
+ if (onlyLethal && !ComputerUtilCombat.canDestroyBlocker(ai, c, attacker, null, false)) {
+ return false;
+ }
+ if (testTapped) {
+ c.setTapped(tapped);
+ }
+ return true;
+ }
+
+ });
+
+ return list;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java
index 24c989fc936..e89c9e87d3e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentAi.java
@@ -35,7 +35,7 @@ public class PermanentAi extends SpellAbilityAi {
final Card card = sa.getHostCard();
- if (card.hasStartOfKeyword("You may cast CARDNAME as though it had flash. If") && !ai.couldCastSorcery(sa)) {
+ if (card.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
// AiPlayDecision.AnotherTime
return false;
}
@@ -61,8 +61,23 @@ public class PermanentAi extends SpellAbilityAi {
if (card.getType().isLegendary()
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
if (ai.isCardInPlay(card.getName())) {
- // AiPlayDecision.WouldDestroyLegend
- return false;
+ if (!card.hasSVar("AILegendaryException")) {
+ // AiPlayDecision.WouldDestroyLegend
+ return false;
+ } else {
+ String specialRule = card.getSVar("AILegendaryException");
+ if ("TwoCopiesAllowed".equals(specialRule)) {
+ // One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
+ if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())).size() > 1) {
+ return false;
+ }
+ } else if ("AlwaysAllowed".equals(specialRule)) {
+ // Nothing to do here, check for Legendary is disabled
+ } else {
+ // Unknown hint, assume two copies not allowed
+ return false;
+ }
+ }
}
}
@@ -192,10 +207,10 @@ public class PermanentAi extends SpellAbilityAi {
// be better to have a pristine copy of the card - might not always be a correct assumption, but sounds
// like a reasonable default for some cards).
for (Card c : ctrld) {
- if (c.getEnchantedBy(false).isEmpty()) {
+ if (c.getEnchantedBy().isEmpty()) {
numControlled++;
} else {
- for (Card att : c.getEnchantedBy(false)) {
+ for (Card att : c.getEnchantedBy()) {
if (!att.getController().isOpponentOf(ai)) {
numControlled++;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
index 3f26a5b377f..1fa1a224098 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
@@ -1,19 +1,23 @@
package forge.ai.ability;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCard;
-import forge.ai.ComputerUtilCost;
+import com.google.common.base.Predicate;
+import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
+import forge.game.ability.ApiType;
import forge.game.card.Card;
+import forge.game.card.CardLists;
import forge.game.card.CardUtil;
+import forge.game.combat.Combat;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
+import forge.game.staticability.StaticAbility;
import forge.game.zone.ZoneType;
+import forge.util.MyRandom;
-/**
+/**
* AbilityFactory for Creature Spells.
*
*/
@@ -79,21 +83,122 @@ public class PermanentCreatureAi extends PermanentAi {
return false;
}
- // save cards with flash for surprise blocking
- if (card.hasKeyword("Flash")
- && (ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
+ // Flash logic
+ boolean advancedFlash = false;
+ if (ai.getController().isAI()) {
+ advancedFlash = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.FLASH_ENABLE_ADVANCED_LOGIC);
+ }
+ if (card.withFlash(ai)) {
+ if (advancedFlash) {
+ return doAdvancedFlashLogic(card, ai, sa);
+ } else {
+ // save cards with flash for surprise blocking
+ if ((ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
- && ai.getManaPool().totalMana() <= 0
- && (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
- && (!card.hasETBTrigger(true) || card.hasSVar("AmbushAI")) && game.getStack().isEmpty()
- && !ComputerUtil.castPermanentInMain1(ai, sa)) {
- // AiPlayDecision.AnotherTime;
- return false;
+ && ai.getManaPool().totalMana() <= 0
+ && (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS))
+ && (!card.hasETBTrigger(true) && !card.hasSVar("AmbushAI"))
+ && game.getStack().isEmpty()
+ && !ComputerUtil.castPermanentInMain1(ai, sa)) {
+ // AiPlayDecision.AnotherTime;
+ return false;
+ }
+ }
}
return super.checkPhaseRestrictions(ai, sa, ph);
}
+ private boolean doAdvancedFlashLogic(Card card, final Player ai, SpellAbility sa) {
+ Game game = ai.getGame();
+ PhaseHandler ph = game.getPhaseHandler();
+ Combat combat = game.getCombat();
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+
+ boolean isOppTurn = ph.getPlayerTurn().isOpponentOf(ai);
+ boolean isOwnEOT = ph.is(PhaseType.END_OF_TURN, ai);
+ boolean isEOTBeforeMyTurn = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
+ boolean isMyDeclareBlockers = ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai) && ai.getGame().getCombat() != null;
+ boolean isOppDeclareAttackers = ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS) && isOppTurn && ai.getGame().getCombat() != null;
+ boolean isMyMain1OrLater = ph.is(PhaseType.MAIN1, ai) || (ph.getPhase().isAfter(PhaseType.MAIN1) && ph.getPlayerTurn().equals(ai));
+ boolean canRespondToStack = false;
+ if (!game.getStack().isEmpty()) {
+ SpellAbility peekSa = game.getStack().peekAbility();
+ Player activator = peekSa.getActivatingPlayer();
+ if (activator != null && activator.isOpponentOf(ai) && peekSa.getApi() != ApiType.DestroyAll
+ && peekSa.getApi() != ApiType.DamageAll) {
+ canRespondToStack = true;
+ }
+ }
+
+ boolean hasETBTrigger = card.hasETBTrigger(true);
+ boolean hasAmbushAI = card.hasSVar("AmbushAI");
+ boolean defOnlyAmbushAI = hasAmbushAI && "BlockOnly".equals(card.getSVar("AmbushAI"));
+ boolean hasFloatMana = ai.getManaPool().totalMana() > 0;
+ boolean willDiscardNow = isOwnEOT && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
+ boolean willDieNow = combat != null && ComputerUtilCombat.lifeInSeriousDanger(ai, combat);
+ boolean wantToCastInMain1 = ph.is(PhaseType.MAIN1, ai) && ComputerUtil.castPermanentInMain1(ai, sa);
+
+ // figure out if the card might be a valuable blocker
+ boolean valuableBlocker = false;
+ if (combat != null && combat.getDefendingPlayers().contains(ai)) {
+ // Currently we use a rather simplistic assumption that if we're behind on creature count on board,
+ // a flashed in creature might prove to be good as an additional defender
+ int numUntappedPotentialBlockers = CardLists.filter(ai.getCreaturesInPlay(), new Predicate() {
+ @Override
+ public boolean apply(final Card card) {
+ return card.isUntapped() && !ComputerUtilCard.isUselessCreature(ai, card);
+ }
+ }).size();
+
+ if (combat.getAttackersOf(ai).size() > numUntappedPotentialBlockers) {
+ valuableBlocker = true;
+ }
+ }
+
+ int chanceToObeyAmbushAI = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_OBEY_AMBUSHAI);
+ int chanceToAddBlocker = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_CAST_AS_VALUABLE_BLOCKER);
+ int chanceToCastForETB = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_CAST_DUE_TO_ETB_EFFECTS);
+ int chanceToRespondToStack = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_RESPOND_TO_STACK_WITH_ETB);
+ int chanceToProcETBBeforeMain1 = aic.getIntProperty(AiProps.FLASH_CHANCE_TO_CAST_FOR_ETB_BEFORE_MAIN1);
+ boolean canCastAtOppTurn = true;
+ for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
+ for (StaticAbility s : c.getStaticAbilities()) {
+ if ("CantBeCast".equals(s.getParam("Mode")) && "True".equals(s.getParam("NonCasterTurn"))) {
+ canCastAtOppTurn = false;
+ }
+ }
+ }
+
+
+ if (hasFloatMana || willDiscardNow || willDieNow) {
+ // Will lose mana in pool or about to discard a card in cleanup or about to die in combat, so use this opportunity
+ return true;
+ } else if (wantToCastInMain1) {
+ // Would rather cast it in Main 1 or as soon as possible anyway, so go for it
+ return isMyMain1OrLater;
+ } else if (hasAmbushAI && MyRandom.percentTrue(chanceToObeyAmbushAI)) {
+ // Is an ambusher, so try to hold for declare blockers in combat where the AI defends, if possible
+ return defOnlyAmbushAI && canCastAtOppTurn ? isOppDeclareAttackers : (isOppDeclareAttackers || isMyDeclareBlockers);
+ } else if (valuableBlocker && isOppDeclareAttackers && MyRandom.percentTrue(chanceToAddBlocker)) {
+ // Might serve as a valuable blocker in a combat where we are behind on untapped blockers
+ return true;
+ } else if (hasETBTrigger && MyRandom.percentTrue(chanceToCastForETB)) {
+ // Instant speed is good when a card has an ETB trigger, but prolly don't cast in own turn before Main 1 not
+ // to mana lock the AI or lose the chance to consider other options. Try to utilize it as a response to stack
+ // if possible.
+ return isMyMain1OrLater || isOppTurn || MyRandom.percentTrue(chanceToProcETBBeforeMain1);
+ } else if (hasETBTrigger && canRespondToStack && MyRandom.percentTrue(chanceToRespondToStack)) {
+ // Try to do something meaningful in response to an opposing effect on stack. Note that this is currently
+ // too random to likely be meaningful, serious improvement might be needed.
+ return canCastAtOppTurn || ph.getPlayerTurn().equals(ai);
+ } else {
+ // Doesn't have a ETB trigger and doesn't seem to be good as an ambusher, try to surprise the opp before my turn
+ // TODO: maybe implement a way to reserve mana for this
+ return canCastAtOppTurn ? isEOTBeforeMyTurn : isOwnEOT;
+ }
+ }
+
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
index 89e3257d7f7..f023a718fb1 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
@@ -7,14 +7,14 @@ import forge.card.CardTypeView;
import forge.game.Game;
import forge.game.GameType;
import forge.game.ability.AbilityUtils;
-import forge.game.card.Card;
-import forge.game.card.CardCollection;
-import forge.game.card.CardLists;
+import forge.game.card.*;
import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
+import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -70,11 +70,28 @@ public class PlayAi extends SpellAbilityAi {
}
}
- if ("ReplaySpell".equals(logic)) {
- return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
+ // Ensure that if a ValidZone is specified, there's at least something to choose from in that zone.
+ CardCollectionView validOpts = new CardCollection();
+ if (sa.hasParam("ValidZone")) {
+ validOpts = AbilityUtils.filterListByType(game.getCardsIn(ZoneType.valueOf(sa.getParam("ValidZone"))),
+ sa.getParam("Valid"), sa);
+ if (validOpts.isEmpty()) {
+ return false;
+ }
}
- if (source != null && source.hasKeyword("Hideaway") && source.hasRemembered()) {
+ if ("ReplaySpell".equals(logic)) {
+ return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
+ } else if (logic.startsWith("NeedsChosenCard")) {
+ int minCMC = 0;
+ if (sa.getPayCosts() != null && sa.getPayCosts().getCostMana() != null) {
+ minCMC = sa.getPayCosts().getCostMana().getMana().getCMC();
+ }
+ validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC));
+ return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null) != null;
+ }
+
+ if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) {
// AI is not very good at playing non-permanent spells this way, at least yet
// (might be possible to enable it for Sorceries in Main1/Main2 if target is available,
// but definitely not for most Instants)
@@ -137,6 +154,15 @@ public class PlayAi extends SpellAbilityAi {
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
if (sa.hasParam("WithoutManaCost")) {
+ // Try to avoid casting instants and sorceries with X in their cost, since X will be assumed to be 0.
+ if (!(spell instanceof SpellPermanent)) {
+ if (spell.getPayCosts() != null
+ && spell.getPayCosts().getCostMana() != null
+ && spell.getPayCosts().getCostMana().getMana().countX() > 0) {
+ continue;
+ }
+ }
+
spell = (Spell) spell.copyWithNoManaCost();
} else if (sa.hasParam("PlayCost")) {
Cost abCost;
@@ -149,6 +175,13 @@ public class PlayAi extends SpellAbilityAi {
spell = (Spell) spell.copyWithDefinedCost(abCost);
}
if( AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
+ // Before accepting, see if the spell has a valid number of targets (it should at this point).
+ // Proceeding past this point if the spell is not correctly targeted will result
+ // in "Failed to add to stack" error and the card disappearing from the game completely.
+ if (!spell.isTargetNumberValid()) {
+ return false;
+ }
+
return true;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java
index dc6deb4ae86..6dd2e5aa767 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java
@@ -17,7 +17,6 @@ import forge.util.MyRandom;
import java.util.Collections;
import java.util.List;
-import java.util.Map;
public class PowerExchangeAi extends SpellAbilityAi {
@@ -38,8 +37,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- final Map vars = c.getSVars();
- return !vars.containsKey("RemAIDeck") && c.canBeTargetedBy(sa);
+ return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
}
});
CardLists.sortByPowerAsc(list);
diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java
index 214e5f5537a..a7030e1180f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java
@@ -175,6 +175,11 @@ public class ProtectAi extends SpellAbilityAi {
final List cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
if (cards.size() == 0) {
return false;
+ } else if (cards.size() == 1) {
+ // Affecting single card
+ if ((getProtectCreatures(ai, sa)).contains(cards.get(0))) {
+ return true;
+ }
}
/*
* when this happens we need to expand AI to consider if its ok
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
index 2d6234fee8b..928a2ed77c9 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
@@ -14,6 +14,7 @@ import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostTapType;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -35,12 +36,7 @@ public class PumpAi extends PumpAiBase {
if (cost == null) {
return true;
}
- for (final CostPart part : cost.getCostParts()) {
- if (part instanceof CostTapType) {
- return true;
- }
- }
- return false;
+ return cost.hasSpecificCostType(CostTapType.class);
}
@Override
@@ -190,7 +186,7 @@ public class PumpAi extends PumpAiBase {
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
- if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying")
+ if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING)
|| card.isToken()) {
return true;
}
@@ -243,7 +239,7 @@ public class PumpAi extends PumpAiBase {
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
- if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying")
+ if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING)
|| card.isToken()) {
return true;
}
@@ -320,6 +316,9 @@ public class PumpAi extends PumpAiBase {
}
} else {
defense = AbilityUtils.calculateAmount(sa.getHostCard(), numDefense, sa);
+ if (numDefense.contains("X") && sa.getSVar("X").equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
+ defense--; // the card will be spent casting the spell, so actual toughness is 1 less
+ }
}
int attack;
@@ -336,6 +335,9 @@ public class PumpAi extends PumpAiBase {
}
} else {
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
+ if (numAttack.contains("X") && sa.getSVar("X").equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
+ attack--; // the card will be spent casting the spell, so actual power is 1 less
+ }
}
if ("ContinuousBonus".equals(aiLogic)) {
@@ -485,6 +487,10 @@ public class PumpAi extends PumpAiBase {
} else if (sa.getParam("AILogic").equals("DonateTargetPerm")) {
// Donate step 2 - target a donatable permanent.
return SpecialCardAi.Donate.considerDonatingPermanent(ai, sa);
+ } else if (sa.getParam("AILogic").equals("SacOneEach")) {
+ // each player sacrifices one permanent, e.g. Vaevictis, Asmadi the Dire - grab the worst for allied and
+ // the best for opponents
+ return SacrificeAi.doSacOneEachLogic(ai, sa);
}
if (isFight) {
return FightAi.canFightAi(ai, sa, attack, defense);
@@ -775,7 +781,7 @@ public class PumpAi extends PumpAiBase {
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
if (source.isCreature()) {
- if (!source.hasKeyword("Indestructible") && source.getNetToughness() + defense <= source.getDamage()) {
+ if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) {
return false;
}
if (source.getNetToughness() + defense <= 0) {
@@ -864,7 +870,7 @@ public class PumpAi extends PumpAiBase {
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
- final boolean isInfect = source.hasKeyword("Infect"); // Flesh-Eater Imp
+ final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
@@ -970,7 +976,7 @@ public class PumpAi extends PumpAiBase {
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
- final boolean isInfect = source.hasKeyword("Infect");
+ final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
index ca1c04873dc..89af0bf5722 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
@@ -12,6 +12,7 @@ import forge.game.Game;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.phase.Untap;
@@ -135,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| card.getNetCombatDamage() <= 0
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| ph.getPhase().isBefore(PhaseType.MAIN1)
- || CardLists.getNotKeyword(ai.getCreaturesInPlay(), "Defender").isEmpty())) {
+ || CardLists.getNotKeyword(ai.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty())) {
return false;
}
if (!ph.isPlayerTurn(ai) && (combat == null || !combat.isAttacking(card) || card.getNetCombatDamage() <= 0)) {
@@ -193,21 +194,26 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.endsWith("Flying")) {
+ CardCollectionView attackingFlyer = CardCollection.EMPTY;
+ if (combat != null) {
+ attackingFlyer = CardLists.getKeyword(combat.getAttackers(), Keyword.FLYING);
+ }
+
if (ph.isPlayerTurn(opp)
&& ph.getPhase() == PhaseType.COMBAT_DECLARE_ATTACKERS
- && !CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty()
- && !card.hasKeyword("Reach")
+ && !attackingFlyer.isEmpty()
+ && !card.hasKeyword(Keyword.REACH)
&& CombatUtil.canBlock(card)
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
return true;
}
- Predicate flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
+ Predicate flyingOrReach = Predicates.or(CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH));
if (ph.isPlayerTurn(opp) && combat != null
- && Iterables.any(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))
+ && !attackingFlyer.isEmpty()
&& CombatUtil.canBlock(card)) {
// Use defensively to destroy the opposing Flying creature when possible, or to block with an indestructible
// creature buffed with Flying
- for (Card c : CardLists.filter(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))) {
+ for (Card c : attackingFlyer) {
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)
&& (card.getNetPower() >= c.getNetToughness() && card.getNetToughness() > c.getNetPower()
|| ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, card))) {
@@ -225,7 +231,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
} else if (keyword.endsWith("Horsemanship")) {
if (ph.isPlayerTurn(opp)
&& ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
- && !CardLists.getKeyword(game.getCombat().getAttackers(), "Horsemanship").isEmpty()
+ && !CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.HORSEMANSHIP).isEmpty()
&& CombatUtil.canBlock(card)
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
return true;
@@ -234,7 +240,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| newPower <= 0
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
- "Horsemanship").isEmpty()) {
+ Keyword.HORSEMANSHIP).isEmpty()) {
return false;
}
} else if (keyword.endsWith("Intimidate")) {
@@ -298,9 +304,9 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("First Strike")) {
- if (card.hasKeyword("Double Strike")) {
- return false;
- }
+ if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ return false;
+ }
if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) {
Card blocker = combat.getBlockers(card).get(0);
if (ComputerUtilCombat.canDestroyAttacker(ai, card, blocker, combat, true)
@@ -338,7 +344,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| newPower <= 0
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
- "Flanking").isEmpty()) {
+ Keyword.FLANKING).isEmpty()) {
return false;
}
} else if (keyword.startsWith("Trample")) {
@@ -353,7 +359,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (newPower <= 0) {
return false;
}
- if (combat != null && combat.isBlocking(card) && !card.hasKeyword("Wither")) {
+ if (combat != null && combat.isBlocking(card) && !card.hasKeyword(Keyword.WITHER)) {
return true;
}
if ((ph.isPlayerTurn(opp))
@@ -362,7 +368,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.endsWith("Wither")) {
- if (newPower <= 0 || card.hasKeyword("Infect")) {
+ if (newPower <= 0 || card.hasKeyword(Keyword.INFECT)) {
return false;
}
return combat != null && ( combat.isBlocking(card) || (combat.isAttacking(card) && combat.isBlocked(card)) );
@@ -375,14 +381,14 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (ph.isPlayerTurn(opp) || !CombatUtil.canAttack(card, opp)
|| newPower <= 0
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
- || CardLists.getNotKeyword(opp.getCreaturesInPlay(), "Defender").isEmpty()) {
+ || CardLists.getNotKeyword(opp.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty()) {
return false;
}
} else if (keyword.equals("Reach")) {
if (ph.isPlayerTurn(ai)
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
- || CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty()
- || card.hasKeyword("Flying")
+ || CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.FLYING).isEmpty()
+ || card.hasKeyword(Keyword.FLYING)
|| !CombatUtil.canBlock(card)) {
return false;
}
@@ -409,7 +415,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.equals("Persist")) {
- if (card.getBaseToughness() <= 1 || card.hasKeyword("Undying")) {
+ if (card.getBaseToughness() <= 1 || card.hasKeyword(Keyword.UNDYING)) {
return false;
}
} else if (keyword.equals("Islandwalk")) {
@@ -445,11 +451,15 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return false;
}
} else if (keyword.endsWith("CARDNAME can attack as though it didn't have defender.")) {
- if (!ph.isPlayerTurn(ai) || !card.hasKeyword("Defender")
+ if (!ph.isPlayerTurn(ai) || !card.hasKeyword(Keyword.DEFENDER)
|| ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)
|| card.isTapped() || newPower <= 0) {
return false;
}
+ } else if (keyword.equals("Prevent all combat damage that would be dealt to CARDNAME.")) {
+ if (combat == null || !(combat.isBlocking(card) || combat.isBlocked(card))) {
+ return false;
+ }
}
return true;
}
@@ -506,7 +516,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
if (c.getSVar("Targeting").equals("Dies") || c.getNetToughness() <= -defense) {
return true; // can kill indestructible creatures
}
- return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword("Indestructible"));
+ return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
}
}); // leaves all creatures that will be destroyed
} // -X/-X end
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
index 6c14f2f5748..dfa5db11642 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
@@ -14,6 +14,8 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
+import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -36,6 +38,15 @@ public class PumpAllAi extends PumpAiBase {
final Game game = ai.getGame();
final Combat combat = game.getCombat();
final Cost abCost = sa.getPayCosts();
+ final String logic = sa.getParamOrDefault("AILogic", "");
+
+ if (logic.equals("UntapCombatTrick")) {
+ PhaseHandler ph = ai.getGame().getPhaseHandler();
+ if (!(ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, ai)
+ || (!ph.getPlayerTurn().equals(ai) && ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)))) {
+ return false;
+ }
+ }
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
@@ -85,7 +96,7 @@ public class PumpAllAi extends PumpAiBase {
if (c.getNetToughness() <= -defense) {
return true; // can kill indestructible creatures
}
- return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible"));
+ return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
}
}); // leaves all creatures that will be destroyed
human = CardLists.filter(human, new Predicate() {
@@ -94,7 +105,7 @@ public class PumpAllAi extends PumpAiBase {
if (c.getNetToughness() <= -defense) {
return true; // can kill indestructible creatures
}
- return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible"));
+ return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
}
}); // leaves all creatures that will be destroyed
} // -X/-X end
diff --git a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java
index 13b30869d33..20f92caa50c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java
@@ -1,11 +1,20 @@
package forge.ai.ability;
-import forge.ai.ComputerUtil;
-import forge.ai.SpellAbilityAi;
+import forge.ai.*;
+import forge.game.ability.AbilityUtils;
+import forge.game.card.Card;
+import forge.game.card.CardLists;
+import forge.game.card.CardPredicates;
+import forge.game.phase.PhaseHandler;
+import forge.game.phase.PhaseType;
import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
+import forge.game.player.PlayerCollection;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
+import forge.game.zone.ZoneType;
+import forge.util.MyRandom;
public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
/* (non-Javadoc)
@@ -13,14 +22,22 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
- return sa.isMandatory(); // AI doesn't do anything with this SA yet, but at least it shouldn't miss mandatory triggers
- }
+ // Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
+ final PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
+ final Card source = sa.getHostCard();
- /* (non-Javadoc)
- * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
- */
- @Override
- protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+ if (source.isPermanent() && sa.getPayCosts() != null
+ && (sa.getPayCosts().hasTapCost() || sa.getPayCosts().hasManaCost())) {
+ // If it has an associated cost, try to only do this before own turn
+ if (!(ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer)) {
+ return false;
+ }
+ }
+
+ // Do it once per turn, generally (may be improved later)
+ if (AiCardMemory.isRememberedCardByName(aiPlayer, source.getName(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
+ return false;
+ }
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -28,21 +45,78 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
// ability is targeted
sa.resetTargets();
- Player opp = ComputerUtil.getOpponentFor(ai);
+ Player opp = ComputerUtil.getOpponentFor(aiPlayer);
+ final boolean canTgtAI = aiPlayer.canBeTargetedBy(sa);
final boolean canTgtHuman = opp.canBeTargetedBy(sa);
- if (!canTgtHuman) {
- return false;
- } else {
+ if (canTgtHuman && canTgtAI) {
+ // TODO: maybe some other consideration rather than random?
+ Player preferredTarget = MyRandom.percentTrue(50) ? aiPlayer : opp;
+ sa.getTargets().add(preferredTarget);
+ } else if (canTgtAI) {
+ sa.getTargets().add(aiPlayer);
+ } else if (canTgtHuman) {
sa.getTargets().add(opp);
+ } else {
+ return false; // could not find a valid target
+ }
+
+ if (!canTgtHuman || !canTgtAI) {
+ // can't target another player anyway, remember for no second activation this turn
+ AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
} else {
// if it's just defined, no big deal
+ AiCardMemory.rememberCard(aiPlayer, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
}
- // TODO: the AI currently doesn't do anything with this ability, consider improving.
- // For now, "true" is returned (without any action) if the SA is mandatory in order
- // not to miss triggers.
- return sa.isMandatory();
+ return true;
+ }
+
+ /* (non-Javadoc)
+ * @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
+ */
+ @Override
+ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+ // Specific details of ordering cards are handled by PlayerControllerAi#orderMoveToZoneList
+ return mandatory || canPlayAI(ai, sa);
+ }
+
+ /* (non-Javadoc)
+ * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
+ */
+ @Override
+ public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
+ // Confirming this action means shuffling the library if asked.
+
+ // First, let's check if we can play the top card of the library
+ PlayerCollection pc = sa.usesTargeting() ? new PlayerCollection(sa.getTargets().getTargetPlayers())
+ : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
+
+ int uncastableCMCThreshold = 2;
+ int minLandsToScryLandsAway = 4;
+ if (player.getController().isAI()) {
+ AiController aic = ((PlayerControllerAi)player.getController()).getAi();
+ minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
+ uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
+ }
+
+ Player p = pc.getFirst(); // FIXME: is this always a single target spell?
+ Card top = p.getCardsIn(ZoneType.Library).getFirst();
+ int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
+ int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC))
+ : top.getCMC();
+ int maxCastable = ComputerUtilMana.getAvailableManaEstimate(p, false);
+
+ if (!top.isLand() && cmc - maxCastable >= uncastableCMCThreshold) {
+ // Can't cast in the foreseeable future. Shuffle if doing it to ourselves or an ally, otherwise keep it
+ return !p.isOpponentOf(player);
+ } else if (top.isLand() && landsOTB <= minLandsToScryLandsAway){
+ // We don't want to give the opponent a free land if his land count is low
+ return p.isOpponentOf(player);
+ }
+
+ // Usually we don't want to shuffle if we arranged things carefully
+ return false;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java
index 79c634f2890..97ebb3004df 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SacrificeAi.java
@@ -4,10 +4,13 @@ import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
+import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
+import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
+import forge.game.keyword.Keyword;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
@@ -82,7 +85,7 @@ public class SacrificeAi extends SpellAbilityAi {
if (!destroy) {
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(sa));
} else {
- if (!CardLists.getKeyword(list, "Indestructible").isEmpty()) {
+ if (!CardLists.getKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
// human can choose to destroy indestructibles
return false;
}
@@ -132,7 +135,7 @@ public class SacrificeAi extends SpellAbilityAi {
List humanList =
CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
- // Since all of the cards have remAIDeck:True, I enabled 1 for 1
+ // Since all of the cards have AI:RemoveDeck:All, I enabled 1 for 1
// (or X for X) trades for special decks
if (humanList.size() < amount) {
return false;
@@ -156,4 +159,41 @@ public class SacrificeAi extends SpellAbilityAi {
return true;
}
+ public static boolean doSacOneEachLogic(Player ai, SpellAbility sa) {
+ Game game = ai.getGame();
+
+ sa.resetTargets();
+ for (Player p : game.getPlayers()) {
+ CardCollection targetable = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
+ if (!targetable.isEmpty()) {
+ CardCollection priorityTgts = new CardCollection();
+ if (p.isOpponentOf(ai)) {
+ priorityTgts.addAll(CardLists.filter(targetable, CardPredicates.canBeSacrificedBy(sa)));
+ if (!priorityTgts.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getBestAI(priorityTgts));
+ } else {
+ sa.getTargets().add(ComputerUtilCard.getBestAI(targetable));
+ }
+ } else {
+ for (Card c : targetable) {
+ if (c.canBeSacrificedBy(sa) && (c.hasSVar("SacMe") || (c.isCreature() && ComputerUtilCard.evaluateCreature(c) <= 135)) && !c.equals(sa.getHostCard())) {
+ priorityTgts.add(c);
+ }
+ }
+ if (!priorityTgts.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getWorstPermanentAI(priorityTgts, false, false, false, false));
+ } else {
+ targetable.remove(sa.getHostCard());
+ if (!targetable.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getWorstPermanentAI(targetable, true, true, true, false));
+ } else {
+ sa.getTargets().add(sa.getHostCard()); // sac self only as a last resort
+ }
+ }
+ }
+ }
+ }
+ return true;
+ }
+
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
index a831818c7da..ea10f386b9e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
@@ -11,7 +11,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
-import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -22,9 +21,8 @@ public class ScryAi extends SpellAbilityAi {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
- final TargetRestrictions tgt = sa.getTargetRestrictions();
- if (tgt != null) { // It doesn't appear that Scry ever targets
+ if (sa.usesTargeting()) { // It doesn't appear that Scry ever targets
// ability is targeted
sa.resetTargets();
diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
index 350f368a817..dc5c4f5e635 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
@@ -3,7 +3,6 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
-import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GlobalRuleChange;
@@ -30,31 +29,8 @@ public class SetStateAi extends SpellAbilityAi {
}
// Prevent transform into legendary creature if copy already exists
- // Check first if Legend Rule does still apply
- if (!aiPlayer.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
- if (!source.hasAlternateState()) {
- System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + ".");
- return false;
- }
-
- // check if the other side is legendary and if such Card already is in Play
- final CardState other = source.getAlternateState();
-
- if (other != null && other.getType().isLegendary() && aiPlayer.isCardInPlay(other.getName())) {
- if (!other.getType().isCreature()) {
- return false;
- }
-
- final Card othercard = aiPlayer.getCardsIn(ZoneType.Battlefield, other.getName()).getFirst();
-
- // for legendary KI counter creatures
- if (othercard.getCounters(CounterType.KI) >= source.getCounters(CounterType.KI)) {
- // if the other legendary is useless try to replace it
- if (!ComputerUtilCard.isUselessCreature(aiPlayer, othercard)) {
- return false;
- }
- }
- }
+ if (!isSafeToTransformIntoLegendary(aiPlayer, source)) {
+ return false;
}
if("Transform".equals(mode) || "Flip".equals(mode)) {
@@ -65,8 +41,6 @@ public class SetStateAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
- final Card source = sa.getHostCard();
-
return super.checkAiLogic(aiPlayer, sa, aiLogic);
}
@@ -87,7 +61,7 @@ public class SetStateAi extends SpellAbilityAi {
if("Transform".equals(mode)) {
if (!sa.usesTargeting()) {
// no Transform with Defined which is not Self
- if (source.hasKeyword("CARDNAME can't transform")) {
+ if (!source.canTransform()) {
return false;
}
return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
@@ -96,15 +70,13 @@ public class SetStateAi extends SpellAbilityAi {
sa.resetTargets();
CardCollection list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
- // select only cards with Transform as SplitType
+ // select only the ones that can transform
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(Card c) {
- return c.hasAlternateState() && c.getRules().getSplitType() == CardSplitType.Transform;
+ return c.canTransform();
}
});
- // select only the ones that can transform
- list = CardLists.getNotKeyword(list, "CARDNAME can't transform");
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {
@@ -260,8 +232,44 @@ public class SetStateAi extends SpellAbilityAi {
return valueCard <= valueTransformed;
}
+ private boolean isSafeToTransformIntoLegendary(Player aiPlayer, Card source) {
+ // Prevent transform into legendary creature if copy already exists
+ // Check first if Legend Rule does still apply
+ if (!aiPlayer.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
+ if (!source.hasAlternateState()) {
+ System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + ".");
+ return false;
+ }
+
+ // check if the other side is legendary and if such Card already is in Play
+ final CardState other = source.getAlternateState();
+
+ if (other != null && other.getType().isLegendary() && aiPlayer.isCardInPlay(other.getName())) {
+ if (!other.getType().isCreature()) {
+ return false;
+ }
+
+ final Card othercard = aiPlayer.getCardsIn(ZoneType.Battlefield, other.getName()).getFirst();
+
+ // for legendary KI counter creatures
+ if (othercard.getCounters(CounterType.KI) >= source.getCounters(CounterType.KI)) {
+ // if the other legendary is useless try to replace it
+ if (!ComputerUtilCard.isUselessCreature(aiPlayer, othercard)) {
+ return false;
+ }
+ }
+ }
+ }
+
+ return true;
+ }
+
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
// TODO: improve the AI for when it may want to transform something that's optional to transform
+ if (!isSafeToTransformIntoLegendary(player, sa.getHostCard())) {
+ return false;
+ }
+
return true;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java
index 30435464eb2..4f6bd05d10a 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ShuffleAi.java
@@ -8,6 +8,12 @@ import forge.game.spellability.SpellAbility;
public class ShuffleAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
+ String logic = sa.getParamOrDefault("AILogic", "");
+ if (logic.equals("Always")) {
+ // We may want to play this for the subability, e.g. Mind's Desire
+ return true;
+ }
+
// not really sure when the compy would use this; maybe only after a
// human
// deliberately put a card on top of their library
@@ -47,7 +53,7 @@ public class ShuffleAi extends SpellAbilityAi {
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
- // ai could analyze parameter denoting the player to shuffle
+ // ai could analyze parameter denoting the player to shuffle
return true;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
new file mode 100644
index 00000000000..23069294e94
--- /dev/null
+++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
@@ -0,0 +1,126 @@
+package forge.ai.ability;
+
+import forge.ai.*;
+import forge.game.card.Card;
+import forge.game.cost.Cost;
+import forge.game.cost.CostPayLife;
+import forge.game.phase.PhaseHandler;
+import forge.game.phase.PhaseType;
+import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
+import forge.game.spellability.SpellAbility;
+import forge.game.zone.ZoneType;
+import forge.util.MyRandom;
+
+public class SurveilAi extends SpellAbilityAi {
+
+ /*
+ * (non-Javadoc)
+ * @see forge.ai.SpellAbilityAi#doTriggerAINoCost(forge.game.player.Player, forge.game.spellability.SpellAbility, boolean)
+ */
+ @Override
+ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+
+ if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
+ sa.resetTargets();
+ sa.getTargets().add(ai);
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see forge.ai.SpellAbilityAi#chkAIDrawback(forge.game.spellability.SpellAbility, forge.game.player.Player)
+ */
+ @Override
+ public boolean chkAIDrawback(SpellAbility sa, Player ai) {
+ return doTriggerAINoCost(ai, sa, false);
+ }
+
+ /**
+ * Checks if the AI will play a SpellAbility based on its phase restrictions
+ */
+ @Override
+ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
+ // if the Surveil ability requires tapping and has a mana cost, it's best done at the end of opponent's turn
+ // and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
+ // try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
+ // even if there's no mana cost.
+ if (sa.getPayCosts() != null) {
+ if (sa.getPayCosts().hasTapCost()
+ && (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
+ && !SpellAbilityAi.isSorcerySpeed(sa)) {
+ return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
+ }
+ }
+
+ // in the player's turn Surveil should only be done in Main1 or in Upkeep if able
+ if (ph.isPlayerTurn(ai)) {
+ if (SpellAbilityAi.isSorcerySpeed(sa)) {
+ return ph.is(PhaseType.MAIN1) || sa.hasParam("Planeswalker");
+ } else {
+ return ph.is(PhaseType.UPKEEP);
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Checks if the AI will play a SpellAbility with the specified AiLogic
+ */
+ @Override
+ protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
+ final Card source = sa.getHostCard();
+
+ if ("Never".equals(aiLogic)) {
+ return false;
+ } else if ("Once".equals(aiLogic)) {
+ if (AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
+ return false;
+ }
+ }
+
+ // TODO: add card-specific Surveil AI logic here when/if necessary
+
+ return true;
+ }
+
+ @Override
+ protected boolean 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;
+ }
+
+ // Only Surveil for life when at decent amount of life remaining
+ final Cost cost = sa.getPayCosts();
+ 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;
+ }
+ }
+
+ double chance = .4; // 40 percent chance for instant speed
+ if (SpellAbilityAi.isSorcerySpeed(sa)) {
+ chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
+ }
+
+ boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
+ if (SpellAbilityAi.playReusable(ai, sa)) {
+ randomReturn = true;
+ }
+
+ if (randomReturn) {
+ AiCardMemory.rememberCard(ai, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
+ }
+
+ return randomReturn;
+ }
+
+ @Override
+ public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
+ return true;
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java
index 859e7a6757c..82b55dce044 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java
@@ -1,8 +1,6 @@
package forge.ai.ability;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCost;
-import forge.ai.SpellAbilityAi;
+import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CounterType;
@@ -26,12 +24,21 @@ public class TapAi extends TapAiBase {
if (turn.isOpponentOf(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
// Tap things down if it's Human's turn
- } else if (turn == ai && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
- // Tap creatures down if in combat -- handled in tapPrefTargeting().
- } else if (SpellAbilityAi.isSorcerySpeed(sa)) {
- // Cast it if it's a sorcery.
+ } else if (turn.equals(ai)) {
+ if (SpellAbilityAi.isSorcerySpeed(sa) && phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
+ // Cast it if it's a sorcery.
+ } else if (phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
+ // Aggro Brains are willing to use TapEffects aggressively instead of defensively
+ AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
+ if (!aic.getBooleanProperty(AiProps.PLAY_AGGRO)) {
+ return false;
+ }
+ } else {
+ // Don't tap down after blockers
+ return false;
+ }
} else if (!SpellAbilityAi.playReusable(ai, sa)){
- // Generally don't want to tap things with an Instant during AI turn outside of combat
+ // Generally don't want to tap things with an Instant during Players turn outside of combat
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
index b1d5d86fe09..4a193850e01 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
@@ -8,12 +8,16 @@ import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
-import forge.game.card.*;
+import forge.game.card.Card;
+import forge.game.card.CardCollection;
+import forge.game.card.CardLists;
+import forge.game.card.CardPredicates;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveCounter;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -41,14 +45,11 @@ import java.util.List;
* @version $Id: AbilityFactoryToken.java 17656 2012-10-22 19:32:56Z Max mtg $
*/
public class TokenAi extends SpellAbilityAi {
-
-
private String tokenAmount;
- private String tokenName;
- private String[] tokenTypes;
- private String[] tokenKeywords;
private String tokenPower;
private String tokenToughness;
+
+ private Card actualToken;
/**
*
* Constructor for AbilityFactory_Token.
@@ -57,23 +58,17 @@ public class TokenAi extends SpellAbilityAi {
* a {@link forge.game.ability.AbilityFactory} object.
*/
private void readParameters(final SpellAbility mapParams) {
- String[] keywords;
-
- if (mapParams.hasParam("TokenKeywords")) {
- // TODO: Change this Split to a semicolon or something else
- keywords = mapParams.getParam("TokenKeywords").split("<>");
- } else {
- keywords = new String[0];
- }
-
-
this.tokenAmount = mapParams.getParamOrDefault("TokenAmount", "1");
- this.tokenPower = mapParams.getParam("TokenPower");
- this.tokenToughness = mapParams.getParam("TokenToughness");
- this.tokenName = mapParams.getParam("TokenName");
- this.tokenTypes = mapParams.getParam("TokenTypes").split(",");
- this.tokenKeywords = keywords;
+ this.actualToken = TokenInfo.getProtoType(mapParams.getParam("TokenScript"), mapParams);
+
+ if (actualToken == null) {
+ this.tokenPower = mapParams.getParam("TokenPower");
+ this.tokenToughness = mapParams.getParam("TokenToughness");
+ } else {
+ this.tokenPower = actualToken.getBasePowerString();
+ this.tokenToughness = actualToken.getBaseToughnessString();
+ }
}
@Override
@@ -102,8 +97,11 @@ public class TokenAi extends SpellAbilityAi {
}
}
- final Card token = spawnToken(ai, sa);
- if (token == null) {
+ if (actualToken == null) {
+ actualToken = spawnToken(ai, sa);
+ }
+
+ if (actualToken == null) {
final AbilitySub sub = sa.getSubAbility();
if (pwPlus || (sub != null && SpellApiToAi.Converter.get(sub.getApi()).chkAIDrawback(sub, ai))) {
return true; // planeswalker plus ability or sub-ability is
@@ -129,24 +127,21 @@ public class TokenAi extends SpellAbilityAi {
}
}
- if (canInterruptSacrifice(ai, sa, token)) {
+ if (canInterruptSacrifice(ai, sa, actualToken)) {
return true;
}
- boolean haste = false;
+ boolean haste = this.actualToken.hasKeyword(Keyword.HASTE);
boolean oneShot = sa.getSubAbility() != null
&& sa.getSubAbility().getApi() == ApiType.DelayedTrigger;
- for (final String kw : this.tokenKeywords) {
- if (kw.equals("Haste")) {
- haste = true;
- }
- }
+ boolean isCreature = this.actualToken.getType().isCreature();
+
// Don't generate tokens without haste before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && ph.isPlayerTurn(ai) && !haste && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) {
boolean buff = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
- if ("Creature".equals(c.getSVar("BuffedBy"))) {
+ if (isCreature && "Creature".equals(c.getSVar("BuffedBy"))) {
buff = true;
}
}
@@ -179,12 +174,9 @@ public class TokenAi extends SpellAbilityAi {
}
// Don't kill AIs Legendary tokens
- for (final String type : this.tokenTypes) {
- if (type.equals("Legendary")) {
- if (ai.isCardInPlay(this.tokenName)) {
- return false;
- }
- }
+ if (this.actualToken.getType().isLegendary() && ai.isCardInPlay(this.actualToken.getName())) {
+ // TODO Check if Token is useless due to an aura or counters?
+ return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -200,29 +192,29 @@ public class TokenAi extends SpellAbilityAi {
if (sa.canTarget(ai)) {
sa.getTargets().add(ai);
} else {
- //Flash Foliage
- CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
- list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
- list = CardLists.getTargetableCards(list, sa);
- CardCollection betterList = CardLists.filter(list, new Predicate() {
- @Override
- public boolean apply(Card c) {
- return c.getLethalDamage() == 1;
- }
- });
- if (!betterList.isEmpty()) {
- list = betterList;
- }
- betterList = CardLists.getNotKeyword(list, "Trample");
- if (!betterList.isEmpty()) {
- list = betterList;
- }
- if (!list.isEmpty()) {
- sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
- } else {
- return false;
- }
-
+ // Flash Foliage
+ CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield),
+ ai.getOpponents());
+ list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
+ list = CardLists.getTargetableCards(list, sa);
+ CardCollection betterList = CardLists.filter(list, new Predicate() {
+ @Override
+ public boolean apply(Card c) {
+ return c.getLethalDamage() == 1;
+ }
+ });
+ if (!betterList.isEmpty()) {
+ list = betterList;
+ }
+ betterList = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
+ if (!betterList.isEmpty()) {
+ list = betterList;
+ }
+ if (!list.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(list));
+ } else {
+ return false;
+ }
}
}
}
@@ -310,6 +302,18 @@ public class TokenAi extends SpellAbilityAi {
}
}
+ if (mandatory) {
+ // Necessary because the AI goes into this method twice, first to set up targets (with mandatory=true)
+ // and then the second time to confirm the trigger (where mandatory may be set to false).
+ return true;
+ }
+
+ if ("OnlyOnAlliedAttack".equals(sa.getParam("AILogic"))) {
+ Combat combat = ai.getGame().getCombat();
+ return combat != null && combat.getAttackingPlayer() != null
+ && !combat.getAttackingPlayer().isOpponentOf(ai);
+ }
+
return true;
}
/* (non-Javadoc)
@@ -327,6 +331,7 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable options) {
// TODO: AILogic
+ readParameters(sa); // remember to call this somewhere!
Combat combat = ai.getGame().getCombat();
// TokenAttacking
if (combat != null && sa.hasParam("TokenAttacking")) {
@@ -346,6 +351,7 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected GameEntity chooseSinglePlayerOrPlaneswalker(Player ai, SpellAbility sa, Iterable options) {
// TODO: AILogic
+ readParameters(sa); // remember to call this somewhere!
Combat combat = ai.getGame().getCombat();
// TokenAttacking
if (combat != null && sa.hasParam("TokenAttacking")) {
@@ -376,6 +382,7 @@ public class TokenAi extends SpellAbilityAi {
* @param sa Token SpellAbility
* @return token creature created by ability
*/
+ @Deprecated
public static Card spawnToken(Player ai, SpellAbility sa) {
return spawnToken(ai, sa, false);
}
@@ -387,9 +394,18 @@ public class TokenAi extends SpellAbilityAi {
* @param notNull if the token would not survive, still return it
* @return token creature created by ability
*/
+ // TODO Is this just completely copied from TokenEffect? Let's just call that thing
+ @Deprecated
public static Card spawnToken(Player ai, SpellAbility sa, boolean notNull) {
final Card host = sa.getHostCard();
+ Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa);
+
+ if (result != null) {
+ result.setController(ai, 0);
+ return result;
+ }
+
String[] tokenKeywords = sa.hasParam("TokenKeywords") ? sa.getParam("TokenKeywords").split("<>") : new String[0];
String tokenPower = sa.getParam("TokenPower");
String tokenToughness = sa.getParam("TokenToughness");
@@ -510,7 +526,7 @@ public class TokenAi extends SpellAbilityAi {
// Apply static abilities and prune dead tokens
final Game game = ai.getGame();
ComputerUtilCard.applyStaticContPT(game, token, null);
- if (!notNull && token.getNetToughness() < 1) {
+ if (!notNull && token.isCreature() && token.getNetToughness() < 1) {
return null;
} else {
return token;
diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java
index 355c8989ac1..a0166598ed1 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UntapAllAi.java
@@ -5,6 +5,7 @@ import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
+import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
@@ -23,7 +24,7 @@ public class UntapAllAi extends SpellAbilityAi {
return false;
}
String valid = "";
- CardCollectionView list = aiPlayer.getGame().getCardsIn(ZoneType.Battlefield);
+ CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED);
if (sa.hasParam("ValidCards")) {
valid = sa.getParam("ValidCards");
}
@@ -35,6 +36,15 @@ public class UntapAllAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
+ Card source = sa.getHostCard();
+
+ if (sa.hasParam("ValidCards")) {
+ String valid = sa.getParam("ValidCards");
+ CardCollectionView list = CardLists.filter(aiPlayer.getGame().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.TAPPED);
+ list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa);
+ return mandatory || !list.isEmpty();
+ }
+
return mandatory;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java
index 952314a35f4..613ad8d8af9 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ZoneExchangeAi.java
@@ -39,7 +39,7 @@ public class ZoneExchangeAi extends SpellAbilityAi {
}
if (type.equals("Aura")) {
Card c = object1.getEnchantingCard();
- if (!c.canBeEnchantedBy(object2)) {
+ if (!c.canBeAttached(object2)) {
return false;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
index 33c614de03b..2ab4d392c64 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
@@ -13,6 +13,7 @@ import forge.LobbyPlayer;
import forge.ai.LobbyPlayerAi;
import forge.card.CardStateName;
import forge.game.Game;
+import forge.game.GameEntity;
import forge.game.GameObject;
import forge.game.GameObjectMap;
import forge.game.GameRules;
@@ -204,19 +205,16 @@ public class GameCopier {
}
}
gameObjectMap = new CopiedGameObjectMap(newGame);
+
for (Card card : origGame.getCardsIn(ZoneType.Battlefield)) {
Card otherCard = cardMap.get(card);
otherCard.setTimestamp(card.getTimestamp());
otherCard.setSickness(card.hasSickness());
otherCard.setState(card.getCurrentStateName(), false);
- if (card.isEnchanting()) {
- otherCard.setEnchanting(gameObjectMap.map(card.getEnchanting()));
- }
- if (card.isEquipping()) {
- otherCard.equipCard(cardMap.get(card.getEquipping()));
- }
- if (card.isFortifying()) {
- otherCard.setFortifying(cardMap.get(card.getFortifying()));
+ if (card.isAttachedToEntity()) {
+ GameEntity ge = gameObjectMap.map(card.getEntityAttachedTo());
+ otherCard.setEntityAttachedTo(ge);
+ ge.addAttachedCard(otherCard);
}
if (card.getCloneOrigin() != null) {
otherCard.setCloneOrigin(cardMap.get(card.getCloneOrigin()));
@@ -312,13 +310,13 @@ public class GameCopier {
newCard.setManifested(true);
// TODO: Should be able to copy other abilities...
if (isCreature && hasManaCost) {
- newCard.addSpellAbility(CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
+ newCard.getState(CardStateName.Original).addSpellAbility(
+ CardFactoryUtil.abilityManifestFaceUp(newCard, newCard.getManaCost()));
}
}
}
if (c.isMonstrous()) {
newCard.setMonstrous(true);
- newCard.setMonstrosityNum(c.getMonstrosityNum());
}
if (c.isRenowned()) {
newCard.setRenowned(true);
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java
index 5974e3e6637..e32de189652 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java
@@ -47,7 +47,12 @@ public class GameStateEvaluator {
}
private Score getScoreForGameOver(Game game, Player aiPlayer) {
- return game.getOutcome().getWinningTeam() == aiPlayer.getTeam() ? new Score(Integer.MAX_VALUE) : new Score(Integer.MIN_VALUE);
+ if (game.getOutcome().getWinningTeam() == aiPlayer.getTeam() ||
+ game.getOutcome().isWinner(aiPlayer.getRegisteredPlayer())) {
+ return new Score(Integer.MAX_VALUE);
+ }
+
+ return new Score(Integer.MIN_VALUE);
}
public Score getScoreForGameState(Game game, Player aiPlayer) {
diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
index d9f9db03937..210930785b7 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
@@ -140,13 +140,13 @@ public class SpellAbilityPicker {
}
}
- private static boolean isSorcerySpeed(SpellAbility sa) {
+ private static boolean isSorcerySpeed(SpellAbility sa, Player player) {
// TODO: Can we use the actual rules engine for this instead of trying to do the logic ourselves?
if (sa instanceof PlayLandAbility) {
return false;
}
if (sa.isSpell()) {
- return !sa.getHostCard().isInstant() && !sa.getHostCard().hasKeyword("Flash");
+ return !sa.getHostCard().isInstant() && !sa.getHostCard().withFlash(player);
}
if (sa.getRestrictions().isPwAbility()) {
return !sa.getHostCard().hasKeyword("CARDNAME's loyalty abilities can be activated at instant speed.");
@@ -167,7 +167,7 @@ public class SpellAbilityPicker {
if (currentPhase.isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
List candidateSAs2 = new ArrayList();
for (SpellAbility sa : candidateSAs) {
- if (!isSorcerySpeed(sa)) {
+ if (!isSorcerySpeed(sa, player)) {
System.err.println("Not sorcery: " + sa);
candidateSAs2.add(sa);
}
diff --git a/forge-core/.classpath b/forge-core/.classpath
deleted file mode 100644
index f7bf6c0c0ab..00000000000
--- a/forge-core/.classpath
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/forge-core/.project b/forge-core/.project
deleted file mode 100644
index 134e5f60689..00000000000
--- a/forge-core/.project
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
- forge-core
-
-
-
-
-
- org.eclipse.jdt.core.javabuilder
-
-
-
-
- org.eclipse.m2e.core.maven2Builder
-
-
-
-
-
- org.eclipse.jdt.core.javanature
- org.eclipse.m2e.core.maven2Nature
-
-
diff --git a/forge-core/.settings/org.eclipse.core.resources.prefs b/forge-core/.settings/org.eclipse.core.resources.prefs
deleted file mode 100644
index ebf9f8ffb65..00000000000
--- a/forge-core/.settings/org.eclipse.core.resources.prefs
+++ /dev/null
@@ -1,4 +0,0 @@
-eclipse.preferences.version=1
-encoding//src/main/java=ISO-8859-1
-encoding//src/test/java=ISO-8859-1
-encoding/=UTF-8
diff --git a/forge-core/.settings/org.eclipse.jdt.core.prefs b/forge-core/.settings/org.eclipse.jdt.core.prefs
deleted file mode 100644
index ec4300d5d09..00000000000
--- a/forge-core/.settings/org.eclipse.jdt.core.prefs
+++ /dev/null
@@ -1,5 +0,0 @@
-eclipse.preferences.version=1
-org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7
-org.eclipse.jdt.core.compiler.compliance=1.7
-org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
-org.eclipse.jdt.core.compiler.source=1.7
diff --git a/forge-core/.settings/org.eclipse.m2e.core.prefs b/forge-core/.settings/org.eclipse.m2e.core.prefs
deleted file mode 100644
index f897a7f1cb2..00000000000
--- a/forge-core/.settings/org.eclipse.m2e.core.prefs
+++ /dev/null
@@ -1,4 +0,0 @@
-activeProfiles=
-eclipse.preferences.version=1
-resolveWorkspaceProjects=true
-version=1
diff --git a/forge-core/pom.xml b/forge-core/pom.xml
index 2d501581046..b580dde38d0 100644
--- a/forge-core/pom.xml
+++ b/forge-core/pom.xml
@@ -6,7 +6,7 @@
forge
forge
- 1.6.10-SNAPSHOT
+ 1.6.23-SNAPSHOT
forge-core
@@ -16,7 +16,7 @@
com.google.guava
guava
- 16.0.1
+ 24.1-android
org.apache.commons
@@ -24,4 +24,5 @@
3.7
+
diff --git a/forge-core/src/main/java/forge/CardStorageReader.java b/forge-core/src/main/java/forge/CardStorageReader.java
index 0f99bf6b87d..92b30bce7ac 100644
--- a/forge-core/src/main/java/forge/CardStorageReader.java
+++ b/forge-core/src/main/java/forge/CardStorageReader.java
@@ -413,6 +413,9 @@ public class CardStorageReader {
return reader.readCard(lines, Files.getNameWithoutExtension(file.getName()));
} catch (final FileNotFoundException ex) {
throw new RuntimeException("CardReader : run error -- file not found: " + file.getPath(), ex);
+ } catch (final Exception ex) {
+ System.out.println("Error loading cardscript " + file.getName() + ". Please close Forge and resolve this.");
+ throw ex;
} finally {
try {
assert fileInputStream != null;
diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java
index 546b5413d59..9e8629baf20 100644
--- a/forge-core/src/main/java/forge/StaticData.java
+++ b/forge-core/src/main/java/forge/StaticData.java
@@ -34,8 +34,11 @@ public class StaticData {
private final CardEdition.Collection editions;
private Predicate standardPredicate;
+ private Predicate brawlPredicate;
private Predicate modernPredicate;
+ private boolean filteredHandsEnabled = false;
+
// Loaded lazily:
private IStorage boosters;
private IStorage specialBoosters;
@@ -196,12 +199,26 @@ public class StaticData {
public void setStandardPredicate(Predicate standardPredicate) { this.standardPredicate = standardPredicate; }
+ public void setBrawlPredicate(Predicate brawlPredicate) { this.brawlPredicate = brawlPredicate; }
+
public void setModernPredicate(Predicate modernPredicate) { this.modernPredicate = standardPredicate; }
public Predicate getModernPredicate() {
return modernPredicate;
}
+ public Predicate getBrawlPredicate() {
+ return brawlPredicate;
+ }
+
+ public void setFilteredHandsEnabled(boolean filteredHandsEnabled){
+ this.filteredHandsEnabled = filteredHandsEnabled;
+ }
+
+ public boolean getFilteredHandsEnabled(){
+ return filteredHandsEnabled;
+ }
+
public PaperCard getCardByEditionDate(PaperCard card, Date editionDate) {
PaperCard c = this.getCommonCards().getCardFromEdition(card.getName(), editionDate, CardDb.SetPreference.LatestCoreExp, card.getArtIndex());
diff --git a/forge-core/src/main/java/forge/card/CardChangedType.java b/forge-core/src/main/java/forge/card/CardChangedType.java
index ad4182bce66..6e855548598 100644
--- a/forge-core/src/main/java/forge/card/CardChangedType.java
+++ b/forge-core/src/main/java/forge/card/CardChangedType.java
@@ -32,16 +32,24 @@ public class CardChangedType {
private final boolean removeSuperTypes;
private final boolean removeCardTypes;
private final boolean removeSubTypes;
+ private final boolean removeLandTypes;
private final boolean removeCreatureTypes;
+ private final boolean removeArtifactTypes;
+ private final boolean removeEnchantmentTypes;
public CardChangedType(final CardType addType0, final CardType removeType0, final boolean removeSuperType0,
- final boolean removeCardType0, final boolean removeSubType0, final boolean removeCreatureType0) {
+ final boolean removeCardType0, final boolean removeSubType0, final boolean removeLandType0,
+ final boolean removeCreatureType0, final boolean removeArtifactType0,
+ final boolean removeEnchantmentTypes0) {
addType = addType0;
removeType = removeType0;
removeSuperTypes = removeSuperType0;
removeCardTypes = removeCardType0;
removeSubTypes = removeSubType0;
+ removeLandTypes = removeLandType0;
removeCreatureTypes = removeCreatureType0;
+ removeArtifactTypes = removeArtifactType0;
+ removeEnchantmentTypes = removeEnchantmentTypes0;
}
public final CardType getAddType() {
@@ -64,7 +72,19 @@ public class CardChangedType {
return removeSubTypes;
}
+ public final boolean isRemoveLandTypes() {
+ return removeLandTypes;
+ }
+
public final boolean isRemoveCreatureTypes() {
return removeCreatureTypes;
}
+
+ public final boolean isRemoveArtifactTypes() {
+ return removeArtifactTypes;
+ }
+
+ public final boolean isRemoveEnchantmentTypes() {
+ return removeEnchantmentTypes;
+ }
}
diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java
index 022a915fdb8..cf9a8b7b4b1 100644
--- a/forge-core/src/main/java/forge/card/CardEdition.java
+++ b/forge-core/src/main/java/forge/card/CardEdition.java
@@ -21,7 +21,6 @@ import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-
import forge.StaticData;
import forge.card.CardDb.SetPreference;
import forge.deck.CardPool;
@@ -31,7 +30,6 @@ import forge.util.*;
import forge.util.storage.StorageBase;
import forge.util.storage.StorageReaderBase;
import forge.util.storage.StorageReaderFolder;
-
import org.apache.commons.lang3.StringUtils;
import java.io.File;
@@ -123,12 +121,19 @@ public final class CardEdition implements Comparable { // immutable
private boolean smallSetOverride = false;
private String boosterMustContain = "";
private final CardInSet[] cards;
+ private final Map