diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java
index 4095b830b0d..319bb369fb5 100644
--- a/forge-ai/src/main/java/forge/ai/AiAttackController.java
+++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java
@@ -76,7 +76,7 @@ public class AiAttackController {
private Player defendingOpponent;
private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances
- private final boolean nextTurn;
+ private final boolean nextTurn; // include creature that can only attack/block next turn
/**
*
@@ -90,22 +90,22 @@ public class AiAttackController {
public AiAttackController(final Player ai, boolean nextTurn) {
this.ai = ai;
- this.defendingOpponent = choosePreferredDefenderPlayer(ai);
- this.oppList = getOpponentCreatures(this.defendingOpponent);
- this.myList = ai.getCreaturesInPlay();
+ defendingOpponent = choosePreferredDefenderPlayer(ai);
+ this.oppList = getOpponentCreatures(defendingOpponent);
+ myList = ai.getCreaturesInPlay();
this.nextTurn = nextTurn;
- refreshAttackers(this.defendingOpponent);
+ refreshAttackers(defendingOpponent);
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
} // overloaded constructor to evaluate attackers that should attack next turn
public AiAttackController(final Player ai, Card attacker) {
this.ai = ai;
- this.defendingOpponent = choosePreferredDefenderPlayer(ai);
- this.oppList = getOpponentCreatures(this.defendingOpponent);
- this.myList = ai.getCreaturesInPlay();
+ defendingOpponent = choosePreferredDefenderPlayer(ai);
+ this.oppList = getOpponentCreatures(defendingOpponent);
+ myList = ai.getCreaturesInPlay();
this.nextTurn = false;
this.attackers = new ArrayList<>();
- if (CombatUtil.canAttack(attacker, this.defendingOpponent)) {
+ if (CombatUtil.canAttack(attacker, defendingOpponent)) {
attackers.add(attacker);
}
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
@@ -168,7 +168,7 @@ public class AiAttackController {
if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players
// TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked
- return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
+ return Aggregates.random(ai.getOpponents());
}
return defender;
}
@@ -199,7 +199,7 @@ public class AiAttackController {
}
return list;
- } // sortAttackers()
+ }
// Is there any reward for attacking? (for 0/1 creatures there is not)
/**
@@ -254,7 +254,7 @@ public class AiAttackController {
onlyIfExalted = true;
}
- if (!onlyIfExalted || this.attackers.size() == 1 || this.aiAggression == 6 /* 6 is Exalted attack */) {
+ if (!onlyIfExalted || this.attackers.size() == 1 || aiAggression == 6 /* 6 is Exalted attack */) {
return true;
}
}
@@ -282,14 +282,12 @@ public class AiAttackController {
}
public final static List getPossibleBlockers(final List blockers, final List attackers, final boolean nextTurn) {
- List possibleBlockers = new ArrayList<>(blockers);
- possibleBlockers = CardLists.filter(possibleBlockers, new Predicate() {
+ return CardLists.filter(blockers, new Predicate() {
@Override
public boolean apply(final Card c) {
return canBlockAnAttacker(c, attackers, nextTurn);
}
});
- return possibleBlockers;
}
public final static boolean canBlockAnAttacker(final Card c, final List attackers, final boolean nextTurn) {
@@ -310,10 +308,7 @@ public class AiAttackController {
}
// this checks to make sure that the computer player doesn't lose when the human player attacks
- public final List notNeededAsBlockers(final Player ai, final List attackers) {
- final List notNeededAsBlockers = new ArrayList<>(attackers);
- int fixedBlockers = 0;
- final List vigilantes = new ArrayList<>();
+ public final List notNeededAsBlockers(final List attackers) {
//check for time walks
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
return attackers;
@@ -329,83 +324,124 @@ public class AiAttackController {
}
}
attackers.removeAll(toRemove);
-
return attackers;
}
- List opponentsAttackers = new ArrayList<>(oppList);
- opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate() {
- @Override
- public boolean apply(final Card c) {
- return c.getNetCombatDamage() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
- }
- });
- for (final Card c : this.myList) {
+ final List vigilantes = new ArrayList<>();
+ for (final Card c : myList) {
if (c.getName().equals("Masako the Humorless")) {
// "Tapped creatures you control can block as though they were untapped."
return attackers;
}
- if (!attackers.contains(c)) { // this creature can't attack anyway
- if (canBlockAnAttacker(c, opponentsAttackers, false)) {
- fixedBlockers++;
- }
- continue;
- }
// no need to block if an effect is in play which untaps all creatures
- // (pseudo-Vigilance akin to Awakening or or Prophet of Kruphix)
+ // (pseudo-Vigilance akin to Awakening or Prophet of Kruphix)
if (c.hasKeyword(Keyword.VIGILANCE) || ComputerUtilCard.willUntap(ai, c)) {
vigilantes.add(c);
- notNeededAsBlockers.remove(c); // they will be re-added later
- if (canBlockAnAttacker(c, opponentsAttackers, false)) {
- fixedBlockers++;
+ }
+ }
+ // reduce the search space
+ final List opponentsAttackers = CardLists.filter(ai.getOpponents().getCreaturesInPlay(), new Predicate() {
+ @Override
+ public boolean apply(final Card c) {
+ return !c.hasSVar("EndOfTurnLeavePlay")
+ && (c.toughnessAssignsDamage() || c.getNetCombatDamage() > 0 // performance shortcuts
+ || c.getNetCombatDamage() + ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true) > 0)
+ && ComputerUtilCombat.canAttackNextTurn(c);
+ }
+ });
+
+ final List notNeededAsBlockers = new CardCollection(attackers);
+
+ // don't hold back creatures that can't block any of the human creatures
+ final List blockers = getPossibleBlockers(attackers, opponentsAttackers, true);
+
+ if (!blockers.isEmpty()) {
+ notNeededAsBlockers.removeAll(blockers);
+
+ boolean playAggro = false;
+ boolean pilotsNonAggroDeck = false;
+ if (ai.getController().isAI()) {
+ PlayerControllerAi aic = ((PlayerControllerAi) ai.getController());
+ pilotsNonAggroDeck = aic.pilotsNonAggroDeck();
+ playAggro = !pilotsNonAggroDeck || aic.getAi().getBooleanProperty(AiProps.PLAY_AGGRO);
+ }
+ // TODO make switchable via AI property
+ int thresholdMod = 0;
+ int lastAcceptableBaselineLife = 0;
+ if (pilotsNonAggroDeck) {
+ lastAcceptableBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers));
+ if (!ai.isCardInPlay("Laboratory Maniac")) {
+ // AI is getting milled out
+ thresholdMod += 3 - Math.min(ai.getCardsIn(ZoneType.Library).size(), 3);
+ }
+ if (aiAggression > 4) {
+ thresholdMod += 1;
+ }
+ }
+
+ // try to use strongest as attacker first
+ CardLists.sortByPowerDesc(blockers);
+
+ for (Card c : blockers) {
+ if (vigilantes.contains(c)) {
+ // TODO predict the chance it might die if attacking
+ continue;
+ }
+ notNeededAsBlockers.add(c);
+ int currentBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers));
+ // AI doesn't know from what it will lose, so it might still keep an unnecessary blocker back sometimes
+ if (currentBaselineLife == Integer.MIN_VALUE) {
+ notNeededAsBlockers.remove(c);
+ break;
+ }
+
+ // in Aggro Decks AI wants to deal as much damage as it can
+ if (pilotsNonAggroDeck) {
+ int ownAttackerDmg = c.getNetCombatDamage();
+ // TODO maybe add performance switch to skip these predictions?
+ if (c.toughnessAssignsDamage()) {
+ ownAttackerDmg += ComputerUtilCombat.predictToughnessBonusOfAttacker(c, null, null, true);
+ } else {
+ ownAttackerDmg += ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true);
+ }
+ if (c.hasDoubleStrike()) {
+ ownAttackerDmg *= 2;
+ }
+ ownAttackerDmg += thresholdMod;
+ // bail if it would cause AI more life loss from counterattack than the damage it provides as attacker
+ if (Math.abs(currentBaselineLife - lastAcceptableBaselineLife) > ownAttackerDmg) {
+ notNeededAsBlockers.remove(c);
+ // try find more
+ continue;
+ } else if (Math.abs(currentBaselineLife - lastAcceptableBaselineLife) == ownAttackerDmg) {
+ // TODO add non sim-AI property for life trade chance that scales down with amount and when difference increases
+ }
+ lastAcceptableBaselineLife = currentBaselineLife;
}
}
}
- CardLists.sortByPowerAsc(attackers);
- int blockersNeeded = opponentsAttackers.size();
- // don't hold back creatures that can't block any of the human creatures
- final List list = getPossibleBlockers(attackers, opponentsAttackers, nextTurn);
-
- //Calculate the amount of creatures necessary
- for (int i = 0; i < list.size(); i++) {
- if (!doesHumanAttackAndWin(ai, i)) {
- blockersNeeded = i;
- break;
- }
- }
- int blockersStillNeeded = blockersNeeded - fixedBlockers;
- blockersStillNeeded = Math.min(blockersStillNeeded, list.size());
- for (int i = 0; i < blockersStillNeeded; i++) {
- notNeededAsBlockers.remove(list.get(i));
- }
-
- // re-add creatures with vigilance
+ // these creatures will be available to block anyway
notNeededAsBlockers.addAll(vigilantes);
- if (blockersNeeded > 1) {
- return notNeededAsBlockers;
- }
-
- final Player opp = this.defendingOpponent;
-
// Increase the total number of blockers needed by 1 if Finest Hour in play
// (human will get an extra first attack with a creature that untaps)
// In addition, if the computer guesses it needs no blockers, make sure
// that it won't be surprised by Exalted
- final int humanExaltedBonus = opp.countExaltedBonus();
+ final int humanExaltedBonus = defendingOpponent.countExaltedBonus();
+ int blockersNeeded = attackers.size() - notNeededAsBlockers.size();
if (humanExaltedBonus > 0) {
- final boolean finestHour = opp.isCardInPlay("Finest Hour");
+ final boolean finestHour = defendingOpponent.isCardInPlay("Finest Hour");
- if ((blockersNeeded == 0 || finestHour) && !this.oppList.isEmpty()) {
+ if ((blockersNeeded <= 0 || finestHour) && !this.oppList.isEmpty()) {
// total attack = biggest creature + exalted, *2 if Rafiq is in play
int humanBasePower = ComputerUtilCombat.getAttack(this.oppList.get(0)) + humanExaltedBonus;
if (finestHour) {
// For Finest Hour, one creature could attack and get the bonus TWICE
humanBasePower += humanExaltedBonus;
}
- final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower
+ final int totalExaltedAttack = defendingOpponent.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower
: humanBasePower;
if (ai.getLife() - 3 <= totalExaltedAttack) {
// We will lose if there is an Exalted attack -- keep one blocker
@@ -423,45 +459,7 @@ public class AiAttackController {
return notNeededAsBlockers;
}
- public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) {
- int totalAttack = 0;
- int totalPoison = 0;
- int blockersLeft = nBlockingCreatures;
-
- if (ai.cantLose()) {
- return false;
- }
-
- // TODO for multiplayer this should either add some heuristics
- // so the other opponents attack power is also measured in
- // or refactor it with aiLifeInDanger somehow if performance impact isn't too bad
- CardLists.sortByPowerDesc(oppList);
- for (Card attacker : oppList) {
- if (!ComputerUtilCombat.canAttackNextTurn(attacker)) {
- continue;
- }
- if (blockersLeft > 0 && CombatUtil.canBeBlocked(attacker, ai)) {
- // TODO doesn't take trample into account
- blockersLeft--;
- continue;
- }
-
- // Test for some special triggers that can change the creature in combat
- Card effectiveAttacker = ComputerUtilCombat.applyPotentialAttackCloneTriggers(attacker);
-
- // TODO commander
-
- totalAttack += ComputerUtilCombat.damageIfUnblocked(effectiveAttacker, ai, null, false);
- totalPoison += ComputerUtilCombat.poisonIfUnblocked(effectiveAttacker, ai);
- }
-
- if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) {
- return true;
- }
- return ai.getPoisonCounters() + totalPoison > 9 && ai.canReceiveCounters(CounterEnumType.POISON);
- }
-
- private boolean doAssault(final Player ai) {
+ private boolean doAssault() {
// Beastmaster Ascension
if (ai.isCardInPlay("Beastmaster Ascension") && this.attackers.size() > 1) {
final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension");
@@ -510,8 +508,6 @@ public class AiAttackController {
}
}
- final Player opp = this.defendingOpponent;
-
// if true, the AI will attempt to identify which blockers will already be taken,
// thus attempting to predict how many creatures with evasion can actively block
boolean predictEvasion = false;
@@ -599,15 +595,14 @@ public class AiAttackController {
}
}
- int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage;
- int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp);
-
- if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife()
- && !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) {
+ int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, defendingOpponent) + trampleDamage;
+ if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, defendingOpponent) >= defendingOpponent.getLife()
+ && !((defendingOpponent.cantLoseForZeroOrLessLife() || ai.cantWin()) && defendingOpponent.getLife() < 1)) {
return true;
}
- if (totalPoisonDamage >= 10 - opp.getPoisonCounters()) {
+ int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, defendingOpponent);
+ if (totalPoisonDamage >= 10 - defendingOpponent.getPoisonCounters()) {
return true;
}
@@ -619,7 +614,7 @@ public class AiAttackController {
if (defs.size() == 1) {
return defs.getFirst();
}
- GameEntity prefDefender = defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0);
+ GameEntity prefDefender = defs.contains(defendingOpponent) ? defendingOpponent : defs.get(0);
// Attempt to see if there's a defined entity that must be attacked strictly this turn...
GameEntity entity = ai.getMustAttackEntityThisTurn();
@@ -660,7 +655,7 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object.
*/
public final int declareAttackers(final Combat combat) {
- final boolean bAssault = doAssault(ai);
+ final boolean bAssault = doAssault();
// Determine who will be attacked
GameEntity defender = chooseDefender(combat, bAssault);
@@ -755,11 +750,11 @@ public class AiAttackController {
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro);
}
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily
- if (!doRevengeOfRavensAttackLogic(ai, defender, attackersLeft, numForcedAttackers, attackMax)) {
+ if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers, attackMax)) {
return aiAggression;
}
- if (bAssault && defender == this.defendingOpponent) { // in case we are forced to attack someone else
+ if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else
if (LOG_AI_ATTACKS)
System.out.println("Assault");
CardLists.sortByPowerDesc(attackersLeft);
@@ -807,9 +802,9 @@ public class AiAttackController {
CardLists.sortByPowerDesc(this.attackers);
if (LOG_AI_ATTACKS)
System.out.println("Exalted");
- this.aiAggression = 6;
+ aiAggression = 6;
for (Card attacker : this.attackers) {
- if (canAttackWrapper(attacker, defender) && shouldAttack(ai, attacker, this.blockers, combat, defender)) {
+ if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) {
combat.addAttacker(attacker, defender);
return aiAggression;
}
@@ -820,12 +815,12 @@ public class AiAttackController {
if (attackMax != -1) {
// should attack with only max if able.
CardLists.sortByPowerDesc(this.attackers);
- this.aiAggression = 6;
+ aiAggression = 6;
for (Card attacker : this.attackers) {
// reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
break;
- if (canAttackWrapper(attacker, defender) && shouldAttack(ai, attacker, this.blockers, combat, defender)) {
+ if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) {
combat.addAttacker(attacker, defender);
}
}
@@ -833,6 +828,7 @@ public class AiAttackController {
return aiAggression;
}
+ // TODO move this lower so it can also switch defender
if (simAI && ComputerUtilCard.isNonDisabledCardInPlay(ai, "Reconnaissance")) {
for (Card attacker : attackersLeft) {
if (canAttackWrapper(attacker, defender)) {
@@ -841,7 +837,7 @@ public class AiAttackController {
}
}
// safe to exert
- this.aiAggression = 6;
+ aiAggression = 6;
return aiAggression;
}
@@ -862,7 +858,7 @@ public class AiAttackController {
// get the potential damage and strength of the AI forces
final List candidateAttackers = new ArrayList<>();
int candidateUnblockedDamage = 0;
- for (final Card pCard : this.myList) {
+ for (final Card pCard : myList) {
// if the creature can attack then it's a potential attacker this
// turn, assume summoning sickness creatures will be able to
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
@@ -991,7 +987,7 @@ public class AiAttackController {
boolean isUnblockableCreature = true;
// check blockers individually, as the bulk canBeBlocked doesn't
// check all circumstances
- for (final Card blocker : this.myList) {
+ for (final Card blocker : myList) {
if (CombatUtil.canBlock(attacker, blocker, true)) {
isUnblockableCreature = false;
break;
@@ -1015,10 +1011,10 @@ public class AiAttackController {
// totals and other considerations some bad "magic numbers" here
// TODO replace with nice descriptive variable names
if (ratioDiff > 0 && doAttritionalAttack) {
- this.aiAggression = 5; // attack at all costs
+ aiAggression = 5; // attack at all costs
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
- this.aiAggression = 4; // attack expecting to trade or damage player.
+ aiAggression = 4; // attack expecting to trade or damage player.
} else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
&& defendingOpponent != null
&& ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent)
@@ -1028,25 +1024,25 @@ public class AiAttackController {
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
- this.aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
+ aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
} else if (ratioDiff >= 0 && this.attackers.size() > 1) {
- this.aiAggression = 3; // attack expecting to make good trades or damage player.
+ aiAggression = 3; // attack expecting to make good trades or damage player.
} else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
|| ratioDiff * -1 < turnsUntilDeathByUnblockable) {
// at 0 ratio expect to potentially gain an advantage by attacking first
// if the ai has a slight advantage
// or the ai has a significant advantage numerically but only a slight disadvantage damage/life
- this.aiAggression = 2; // attack expecting to destroy creatures/be unblockable
+ aiAggression = 2; // attack expecting to destroy creatures/be unblockable
} else if (doUnblockableAttack) {
- this.aiAggression = 1;
+ aiAggression = 1;
// look for unblockable creatures that might be
// able to attack for a bit of fatal damage even if the player is significantly better
} else {
- this.aiAggression = 0;
+ aiAggression = 0;
} // stay at home to block
if ( LOG_AI_ATTACKS )
- System.out.println(this.aiAggression + " = ai aggression");
+ System.out.println(aiAggression + " = ai aggression");
// ****************
// Evaluation the end
@@ -1055,7 +1051,7 @@ public class AiAttackController {
if ( LOG_AI_ATTACKS )
System.out.println("Normal attack");
- attackersLeft = notNeededAsBlockers(ai, attackersLeft);
+ attackersLeft = notNeededAsBlockers(attackersLeft);
attackersLeft = sortAttackers(attackersLeft);
if ( LOG_AI_ATTACKS )
@@ -1068,13 +1064,15 @@ public class AiAttackController {
CardCollection attackersAssigned = new CardCollection();
for (int i = 0; i < attackersLeft.size(); i++) {
final Card attacker = attackersLeft.get(i);
- if (this.aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
- && ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, this.defendingOpponent)
+ if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
+ && ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent)
>= ComputerUtilCombat.getDamageToKill(attacker, false)) {
continue;
}
- if (shouldAttack(ai, attacker, this.blockers, combat, defender) && canAttackWrapper(attacker, defender)) {
+ // TODO logic for Questing Beast to prefer players
+
+ if (shouldAttack(attacker, this.blockers, combat, defender) && canAttackWrapper(attacker, defender)) {
combat.addAttacker(attacker, defender);
attackersAssigned.add(attacker);
@@ -1132,7 +1130,7 @@ public class AiAttackController {
* a {@link forge.game.combat.Combat} object.
* @return a boolean.
*/
- public final boolean shouldAttack(final Player ai, final Card attacker, final List defenders, final Combat combat, final GameEntity defender) {
+ public final boolean shouldAttack(final Card attacker, final List defenders, final Combat combat, final GameEntity defender) {
boolean canBeKilled = false; // indicates if the attacker can be killed
boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker
boolean canKillAll = true; // indicates if the attacker can kill all single blockers
@@ -1270,12 +1268,12 @@ public class AiAttackController {
}
if (numberOfPossibleBlockers > 2
- || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent))
- || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) {
+ || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
+ || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent))) {
canBeBlocked = true;
}
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
- switch (this.aiAggression) {
+ switch (aiAggression) {
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
if (LOG_AI_ATTACKS)
@@ -1325,7 +1323,7 @@ public class AiAttackController {
return false; // don't attack
}
- public static List exertAttackers(List attackers, int aggression) {
+ public static List exertAttackers(final List attackers, int aggression) {
List exerters = Lists.newArrayList();
for (Card c : attackers) {
boolean shouldExert = false;
@@ -1463,7 +1461,7 @@ public class AiAttackController {
return null; //should never get here
}
- private void doLightmineFieldAttackLogic(List attackersLeft, int numForcedAttackers, boolean playAggro) {
+ private void doLightmineFieldAttackLogic(final List attackersLeft, int numForcedAttackers, boolean playAggro) {
CardCollection attSorted = new CardCollection(attackersLeft);
CardCollection attUnsafe = new CardCollection();
CardLists.sortByToughnessDesc(attSorted);
@@ -1493,7 +1491,7 @@ public class AiAttackController {
attackersLeft.removeAll(attUnsafe);
}
- private boolean doRevengeOfRavensAttackLogic(Player ai, GameEntity defender, List attackersLeft, int numForcedAttackers, int maxAttack) {
+ private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final List attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false;
if (defender instanceof Player) {
diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java
index 6c276c466d6..92b54c16901 100644
--- a/forge-ai/src/main/java/forge/ai/AiBlockController.java
+++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java
@@ -102,14 +102,13 @@ public class AiBlockController {
private List getSafeBlockers(final Combat combat, final Card attacker, final List blockersLeft) {
final List blockers = new ArrayList<>();
- // We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
- // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
+ // Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus,
+ // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness unless we're simulating an outcome outside of real combat
for (final Card b : blockersLeft) {
- if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, true)) {
+ if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, attacker.getGame().getPhaseHandler().inCombat())) {
blockers.add(b);
}
}
-
return blockers;
}
@@ -117,10 +116,10 @@ public class AiBlockController {
private List getKillingBlockers(final Combat combat, final Card attacker, final List blockersLeft) {
final List blockers = new ArrayList<>();
- // We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
- // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
+ // Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus,
+ // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness unless we're simulating an outcome outside of real combat
for (final Card b : blockersLeft) {
- if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, true)) {
+ if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, attacker.getGame().getPhaseHandler().inCombat())) {
blockers.add(b);
}
}
@@ -131,7 +130,6 @@ public class AiBlockController {
private List sortPotentialAttackers(final Combat combat) {
final CardCollection sortedAttackers = new CardCollection();
CardCollection firstAttacker = new CardCollection();
-
final FCollectionView defenders = combat.getDefenders();
// If I don't have any planeswalkers then sorting doesn't really matter
@@ -155,13 +153,11 @@ public class AiBlockController {
return attackers;
}
- final boolean bLifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
-
// TODO Add creatures attacking Planeswalkers in order of which we want to protect
// 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) {
+ if (defender instanceof Card && ((Card) defender).getController().equals(ai)) {
final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers);
@@ -171,7 +167,7 @@ public class AiBlockController {
}
}
- if (bLifeInDanger) {
+ if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
// add creatures attacking the Player to the front of the list
for (final Card c : firstAttacker) {
sortedAttackers.add(0, c);
@@ -183,9 +179,6 @@ public class AiBlockController {
return sortedAttackers;
}
- // ======================= block assignment functions
- // ================================
-
// Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) {
List currentAttackers = new ArrayList<>(attackersLeft);
@@ -573,14 +566,14 @@ public class AiBlockController {
}
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
- List usableBlockers;
final List blockGang = new ArrayList<>();
int absorbedDamage; // The amount of damage needed to kill the first blocker
- usableBlockers = CardLists.filter(blockers, new Predicate() {
+ List usableBlockers = CardLists.filter(blockers, new Predicate() {
@Override
public boolean apply(final Card c) {
- return c.getNetToughness() > attacker.getNetCombatDamage();
+ return c.getNetToughness() > attacker.getNetCombatDamage() // performance shortcut
+ || c.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, c, true) > attacker.getNetCombatDamage();
}
});
if (usableBlockers.size() < 2) {
@@ -820,7 +813,7 @@ public class AiBlockController {
}
}
// don't try to kill what can't be killed
- if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) {
+ if (ComputerUtilCombat.combatantCantBeDestroyed(ai, attacker)) {
continue;
}
@@ -877,7 +870,7 @@ public class AiBlockController {
int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) {
if (!combat.isBlocked(pwatkr)) {
- damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true);
+ damageToPW += ComputerUtilCombat.predictDamageTo(def, pwatkr.getNetCombatDamage(), pwatkr, true);
}
}
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterEnumType.LOYALTY)) {
@@ -965,11 +958,16 @@ public class AiBlockController {
/** Assigns blockers for the provided combat instance (in favor of player passes to ctor) */
public void assignBlockersForCombat(final Combat combat) {
+ assignBlockersForCombat(combat, null);
+ }
+ public void assignBlockersForCombat(final Combat combat, final CardCollection exludedBlockers) {
List possibleBlockers = ai.getCreaturesInPlay();
+ if (exludedBlockers != null && !exludedBlockers.isEmpty()) {
+ possibleBlockers.removeAll(exludedBlockers);
+ }
attackers = sortPotentialAttackers(combat);
assignBlockers(combat, possibleBlockers);
}
-
/**
* assignBlockersForCombat() with additional and possibly "virtual" blockers.
* @param combat combat instance
@@ -1025,6 +1023,10 @@ public class AiBlockController {
}
}
+ if (attackersLeft.isEmpty()) {
+ return;
+ }
+
// remove all blockers that can't block anyway
for (final Card b : possibleBlockers) {
if (!CombatUtil.canBlock(b, combat)) {
@@ -1032,10 +1034,6 @@ public class AiBlockController {
}
}
- if (attackersLeft.isEmpty()) {
- return;
- }
-
// Begin with the weakest blockers
CardLists.sortByPowerAsc(blockersLeft);
@@ -1070,7 +1068,6 @@ public class AiBlockController {
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)) {
@@ -1090,22 +1087,18 @@ public class AiBlockController {
// == 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
+ clearBlockers(combat, possibleBlockers);
+ makeChumpBlocks(combat);
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
- makeTradeBlocks(combat); // choose necessary trade
+ makeTradeBlocks(combat);
}
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeGoodBlocks(combat);
- }
- // Reinforce blockers blocking attackers with trample if life is still in danger
- else {
+ } else {
reinforceBlockersAgainstTrample(combat);
}
makeGangBlocks(combat);
- // Support blockers not destroying the attacker with more
- // blockers to try to kill the attacker
reinforceBlockersToKill(combat);
}
}
diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java
index 507bae8db2e..a44490dc4b9 100644
--- a/forge-ai/src/main/java/forge/ai/AiController.java
+++ b/forge-ai/src/main/java/forge/ai/AiController.java
@@ -29,7 +29,6 @@ import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
-import forge.deck.CardPool;
import forge.deck.Deck;
import forge.deck.DeckSection;
import forge.game.*;
@@ -69,7 +68,6 @@ import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder;
import java.util.*;
-import java.util.Map.Entry;
/**
*
@@ -1868,10 +1866,8 @@ public class AiController {
toRemove.add(sa);
}
}
- for(SpellAbility sa : toRemove) {
- result.remove(sa);
- }
-
+ result.removeAll(toRemove);
+
// Play them last
if (saGemstones != null) {
result.remove(saGemstones);
@@ -2045,17 +2041,7 @@ public class AiController {
Map> complaints = new HashMap<>();
// When using simulation, AI should be able to figure out most cards.
if (!useSimulation) {
- for (Entry ds : myDeck) {
- List result = Lists.newArrayList();
- for (Entry cp : ds.getValue()) {
- if (cp.getKey().getRules().getAiHints().getRemAIDecks()) {
- result.add(cp.getKey());
- }
- }
- if (!result.isEmpty()) {
- complaints.put(ds.getKey(), result);
- }
- }
+ complaints = myDeck.getUnplayableAICards().unplayable;
}
return complaints;
}
diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java
index d18fe49da37..16b01575594 100644
--- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java
+++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java
@@ -196,8 +196,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
- CardLists.sortByPowerAsc(typeList);
- Collections.reverse(typeList);
+ CardLists.sortByPowerDesc(typeList);
for (int i = 0; i < c; i++) {
chosen.add(typeList.get(i));
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
index abbdc9be665..4f739ee9fbf 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
@@ -1492,7 +1492,7 @@ public class ComputerUtil {
return false;
}
- public static int possibleNonCombatDamage(Player ai, Player enemy) {
+ public static int possibleNonCombatDamage(final Player ai, final Player enemy) {
int damage = 0;
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.addAll(ai.getCardsActivableInExternalZones(true));
@@ -1550,8 +1550,8 @@ public class ComputerUtil {
/**
* Overload of predictThreatenedObjects that evaluates the full stack
*/
- public static List predictThreatenedObjects(final Player aiPlayer, final SpellAbility sa) {
- return predictThreatenedObjects(aiPlayer, sa, false);
+ public static List predictThreatenedObjects(final Player ai, final SpellAbility sa) {
+ return predictThreatenedObjects(ai, sa, false);
}
/**
@@ -2971,7 +2971,7 @@ public class ComputerUtil {
// TODO: either add SVars to other reanimator cards, or improve the prediction so that it avoids using a SVar
// at all but detects this effect from SA parameters (preferred, but difficult)
CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
- CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library});
+ CardCollectionView inDeck = ai.getCardsIn(ZoneType.Library);
Predicate markedAsReanimator = new Predicate() {
@Override
@@ -3036,13 +3036,30 @@ public class ComputerUtil {
// call this to determine if it's safe to use a life payment spell
// or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell.
public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) {
- // TODO should also consider them as teams
- for (Player opponent: ai.getOpponents()) {
- Combat combat = new Combat(opponent);
+ return predictNextCombatsRemainingLife(ai, serious, false, payment, null) == Integer.MIN_VALUE;
+ }
+ public static int predictNextCombatsRemainingLife(Player ai, boolean serious, boolean checkDiff, int payment, final CardCollection excludedBlockers) {
+ // life won't change
+ int remainingLife = Integer.MAX_VALUE;
+
+ // performance shortcut
+ // TODO if checking upcoming turn it should be a permanent effect
+ if (ai.cantLose()) {
+ return remainingLife;
+ }
+
+ // TODO should also consider them as teams (with increased likelihood to be attacked by multiple if ai is biggest threat)
+ // TODO worth it to sort by creature amount for chance to terminate earlier?
+ for (Player opp: ai.getOpponents()) {
+ Combat combat = new Combat(opp);
boolean containsAttacker = false;
- boolean thisCombat = ai.getGame().getPhaseHandler().isPlayerTurn(opponent) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_BEGIN);
- for (Card att : opponent.getCreaturesInPlay()) {
+ boolean thisCombat = ai.getGame().getPhaseHandler().isPlayerTurn(opp) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_BEGIN);
+
+ // TODO !thisCombat should include cards that will phase in
+ for (Card att : opp.getCreaturesInPlay()) {
if ((thisCombat && CombatUtil.canAttack(att, ai)) || (!thisCombat && ComputerUtilCombat.canAttackNextTurn(att, ai))) {
+ // TODO need to copy the card
+ // att = ComputerUtilCombat.applyPotentialAttackCloneTriggers(att);
combat.addAttacker(att, ai);
containsAttacker = true;
}
@@ -3050,23 +3067,28 @@ public class ComputerUtil {
if (!containsAttacker) {
continue;
}
-
// TODO if it's next turn ignore mustBlockCards
AiBlockController block = new AiBlockController(ai, false);
- block.assignBlockersForCombat(combat);
+ // TODO for performance skip ahead to safer blocking approach (though probably only when not in checkDiff mode as that could lead to inflated prediction)
+ block.assignBlockersForCombat(combat, excludedBlockers);
// TODO predict other, noncombat sources of damage and add them to the "payment" variable.
// examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc
// If added, might need a parameter to define whether we want to check all threats or combat threats.
if (serious && ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment)) {
- return true;
+ return Integer.MIN_VALUE;
}
if (!serious && ComputerUtilCombat.lifeInDanger(ai, combat, payment)) {
- return true;
+ return Integer.MIN_VALUE;
+ }
+
+ if (checkDiff && !ai.cantLoseForZeroOrLessLife()) {
+ // find out the worst possible outcome
+ remainingLife = Math.min(ComputerUtilCombat.lifeThatWouldRemain(ai, combat), remainingLife);
}
}
- return false;
+ return remainingLife;
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
index 6a2e830b960..8f9ad96bed8 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
@@ -611,35 +611,6 @@ public class ComputerUtilCard {
return combat.isAttacking(card);
}
- public static boolean canBeKilledByRoyalAssassin(final Player ai, final Card card) {
- boolean wasTapped = card.isTapped();
- for (Player opp : ai.getOpponents()) {
- for (Card c : opp.getCardsIn(ZoneType.Battlefield)) {
- for (SpellAbility sa : c.getSpellAbilities()) {
- if (sa.getApi() != ApiType.Destroy) {
- continue;
- }
- if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) {
- continue;
- }
- sa.setActivatingPlayer(opp);
- if (sa.canTarget(card)) {
- continue;
- }
- // check whether the ability can only target tapped creatures
- card.setTapped(true);
- if (!sa.canTarget(card)) {
- card.setTapped(wasTapped);
- continue;
- }
- card.setTapped(wasTapped);
- return true;
- }
- }
- }
- return false;
- }
-
/**
* Create a mock combat where ai is being attacked and returns the list of likely blockers.
* @param ai blocking player
@@ -696,6 +667,35 @@ public class ComputerUtilCard {
return ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, combat);
}
+ public static boolean canBeKilledByRoyalAssassin(final Player ai, final Card card) {
+ boolean wasTapped = card.isTapped();
+ for (Player opp : ai.getOpponents()) {
+ for (Card c : opp.getCardsIn(ZoneType.Battlefield)) {
+ for (SpellAbility sa : c.getSpellAbilities()) {
+ if (sa.getApi() != ApiType.Destroy) {
+ continue;
+ }
+ if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) {
+ continue;
+ }
+ sa.setActivatingPlayer(opp);
+ if (sa.canTarget(card)) {
+ continue;
+ }
+ // check whether the ability can only target tapped creatures
+ card.setTapped(true);
+ if (!sa.canTarget(card)) {
+ card.setTapped(wasTapped);
+ continue;
+ }
+ card.setTapped(wasTapped);
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
/**
* getMostExpensivePermanentAI.
*
@@ -1967,13 +1967,11 @@ public class ComputerUtilCard {
public static Cost getTotalWardCost(Card c) {
Cost totalCost = new Cost(ManaCost.NO_COST, false);
- for (final KeywordInterface inst : c.getKeywords()) {
- if (inst.getKeyword() == Keyword.WARD) {
- final String keyword = inst.getOriginal();
- final String[] k = keyword.split(":");
- final Cost wardCost = new Cost(k[1], false);
- totalCost = totalCost.add(wardCost);
- }
+ for (final KeywordInterface inst : c.getKeywords(Keyword.WARD)) {
+ final String keyword = inst.getOriginal();
+ final String[] k = keyword.split(":");
+ final Cost wardCost = new Cost(k[1], false);
+ totalCost = totalCost.add(wardCost);
}
return totalCost;
}
@@ -2000,6 +1998,7 @@ public class ComputerUtilCard {
return false;
}
+ // TODO replace most calls to Player.isCardInPlay because they include phased out
public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) {
for (Card card : ai.getCardsIn(ZoneType.Battlefield, cardName)) {
// TODO - Better logic to determine if a permanent is disabled by local effects
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
index ed8202a1422..56b0e609793 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
@@ -154,13 +154,11 @@ public class ComputerUtilCombat {
* @return a int.
*/
public static int getTotalFirstStrikeBlockPower(final Card attacker, final Player player) {
- final Card att = attacker;
-
List list = player.getCreaturesInPlay();
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
- return (c.hasFirstStrike() || c.hasDoubleStrike()) && CombatUtil.canBlock(att, c);
+ return (c.hasFirstStrike() || c.hasDoubleStrike()) && CombatUtil.canBlock(attacker, c);
}
});
@@ -216,7 +214,7 @@ public class ComputerUtilCombat {
damage += predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
if (!attacker.hasKeyword(Keyword.INFECT)) {
sum = predictDamageTo(attacked, damage, attacker, true);
- if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ if (attacker.hasDoubleStrike()) {
sum *= 2;
}
}
@@ -249,7 +247,7 @@ public class ComputerUtilCombat {
pd = 0;
}
poison += pd;
- if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ if (attacker.hasDoubleStrike()) {
poison += pd;
}
}
@@ -304,6 +302,20 @@ public class ComputerUtilCombat {
return sum;
}
+ // Checks if the life of the attacked Player would be reduced
+ /**
+ *
+ * wouldLoseLife.
+ *
+ *
+ * @param combat
+ * a {@link forge.game.combat.Combat} object.
+ * @return a boolean.
+ */
+ public static boolean wouldLoseLife(final Player ai, final Combat combat) {
+ return lifeThatWouldRemain(ai, combat) < ai.getLife();
+ }
+
// calculates the amount of life that will remain after the attack
/**
*
@@ -398,7 +410,6 @@ public class ComputerUtilCombat {
return res;
}
- // Checks if the life of the attacked Player/Planeswalker is in danger
/**
*
* lifeInDanger.
@@ -406,7 +417,7 @@ public class ComputerUtilCombat {
*
* @param combat
* a {@link forge.game.combat.Combat} object.
- * @return a boolean.
+ * @return boolean true if life/poison changes and will be in dangerous range as specified by AI profile.
*/
public static boolean lifeInDanger(final Player ai, final Combat combat) {
return lifeInDanger(ai, combat, 0);
@@ -473,29 +484,13 @@ public class ComputerUtilCombat {
maxTreshold--;
}
- if (lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())
- && !ai.cantLoseForZeroOrLessLife()) {
+ if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())) {
return true;
}
return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters());
}
- // Checks if the life of the attacked Player would be reduced
- /**
- *
- * wouldLoseLife.
- *
- *
- * @param combat
- * a {@link forge.game.combat.Combat} object.
- * @return a boolean.
- */
- public static boolean wouldLoseLife(final Player ai, final Combat combat) {
- return lifeThatWouldRemain(ai, combat) < ai.getLife();
- }
-
- // Checks if the life of the attacked Player/Planeswalker is in danger
/**
*
* lifeInSeriousDanger.
@@ -503,7 +498,7 @@ public class ComputerUtilCombat {
*
* @param combat
* a {@link forge.game.combat.Combat} object.
- * @return a boolean.
+ * @return boolean - true if player would lose.
*/
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) {
return lifeInSeriousDanger(ai, combat, 0);
@@ -532,7 +527,7 @@ public class ComputerUtilCombat {
}
}
- if (lifeThatWouldRemain(ai, combat) - payment < 1 && !ai.cantLoseForZeroOrLessLife()) {
+ if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1) {
return true;
}
@@ -597,7 +592,7 @@ public class ComputerUtilCombat {
public static int dealsDamageAsBlocker(final Card attacker, final Card defender) {
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
- if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ if (defender.hasDoubleStrike()) {
defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true);
}
@@ -743,8 +738,8 @@ public class ComputerUtilCombat {
int firstStrikeBlockerDmg = 0;
for (final Card defender : blockers) {
- if (canDestroyAttacker(ai, attacker, defender, combat, true)
- && !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
+ if (!(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))
+ && canDestroyAttacker(ai, attacker, defender, combat, true)) {
return true;
}
if (defender.hasFirstStrike() || defender.hasDoubleStrike()) {
@@ -927,7 +922,7 @@ public class ComputerUtilCombat {
if (dealsFirstStrikeDamage(attacker, withoutAbilities, null)
&& (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, null)
- && !blocker.canReceiveCounters(CounterEnumType.M1M1)) {
+ && blocker.canReceiveCounters(CounterEnumType.M1M1)) {
power -= attacker.getNetCombatDamage();
}
@@ -1224,7 +1219,7 @@ public class ComputerUtilCombat {
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
&& (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))
&& !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
- && !attacker.canReceiveCounters(CounterEnumType.M1M1)) {
+ && attacker.canReceiveCounters(CounterEnumType.M1M1)) {
power -= blocker.getNetCombatDamage();
}
theTriggers.addAll(blocker.getTriggers());
@@ -1272,6 +1267,10 @@ public class ComputerUtilCombat {
continue;
}
+ if (!sa.hasParam("NumAtt")) {
+ continue;
+ }
+
sa.setActivatingPlayer(source.getController());
if (sa.hasParam("Cost")) {
@@ -1300,9 +1299,6 @@ public class ComputerUtilCombat {
if (!list.contains(attacker)) {
continue;
}
- if (!sa.hasParam("NumAtt")) {
- continue;
- }
String att = sa.getParam("NumAtt");
if (att.startsWith("+")) {
@@ -1311,7 +1307,7 @@ public class ComputerUtilCombat {
if (att.matches("[0-9][0-9]?") || att.matches("-" + "[0-9][0-9]?")) {
power += Integer.parseInt(att);
} else {
- String bonus = source.getSVar(att);
+ String bonus = AbilityUtils.getSVar(sa, att);
if (bonus.contains("TriggerCount$NumBlockers")) {
bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
@@ -1601,7 +1597,7 @@ public class ComputerUtilCombat {
if (blocker.isEquippedBy("Godsend")) {
return true;
}
- if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
+ if (combatantCantBeDestroyed(attacker.getController(), attacker)) {
return false;
}
@@ -1718,7 +1714,7 @@ public class ComputerUtilCombat {
}
} // flanking
- if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
+ if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (!withoutAbilities && ComputerUtil.canRegenerate(ai, attacker)))
&& !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)))
|| (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterEnumType.M1M1) && (attacker
.getCounters(CounterEnumType.M1M1) == 0))
@@ -1728,7 +1724,6 @@ public class ComputerUtilCombat {
}
int defenderDamage;
- int attackerDamage;
if (blocker.toughnessAssignsDamage()) {
defenderDamage = blocker.getNetToughness()
+ predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
@@ -1736,13 +1731,6 @@ public class ComputerUtilCombat {
defenderDamage = blocker.getNetPower()
+ predictPowerBonusOfBlocker(attacker, blocker, withoutAbilities);
}
- if (attacker.toughnessAssignsDamage()) {
- attackerDamage = attacker.getNetToughness()
- + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
- } else {
- attackerDamage = attacker.getNetPower()
- + predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
- }
int possibleDefenderPrevention = 0;
int possibleAttackerPrevention = 0;
@@ -1753,17 +1741,26 @@ public class ComputerUtilCombat {
// consider Damage Prevention/Replacement
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
- attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
return false;
}
+ int attackerDamage;
+ if (attacker.toughnessAssignsDamage()) {
+ attackerDamage = attacker.getNetToughness()
+ + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
+ } else {
+ attackerDamage = attacker.getNetPower()
+ + predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
+ }
+ attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
+
final int defenderLife = getDamageToKill(blocker, false)
+ predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
- if (blocker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ if (blocker.hasDoubleStrike()) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true;
}
@@ -1824,8 +1821,8 @@ public class ComputerUtilCombat {
final List attackers = combat.getAttackersBlockedBy(blocker);
for (Card attacker : attackers) {
- if (canDestroyBlocker(ai, blocker, attacker, combat, true)
- && !(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) {
+ if (!(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
+ && canDestroyBlocker(ai, blocker, attacker, combat, true)) {
return true;
}
}
@@ -1931,7 +1928,7 @@ public class ComputerUtilCombat {
return true;
}
- if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
+ if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (!withoutAbilities && ComputerUtil.canRegenerate(ai, blocker))) && !(attacker
.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)))
|| (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterEnumType.M1M1) && blocker
.getCounters(CounterEnumType.M1M1) == 0)
@@ -1993,11 +1990,11 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
- if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
- if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
+ if (attacker.hasDoubleStrike()) {
+ if (attackerDamage >= defenderLife) {
return true;
}
- if (attackerDamage >= defenderLife) {
+ if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
return true;
}
@@ -2285,7 +2282,7 @@ public class ComputerUtilCombat {
}
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
- if (combatant.hasFirstStrike()|| combatant.hasDoubleStrike()) {
+ if (combatant.hasFirstStrike() || combatant.hasDoubleStrike()) {
return true;
}
@@ -2510,7 +2507,7 @@ public class ComputerUtilCombat {
}
}
poison += pd;
- if (pd > 0 && attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ if (pd > 0 && attacker.hasDoubleStrike()) {
poison += pd;
}
// TODO: Predict replacement effects for counters (doubled, reduced, additional counters, etc.)
diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
index 4aea82e3967..40aaa132c3f 100644
--- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
@@ -219,8 +219,9 @@ public class CreatureEvaluator implements Function {
if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "vanishing");
}
+ // use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) {
- value -= subValue(10, "phasing");
+ value -= subValue(Math.max(20, value / 2), "phasing");
}
// TODO no longer a KW
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index b875e23c26d..536dabf7d79 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -89,12 +89,22 @@ import forge.util.collect.FCollectionView;
public class PlayerControllerAi extends PlayerController {
private final AiController brains;
+ private boolean pilotsNonAggroDeck = false;
+
public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) {
super(game, p, lp);
brains = new AiController(p, game);
}
+ public boolean pilotsNonAggroDeck() {
+ return pilotsNonAggroDeck;
+ }
+
+ public void setupAutoProfile(Deck deck) {
+ pilotsNonAggroDeck = deck.getName().contains("Control") || Deck.getAverageCMC(deck) > 3;
+ }
+
public void allowCheatShuffle(boolean value) {
brains.allowCheatShuffle(value);
}
@@ -1132,6 +1142,9 @@ public class PlayerControllerAi extends PlayerController {
@Override
public Map> complainCardsCantPlayWell(Deck myDeck) {
+ // TODO check if profile detection set to Auto
+ setupAutoProfile(myDeck);
+
return brains.complainCardsCantPlayWell(myDeck);
}
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 d6dc1101744..89a39c7dc64 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
@@ -1587,7 +1587,7 @@ public class AttachAi extends SpellAbilityAi {
return card.getNetCombatDamage() + powerBonus > 0
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.equals("First Strike")) {
- return card.getNetCombatDamage() + powerBonus > 0 && !card.hasKeyword(Keyword.DOUBLE_STRIKE)
+ return card.getNetCombatDamage() + powerBonus > 0 && !card.hasDoubleStrike()
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0
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 c623c5fbdaa..d54fdfc8cbb 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
@@ -156,7 +156,7 @@ public class CountersPutAi extends CountersAi {
final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined"))
&& "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X"))
&& sa.hasParam("MaxFromEffect");
- boolean playAggro = ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true");
+ boolean playAggro = ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO);
if ("ExistingCounter".equals(type)) {
final boolean eachExisting = sa.hasParam("EachExistingCounter");
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 80d0f6696ef..27fd6daf191 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
@@ -1024,13 +1024,13 @@ public class DamageDealAi extends DamageAiBase {
return false;
}
- CardCollection creatures = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
+ CardCollection creatures = ai.getOpponents().getCreaturesInPlay();
Card tgtCreature = null;
for (Card c : creatures) {
int power = c.getNetPower();
int toughness = c.getNetToughness();
- boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c));
+ boolean canDie = !ComputerUtilCombat.combatantCantBeDestroyed(c.getController(), c);
// Currently will target creatures with toughness 3+ (or power 5+)
// and only if the creature can actually die, do not "underdrain"
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 fdb61f45d76..406ab3bd033 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java
@@ -1,6 +1,5 @@
package forge.ai.ability;
-import java.util.Collections;
import java.util.List;
import com.google.common.base.Predicate;
@@ -44,8 +43,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
}
else if (tgt.getMinTargets(sa.getHostCard(), sa) > 1) {
CardCollection list2 = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
- CardLists.sortByPowerAsc(list2);
- Collections.reverse(list2);
+ CardLists.sortByPowerDesc(list2);
c2 = list2.isEmpty() ? null : list2.get(0);
sa.getTargets().add(c2);
}
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 09c67a1eab8..0d88c421653 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
@@ -302,7 +302,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
&& !opp.getCreaturesInPlay().isEmpty()
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
} else if (keyword.equals("First Strike")) {
- if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) {
+ if (card.hasDoubleStrike()) {
return false;
}
if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) {
diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java
index f4abe8a3b2b..c91d95f333f 100644
--- a/forge-core/src/main/java/forge/deck/Deck.java
+++ b/forge-core/src/main/java/forge/deck/Deck.java
@@ -23,6 +23,8 @@ import com.google.common.collect.Lists;
import forge.StaticData;
import forge.card.CardDb;
import forge.card.CardEdition;
+import forge.card.CardRules;
+import forge.card.CardType;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils;
@@ -613,4 +615,27 @@ public class Deck extends DeckBase implements Iterable deckEntry : deck) {
+ switch (deckEntry.getKey()) {
+ case Main:
+ case Commander:
+ for (final Entry poolEntry : deckEntry.getValue()) {
+ CardRules rules = poolEntry.getKey().getRules();
+ CardType type = rules.getType();
+ if (!type.isLand() && (type.isArtifact() || type.isCreature() || type.isEnchantment() || type.isPlaneswalker() || type.isInstant() || type.isSorcery())) {
+ totalCMC += rules.getManaCost().getCMC();
+ totalCount++;
+ }
+ }
+ break;
+ default:
+ break; //ignore other sections
+ }
+ }
+ return totalCount == 0 ? 0 : Math.round(totalCMC / totalCount);
+ }
}
\ No newline at end of file
diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java
index 72efb11b5e1..c440fd94eb8 100644
--- a/forge-game/src/main/java/forge/game/Game.java
+++ b/forge-game/src/main/java/forge/game/Game.java
@@ -861,7 +861,7 @@ public class Game {
getNextPlayerAfter(p).initPlane();
}
- if (p != null && p.isMonarch()) {
+ if (p.isMonarch()) {
// if the player who lost was the Monarch, someone else will be the monarch
// TODO need to check rules if it should try the next player if able
if (p.equals(getPhaseHandler().getPlayerTurn())) {
diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java
index 812bcbe19f4..1ff702d76ec 100644
--- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java
+++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java
@@ -653,12 +653,12 @@ public abstract class SpellAbilityEffect {
@Override
public void run() {
- CardZoneTable untilTable = new CardZoneTable();
CardCollectionView untilCards = hostCard.getUntilLeavesBattlefield();
// if the list is empty, then the table doesn't need to be checked anymore
if (untilCards.isEmpty()) {
return;
}
+ CardZoneTable untilTable = new CardZoneTable();
Map moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateGraveyard());
diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java
index bac54df3eab..23cdeaf3f09 100644
--- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java
+++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java
@@ -226,7 +226,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
}
}
}
- if (remLKI && movedCard != null) {
+ if (remLKI) {
final Card lki = CardUtil.getLKICopy(c);
game.getCardState(source).addRemembered(lki);
if (!source.isRemembered(lki)) {
diff --git a/forge-gui/res/cardsfolder/w/war_cadence.txt b/forge-gui/res/cardsfolder/w/war_cadence.txt
index d032a226282..be2378f982a 100644
--- a/forge-gui/res/cardsfolder/w/war_cadence.txt
+++ b/forge-gui/res/cardsfolder/w/war_cadence.txt
@@ -4,7 +4,6 @@ Types:Enchantment
A:AB$ StoreSVar | Cost$ X R | SVar$ PaidNum | Type$ Count | Expression$ xPaid | SubAbility$ CadenceEffect | AILogic$ RestrictBlocking | SpellDescription$ This turn, creatures can't block unless their controller pays {X} for each blocking creature they control.
SVar:CadenceEffect:DB$ Effect | StaticAbilities$ CadenceStaticAb | Stackable$ False | RememberObjects$ Valid Creature.blocking
SVar:CadenceStaticAb:Mode$ CantBlockUnless | ValidCard$ Card.IsNotRemembered | Cost$ PaidNum | EffectZone$ Command | Description$ This turn, creatures can't block unless their controller pays {X} for each blocking creature they control.
-# According to the 10/4/2004 ruling: The ability only applies to blocks declared after it resolves. It will not add costs to any blockers already announced.
SVar:X:Count$xPaid
SVar:PaidNum:Number$0
SVar:NonStackingEffect:True
diff --git a/forge-gui/src/main/java/forge/deck/DeckProxy.java b/forge-gui/src/main/java/forge/deck/DeckProxy.java
index 750b16579c1..5a0ff314240 100644
--- a/forge-gui/src/main/java/forge/deck/DeckProxy.java
+++ b/forge-gui/src/main/java/forge/deck/DeckProxy.java
@@ -347,32 +347,9 @@ public class DeckProxy implements InventoryItem {
return sbSize;
}
- public static int getAverageCMC(Deck deck) {
- int totalCMC = 0;
- int totalCount = 0;
- for (final Entry deckEntry : deck) {
- switch (deckEntry.getKey()) {
- case Main:
- case Commander:
- for (final Entry poolEntry : deckEntry.getValue()) {
- CardRules rules = poolEntry.getKey().getRules();
- CardType type = rules.getType();
- if (!type.isLand() && (type.isArtifact() || type.isCreature() || type.isEnchantment() || type.isPlaneswalker() || type.isInstant() || type.isSorcery())) {
- totalCMC += rules.getManaCost().getCMC();
- totalCount++;
- }
- }
- break;
- default:
- break; //ignore other sections
- }
- }
- return Math.round(totalCMC / totalCount);
- }
-
public Integer getAverageCMC() {
if (avgCMC == null) {
- avgCMC = getAverageCMC(getDeck());
+ avgCMC = Deck.getAverageCMC(getDeck());
}
return avgCMC;
}
diff --git a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java
index 80f01c0513f..66817b55afd 100644
--- a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java
+++ b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java
@@ -211,7 +211,7 @@ public class GauntletIO {
final boolean foil = "1".equals(reader.getAttribute("foil"));
PaperCard card = FModel.getMagicDb().getOrLoadCommonCard(name, set, index, foil);
if (null == card) {
- throw new RuntimeException("Unsupported card found in quest save: " + name + " from edition " + set);
+ throw new RuntimeException("Unsupported card found in gauntlet save: " + name + " from edition " + set);
}
return card;
}
diff --git a/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java b/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java
index 1c31589355c..2874be6e78e 100644
--- a/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java
+++ b/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java
@@ -25,6 +25,7 @@ import forge.card.CardType;
import forge.card.CardType.CoreType;
import forge.card.CardType.Supertype;
import forge.card.MagicColor;
+import forge.deck.Deck;
import forge.deck.CardPool;
import forge.deck.DeckProxy;
import forge.deck.DeckSection;
@@ -681,7 +682,7 @@ public class AdvancedSearch {
COMMANDER_DECK_AVERAGE_CMC("lblDeckAverageCMC", ConquestCommander.class, FilterOperator.NUMBER_OPS, new NumericEvaluator(0, 20) {
@Override
protected Integer getItemValue(ConquestCommander input) {
- return DeckProxy.getAverageCMC(input.getDeck());
+ return Deck.getAverageCMC(input.getDeck());
}
}),
COMMANDER_DECK_CONTENTS("lblDeckContents", ConquestCommander.class, FilterOperator.DECK_CONTENT_OPS, new DeckContentEvaluator() {