Merge branch 'combatAI' into 'master'

Improve notNeededAsBlockers

See merge request core-developers/forge!6398
This commit is contained in:
Michael Kamensky
2022-03-17 08:18:58 +00:00
22 changed files with 349 additions and 341 deletions

View File

@@ -76,7 +76,7 @@ public class AiAttackController {
private Player defendingOpponent; private Player defendingOpponent;
private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances 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
/** /**
* <p> * <p>
@@ -90,22 +90,22 @@ public class AiAttackController {
public AiAttackController(final Player ai, boolean nextTurn) { public AiAttackController(final Player ai, boolean nextTurn) {
this.ai = ai; this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer(ai); defendingOpponent = choosePreferredDefenderPlayer(ai);
this.oppList = getOpponentCreatures(this.defendingOpponent); this.oppList = getOpponentCreatures(defendingOpponent);
this.myList = ai.getCreaturesInPlay(); myList = ai.getCreaturesInPlay();
this.nextTurn = nextTurn; this.nextTurn = nextTurn;
refreshAttackers(this.defendingOpponent); refreshAttackers(defendingOpponent);
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn); this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn);
} // overloaded constructor to evaluate attackers that should attack next turn } // overloaded constructor to evaluate attackers that should attack next turn
public AiAttackController(final Player ai, Card attacker) { public AiAttackController(final Player ai, Card attacker) {
this.ai = ai; this.ai = ai;
this.defendingOpponent = choosePreferredDefenderPlayer(ai); defendingOpponent = choosePreferredDefenderPlayer(ai);
this.oppList = getOpponentCreatures(this.defendingOpponent); this.oppList = getOpponentCreatures(defendingOpponent);
this.myList = ai.getCreaturesInPlay(); myList = ai.getCreaturesInPlay();
this.nextTurn = false; this.nextTurn = false;
this.attackers = new ArrayList<>(); this.attackers = new ArrayList<>();
if (CombatUtil.canAttack(attacker, this.defendingOpponent)) { if (CombatUtil.canAttack(attacker, defendingOpponent)) {
attackers.add(attacker); attackers.add(attacker);
} }
this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn); 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 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 // 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; return defender;
} }
@@ -199,7 +199,7 @@ public class AiAttackController {
} }
return list; return list;
} // sortAttackers() }
// Is there any reward for attacking? (for 0/1 creatures there is not) // Is there any reward for attacking? (for 0/1 creatures there is not)
/** /**
@@ -254,7 +254,7 @@ public class AiAttackController {
onlyIfExalted = true; 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; return true;
} }
} }
@@ -282,14 +282,12 @@ public class AiAttackController {
} }
public final static List<Card> getPossibleBlockers(final List<Card> blockers, final List<Card> attackers, final boolean nextTurn) { public final static List<Card> getPossibleBlockers(final List<Card> blockers, final List<Card> attackers, final boolean nextTurn) {
List<Card> possibleBlockers = new ArrayList<>(blockers); return CardLists.filter(blockers, new Predicate<Card>() {
possibleBlockers = CardLists.filter(possibleBlockers, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { public boolean apply(final Card c) {
return canBlockAnAttacker(c, attackers, nextTurn); return canBlockAnAttacker(c, attackers, nextTurn);
} }
}); });
return possibleBlockers;
} }
public final static boolean canBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) { public final static boolean canBlockAnAttacker(final Card c, final List<Card> 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 // this checks to make sure that the computer player doesn't lose when the human player attacks
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) { public final List<Card> notNeededAsBlockers(final List<Card> attackers) {
final List<Card> notNeededAsBlockers = new ArrayList<>(attackers);
int fixedBlockers = 0;
final List<Card> vigilantes = new ArrayList<>();
//check for time walks //check for time walks
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) { if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
return attackers; return attackers;
@@ -329,83 +324,124 @@ public class AiAttackController {
} }
} }
attackers.removeAll(toRemove); attackers.removeAll(toRemove);
return attackers; return attackers;
} }
List<Card> opponentsAttackers = new ArrayList<>(oppList); final List<Card> vigilantes = new ArrayList<>();
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() { for (final Card c : myList) {
@Override
public boolean apply(final Card c) {
return c.getNetCombatDamage() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
}
});
for (final Card c : this.myList) {
if (c.getName().equals("Masako the Humorless")) { if (c.getName().equals("Masako the Humorless")) {
// "Tapped creatures you control can block as though they were untapped." // "Tapped creatures you control can block as though they were untapped."
return attackers; 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 // 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)) { if (c.hasKeyword(Keyword.VIGILANCE) || ComputerUtilCard.willUntap(ai, c)) {
vigilantes.add(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<Card> opponentsAttackers = CardLists.filter(ai.getOpponents().getCreaturesInPlay(), new Predicate<Card>() {
@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);
} }
CardLists.sortByPowerAsc(attackers); });
int blockersNeeded = opponentsAttackers.size();
final List<Card> notNeededAsBlockers = new CardCollection(attackers);
// don't hold back creatures that can't block any of the human creatures // don't hold back creatures that can't block any of the human creatures
final List<Card> list = getPossibleBlockers(attackers, opponentsAttackers, nextTurn); final List<Card> blockers = getPossibleBlockers(attackers, opponentsAttackers, true);
//Calculate the amount of creatures necessary if (!blockers.isEmpty()) {
for (int i = 0; i < list.size(); i++) { notNeededAsBlockers.removeAll(blockers);
if (!doesHumanAttackAndWin(ai, i)) {
blockersNeeded = i; 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; 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;
}
} }
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); 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 // 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) // (human will get an extra first attack with a creature that untaps)
// In addition, if the computer guesses it needs no blockers, make sure // In addition, if the computer guesses it needs no blockers, make sure
// that it won't be surprised by Exalted // 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) { 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 // total attack = biggest creature + exalted, *2 if Rafiq is in play
int humanBasePower = ComputerUtilCombat.getAttack(this.oppList.get(0)) + humanExaltedBonus; int humanBasePower = ComputerUtilCombat.getAttack(this.oppList.get(0)) + humanExaltedBonus;
if (finestHour) { if (finestHour) {
// For Finest Hour, one creature could attack and get the bonus TWICE // For Finest Hour, one creature could attack and get the bonus TWICE
humanBasePower += humanExaltedBonus; 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; : humanBasePower;
if (ai.getLife() - 3 <= totalExaltedAttack) { if (ai.getLife() - 3 <= totalExaltedAttack) {
// We will lose if there is an Exalted attack -- keep one blocker // We will lose if there is an Exalted attack -- keep one blocker
@@ -423,45 +459,7 @@ public class AiAttackController {
return notNeededAsBlockers; return notNeededAsBlockers;
} }
public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) { private boolean doAssault() {
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) {
// Beastmaster Ascension // Beastmaster Ascension
if (ai.isCardInPlay("Beastmaster Ascension") && this.attackers.size() > 1) { if (ai.isCardInPlay("Beastmaster Ascension") && this.attackers.size() > 1) {
final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension"); 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, // 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 // thus attempting to predict how many creatures with evasion can actively block
boolean predictEvasion = false; boolean predictEvasion = false;
@@ -599,15 +595,14 @@ public class AiAttackController {
} }
} }
int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage; int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, defendingOpponent) + trampleDamage;
int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp); if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, defendingOpponent) >= defendingOpponent.getLife()
&& !((defendingOpponent.cantLoseForZeroOrLessLife() || ai.cantWin()) && defendingOpponent.getLife() < 1)) {
if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife()
&& !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) {
return true; return true;
} }
if (totalPoisonDamage >= 10 - opp.getPoisonCounters()) { int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, defendingOpponent);
if (totalPoisonDamage >= 10 - defendingOpponent.getPoisonCounters()) {
return true; return true;
} }
@@ -619,7 +614,7 @@ public class AiAttackController {
if (defs.size() == 1) { if (defs.size() == 1) {
return defs.getFirst(); 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... // Attempt to see if there's a defined entity that must be attacked strictly this turn...
GameEntity entity = ai.getMustAttackEntityThisTurn(); GameEntity entity = ai.getMustAttackEntityThisTurn();
@@ -660,7 +655,7 @@ public class AiAttackController {
* @return a {@link forge.game.combat.Combat} object. * @return a {@link forge.game.combat.Combat} object.
*/ */
public final int declareAttackers(final Combat combat) { public final int declareAttackers(final Combat combat) {
final boolean bAssault = doAssault(ai); final boolean bAssault = doAssault();
// Determine who will be attacked // Determine who will be attacked
GameEntity defender = chooseDefender(combat, bAssault); GameEntity defender = chooseDefender(combat, bAssault);
@@ -755,11 +750,11 @@ public class AiAttackController {
doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro); doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro);
} }
// Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily // 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; 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) if (LOG_AI_ATTACKS)
System.out.println("Assault"); System.out.println("Assault");
CardLists.sortByPowerDesc(attackersLeft); CardLists.sortByPowerDesc(attackersLeft);
@@ -807,9 +802,9 @@ public class AiAttackController {
CardLists.sortByPowerDesc(this.attackers); CardLists.sortByPowerDesc(this.attackers);
if (LOG_AI_ATTACKS) if (LOG_AI_ATTACKS)
System.out.println("Exalted"); System.out.println("Exalted");
this.aiAggression = 6; aiAggression = 6;
for (Card attacker : this.attackers) { 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); combat.addAttacker(attacker, defender);
return aiAggression; return aiAggression;
} }
@@ -820,12 +815,12 @@ public class AiAttackController {
if (attackMax != -1) { if (attackMax != -1) {
// should attack with only max if able. // should attack with only max if able.
CardLists.sortByPowerDesc(this.attackers); CardLists.sortByPowerDesc(this.attackers);
this.aiAggression = 6; aiAggression = 6;
for (Card attacker : this.attackers) { for (Card attacker : this.attackers) {
// reached max, breakup // reached max, breakup
if (attackMax != -1 && combat.getAttackers().size() >= attackMax) if (attackMax != -1 && combat.getAttackers().size() >= attackMax)
break; 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); combat.addAttacker(attacker, defender);
} }
} }
@@ -833,6 +828,7 @@ public class AiAttackController {
return aiAggression; return aiAggression;
} }
// TODO move this lower so it can also switch defender
if (simAI && ComputerUtilCard.isNonDisabledCardInPlay(ai, "Reconnaissance")) { if (simAI && ComputerUtilCard.isNonDisabledCardInPlay(ai, "Reconnaissance")) {
for (Card attacker : attackersLeft) { for (Card attacker : attackersLeft) {
if (canAttackWrapper(attacker, defender)) { if (canAttackWrapper(attacker, defender)) {
@@ -841,7 +837,7 @@ public class AiAttackController {
} }
} }
// safe to exert // safe to exert
this.aiAggression = 6; aiAggression = 6;
return aiAggression; return aiAggression;
} }
@@ -862,7 +858,7 @@ public class AiAttackController {
// get the potential damage and strength of the AI forces // get the potential damage and strength of the AI forces
final List<Card> candidateAttackers = new ArrayList<>(); final List<Card> candidateAttackers = new ArrayList<>();
int candidateUnblockedDamage = 0; 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 // if the creature can attack then it's a potential attacker this
// turn, assume summoning sickness creatures will be able to // turn, assume summoning sickness creatures will be able to
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) { if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
@@ -991,7 +987,7 @@ public class AiAttackController {
boolean isUnblockableCreature = true; boolean isUnblockableCreature = true;
// check blockers individually, as the bulk canBeBlocked doesn't // check blockers individually, as the bulk canBeBlocked doesn't
// check all circumstances // check all circumstances
for (final Card blocker : this.myList) { for (final Card blocker : myList) {
if (CombatUtil.canBlock(attacker, blocker, true)) { if (CombatUtil.canBlock(attacker, blocker, true)) {
isUnblockableCreature = false; isUnblockableCreature = false;
break; break;
@@ -1015,10 +1011,10 @@ public class AiAttackController {
// totals and other considerations some bad "magic numbers" here // totals and other considerations some bad "magic numbers" here
// TODO replace with nice descriptive variable names // TODO replace with nice descriptive variable names
if (ratioDiff > 0 && doAttritionalAttack) { 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)) } else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|| (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) { || (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 } else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
&& defendingOpponent != null && defendingOpponent != null
&& ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent) && ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent)
@@ -1028,25 +1024,25 @@ public class AiAttackController {
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana) && (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() < && (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.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) { } 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 } else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
|| ratioDiff * -1 < turnsUntilDeathByUnblockable) { || ratioDiff * -1 < turnsUntilDeathByUnblockable) {
// at 0 ratio expect to potentially gain an advantage by attacking first // at 0 ratio expect to potentially gain an advantage by attacking first
// if the ai has a slight advantage // if the ai has a slight advantage
// or the ai has a significant advantage numerically but only a slight disadvantage damage/life // 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) { } else if (doUnblockableAttack) {
this.aiAggression = 1; aiAggression = 1;
// look for unblockable creatures that might be // look for unblockable creatures that might be
// able to attack for a bit of fatal damage even if the player is significantly better // able to attack for a bit of fatal damage even if the player is significantly better
} else { } else {
this.aiAggression = 0; aiAggression = 0;
} // stay at home to block } // stay at home to block
if ( LOG_AI_ATTACKS ) if ( LOG_AI_ATTACKS )
System.out.println(this.aiAggression + " = ai aggression"); System.out.println(aiAggression + " = ai aggression");
// **************** // ****************
// Evaluation the end // Evaluation the end
@@ -1055,7 +1051,7 @@ public class AiAttackController {
if ( LOG_AI_ATTACKS ) if ( LOG_AI_ATTACKS )
System.out.println("Normal attack"); System.out.println("Normal attack");
attackersLeft = notNeededAsBlockers(ai, attackersLeft); attackersLeft = notNeededAsBlockers(attackersLeft);
attackersLeft = sortAttackers(attackersLeft); attackersLeft = sortAttackers(attackersLeft);
if ( LOG_AI_ATTACKS ) if ( LOG_AI_ATTACKS )
@@ -1068,13 +1064,15 @@ public class AiAttackController {
CardCollection attackersAssigned = new CardCollection(); CardCollection attackersAssigned = new CardCollection();
for (int i = 0; i < attackersLeft.size(); i++) { for (int i = 0; i < attackersLeft.size(); i++) {
final Card attacker = attackersLeft.get(i); final Card attacker = attackersLeft.get(i);
if (this.aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike() if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike()
&& ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, this.defendingOpponent) && ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent)
>= ComputerUtilCombat.getDamageToKill(attacker, false)) { >= ComputerUtilCombat.getDamageToKill(attacker, false)) {
continue; 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); combat.addAttacker(attacker, defender);
attackersAssigned.add(attacker); attackersAssigned.add(attacker);
@@ -1132,7 +1130,7 @@ public class AiAttackController {
* a {@link forge.game.combat.Combat} object. * a {@link forge.game.combat.Combat} object.
* @return a boolean. * @return a boolean.
*/ */
public final boolean shouldAttack(final Player ai, final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) { public final boolean shouldAttack(final Card attacker, final List<Card> defenders, final Combat combat, final GameEntity defender) {
boolean canBeKilled = false; // indicates if the attacker can be killed 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 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 boolean canKillAll = true; // indicates if the attacker can kill all single blockers
@@ -1270,12 +1268,12 @@ public class AiAttackController {
} }
if (numberOfPossibleBlockers > 2 if (numberOfPossibleBlockers > 2
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent)) || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent))
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) { || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent))) {
canBeBlocked = true; canBeBlocked = true;
} }
// decide if the creature should attack based on the prevailing strategy choice in aiAggression // 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 case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) { if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
if (LOG_AI_ATTACKS) if (LOG_AI_ATTACKS)
@@ -1325,7 +1323,7 @@ public class AiAttackController {
return false; // don't attack return false; // don't attack
} }
public static List<Card> exertAttackers(List<Card> attackers, int aggression) { public static List<Card> exertAttackers(final List<Card> attackers, int aggression) {
List<Card> exerters = Lists.newArrayList(); List<Card> exerters = Lists.newArrayList();
for (Card c : attackers) { for (Card c : attackers) {
boolean shouldExert = false; boolean shouldExert = false;
@@ -1463,7 +1461,7 @@ public class AiAttackController {
return null; //should never get here return null; //should never get here
} }
private void doLightmineFieldAttackLogic(List<Card> attackersLeft, int numForcedAttackers, boolean playAggro) { private void doLightmineFieldAttackLogic(final List<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
CardCollection attSorted = new CardCollection(attackersLeft); CardCollection attSorted = new CardCollection(attackersLeft);
CardCollection attUnsafe = new CardCollection(); CardCollection attUnsafe = new CardCollection();
CardLists.sortByToughnessDesc(attSorted); CardLists.sortByToughnessDesc(attSorted);
@@ -1493,7 +1491,7 @@ public class AiAttackController {
attackersLeft.removeAll(attUnsafe); attackersLeft.removeAll(attUnsafe);
} }
private boolean doRevengeOfRavensAttackLogic(Player ai, GameEntity defender, List<Card> attackersLeft, int numForcedAttackers, int maxAttack) { private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final List<Card> attackersLeft, int numForcedAttackers, int maxAttack) {
// TODO: detect Revenge of Ravens by the trigger instead of by name // TODO: detect Revenge of Ravens by the trigger instead of by name
boolean revengeOfRavens = false; boolean revengeOfRavens = false;
if (defender instanceof Player) { if (defender instanceof Player) {

View File

@@ -102,14 +102,13 @@ public class AiBlockController {
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) { private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>(); final List<Card> blockers = new ArrayList<>();
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus, // 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 // 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) { 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); blockers.add(b);
} }
} }
return blockers; return blockers;
} }
@@ -117,10 +116,10 @@ public class AiBlockController {
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) { private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>(); final List<Card> blockers = new ArrayList<>();
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus, // 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 // 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) { 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); blockers.add(b);
} }
} }
@@ -131,7 +130,6 @@ public class AiBlockController {
private List<Card> sortPotentialAttackers(final Combat combat) { private List<Card> sortPotentialAttackers(final Combat combat) {
final CardCollection sortedAttackers = new CardCollection(); final CardCollection sortedAttackers = new CardCollection();
CardCollection firstAttacker = new CardCollection(); CardCollection firstAttacker = new CardCollection();
final FCollectionView<GameEntity> defenders = combat.getDefenders(); final FCollectionView<GameEntity> defenders = combat.getDefenders();
// If I don't have any planeswalkers then sorting doesn't really matter // If I don't have any planeswalkers then sorting doesn't really matter
@@ -155,13 +153,11 @@ public class AiBlockController {
return attackers; return attackers;
} }
final boolean bLifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
// TODO Add creatures attacking Planeswalkers in order of which we want to protect // TODO Add creatures attacking Planeswalkers in order of which we want to protect
// defend planeswalkers with more loyalty before planeswalkers with less loyalty // defend planeswalkers with more loyalty before planeswalkers with less loyalty
// if planeswalker will be too difficult to defend don't even bother // if planeswalker will be too difficult to defend don't even bother
for (GameEntity defender : defenders) { for (GameEntity defender : defenders) {
if (defender instanceof Card) { if (defender instanceof Card && ((Card) defender).getController().equals(ai)) {
final CardCollection attackers = combat.getAttackersOf(defender); final CardCollection attackers = combat.getAttackersOf(defender);
// Begin with the attackers that pose the biggest threat // Begin with the attackers that pose the biggest threat
CardLists.sortByPowerDesc(attackers); 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 // add creatures attacking the Player to the front of the list
for (final Card c : firstAttacker) { for (final Card c : firstAttacker) {
sortedAttackers.add(0, c); sortedAttackers.add(0, c);
@@ -183,9 +179,6 @@ public class AiBlockController {
return sortedAttackers; return sortedAttackers;
} }
// ======================= block assignment functions
// ================================
// Good Blocks means a good trade or no trade // Good Blocks means a good trade or no trade
private void makeGoodBlocks(final Combat combat) { private void makeGoodBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft); List<Card> currentAttackers = new ArrayList<>(attackersLeft);
@@ -573,14 +566,14 @@ public class AiBlockController {
} }
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false); blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
List<Card> usableBlockers;
final List<Card> blockGang = new ArrayList<>(); final List<Card> blockGang = new ArrayList<>();
int absorbedDamage; // The amount of damage needed to kill the first blocker int absorbedDamage; // The amount of damage needed to kill the first blocker
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() { List<Card> usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { 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) { if (usableBlockers.size() < 2) {
@@ -820,7 +813,7 @@ public class AiBlockController {
} }
} }
// don't try to kill what can't be killed // 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; continue;
} }
@@ -877,7 +870,7 @@ public class AiBlockController {
int damageToPW = 0; int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) { for (final Card pwatkr : combat.getAttackersOf(def)) {
if (!combat.isBlocked(pwatkr)) { 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)) { 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) */ /** Assigns blockers for the provided combat instance (in favor of player passes to ctor) */
public void assignBlockersForCombat(final Combat combat) { public void assignBlockersForCombat(final Combat combat) {
assignBlockersForCombat(combat, null);
}
public void assignBlockersForCombat(final Combat combat, final CardCollection exludedBlockers) {
List<Card> possibleBlockers = ai.getCreaturesInPlay(); List<Card> possibleBlockers = ai.getCreaturesInPlay();
if (exludedBlockers != null && !exludedBlockers.isEmpty()) {
possibleBlockers.removeAll(exludedBlockers);
}
attackers = sortPotentialAttackers(combat); attackers = sortPotentialAttackers(combat);
assignBlockers(combat, possibleBlockers); assignBlockers(combat, possibleBlockers);
} }
/** /**
* assignBlockersForCombat() with additional and possibly "virtual" blockers. * assignBlockersForCombat() with additional and possibly "virtual" blockers.
* @param combat combat instance * @param combat combat instance
@@ -1025,6 +1023,10 @@ public class AiBlockController {
} }
} }
if (attackersLeft.isEmpty()) {
return;
}
// remove all blockers that can't block anyway // remove all blockers that can't block anyway
for (final Card b : possibleBlockers) { for (final Card b : possibleBlockers) {
if (!CombatUtil.canBlock(b, combat)) { if (!CombatUtil.canBlock(b, combat)) {
@@ -1032,10 +1034,6 @@ public class AiBlockController {
} }
} }
if (attackersLeft.isEmpty()) {
return;
}
// Begin with the weakest blockers // Begin with the weakest blockers
CardLists.sortByPowerAsc(blockersLeft); CardLists.sortByPowerAsc(blockersLeft);
@@ -1070,7 +1068,6 @@ public class AiBlockController {
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block assignment clearBlockers(combat, possibleBlockers); // reset every block assignment
makeTradeBlocks(combat); // choose necessary trade blocks makeTradeBlocks(combat); // choose necessary trade blocks
// if life is in danger
makeGoodBlocks(combat); makeGoodBlocks(combat);
// choose necessary chump blocks if life is still in danger // choose necessary chump blocks if life is still in danger
if (ComputerUtilCombat.lifeInDanger(ai, combat)) { 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 == // == 3. If the AI life would be in serious danger make an even safer approach ==
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
clearBlockers(combat, possibleBlockers); // reset every block assignment clearBlockers(combat, possibleBlockers);
makeChumpBlocks(combat); // choose chump blocks makeChumpBlocks(combat);
if (ComputerUtilCombat.lifeInDanger(ai, combat)) { if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeTradeBlocks(combat); // choose necessary trade makeTradeBlocks(combat);
} }
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeGoodBlocks(combat); makeGoodBlocks(combat);
} } else {
// Reinforce blockers blocking attackers with trample if life is still in danger
else {
reinforceBlockersAgainstTrample(combat); reinforceBlockersAgainstTrample(combat);
} }
makeGangBlocks(combat); makeGangBlocks(combat);
// Support blockers not destroying the attacker with more
// blockers to try to kill the attacker
reinforceBlockersToKill(combat); reinforceBlockersToKill(combat);
} }
} }

View File

@@ -29,7 +29,6 @@ import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.deck.CardPool;
import forge.deck.Deck; import forge.deck.Deck;
import forge.deck.DeckSection; import forge.deck.DeckSection;
import forge.game.*; import forge.game.*;
@@ -69,7 +68,6 @@ import io.sentry.Sentry;
import io.sentry.event.BreadcrumbBuilder; import io.sentry.event.BreadcrumbBuilder;
import java.util.*; import java.util.*;
import java.util.Map.Entry;
/** /**
* <p> * <p>
@@ -1868,9 +1866,7 @@ public class AiController {
toRemove.add(sa); toRemove.add(sa);
} }
} }
for(SpellAbility sa : toRemove) { result.removeAll(toRemove);
result.remove(sa);
}
// Play them last // Play them last
if (saGemstones != null) { if (saGemstones != null) {
@@ -2045,17 +2041,7 @@ public class AiController {
Map<DeckSection, List<? extends PaperCard>> complaints = new HashMap<>(); Map<DeckSection, List<? extends PaperCard>> complaints = new HashMap<>();
// When using simulation, AI should be able to figure out most cards. // When using simulation, AI should be able to figure out most cards.
if (!useSimulation) { if (!useSimulation) {
for (Entry<DeckSection, CardPool> ds : myDeck) { complaints = myDeck.getUnplayableAICards().unplayable;
List<PaperCard> result = Lists.newArrayList();
for (Entry<PaperCard, Integer> cp : ds.getValue()) {
if (cp.getKey().getRules().getAiHints().getRemAIDecks()) {
result.add(cp.getKey());
}
}
if (!result.isEmpty()) {
complaints.put(ds.getKey(), result);
}
}
} }
return complaints; return complaints;
} }

View File

@@ -196,8 +196,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null; return null;
} }
CardLists.sortByPowerAsc(typeList); CardLists.sortByPowerDesc(typeList);
Collections.reverse(typeList);
for (int i = 0; i < c; i++) { for (int i = 0; i < c; i++) {
chosen.add(typeList.get(i)); chosen.add(typeList.get(i));

View File

@@ -1492,7 +1492,7 @@ public class ComputerUtil {
return false; return false;
} }
public static int possibleNonCombatDamage(Player ai, Player enemy) { public static int possibleNonCombatDamage(final Player ai, final Player enemy) {
int damage = 0; int damage = 0;
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.addAll(ai.getCardsActivableInExternalZones(true)); all.addAll(ai.getCardsActivableInExternalZones(true));
@@ -1550,8 +1550,8 @@ public class ComputerUtil {
/** /**
* Overload of predictThreatenedObjects that evaluates the full stack * Overload of predictThreatenedObjects that evaluates the full stack
*/ */
public static List<GameObject> predictThreatenedObjects(final Player aiPlayer, final SpellAbility sa) { public static List<GameObject> predictThreatenedObjects(final Player ai, final SpellAbility sa) {
return predictThreatenedObjects(aiPlayer, sa, false); 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 // 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) // at all but detects this effect from SA parameters (preferred, but difficult)
CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand); CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library}); CardCollectionView inDeck = ai.getCardsIn(ZoneType.Library);
Predicate<Card> markedAsReanimator = new Predicate<Card>() { Predicate<Card> markedAsReanimator = new Predicate<Card>() {
@Override @Override
@@ -3036,13 +3036,30 @@ public class ComputerUtil {
// call this to determine if it's safe to use a life payment spell // 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. // or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell.
public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) { public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) {
// TODO should also consider them as teams return predictNextCombatsRemainingLife(ai, serious, false, payment, null) == Integer.MIN_VALUE;
for (Player opponent: ai.getOpponents()) { }
Combat combat = new Combat(opponent); 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 containsAttacker = false;
boolean thisCombat = ai.getGame().getPhaseHandler().isPlayerTurn(opponent) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_BEGIN); boolean thisCombat = ai.getGame().getPhaseHandler().isPlayerTurn(opp) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_BEGIN);
for (Card att : opponent.getCreaturesInPlay()) {
// 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))) { 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); combat.addAttacker(att, ai);
containsAttacker = true; containsAttacker = true;
} }
@@ -3050,23 +3067,28 @@ public class ComputerUtil {
if (!containsAttacker) { if (!containsAttacker) {
continue; continue;
} }
// TODO if it's next turn ignore mustBlockCards // TODO if it's next turn ignore mustBlockCards
AiBlockController block = new AiBlockController(ai, false); 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. // 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 // 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 added, might need a parameter to define whether we want to check all threats or combat threats.
if (serious && ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment)) { if (serious && ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment)) {
return true; return Integer.MIN_VALUE;
} }
if (!serious && ComputerUtilCombat.lifeInDanger(ai, combat, payment)) { 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;
} }
} }

View File

@@ -611,35 +611,6 @@ public class ComputerUtilCard {
return combat.isAttacking(card); 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. * Create a mock combat where ai is being attacked and returns the list of likely blockers.
* @param ai blocking player * @param ai blocking player
@@ -696,6 +667,35 @@ public class ComputerUtilCard {
return ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, combat); 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. * getMostExpensivePermanentAI.
* *
@@ -1967,14 +1967,12 @@ public class ComputerUtilCard {
public static Cost getTotalWardCost(Card c) { public static Cost getTotalWardCost(Card c) {
Cost totalCost = new Cost(ManaCost.NO_COST, false); Cost totalCost = new Cost(ManaCost.NO_COST, false);
for (final KeywordInterface inst : c.getKeywords()) { for (final KeywordInterface inst : c.getKeywords(Keyword.WARD)) {
if (inst.getKeyword() == Keyword.WARD) {
final String keyword = inst.getOriginal(); final String keyword = inst.getOriginal();
final String[] k = keyword.split(":"); final String[] k = keyword.split(":");
final Cost wardCost = new Cost(k[1], false); final Cost wardCost = new Cost(k[1], false);
totalCost = totalCost.add(wardCost); totalCost = totalCost.add(wardCost);
} }
}
return totalCost; return totalCost;
} }
@@ -2000,6 +1998,7 @@ public class ComputerUtilCard {
return false; return false;
} }
// TODO replace most calls to Player.isCardInPlay because they include phased out
public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) { public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) {
for (Card card : ai.getCardsIn(ZoneType.Battlefield, cardName)) { for (Card card : ai.getCardsIn(ZoneType.Battlefield, cardName)) {
// TODO - Better logic to determine if a permanent is disabled by local effects // TODO - Better logic to determine if a permanent is disabled by local effects

View File

@@ -154,13 +154,11 @@ public class ComputerUtilCombat {
* @return a int. * @return a int.
*/ */
public static int getTotalFirstStrikeBlockPower(final Card attacker, final Player player) { public static int getTotalFirstStrikeBlockPower(final Card attacker, final Player player) {
final Card att = attacker;
List<Card> list = player.getCreaturesInPlay(); List<Card> list = player.getCreaturesInPlay();
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
public boolean apply(final Card c) { 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); damage += predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
if (!attacker.hasKeyword(Keyword.INFECT)) { if (!attacker.hasKeyword(Keyword.INFECT)) {
sum = predictDamageTo(attacked, damage, attacker, true); sum = predictDamageTo(attacked, damage, attacker, true);
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (attacker.hasDoubleStrike()) {
sum *= 2; sum *= 2;
} }
} }
@@ -249,7 +247,7 @@ public class ComputerUtilCombat {
pd = 0; pd = 0;
} }
poison += pd; poison += pd;
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (attacker.hasDoubleStrike()) {
poison += pd; poison += pd;
} }
} }
@@ -304,6 +302,20 @@ public class ComputerUtilCombat {
return sum; return sum;
} }
// Checks if the life of the attacked Player would be reduced
/**
* <p>
* wouldLoseLife.
* </p>
*
* @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 // calculates the amount of life that will remain after the attack
/** /**
* <p> * <p>
@@ -398,7 +410,6 @@ public class ComputerUtilCombat {
return res; return res;
} }
// Checks if the life of the attacked Player/Planeswalker is in danger
/** /**
* <p> * <p>
* lifeInDanger. * lifeInDanger.
@@ -406,7 +417,7 @@ public class ComputerUtilCombat {
* *
* @param combat * @param combat
* a {@link forge.game.combat.Combat} object. * 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) { public static boolean lifeInDanger(final Player ai, final Combat combat) {
return lifeInDanger(ai, combat, 0); return lifeInDanger(ai, combat, 0);
@@ -473,29 +484,13 @@ public class ComputerUtilCombat {
maxTreshold--; maxTreshold--;
} }
if (lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife()) if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())) {
&& !ai.cantLoseForZeroOrLessLife()) {
return true; return true;
} }
return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters()); return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters());
} }
// Checks if the life of the attacked Player would be reduced
/**
* <p>
* wouldLoseLife.
* </p>
*
* @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
/** /**
* <p> * <p>
* lifeInSeriousDanger. * lifeInSeriousDanger.
@@ -503,7 +498,7 @@ public class ComputerUtilCombat {
* *
* @param combat * @param combat
* a {@link forge.game.combat.Combat} object. * 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) { public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) {
return lifeInSeriousDanger(ai, combat, 0); 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; return true;
} }
@@ -597,7 +592,7 @@ public class ComputerUtilCombat {
public static int dealsDamageAsBlocker(final Card attacker, final Card defender) { public static int dealsDamageAsBlocker(final Card attacker, final Card defender) {
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender); int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (defender.hasDoubleStrike()) {
defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true); defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true);
} }
@@ -743,8 +738,8 @@ public class ComputerUtilCombat {
int firstStrikeBlockerDmg = 0; int firstStrikeBlockerDmg = 0;
for (final Card defender : blockers) { for (final Card defender : blockers) {
if (canDestroyAttacker(ai, attacker, defender, combat, true) if (!(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))
&& !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) { && canDestroyAttacker(ai, attacker, defender, combat, true)) {
return true; return true;
} }
if (defender.hasFirstStrike() || defender.hasDoubleStrike()) { if (defender.hasFirstStrike() || defender.hasDoubleStrike()) {
@@ -927,7 +922,7 @@ public class ComputerUtilCombat {
if (dealsFirstStrikeDamage(attacker, withoutAbilities, null) if (dealsFirstStrikeDamage(attacker, withoutAbilities, null)
&& (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)) && (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, null) && !dealsFirstStrikeDamage(blocker, withoutAbilities, null)
&& !blocker.canReceiveCounters(CounterEnumType.M1M1)) { && blocker.canReceiveCounters(CounterEnumType.M1M1)) {
power -= attacker.getNetCombatDamage(); power -= attacker.getNetCombatDamage();
} }
@@ -1224,7 +1219,7 @@ public class ComputerUtilCombat {
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
&& (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)) && (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))
&& !dealsFirstStrikeDamage(attacker, withoutAbilities, combat) && !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
&& !attacker.canReceiveCounters(CounterEnumType.M1M1)) { && attacker.canReceiveCounters(CounterEnumType.M1M1)) {
power -= blocker.getNetCombatDamage(); power -= blocker.getNetCombatDamage();
} }
theTriggers.addAll(blocker.getTriggers()); theTriggers.addAll(blocker.getTriggers());
@@ -1272,6 +1267,10 @@ public class ComputerUtilCombat {
continue; continue;
} }
if (!sa.hasParam("NumAtt")) {
continue;
}
sa.setActivatingPlayer(source.getController()); sa.setActivatingPlayer(source.getController());
if (sa.hasParam("Cost")) { if (sa.hasParam("Cost")) {
@@ -1300,9 +1299,6 @@ public class ComputerUtilCombat {
if (!list.contains(attacker)) { if (!list.contains(attacker)) {
continue; continue;
} }
if (!sa.hasParam("NumAtt")) {
continue;
}
String att = sa.getParam("NumAtt"); String att = sa.getParam("NumAtt");
if (att.startsWith("+")) { if (att.startsWith("+")) {
@@ -1311,7 +1307,7 @@ public class ComputerUtilCombat {
if (att.matches("[0-9][0-9]?") || att.matches("-" + "[0-9][0-9]?")) { if (att.matches("[0-9][0-9]?") || att.matches("-" + "[0-9][0-9]?")) {
power += Integer.parseInt(att); power += Integer.parseInt(att);
} else { } else {
String bonus = source.getSVar(att); String bonus = AbilityUtils.getSVar(sa, att);
if (bonus.contains("TriggerCount$NumBlockers")) { if (bonus.contains("TriggerCount$NumBlockers")) {
bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
@@ -1601,7 +1597,7 @@ public class ComputerUtilCombat {
if (blocker.isEquippedBy("Godsend")) { if (blocker.isEquippedBy("Godsend")) {
return true; return true;
} }
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) { if (combatantCantBeDestroyed(attacker.getController(), attacker)) {
return false; return false;
} }
@@ -1718,7 +1714,7 @@ public class ComputerUtilCombat {
} }
} // flanking } // 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))) && !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)))
|| (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterEnumType.M1M1) && (attacker || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterEnumType.M1M1) && (attacker
.getCounters(CounterEnumType.M1M1) == 0)) .getCounters(CounterEnumType.M1M1) == 0))
@@ -1728,7 +1724,6 @@ public class ComputerUtilCombat {
} }
int defenderDamage; int defenderDamage;
int attackerDamage;
if (blocker.toughnessAssignsDamage()) { if (blocker.toughnessAssignsDamage()) {
defenderDamage = blocker.getNetToughness() defenderDamage = blocker.getNetToughness()
+ predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities); + predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
@@ -1736,13 +1731,6 @@ public class ComputerUtilCombat {
defenderDamage = blocker.getNetPower() defenderDamage = blocker.getNetPower()
+ predictPowerBonusOfBlocker(attacker, blocker, withoutAbilities); + 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 possibleDefenderPrevention = 0;
int possibleAttackerPrevention = 0; int possibleAttackerPrevention = 0;
@@ -1753,17 +1741,26 @@ public class ComputerUtilCombat {
// consider Damage Prevention/Replacement // consider Damage Prevention/Replacement
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true); defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) { if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
return false; 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) final int defenderLife = getDamageToKill(blocker, false)
+ predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities); + predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
final int attackerLife = getDamageToKill(attacker, false) final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); + 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"))) { if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true; return true;
} }
@@ -1824,8 +1821,8 @@ public class ComputerUtilCombat {
final List<Card> attackers = combat.getAttackersBlockedBy(blocker); final List<Card> attackers = combat.getAttackersBlockedBy(blocker);
for (Card attacker : attackers) { for (Card attacker : attackers) {
if (canDestroyBlocker(ai, blocker, attacker, combat, true) if (!(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
&& !(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) { && canDestroyBlocker(ai, blocker, attacker, combat, true)) {
return true; return true;
} }
} }
@@ -1931,7 +1928,7 @@ public class ComputerUtilCombat {
return true; 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))) .hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)))
|| (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterEnumType.M1M1) && blocker || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterEnumType.M1M1) && blocker
.getCounters(CounterEnumType.M1M1) == 0) .getCounters(CounterEnumType.M1M1) == 0)
@@ -1993,11 +1990,11 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false) final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (attacker.hasDoubleStrike()) {
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) { if (attackerDamage >= defenderLife) {
return true; return true;
} }
if (attackerDamage >= defenderLife) { if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
return true; return true;
} }
@@ -2510,7 +2507,7 @@ public class ComputerUtilCombat {
} }
} }
poison += pd; poison += pd;
if (pd > 0 && attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (pd > 0 && attacker.hasDoubleStrike()) {
poison += pd; poison += pd;
} }
// TODO: Predict replacement effects for counters (doubled, reduced, additional counters, etc.) // TODO: Predict replacement effects for counters (doubled, reduced, additional counters, etc.)

View File

@@ -219,8 +219,9 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (c.hasKeyword(Keyword.VANISHING)) { if (c.hasKeyword(Keyword.VANISHING)) {
value -= subValue(20, "vanishing"); value -= subValue(20, "vanishing");
} }
// use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) { if (c.hasKeyword(Keyword.PHASING)) {
value -= subValue(10, "phasing"); value -= subValue(Math.max(20, value / 2), "phasing");
} }
// TODO no longer a KW // TODO no longer a KW

View File

@@ -89,12 +89,22 @@ import forge.util.collect.FCollectionView;
public class PlayerControllerAi extends PlayerController { public class PlayerControllerAi extends PlayerController {
private final AiController brains; private final AiController brains;
private boolean pilotsNonAggroDeck = false;
public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) { public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) {
super(game, p, lp); super(game, p, lp);
brains = new AiController(p, game); 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) { public void allowCheatShuffle(boolean value) {
brains.allowCheatShuffle(value); brains.allowCheatShuffle(value);
} }
@@ -1132,6 +1142,9 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) { public Map<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto
setupAutoProfile(myDeck);
return brains.complainCardsCantPlayWell(myDeck); return brains.complainCardsCantPlayWell(myDeck);
} }

View File

@@ -1587,7 +1587,7 @@ public class AttachAi extends SpellAbilityAi {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0
&& (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true)); && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.equals("First Strike")) { } 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)); && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true));
} else if (keyword.startsWith("Flanking")) { } else if (keyword.startsWith("Flanking")) {
return card.getNetCombatDamage() + powerBonus > 0 return card.getNetCombatDamage() + powerBonus > 0

View File

@@ -156,7 +156,7 @@ public class CountersPutAi extends CountersAi {
final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined")) final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined"))
&& "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X")) && "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X"))
&& sa.hasParam("MaxFromEffect"); && 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)) { if ("ExistingCounter".equals(type)) {
final boolean eachExisting = sa.hasParam("EachExistingCounter"); final boolean eachExisting = sa.hasParam("EachExistingCounter");

View File

@@ -1024,13 +1024,13 @@ public class DamageDealAi extends DamageAiBase {
return false; return false;
} }
CardCollection creatures = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES); CardCollection creatures = ai.getOpponents().getCreaturesInPlay();
Card tgtCreature = null; Card tgtCreature = null;
for (Card c : creatures) { for (Card c : creatures) {
int power = c.getNetPower(); int power = c.getNetPower();
int toughness = c.getNetToughness(); 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+) // Currently will target creatures with toughness 3+ (or power 5+)
// and only if the creature can actually die, do not "underdrain" // and only if the creature can actually die, do not "underdrain"

View File

@@ -1,6 +1,5 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Collections;
import java.util.List; import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
@@ -44,8 +43,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
} }
else if (tgt.getMinTargets(sa.getHostCard(), sa) > 1) { else if (tgt.getMinTargets(sa.getHostCard(), sa) > 1) {
CardCollection list2 = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardCollection list2 = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
CardLists.sortByPowerAsc(list2); CardLists.sortByPowerDesc(list2);
Collections.reverse(list2);
c2 = list2.isEmpty() ? null : list2.get(0); c2 = list2.isEmpty() ? null : list2.get(0);
sa.getTargets().add(c2); sa.getTargets().add(c2);
} }

View File

@@ -302,7 +302,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
&& !opp.getCreaturesInPlay().isEmpty() && !opp.getCreaturesInPlay().isEmpty()
&& Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)); && Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card));
} else if (keyword.equals("First Strike")) { } else if (keyword.equals("First Strike")) {
if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) { if (card.hasDoubleStrike()) {
return false; return false;
} }
if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) { if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) {

View File

@@ -23,6 +23,8 @@ import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.card.CardDb; import forge.card.CardDb;
import forge.card.CardEdition; import forge.card.CardEdition;
import forge.card.CardRules;
import forge.card.CardType;
import forge.item.IPaperCard; import forge.item.IPaperCard;
import forge.item.PaperCard; import forge.item.PaperCard;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@@ -613,4 +615,27 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
} }
return false; return false;
} }
public static int getAverageCMC(Deck deck) {
int totalCMC = 0;
int totalCount = 0;
for (final Entry<DeckSection, CardPool> deckEntry : deck) {
switch (deckEntry.getKey()) {
case Main:
case Commander:
for (final Entry<PaperCard, Integer> 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);
}
} }

View File

@@ -861,7 +861,7 @@ public class Game {
getNextPlayerAfter(p).initPlane(); 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 // 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 // TODO need to check rules if it should try the next player if able
if (p.equals(getPhaseHandler().getPlayerTurn())) { if (p.equals(getPhaseHandler().getPlayerTurn())) {

View File

@@ -653,12 +653,12 @@ public abstract class SpellAbilityEffect {
@Override @Override
public void run() { public void run() {
CardZoneTable untilTable = new CardZoneTable();
CardCollectionView untilCards = hostCard.getUntilLeavesBattlefield(); CardCollectionView untilCards = hostCard.getUntilLeavesBattlefield();
// if the list is empty, then the table doesn't need to be checked anymore // if the list is empty, then the table doesn't need to be checked anymore
if (untilCards.isEmpty()) { if (untilCards.isEmpty()) {
return; return;
} }
CardZoneTable untilTable = new CardZoneTable();
Map<AbilityKey, Object> moveParams = AbilityKey.newMap(); Map<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield()); moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateGraveyard()); moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateGraveyard());

View File

@@ -226,7 +226,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
} }
} }
} }
if (remLKI && movedCard != null) { if (remLKI) {
final Card lki = CardUtil.getLKICopy(c); final Card lki = CardUtil.getLKICopy(c);
game.getCardState(source).addRemembered(lki); game.getCardState(source).addRemembered(lki);
if (!source.isRemembered(lki)) { if (!source.isRemembered(lki)) {

View File

@@ -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. 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: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. 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:X:Count$xPaid
SVar:PaidNum:Number$0 SVar:PaidNum:Number$0
SVar:NonStackingEffect:True SVar:NonStackingEffect:True

View File

@@ -347,32 +347,9 @@ public class DeckProxy implements InventoryItem {
return sbSize; return sbSize;
} }
public static int getAverageCMC(Deck deck) {
int totalCMC = 0;
int totalCount = 0;
for (final Entry<DeckSection, CardPool> deckEntry : deck) {
switch (deckEntry.getKey()) {
case Main:
case Commander:
for (final Entry<PaperCard, Integer> 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() { public Integer getAverageCMC() {
if (avgCMC == null) { if (avgCMC == null) {
avgCMC = getAverageCMC(getDeck()); avgCMC = Deck.getAverageCMC(getDeck());
} }
return avgCMC; return avgCMC;
} }

View File

@@ -211,7 +211,7 @@ public class GauntletIO {
final boolean foil = "1".equals(reader.getAttribute("foil")); final boolean foil = "1".equals(reader.getAttribute("foil"));
PaperCard card = FModel.getMagicDb().getOrLoadCommonCard(name, set, index, foil); PaperCard card = FModel.getMagicDb().getOrLoadCommonCard(name, set, index, foil);
if (null == card) { 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; return card;
} }

View File

@@ -25,6 +25,7 @@ import forge.card.CardType;
import forge.card.CardType.CoreType; import forge.card.CardType.CoreType;
import forge.card.CardType.Supertype; import forge.card.CardType.Supertype;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.deck.Deck;
import forge.deck.CardPool; import forge.deck.CardPool;
import forge.deck.DeckProxy; import forge.deck.DeckProxy;
import forge.deck.DeckSection; import forge.deck.DeckSection;
@@ -681,7 +682,7 @@ public class AdvancedSearch {
COMMANDER_DECK_AVERAGE_CMC("lblDeckAverageCMC", ConquestCommander.class, FilterOperator.NUMBER_OPS, new NumericEvaluator<ConquestCommander>(0, 20) { COMMANDER_DECK_AVERAGE_CMC("lblDeckAverageCMC", ConquestCommander.class, FilterOperator.NUMBER_OPS, new NumericEvaluator<ConquestCommander>(0, 20) {
@Override @Override
protected Integer getItemValue(ConquestCommander input) { 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<ConquestCommander>() { COMMANDER_DECK_CONTENTS("lblDeckContents", ConquestCommander.class, FilterOperator.DECK_CONTENT_OPS, new DeckContentEvaluator<ConquestCommander>() {