mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-19 12:18:00 +00:00
Merge branch 'combatAI' into 'master'
Improve notNeededAsBlockers See merge request core-developers/forge!6398
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
// these creatures will be available to block anyway
|
||||||
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
|
|
||||||
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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,13 +1967,11 @@ 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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2285,7 +2282,7 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
|
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;
|
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.)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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())) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>() {
|
||||||
|
|||||||
Reference in New Issue
Block a user