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