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 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>
@@ -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<Card> getPossibleBlockers(final List<Card> blockers, final List<Card> attackers, final boolean nextTurn) {
List<Card> possibleBlockers = new ArrayList<>(blockers);
possibleBlockers = CardLists.filter(possibleBlockers, new Predicate<Card>() {
return CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return canBlockAnAttacker(c, attackers, nextTurn);
}
});
return possibleBlockers;
}
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
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
final List<Card> notNeededAsBlockers = new ArrayList<>(attackers);
int fixedBlockers = 0;
final List<Card> vigilantes = new ArrayList<>();
public final List<Card> notNeededAsBlockers(final List<Card> 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<Card> opponentsAttackers = new ArrayList<>(oppList);
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getNetCombatDamage() > 0 && ComputerUtilCombat.canAttackNextTurn(c);
}
});
for (final Card c : this.myList) {
final List<Card> 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<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);
}
});
final List<Card> notNeededAsBlockers = new CardCollection(attackers);
// don't hold back creatures that can't block any of the human creatures
final List<Card> 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<Card> 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<Card> 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<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 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<Card> exertAttackers(List<Card> attackers, int aggression) {
public static List<Card> exertAttackers(final List<Card> attackers, int aggression) {
List<Card> 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<Card> attackersLeft, int numForcedAttackers, boolean playAggro) {
private void doLightmineFieldAttackLogic(final List<Card> 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<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
boolean revengeOfRavens = false;
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) {
final List<Card> 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<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> 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<Card> sortPotentialAttackers(final Combat combat) {
final CardCollection sortedAttackers = new CardCollection();
CardCollection firstAttacker = new CardCollection();
final FCollectionView<GameEntity> 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<Card> currentAttackers = new ArrayList<>(attackersLeft);
@@ -573,14 +566,14 @@ public class AiBlockController {
}
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
List<Card> usableBlockers;
final List<Card> blockGang = new ArrayList<>();
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
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<Card> 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);
}
}

View File

@@ -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;
/**
* <p>
@@ -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<DeckSection, List<? extends PaperCard>> complaints = new HashMap<>();
// When using simulation, AI should be able to figure out most cards.
if (!useSimulation) {
for (Entry<DeckSection, CardPool> ds : myDeck) {
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);
}
}
complaints = myDeck.getUnplayableAICards().unplayable;
}
return complaints;
}

View File

@@ -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));

View File

@@ -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<GameObject> predictThreatenedObjects(final Player aiPlayer, final SpellAbility sa) {
return predictThreatenedObjects(aiPlayer, sa, false);
public static List<GameObject> 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<Card> markedAsReanimator = new Predicate<Card>() {
@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;
}
}

View File

@@ -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

View File

@@ -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<Card> list = player.getCreaturesInPlay();
list = CardLists.filter(list, new Predicate<Card>() {
@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
/**
* <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
/**
* <p>
@@ -398,7 +410,6 @@ public class ComputerUtilCombat {
return res;
}
// Checks if the life of the attacked Player/Planeswalker is in danger
/**
* <p>
* 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
/**
* <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>
* 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<Card> 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.)

View File

@@ -219,8 +219,9 @@ public class CreatureEvaluator implements Function<Card, Integer> {
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

View File

@@ -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<DeckSection, List<? extends PaperCard>> complainCardsCantPlayWell(Deck myDeck) {
// TODO check if profile detection set to Auto
setupAutoProfile(myDeck);
return brains.complainCardsCantPlayWell(myDeck);
}

View File

@@ -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

View File

@@ -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");

View File

@@ -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"

View File

@@ -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);
}

View File

@@ -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()) {

View File

@@ -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<Entry<DeckSection, CardPo
}
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();
}
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())) {

View File

@@ -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<AbilityKey, Object> moveParams = AbilityKey.newMap();
moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield());
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);
game.getCardState(source).addRemembered(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.
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

View File

@@ -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<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() {
if (avgCMC == null) {
avgCMC = getAverageCMC(getDeck());
avgCMC = Deck.getAverageCMC(getDeck());
}
return avgCMC;
}

View File

@@ -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;
}

View File

@@ -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<ConquestCommander>(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<ConquestCommander>() {