diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 4095b830b0d..319bb369fb5 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -76,7 +76,7 @@ public class AiAttackController { private Player defendingOpponent; private int aiAggression = 0; // how aggressive the ai is attack will be depending on circumstances - private final boolean nextTurn; + private final boolean nextTurn; // include creature that can only attack/block next turn /** *

@@ -90,22 +90,22 @@ public class AiAttackController { public AiAttackController(final Player ai, boolean nextTurn) { this.ai = ai; - this.defendingOpponent = choosePreferredDefenderPlayer(ai); - this.oppList = getOpponentCreatures(this.defendingOpponent); - this.myList = ai.getCreaturesInPlay(); + defendingOpponent = choosePreferredDefenderPlayer(ai); + this.oppList = getOpponentCreatures(defendingOpponent); + myList = ai.getCreaturesInPlay(); this.nextTurn = nextTurn; - refreshAttackers(this.defendingOpponent); + refreshAttackers(defendingOpponent); this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn); } // overloaded constructor to evaluate attackers that should attack next turn public AiAttackController(final Player ai, Card attacker) { this.ai = ai; - this.defendingOpponent = choosePreferredDefenderPlayer(ai); - this.oppList = getOpponentCreatures(this.defendingOpponent); - this.myList = ai.getCreaturesInPlay(); + defendingOpponent = choosePreferredDefenderPlayer(ai); + this.oppList = getOpponentCreatures(defendingOpponent); + myList = ai.getCreaturesInPlay(); this.nextTurn = false; this.attackers = new ArrayList<>(); - if (CombatUtil.canAttack(attacker, this.defendingOpponent)) { + if (CombatUtil.canAttack(attacker, defendingOpponent)) { attackers.add(attacker); } this.blockers = getPossibleBlockers(oppList, this.attackers, this.nextTurn); @@ -168,7 +168,7 @@ public class AiAttackController { if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players // TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked - return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size())); + return Aggregates.random(ai.getOpponents()); } return defender; } @@ -199,7 +199,7 @@ public class AiAttackController { } return list; - } // sortAttackers() + } // Is there any reward for attacking? (for 0/1 creatures there is not) /** @@ -254,7 +254,7 @@ public class AiAttackController { onlyIfExalted = true; } - if (!onlyIfExalted || this.attackers.size() == 1 || this.aiAggression == 6 /* 6 is Exalted attack */) { + if (!onlyIfExalted || this.attackers.size() == 1 || aiAggression == 6 /* 6 is Exalted attack */) { return true; } } @@ -282,14 +282,12 @@ public class AiAttackController { } public final static List getPossibleBlockers(final List blockers, final List attackers, final boolean nextTurn) { - List possibleBlockers = new ArrayList<>(blockers); - possibleBlockers = CardLists.filter(possibleBlockers, new Predicate() { + return CardLists.filter(blockers, new Predicate() { @Override public boolean apply(final Card c) { return canBlockAnAttacker(c, attackers, nextTurn); } }); - return possibleBlockers; } public final static boolean canBlockAnAttacker(final Card c, final List attackers, final boolean nextTurn) { @@ -310,10 +308,7 @@ public class AiAttackController { } // this checks to make sure that the computer player doesn't lose when the human player attacks - public final List notNeededAsBlockers(final Player ai, final List attackers) { - final List notNeededAsBlockers = new ArrayList<>(attackers); - int fixedBlockers = 0; - final List vigilantes = new ArrayList<>(); + public final List notNeededAsBlockers(final List attackers) { //check for time walks if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) { return attackers; @@ -329,83 +324,124 @@ public class AiAttackController { } } attackers.removeAll(toRemove); - return attackers; } - List opponentsAttackers = new ArrayList<>(oppList); - opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate() { - @Override - public boolean apply(final Card c) { - return c.getNetCombatDamage() > 0 && ComputerUtilCombat.canAttackNextTurn(c); - } - }); - for (final Card c : this.myList) { + final List vigilantes = new ArrayList<>(); + for (final Card c : myList) { if (c.getName().equals("Masako the Humorless")) { // "Tapped creatures you control can block as though they were untapped." return attackers; } - if (!attackers.contains(c)) { // this creature can't attack anyway - if (canBlockAnAttacker(c, opponentsAttackers, false)) { - fixedBlockers++; - } - continue; - } // no need to block if an effect is in play which untaps all creatures - // (pseudo-Vigilance akin to Awakening or or Prophet of Kruphix) + // (pseudo-Vigilance akin to Awakening or Prophet of Kruphix) if (c.hasKeyword(Keyword.VIGILANCE) || ComputerUtilCard.willUntap(ai, c)) { vigilantes.add(c); - notNeededAsBlockers.remove(c); // they will be re-added later - if (canBlockAnAttacker(c, opponentsAttackers, false)) { - fixedBlockers++; + } + } + // reduce the search space + final List opponentsAttackers = CardLists.filter(ai.getOpponents().getCreaturesInPlay(), new Predicate() { + @Override + public boolean apply(final Card c) { + return !c.hasSVar("EndOfTurnLeavePlay") + && (c.toughnessAssignsDamage() || c.getNetCombatDamage() > 0 // performance shortcuts + || c.getNetCombatDamage() + ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true) > 0) + && ComputerUtilCombat.canAttackNextTurn(c); + } + }); + + final List notNeededAsBlockers = new CardCollection(attackers); + + // don't hold back creatures that can't block any of the human creatures + final List blockers = getPossibleBlockers(attackers, opponentsAttackers, true); + + if (!blockers.isEmpty()) { + notNeededAsBlockers.removeAll(blockers); + + boolean playAggro = false; + boolean pilotsNonAggroDeck = false; + if (ai.getController().isAI()) { + PlayerControllerAi aic = ((PlayerControllerAi) ai.getController()); + pilotsNonAggroDeck = aic.pilotsNonAggroDeck(); + playAggro = !pilotsNonAggroDeck || aic.getAi().getBooleanProperty(AiProps.PLAY_AGGRO); + } + // TODO make switchable via AI property + int thresholdMod = 0; + int lastAcceptableBaselineLife = 0; + if (pilotsNonAggroDeck) { + lastAcceptableBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers)); + if (!ai.isCardInPlay("Laboratory Maniac")) { + // AI is getting milled out + thresholdMod += 3 - Math.min(ai.getCardsIn(ZoneType.Library).size(), 3); + } + if (aiAggression > 4) { + thresholdMod += 1; + } + } + + // try to use strongest as attacker first + CardLists.sortByPowerDesc(blockers); + + for (Card c : blockers) { + if (vigilantes.contains(c)) { + // TODO predict the chance it might die if attacking + continue; + } + notNeededAsBlockers.add(c); + int currentBaselineLife = ComputerUtil.predictNextCombatsRemainingLife(ai, playAggro, pilotsNonAggroDeck, 0, new CardCollection(notNeededAsBlockers)); + // AI doesn't know from what it will lose, so it might still keep an unnecessary blocker back sometimes + if (currentBaselineLife == Integer.MIN_VALUE) { + notNeededAsBlockers.remove(c); + break; + } + + // in Aggro Decks AI wants to deal as much damage as it can + if (pilotsNonAggroDeck) { + int ownAttackerDmg = c.getNetCombatDamage(); + // TODO maybe add performance switch to skip these predictions? + if (c.toughnessAssignsDamage()) { + ownAttackerDmg += ComputerUtilCombat.predictToughnessBonusOfAttacker(c, null, null, true); + } else { + ownAttackerDmg += ComputerUtilCombat.predictPowerBonusOfAttacker(c, null, null, true); + } + if (c.hasDoubleStrike()) { + ownAttackerDmg *= 2; + } + ownAttackerDmg += thresholdMod; + // bail if it would cause AI more life loss from counterattack than the damage it provides as attacker + if (Math.abs(currentBaselineLife - lastAcceptableBaselineLife) > ownAttackerDmg) { + notNeededAsBlockers.remove(c); + // try find more + continue; + } else if (Math.abs(currentBaselineLife - lastAcceptableBaselineLife) == ownAttackerDmg) { + // TODO add non sim-AI property for life trade chance that scales down with amount and when difference increases + } + lastAcceptableBaselineLife = currentBaselineLife; } } } - CardLists.sortByPowerAsc(attackers); - int blockersNeeded = opponentsAttackers.size(); - // don't hold back creatures that can't block any of the human creatures - final List list = getPossibleBlockers(attackers, opponentsAttackers, nextTurn); - - //Calculate the amount of creatures necessary - for (int i = 0; i < list.size(); i++) { - if (!doesHumanAttackAndWin(ai, i)) { - blockersNeeded = i; - break; - } - } - int blockersStillNeeded = blockersNeeded - fixedBlockers; - blockersStillNeeded = Math.min(blockersStillNeeded, list.size()); - for (int i = 0; i < blockersStillNeeded; i++) { - notNeededAsBlockers.remove(list.get(i)); - } - - // re-add creatures with vigilance + // these creatures will be available to block anyway notNeededAsBlockers.addAll(vigilantes); - if (blockersNeeded > 1) { - return notNeededAsBlockers; - } - - final Player opp = this.defendingOpponent; - // Increase the total number of blockers needed by 1 if Finest Hour in play // (human will get an extra first attack with a creature that untaps) // In addition, if the computer guesses it needs no blockers, make sure // that it won't be surprised by Exalted - final int humanExaltedBonus = opp.countExaltedBonus(); + final int humanExaltedBonus = defendingOpponent.countExaltedBonus(); + int blockersNeeded = attackers.size() - notNeededAsBlockers.size(); if (humanExaltedBonus > 0) { - final boolean finestHour = opp.isCardInPlay("Finest Hour"); + final boolean finestHour = defendingOpponent.isCardInPlay("Finest Hour"); - if ((blockersNeeded == 0 || finestHour) && !this.oppList.isEmpty()) { + if ((blockersNeeded <= 0 || finestHour) && !this.oppList.isEmpty()) { // total attack = biggest creature + exalted, *2 if Rafiq is in play int humanBasePower = ComputerUtilCombat.getAttack(this.oppList.get(0)) + humanExaltedBonus; if (finestHour) { // For Finest Hour, one creature could attack and get the bonus TWICE humanBasePower += humanExaltedBonus; } - final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower + final int totalExaltedAttack = defendingOpponent.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower : humanBasePower; if (ai.getLife() - 3 <= totalExaltedAttack) { // We will lose if there is an Exalted attack -- keep one blocker @@ -423,45 +459,7 @@ public class AiAttackController { return notNeededAsBlockers; } - public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) { - int totalAttack = 0; - int totalPoison = 0; - int blockersLeft = nBlockingCreatures; - - if (ai.cantLose()) { - return false; - } - - // TODO for multiplayer this should either add some heuristics - // so the other opponents attack power is also measured in - // or refactor it with aiLifeInDanger somehow if performance impact isn't too bad - CardLists.sortByPowerDesc(oppList); - for (Card attacker : oppList) { - if (!ComputerUtilCombat.canAttackNextTurn(attacker)) { - continue; - } - if (blockersLeft > 0 && CombatUtil.canBeBlocked(attacker, ai)) { - // TODO doesn't take trample into account - blockersLeft--; - continue; - } - - // Test for some special triggers that can change the creature in combat - Card effectiveAttacker = ComputerUtilCombat.applyPotentialAttackCloneTriggers(attacker); - - // TODO commander - - totalAttack += ComputerUtilCombat.damageIfUnblocked(effectiveAttacker, ai, null, false); - totalPoison += ComputerUtilCombat.poisonIfUnblocked(effectiveAttacker, ai); - } - - if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) { - return true; - } - return ai.getPoisonCounters() + totalPoison > 9 && ai.canReceiveCounters(CounterEnumType.POISON); - } - - private boolean doAssault(final Player ai) { + private boolean doAssault() { // Beastmaster Ascension if (ai.isCardInPlay("Beastmaster Ascension") && this.attackers.size() > 1) { final CardCollectionView beastions = ai.getCardsIn(ZoneType.Battlefield, "Beastmaster Ascension"); @@ -510,8 +508,6 @@ public class AiAttackController { } } - final Player opp = this.defendingOpponent; - // if true, the AI will attempt to identify which blockers will already be taken, // thus attempting to predict how many creatures with evasion can actively block boolean predictEvasion = false; @@ -599,15 +595,14 @@ public class AiAttackController { } } - int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage; - int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp); - - if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife() - && !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) { + int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, defendingOpponent) + trampleDamage; + if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, defendingOpponent) >= defendingOpponent.getLife() + && !((defendingOpponent.cantLoseForZeroOrLessLife() || ai.cantWin()) && defendingOpponent.getLife() < 1)) { return true; } - if (totalPoisonDamage >= 10 - opp.getPoisonCounters()) { + int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, defendingOpponent); + if (totalPoisonDamage >= 10 - defendingOpponent.getPoisonCounters()) { return true; } @@ -619,7 +614,7 @@ public class AiAttackController { if (defs.size() == 1) { return defs.getFirst(); } - GameEntity prefDefender = defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0); + GameEntity prefDefender = defs.contains(defendingOpponent) ? defendingOpponent : defs.get(0); // Attempt to see if there's a defined entity that must be attacked strictly this turn... GameEntity entity = ai.getMustAttackEntityThisTurn(); @@ -660,7 +655,7 @@ public class AiAttackController { * @return a {@link forge.game.combat.Combat} object. */ public final int declareAttackers(final Combat combat) { - final boolean bAssault = doAssault(ai); + final boolean bAssault = doAssault(); // Determine who will be attacked GameEntity defender = chooseDefender(combat, bAssault); @@ -755,11 +750,11 @@ public class AiAttackController { doLightmineFieldAttackLogic(attackersLeft, numForcedAttackers, playAggro); } // Revenge of Ravens: make sure the AI doesn't kill itself and doesn't damage itself unnecessarily - if (!doRevengeOfRavensAttackLogic(ai, defender, attackersLeft, numForcedAttackers, attackMax)) { + if (!doRevengeOfRavensAttackLogic(defender, attackersLeft, numForcedAttackers, attackMax)) { return aiAggression; } - if (bAssault && defender == this.defendingOpponent) { // in case we are forced to attack someone else + if (bAssault && defender == defendingOpponent) { // in case we are forced to attack someone else if (LOG_AI_ATTACKS) System.out.println("Assault"); CardLists.sortByPowerDesc(attackersLeft); @@ -807,9 +802,9 @@ public class AiAttackController { CardLists.sortByPowerDesc(this.attackers); if (LOG_AI_ATTACKS) System.out.println("Exalted"); - this.aiAggression = 6; + aiAggression = 6; for (Card attacker : this.attackers) { - if (canAttackWrapper(attacker, defender) && shouldAttack(ai, attacker, this.blockers, combat, defender)) { + if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) { combat.addAttacker(attacker, defender); return aiAggression; } @@ -820,12 +815,12 @@ public class AiAttackController { if (attackMax != -1) { // should attack with only max if able. CardLists.sortByPowerDesc(this.attackers); - this.aiAggression = 6; + aiAggression = 6; for (Card attacker : this.attackers) { // reached max, breakup if (attackMax != -1 && combat.getAttackers().size() >= attackMax) break; - if (canAttackWrapper(attacker, defender) && shouldAttack(ai, attacker, this.blockers, combat, defender)) { + if (canAttackWrapper(attacker, defender) && shouldAttack(attacker, this.blockers, combat, defender)) { combat.addAttacker(attacker, defender); } } @@ -833,6 +828,7 @@ public class AiAttackController { return aiAggression; } + // TODO move this lower so it can also switch defender if (simAI && ComputerUtilCard.isNonDisabledCardInPlay(ai, "Reconnaissance")) { for (Card attacker : attackersLeft) { if (canAttackWrapper(attacker, defender)) { @@ -841,7 +837,7 @@ public class AiAttackController { } } // safe to exert - this.aiAggression = 6; + aiAggression = 6; return aiAggression; } @@ -862,7 +858,7 @@ public class AiAttackController { // get the potential damage and strength of the AI forces final List candidateAttackers = new ArrayList<>(); int candidateUnblockedDamage = 0; - for (final Card pCard : this.myList) { + for (final Card pCard : myList) { // if the creature can attack then it's a potential attacker this // turn, assume summoning sickness creatures will be able to if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) { @@ -991,7 +987,7 @@ public class AiAttackController { boolean isUnblockableCreature = true; // check blockers individually, as the bulk canBeBlocked doesn't // check all circumstances - for (final Card blocker : this.myList) { + for (final Card blocker : myList) { if (CombatUtil.canBlock(attacker, blocker, true)) { isUnblockableCreature = false; break; @@ -1015,10 +1011,10 @@ public class AiAttackController { // totals and other considerations some bad "magic numbers" here // TODO replace with nice descriptive variable names if (ratioDiff > 0 && doAttritionalAttack) { - this.aiAggression = 5; // attack at all costs + aiAggression = 5; // attack at all costs } else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0)) || (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) { - this.aiAggression = 4; // attack expecting to trade or damage player. + aiAggression = 4; // attack expecting to trade or damage player. } else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1 && defendingOpponent != null && ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent) @@ -1028,25 +1024,25 @@ public class AiAttackController { && (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana) && (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() < defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) { - this.aiAggression = 4; // random (chance-based) attack expecting to trade or damage player. + aiAggression = 4; // random (chance-based) attack expecting to trade or damage player. } else if (ratioDiff >= 0 && this.attackers.size() > 1) { - this.aiAggression = 3; // attack expecting to make good trades or damage player. + aiAggression = 3; // attack expecting to make good trades or damage player. } else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1 || ratioDiff * -1 < turnsUntilDeathByUnblockable) { // at 0 ratio expect to potentially gain an advantage by attacking first // if the ai has a slight advantage // or the ai has a significant advantage numerically but only a slight disadvantage damage/life - this.aiAggression = 2; // attack expecting to destroy creatures/be unblockable + aiAggression = 2; // attack expecting to destroy creatures/be unblockable } else if (doUnblockableAttack) { - this.aiAggression = 1; + aiAggression = 1; // look for unblockable creatures that might be // able to attack for a bit of fatal damage even if the player is significantly better } else { - this.aiAggression = 0; + aiAggression = 0; } // stay at home to block if ( LOG_AI_ATTACKS ) - System.out.println(this.aiAggression + " = ai aggression"); + System.out.println(aiAggression + " = ai aggression"); // **************** // Evaluation the end @@ -1055,7 +1051,7 @@ public class AiAttackController { if ( LOG_AI_ATTACKS ) System.out.println("Normal attack"); - attackersLeft = notNeededAsBlockers(ai, attackersLeft); + attackersLeft = notNeededAsBlockers(attackersLeft); attackersLeft = sortAttackers(attackersLeft); if ( LOG_AI_ATTACKS ) @@ -1068,13 +1064,15 @@ public class AiAttackController { CardCollection attackersAssigned = new CardCollection(); for (int i = 0; i < attackersLeft.size(); i++) { final Card attacker = attackersLeft.get(i); - if (this.aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike() - && ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, this.defendingOpponent) + if (aiAggression < 5 && !attacker.hasFirstStrike() && !attacker.hasDoubleStrike() + && ComputerUtilCombat.getTotalFirstStrikeBlockPower(attacker, defendingOpponent) >= ComputerUtilCombat.getDamageToKill(attacker, false)) { continue; } - if (shouldAttack(ai, attacker, this.blockers, combat, defender) && canAttackWrapper(attacker, defender)) { + // TODO logic for Questing Beast to prefer players + + if (shouldAttack(attacker, this.blockers, combat, defender) && canAttackWrapper(attacker, defender)) { combat.addAttacker(attacker, defender); attackersAssigned.add(attacker); @@ -1132,7 +1130,7 @@ public class AiAttackController { * a {@link forge.game.combat.Combat} object. * @return a boolean. */ - public final boolean shouldAttack(final Player ai, final Card attacker, final List defenders, final Combat combat, final GameEntity defender) { + public final boolean shouldAttack(final Card attacker, final List defenders, final Combat combat, final GameEntity defender) { boolean canBeKilled = false; // indicates if the attacker can be killed boolean canBeKilledByOne = false; // indicates if the attacker can be killed by a single blocker boolean canKillAll = true; // indicates if the attacker can kill all single blockers @@ -1270,12 +1268,12 @@ public class AiAttackController { } if (numberOfPossibleBlockers > 2 - || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent)) - || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) { + || (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, defendingOpponent)) + || (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, defendingOpponent))) { canBeBlocked = true; } // decide if the creature should attack based on the prevailing strategy choice in aiAggression - switch (this.aiAggression) { + switch (aiAggression) { case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) { if (LOG_AI_ATTACKS) @@ -1325,7 +1323,7 @@ public class AiAttackController { return false; // don't attack } - public static List exertAttackers(List attackers, int aggression) { + public static List exertAttackers(final List attackers, int aggression) { List exerters = Lists.newArrayList(); for (Card c : attackers) { boolean shouldExert = false; @@ -1463,7 +1461,7 @@ public class AiAttackController { return null; //should never get here } - private void doLightmineFieldAttackLogic(List attackersLeft, int numForcedAttackers, boolean playAggro) { + private void doLightmineFieldAttackLogic(final List attackersLeft, int numForcedAttackers, boolean playAggro) { CardCollection attSorted = new CardCollection(attackersLeft); CardCollection attUnsafe = new CardCollection(); CardLists.sortByToughnessDesc(attSorted); @@ -1493,7 +1491,7 @@ public class AiAttackController { attackersLeft.removeAll(attUnsafe); } - private boolean doRevengeOfRavensAttackLogic(Player ai, GameEntity defender, List attackersLeft, int numForcedAttackers, int maxAttack) { + private boolean doRevengeOfRavensAttackLogic(final GameEntity defender, final List attackersLeft, int numForcedAttackers, int maxAttack) { // TODO: detect Revenge of Ravens by the trigger instead of by name boolean revengeOfRavens = false; if (defender instanceof Player) { diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index 6c276c466d6..92b54c16901 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -102,14 +102,13 @@ public class AiBlockController { private List getSafeBlockers(final Combat combat, final Card attacker, final List blockersLeft) { final List blockers = new ArrayList<>(); - // We don't check attacker static abilities at this point since the attackers have already attacked and, thus, - // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness + // Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus, + // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness unless we're simulating an outcome outside of real combat for (final Card b : blockersLeft) { - if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, true)) { + if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, attacker.getGame().getPhaseHandler().inCombat())) { blockers.add(b); } } - return blockers; } @@ -117,10 +116,10 @@ public class AiBlockController { private List getKillingBlockers(final Combat combat, final Card attacker, final List blockersLeft) { final List blockers = new ArrayList<>(); - // We don't check attacker static abilities at this point since the attackers have already attacked and, thus, - // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness + // Usually don't check attacker static abilities at this point since the attackers have already attacked and, thus, + // their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness unless we're simulating an outcome outside of real combat for (final Card b : blockersLeft) { - if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, true)) { + if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, attacker.getGame().getPhaseHandler().inCombat())) { blockers.add(b); } } @@ -131,7 +130,6 @@ public class AiBlockController { private List sortPotentialAttackers(final Combat combat) { final CardCollection sortedAttackers = new CardCollection(); CardCollection firstAttacker = new CardCollection(); - final FCollectionView defenders = combat.getDefenders(); // If I don't have any planeswalkers then sorting doesn't really matter @@ -155,13 +153,11 @@ public class AiBlockController { return attackers; } - final boolean bLifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat); - // TODO Add creatures attacking Planeswalkers in order of which we want to protect // defend planeswalkers with more loyalty before planeswalkers with less loyalty // if planeswalker will be too difficult to defend don't even bother for (GameEntity defender : defenders) { - if (defender instanceof Card) { + if (defender instanceof Card && ((Card) defender).getController().equals(ai)) { final CardCollection attackers = combat.getAttackersOf(defender); // Begin with the attackers that pose the biggest threat CardLists.sortByPowerDesc(attackers); @@ -171,7 +167,7 @@ public class AiBlockController { } } - if (bLifeInDanger) { + if (ComputerUtilCombat.lifeInDanger(ai, combat)) { // add creatures attacking the Player to the front of the list for (final Card c : firstAttacker) { sortedAttackers.add(0, c); @@ -183,9 +179,6 @@ public class AiBlockController { return sortedAttackers; } - // ======================= block assignment functions - // ================================ - // Good Blocks means a good trade or no trade private void makeGoodBlocks(final Combat combat) { List currentAttackers = new ArrayList<>(attackersLeft); @@ -573,14 +566,14 @@ public class AiBlockController { } blockers = getPossibleBlockers(combat, attacker, blockersLeft, false); - List usableBlockers; final List blockGang = new ArrayList<>(); int absorbedDamage; // The amount of damage needed to kill the first blocker - usableBlockers = CardLists.filter(blockers, new Predicate() { + List usableBlockers = CardLists.filter(blockers, new Predicate() { @Override public boolean apply(final Card c) { - return c.getNetToughness() > attacker.getNetCombatDamage(); + return c.getNetToughness() > attacker.getNetCombatDamage() // performance shortcut + || c.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, c, true) > attacker.getNetCombatDamage(); } }); if (usableBlockers.size() < 2) { @@ -820,7 +813,7 @@ public class AiBlockController { } } // don't try to kill what can't be killed - if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) { + if (ComputerUtilCombat.combatantCantBeDestroyed(ai, attacker)) { continue; } @@ -877,7 +870,7 @@ public class AiBlockController { int damageToPW = 0; for (final Card pwatkr : combat.getAttackersOf(def)) { if (!combat.isBlocked(pwatkr)) { - damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true); + damageToPW += ComputerUtilCombat.predictDamageTo(def, pwatkr.getNetCombatDamage(), pwatkr, true); } } if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= def.getCounters(CounterEnumType.LOYALTY)) { @@ -965,11 +958,16 @@ public class AiBlockController { /** Assigns blockers for the provided combat instance (in favor of player passes to ctor) */ public void assignBlockersForCombat(final Combat combat) { + assignBlockersForCombat(combat, null); + } + public void assignBlockersForCombat(final Combat combat, final CardCollection exludedBlockers) { List possibleBlockers = ai.getCreaturesInPlay(); + if (exludedBlockers != null && !exludedBlockers.isEmpty()) { + possibleBlockers.removeAll(exludedBlockers); + } attackers = sortPotentialAttackers(combat); assignBlockers(combat, possibleBlockers); } - /** * assignBlockersForCombat() with additional and possibly "virtual" blockers. * @param combat combat instance @@ -1025,6 +1023,10 @@ public class AiBlockController { } } + if (attackersLeft.isEmpty()) { + return; + } + // remove all blockers that can't block anyway for (final Card b : possibleBlockers) { if (!CombatUtil.canBlock(b, combat)) { @@ -1032,10 +1034,6 @@ public class AiBlockController { } } - if (attackersLeft.isEmpty()) { - return; - } - // Begin with the weakest blockers CardLists.sortByPowerAsc(blockersLeft); @@ -1070,7 +1068,6 @@ public class AiBlockController { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { clearBlockers(combat, possibleBlockers); // reset every block assignment makeTradeBlocks(combat); // choose necessary trade blocks - // if life is in danger makeGoodBlocks(combat); // choose necessary chump blocks if life is still in danger if (ComputerUtilCombat.lifeInDanger(ai, combat)) { @@ -1090,22 +1087,18 @@ public class AiBlockController { // == 3. If the AI life would be in serious danger make an even safer approach == if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) { - clearBlockers(combat, possibleBlockers); // reset every block assignment - makeChumpBlocks(combat); // choose chump blocks + clearBlockers(combat, possibleBlockers); + makeChumpBlocks(combat); if (ComputerUtilCombat.lifeInDanger(ai, combat)) { - makeTradeBlocks(combat); // choose necessary trade + makeTradeBlocks(combat); } if (!ComputerUtilCombat.lifeInDanger(ai, combat)) { makeGoodBlocks(combat); - } - // Reinforce blockers blocking attackers with trample if life is still in danger - else { + } else { reinforceBlockersAgainstTrample(combat); } makeGangBlocks(combat); - // Support blockers not destroying the attacker with more - // blockers to try to kill the attacker reinforceBlockersToKill(combat); } } diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 507bae8db2e..a44490dc4b9 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -29,7 +29,6 @@ import forge.ai.simulation.SpellAbilityPicker; import forge.card.CardStateName; import forge.card.MagicColor; import forge.card.mana.ManaCost; -import forge.deck.CardPool; import forge.deck.Deck; import forge.deck.DeckSection; import forge.game.*; @@ -69,7 +68,6 @@ import io.sentry.Sentry; import io.sentry.event.BreadcrumbBuilder; import java.util.*; -import java.util.Map.Entry; /** *

@@ -1868,10 +1866,8 @@ public class AiController { toRemove.add(sa); } } - for(SpellAbility sa : toRemove) { - result.remove(sa); - } - + result.removeAll(toRemove); + // Play them last if (saGemstones != null) { result.remove(saGemstones); @@ -2045,17 +2041,7 @@ public class AiController { Map> complaints = new HashMap<>(); // When using simulation, AI should be able to figure out most cards. if (!useSimulation) { - for (Entry ds : myDeck) { - List result = Lists.newArrayList(); - for (Entry cp : ds.getValue()) { - if (cp.getKey().getRules().getAiHints().getRemAIDecks()) { - result.add(cp.getKey()); - } - } - if (!result.isEmpty()) { - complaints.put(ds.getKey(), result); - } - } + complaints = myDeck.getUnplayableAICards().unplayable; } return complaints; } diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index d18fe49da37..16b01575594 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -196,8 +196,7 @@ public class AiCostDecision extends CostDecisionMakerBase { return null; } - CardLists.sortByPowerAsc(typeList); - Collections.reverse(typeList); + CardLists.sortByPowerDesc(typeList); for (int i = 0; i < c; i++) { chosen.add(typeList.get(i)); diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index abbdc9be665..4f739ee9fbf 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -1492,7 +1492,7 @@ public class ComputerUtil { return false; } - public static int possibleNonCombatDamage(Player ai, Player enemy) { + public static int possibleNonCombatDamage(final Player ai, final Player enemy) { int damage = 0; final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield)); all.addAll(ai.getCardsActivableInExternalZones(true)); @@ -1550,8 +1550,8 @@ public class ComputerUtil { /** * Overload of predictThreatenedObjects that evaluates the full stack */ - public static List predictThreatenedObjects(final Player aiPlayer, final SpellAbility sa) { - return predictThreatenedObjects(aiPlayer, sa, false); + public static List predictThreatenedObjects(final Player ai, final SpellAbility sa) { + return predictThreatenedObjects(ai, sa, false); } /** @@ -2971,7 +2971,7 @@ public class ComputerUtil { // TODO: either add SVars to other reanimator cards, or improve the prediction so that it avoids using a SVar // at all but detects this effect from SA parameters (preferred, but difficult) CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand); - CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library}); + CardCollectionView inDeck = ai.getCardsIn(ZoneType.Library); Predicate markedAsReanimator = new Predicate() { @Override @@ -3036,13 +3036,30 @@ public class ComputerUtil { // call this to determine if it's safe to use a life payment spell // or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell. public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) { - // TODO should also consider them as teams - for (Player opponent: ai.getOpponents()) { - Combat combat = new Combat(opponent); + return predictNextCombatsRemainingLife(ai, serious, false, payment, null) == Integer.MIN_VALUE; + } + public static int predictNextCombatsRemainingLife(Player ai, boolean serious, boolean checkDiff, int payment, final CardCollection excludedBlockers) { + // life won't change + int remainingLife = Integer.MAX_VALUE; + + // performance shortcut + // TODO if checking upcoming turn it should be a permanent effect + if (ai.cantLose()) { + return remainingLife; + } + + // TODO should also consider them as teams (with increased likelihood to be attacked by multiple if ai is biggest threat) + // TODO worth it to sort by creature amount for chance to terminate earlier? + for (Player opp: ai.getOpponents()) { + Combat combat = new Combat(opp); boolean containsAttacker = false; - boolean thisCombat = ai.getGame().getPhaseHandler().isPlayerTurn(opponent) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_BEGIN); - for (Card att : opponent.getCreaturesInPlay()) { + boolean thisCombat = ai.getGame().getPhaseHandler().isPlayerTurn(opp) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_BEGIN); + + // TODO !thisCombat should include cards that will phase in + for (Card att : opp.getCreaturesInPlay()) { if ((thisCombat && CombatUtil.canAttack(att, ai)) || (!thisCombat && ComputerUtilCombat.canAttackNextTurn(att, ai))) { + // TODO need to copy the card + // att = ComputerUtilCombat.applyPotentialAttackCloneTriggers(att); combat.addAttacker(att, ai); containsAttacker = true; } @@ -3050,23 +3067,28 @@ public class ComputerUtil { if (!containsAttacker) { continue; } - // TODO if it's next turn ignore mustBlockCards AiBlockController block = new AiBlockController(ai, false); - block.assignBlockersForCombat(combat); + // TODO for performance skip ahead to safer blocking approach (though probably only when not in checkDiff mode as that could lead to inflated prediction) + block.assignBlockersForCombat(combat, excludedBlockers); // TODO predict other, noncombat sources of damage and add them to the "payment" variable. // examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc // If added, might need a parameter to define whether we want to check all threats or combat threats. if (serious && ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment)) { - return true; + return Integer.MIN_VALUE; } if (!serious && ComputerUtilCombat.lifeInDanger(ai, combat, payment)) { - return true; + return Integer.MIN_VALUE; + } + + if (checkDiff && !ai.cantLoseForZeroOrLessLife()) { + // find out the worst possible outcome + remainingLife = Math.min(ComputerUtilCombat.lifeThatWouldRemain(ai, combat), remainingLife); } } - return false; + return remainingLife; } } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index 6a2e830b960..8f9ad96bed8 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -611,35 +611,6 @@ public class ComputerUtilCard { return combat.isAttacking(card); } - public static boolean canBeKilledByRoyalAssassin(final Player ai, final Card card) { - boolean wasTapped = card.isTapped(); - for (Player opp : ai.getOpponents()) { - for (Card c : opp.getCardsIn(ZoneType.Battlefield)) { - for (SpellAbility sa : c.getSpellAbilities()) { - if (sa.getApi() != ApiType.Destroy) { - continue; - } - if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) { - continue; - } - sa.setActivatingPlayer(opp); - if (sa.canTarget(card)) { - continue; - } - // check whether the ability can only target tapped creatures - card.setTapped(true); - if (!sa.canTarget(card)) { - card.setTapped(wasTapped); - continue; - } - card.setTapped(wasTapped); - return true; - } - } - } - return false; - } - /** * Create a mock combat where ai is being attacked and returns the list of likely blockers. * @param ai blocking player @@ -696,6 +667,35 @@ public class ComputerUtilCard { return ComputerUtilCombat.attackerWouldBeDestroyed(ai, attacker, combat); } + public static boolean canBeKilledByRoyalAssassin(final Player ai, final Card card) { + boolean wasTapped = card.isTapped(); + for (Player opp : ai.getOpponents()) { + for (Card c : opp.getCardsIn(ZoneType.Battlefield)) { + for (SpellAbility sa : c.getSpellAbilities()) { + if (sa.getApi() != ApiType.Destroy) { + continue; + } + if (!ComputerUtilCost.canPayCost(sa, opp, sa.isTrigger())) { + continue; + } + sa.setActivatingPlayer(opp); + if (sa.canTarget(card)) { + continue; + } + // check whether the ability can only target tapped creatures + card.setTapped(true); + if (!sa.canTarget(card)) { + card.setTapped(wasTapped); + continue; + } + card.setTapped(wasTapped); + return true; + } + } + } + return false; + } + /** * getMostExpensivePermanentAI. * @@ -1967,13 +1967,11 @@ public class ComputerUtilCard { public static Cost getTotalWardCost(Card c) { Cost totalCost = new Cost(ManaCost.NO_COST, false); - for (final KeywordInterface inst : c.getKeywords()) { - if (inst.getKeyword() == Keyword.WARD) { - final String keyword = inst.getOriginal(); - final String[] k = keyword.split(":"); - final Cost wardCost = new Cost(k[1], false); - totalCost = totalCost.add(wardCost); - } + for (final KeywordInterface inst : c.getKeywords(Keyword.WARD)) { + final String keyword = inst.getOriginal(); + final String[] k = keyword.split(":"); + final Cost wardCost = new Cost(k[1], false); + totalCost = totalCost.add(wardCost); } return totalCost; } @@ -2000,6 +1998,7 @@ public class ComputerUtilCard { return false; } + // TODO replace most calls to Player.isCardInPlay because they include phased out public static boolean isNonDisabledCardInPlay(final Player ai, final String cardName) { for (Card card : ai.getCardsIn(ZoneType.Battlefield, cardName)) { // TODO - Better logic to determine if a permanent is disabled by local effects diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java index ed8202a1422..56b0e609793 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java @@ -154,13 +154,11 @@ public class ComputerUtilCombat { * @return a int. */ public static int getTotalFirstStrikeBlockPower(final Card attacker, final Player player) { - final Card att = attacker; - List list = player.getCreaturesInPlay(); list = CardLists.filter(list, new Predicate() { @Override public boolean apply(final Card c) { - return (c.hasFirstStrike() || c.hasDoubleStrike()) && CombatUtil.canBlock(att, c); + return (c.hasFirstStrike() || c.hasDoubleStrike()) && CombatUtil.canBlock(attacker, c); } }); @@ -216,7 +214,7 @@ public class ComputerUtilCombat { damage += predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities); if (!attacker.hasKeyword(Keyword.INFECT)) { sum = predictDamageTo(attacked, damage, attacker, true); - if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { + if (attacker.hasDoubleStrike()) { sum *= 2; } } @@ -249,7 +247,7 @@ public class ComputerUtilCombat { pd = 0; } poison += pd; - if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { + if (attacker.hasDoubleStrike()) { poison += pd; } } @@ -304,6 +302,20 @@ public class ComputerUtilCombat { return sum; } + // Checks if the life of the attacked Player would be reduced + /** + *

+ * wouldLoseLife. + *

+ * + * @param combat + * a {@link forge.game.combat.Combat} object. + * @return a boolean. + */ + public static boolean wouldLoseLife(final Player ai, final Combat combat) { + return lifeThatWouldRemain(ai, combat) < ai.getLife(); + } + // calculates the amount of life that will remain after the attack /** *

@@ -398,7 +410,6 @@ public class ComputerUtilCombat { return res; } - // Checks if the life of the attacked Player/Planeswalker is in danger /** *

* lifeInDanger. @@ -406,7 +417,7 @@ public class ComputerUtilCombat { * * @param combat * a {@link forge.game.combat.Combat} object. - * @return a boolean. + * @return boolean true if life/poison changes and will be in dangerous range as specified by AI profile. */ public static boolean lifeInDanger(final Player ai, final Combat combat) { return lifeInDanger(ai, combat, 0); @@ -473,29 +484,13 @@ public class ComputerUtilCombat { maxTreshold--; } - if (lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife()) - && !ai.cantLoseForZeroOrLessLife()) { + if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())) { return true; } return resultingPoison(ai, combat) > Math.max(7, ai.getPoisonCounters()); } - // Checks if the life of the attacked Player would be reduced - /** - *

- * wouldLoseLife. - *

- * - * @param combat - * a {@link forge.game.combat.Combat} object. - * @return a boolean. - */ - public static boolean wouldLoseLife(final Player ai, final Combat combat) { - return lifeThatWouldRemain(ai, combat) < ai.getLife(); - } - - // Checks if the life of the attacked Player/Planeswalker is in danger /** *

* lifeInSeriousDanger. @@ -503,7 +498,7 @@ public class ComputerUtilCombat { * * @param combat * a {@link forge.game.combat.Combat} object. - * @return a boolean. + * @return boolean - true if player would lose. */ public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) { return lifeInSeriousDanger(ai, combat, 0); @@ -532,7 +527,7 @@ public class ComputerUtilCombat { } } - if (lifeThatWouldRemain(ai, combat) - payment < 1 && !ai.cantLoseForZeroOrLessLife()) { + if (!ai.cantLoseForZeroOrLessLife() && lifeThatWouldRemain(ai, combat) - payment < 1) { return true; } @@ -597,7 +592,7 @@ public class ComputerUtilCombat { public static int dealsDamageAsBlocker(final Card attacker, final Card defender) { int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender); - if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) { + if (defender.hasDoubleStrike()) { defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true); } @@ -743,8 +738,8 @@ public class ComputerUtilCombat { int firstStrikeBlockerDmg = 0; for (final Card defender : blockers) { - if (canDestroyAttacker(ai, attacker, defender, combat, true) - && !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) { + if (!(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT)) + && canDestroyAttacker(ai, attacker, defender, combat, true)) { return true; } if (defender.hasFirstStrike() || defender.hasDoubleStrike()) { @@ -927,7 +922,7 @@ public class ComputerUtilCombat { if (dealsFirstStrikeDamage(attacker, withoutAbilities, null) && (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)) && !dealsFirstStrikeDamage(blocker, withoutAbilities, null) - && !blocker.canReceiveCounters(CounterEnumType.M1M1)) { + && blocker.canReceiveCounters(CounterEnumType.M1M1)) { power -= attacker.getNetCombatDamage(); } @@ -1224,7 +1219,7 @@ public class ComputerUtilCombat { if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)) && !dealsFirstStrikeDamage(attacker, withoutAbilities, combat) - && !attacker.canReceiveCounters(CounterEnumType.M1M1)) { + && attacker.canReceiveCounters(CounterEnumType.M1M1)) { power -= blocker.getNetCombatDamage(); } theTriggers.addAll(blocker.getTriggers()); @@ -1272,6 +1267,10 @@ public class ComputerUtilCombat { continue; } + if (!sa.hasParam("NumAtt")) { + continue; + } + sa.setActivatingPlayer(source.getController()); if (sa.hasParam("Cost")) { @@ -1300,9 +1299,6 @@ public class ComputerUtilCombat { if (!list.contains(attacker)) { continue; } - if (!sa.hasParam("NumAtt")) { - continue; - } String att = sa.getParam("NumAtt"); if (att.startsWith("+")) { @@ -1311,7 +1307,7 @@ public class ComputerUtilCombat { if (att.matches("[0-9][0-9]?") || att.matches("-" + "[0-9][0-9]?")) { power += Integer.parseInt(att); } else { - String bonus = source.getSVar(att); + String bonus = AbilityUtils.getSVar(sa, att); if (bonus.contains("TriggerCount$NumBlockers")) { bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1"); } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee @@ -1601,7 +1597,7 @@ public class ComputerUtilCombat { if (blocker.isEquippedBy("Godsend")) { return true; } - if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) { + if (combatantCantBeDestroyed(attacker.getController(), attacker)) { return false; } @@ -1718,7 +1714,7 @@ public class ComputerUtilCombat { } } // flanking - if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities)) + if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (!withoutAbilities && ComputerUtil.canRegenerate(ai, attacker))) && !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))) || (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterEnumType.M1M1) && (attacker .getCounters(CounterEnumType.M1M1) == 0)) @@ -1728,7 +1724,6 @@ public class ComputerUtilCombat { } int defenderDamage; - int attackerDamage; if (blocker.toughnessAssignsDamage()) { defenderDamage = blocker.getNetToughness() + predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities); @@ -1736,13 +1731,6 @@ public class ComputerUtilCombat { defenderDamage = blocker.getNetPower() + predictPowerBonusOfBlocker(attacker, blocker, withoutAbilities); } - if (attacker.toughnessAssignsDamage()) { - attackerDamage = attacker.getNetToughness() - + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); - } else { - attackerDamage = attacker.getNetPower() - + predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); - } int possibleDefenderPrevention = 0; int possibleAttackerPrevention = 0; @@ -1753,17 +1741,26 @@ public class ComputerUtilCombat { // consider Damage Prevention/Replacement defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true); - attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true); if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) { return false; } + int attackerDamage; + if (attacker.toughnessAssignsDamage()) { + attackerDamage = attacker.getNetToughness() + + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); + } else { + attackerDamage = attacker.getNetPower() + + predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); + } + attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true); + final int defenderLife = getDamageToKill(blocker, false) + predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities); final int attackerLife = getDamageToKill(attacker, false) + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); - if (blocker.hasKeyword(Keyword.DOUBLE_STRIKE)) { + if (blocker.hasDoubleStrike()) { if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) { return true; } @@ -1824,8 +1821,8 @@ public class ComputerUtilCombat { final List attackers = combat.getAttackersBlockedBy(blocker); for (Card attacker : attackers) { - if (canDestroyBlocker(ai, blocker, attacker, combat, true) - && !(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) { + if (!(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)) + && canDestroyBlocker(ai, blocker, attacker, combat, true)) { return true; } } @@ -1931,7 +1928,7 @@ public class ComputerUtilCombat { return true; } - if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker + if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (!withoutAbilities && ComputerUtil.canRegenerate(ai, blocker))) && !(attacker .hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) || (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterEnumType.M1M1) && blocker .getCounters(CounterEnumType.M1M1) == 0) @@ -1993,11 +1990,11 @@ public class ComputerUtilCombat { final int attackerLife = getDamageToKill(attacker, false) + predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities); - if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { - if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) { + if (attacker.hasDoubleStrike()) { + if (attackerDamage >= defenderLife) { return true; } - if (attackerDamage >= defenderLife) { + if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) { return true; } @@ -2285,7 +2282,7 @@ public class ComputerUtilCombat { } public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) { - if (combatant.hasFirstStrike()|| combatant.hasDoubleStrike()) { + if (combatant.hasFirstStrike() || combatant.hasDoubleStrike()) { return true; } @@ -2510,7 +2507,7 @@ public class ComputerUtilCombat { } } poison += pd; - if (pd > 0 && attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) { + if (pd > 0 && attacker.hasDoubleStrike()) { poison += pd; } // TODO: Predict replacement effects for counters (doubled, reduced, additional counters, etc.) diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java index 4aea82e3967..40aaa132c3f 100644 --- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java +++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java @@ -219,8 +219,9 @@ public class CreatureEvaluator implements Function { if (c.hasKeyword(Keyword.VANISHING)) { value -= subValue(20, "vanishing"); } + // use scaling because the creature is only available halfway if (c.hasKeyword(Keyword.PHASING)) { - value -= subValue(10, "phasing"); + value -= subValue(Math.max(20, value / 2), "phasing"); } // TODO no longer a KW diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java index b875e23c26d..536dabf7d79 100644 --- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java +++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java @@ -89,12 +89,22 @@ import forge.util.collect.FCollectionView; public class PlayerControllerAi extends PlayerController { private final AiController brains; + private boolean pilotsNonAggroDeck = false; + public PlayerControllerAi(Game game, Player p, LobbyPlayer lp) { super(game, p, lp); brains = new AiController(p, game); } + public boolean pilotsNonAggroDeck() { + return pilotsNonAggroDeck; + } + + public void setupAutoProfile(Deck deck) { + pilotsNonAggroDeck = deck.getName().contains("Control") || Deck.getAverageCMC(deck) > 3; + } + public void allowCheatShuffle(boolean value) { brains.allowCheatShuffle(value); } @@ -1132,6 +1142,9 @@ public class PlayerControllerAi extends PlayerController { @Override public Map> complainCardsCantPlayWell(Deck myDeck) { + // TODO check if profile detection set to Auto + setupAutoProfile(myDeck); + return brains.complainCardsCantPlayWell(myDeck); } diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java index d6dc1101744..89a39c7dc64 100644 --- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java @@ -1587,7 +1587,7 @@ public class AttachAi extends SpellAbilityAi { return card.getNetCombatDamage() + powerBonus > 0 && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true)); } else if (keyword.equals("First Strike")) { - return card.getNetCombatDamage() + powerBonus > 0 && !card.hasKeyword(Keyword.DOUBLE_STRIKE) + return card.getNetCombatDamage() + powerBonus > 0 && !card.hasDoubleStrike() && (ComputerUtilCombat.canAttackNextTurn(card) || CombatUtil.canBlock(card, true)); } else if (keyword.startsWith("Flanking")) { return card.getNetCombatDamage() + powerBonus > 0 diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java index c623c5fbdaa..d54fdfc8cbb 100644 --- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java @@ -156,7 +156,7 @@ public class CountersPutAi extends CountersAi { final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined")) && "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X")) && sa.hasParam("MaxFromEffect"); - boolean playAggro = ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true"); + boolean playAggro = ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO); if ("ExistingCounter".equals(type)) { final boolean eachExisting = sa.hasParam("EachExistingCounter"); diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java index 80d0f6696ef..27fd6daf191 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java @@ -1024,13 +1024,13 @@ public class DamageDealAi extends DamageAiBase { return false; } - CardCollection creatures = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES); + CardCollection creatures = ai.getOpponents().getCreaturesInPlay(); Card tgtCreature = null; for (Card c : creatures) { int power = c.getNetPower(); int toughness = c.getNetToughness(); - boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c)); + boolean canDie = !ComputerUtilCombat.combatantCantBeDestroyed(c.getController(), c); // Currently will target creatures with toughness 3+ (or power 5+) // and only if the creature can actually die, do not "underdrain" diff --git a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java index fdb61f45d76..406ab3bd033 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/PowerExchangeAi.java @@ -1,6 +1,5 @@ package forge.ai.ability; -import java.util.Collections; import java.util.List; import com.google.common.base.Predicate; @@ -44,8 +43,7 @@ public class PowerExchangeAi extends SpellAbilityAi { } else if (tgt.getMinTargets(sa.getHostCard(), sa) > 1) { CardCollection list2 = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); - CardLists.sortByPowerAsc(list2); - Collections.reverse(list2); + CardLists.sortByPowerDesc(list2); c2 = list2.isEmpty() ? null : list2.get(0); sa.getTargets().add(c2); } diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java index 09c67a1eab8..0d88c421653 100644 --- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java +++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java @@ -302,7 +302,7 @@ public abstract class PumpAiBase extends SpellAbilityAi { && !opp.getCreaturesInPlay().isEmpty() && Iterables.any(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)); } else if (keyword.equals("First Strike")) { - if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) { + if (card.hasDoubleStrike()) { return false; } if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) { diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index f4abe8a3b2b..c91d95f333f 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -23,6 +23,8 @@ import com.google.common.collect.Lists; import forge.StaticData; import forge.card.CardDb; import forge.card.CardEdition; +import forge.card.CardRules; +import forge.card.CardType; import forge.item.IPaperCard; import forge.item.PaperCard; import org.apache.commons.lang3.StringUtils; @@ -613,4 +615,27 @@ public class Deck extends DeckBase implements Iterable deckEntry : deck) { + switch (deckEntry.getKey()) { + case Main: + case Commander: + for (final Entry poolEntry : deckEntry.getValue()) { + CardRules rules = poolEntry.getKey().getRules(); + CardType type = rules.getType(); + if (!type.isLand() && (type.isArtifact() || type.isCreature() || type.isEnchantment() || type.isPlaneswalker() || type.isInstant() || type.isSorcery())) { + totalCMC += rules.getManaCost().getCMC(); + totalCount++; + } + } + break; + default: + break; //ignore other sections + } + } + return totalCount == 0 ? 0 : Math.round(totalCMC / totalCount); + } } \ No newline at end of file diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java index 72efb11b5e1..c440fd94eb8 100644 --- a/forge-game/src/main/java/forge/game/Game.java +++ b/forge-game/src/main/java/forge/game/Game.java @@ -861,7 +861,7 @@ public class Game { getNextPlayerAfter(p).initPlane(); } - if (p != null && p.isMonarch()) { + if (p.isMonarch()) { // if the player who lost was the Monarch, someone else will be the monarch // TODO need to check rules if it should try the next player if able if (p.equals(getPhaseHandler().getPlayerTurn())) { diff --git a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java index 812bcbe19f4..1ff702d76ec 100644 --- a/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java +++ b/forge-game/src/main/java/forge/game/ability/SpellAbilityEffect.java @@ -653,12 +653,12 @@ public abstract class SpellAbilityEffect { @Override public void run() { - CardZoneTable untilTable = new CardZoneTable(); CardCollectionView untilCards = hostCard.getUntilLeavesBattlefield(); // if the list is empty, then the table doesn't need to be checked anymore if (untilCards.isEmpty()) { return; } + CardZoneTable untilTable = new CardZoneTable(); Map moveParams = AbilityKey.newMap(); moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateBattlefield()); moveParams.put(AbilityKey.LastStateBattlefield, game.copyLastStateGraveyard()); diff --git a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java index bac54df3eab..23cdeaf3f09 100644 --- a/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java +++ b/forge-game/src/main/java/forge/game/ability/effects/ChangeZoneAllEffect.java @@ -226,7 +226,7 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect { } } } - if (remLKI && movedCard != null) { + if (remLKI) { final Card lki = CardUtil.getLKICopy(c); game.getCardState(source).addRemembered(lki); if (!source.isRemembered(lki)) { diff --git a/forge-gui/res/cardsfolder/w/war_cadence.txt b/forge-gui/res/cardsfolder/w/war_cadence.txt index d032a226282..be2378f982a 100644 --- a/forge-gui/res/cardsfolder/w/war_cadence.txt +++ b/forge-gui/res/cardsfolder/w/war_cadence.txt @@ -4,7 +4,6 @@ Types:Enchantment A:AB$ StoreSVar | Cost$ X R | SVar$ PaidNum | Type$ Count | Expression$ xPaid | SubAbility$ CadenceEffect | AILogic$ RestrictBlocking | SpellDescription$ This turn, creatures can't block unless their controller pays {X} for each blocking creature they control. SVar:CadenceEffect:DB$ Effect | StaticAbilities$ CadenceStaticAb | Stackable$ False | RememberObjects$ Valid Creature.blocking SVar:CadenceStaticAb:Mode$ CantBlockUnless | ValidCard$ Card.IsNotRemembered | Cost$ PaidNum | EffectZone$ Command | Description$ This turn, creatures can't block unless their controller pays {X} for each blocking creature they control. -# According to the 10/4/2004 ruling: The ability only applies to blocks declared after it resolves. It will not add costs to any blockers already announced. SVar:X:Count$xPaid SVar:PaidNum:Number$0 SVar:NonStackingEffect:True diff --git a/forge-gui/src/main/java/forge/deck/DeckProxy.java b/forge-gui/src/main/java/forge/deck/DeckProxy.java index 750b16579c1..5a0ff314240 100644 --- a/forge-gui/src/main/java/forge/deck/DeckProxy.java +++ b/forge-gui/src/main/java/forge/deck/DeckProxy.java @@ -347,32 +347,9 @@ public class DeckProxy implements InventoryItem { return sbSize; } - public static int getAverageCMC(Deck deck) { - int totalCMC = 0; - int totalCount = 0; - for (final Entry deckEntry : deck) { - switch (deckEntry.getKey()) { - case Main: - case Commander: - for (final Entry poolEntry : deckEntry.getValue()) { - CardRules rules = poolEntry.getKey().getRules(); - CardType type = rules.getType(); - if (!type.isLand() && (type.isArtifact() || type.isCreature() || type.isEnchantment() || type.isPlaneswalker() || type.isInstant() || type.isSorcery())) { - totalCMC += rules.getManaCost().getCMC(); - totalCount++; - } - } - break; - default: - break; //ignore other sections - } - } - return Math.round(totalCMC / totalCount); - } - public Integer getAverageCMC() { if (avgCMC == null) { - avgCMC = getAverageCMC(getDeck()); + avgCMC = Deck.getAverageCMC(getDeck()); } return avgCMC; } diff --git a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java index 80f01c0513f..66817b55afd 100644 --- a/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java +++ b/forge-gui/src/main/java/forge/gamemodes/gauntlet/GauntletIO.java @@ -211,7 +211,7 @@ public class GauntletIO { final boolean foil = "1".equals(reader.getAttribute("foil")); PaperCard card = FModel.getMagicDb().getOrLoadCommonCard(name, set, index, foil); if (null == card) { - throw new RuntimeException("Unsupported card found in quest save: " + name + " from edition " + set); + throw new RuntimeException("Unsupported card found in gauntlet save: " + name + " from edition " + set); } return card; } diff --git a/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java b/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java index 1c31589355c..2874be6e78e 100644 --- a/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java +++ b/forge-gui/src/main/java/forge/itemmanager/AdvancedSearch.java @@ -25,6 +25,7 @@ import forge.card.CardType; import forge.card.CardType.CoreType; import forge.card.CardType.Supertype; import forge.card.MagicColor; +import forge.deck.Deck; import forge.deck.CardPool; import forge.deck.DeckProxy; import forge.deck.DeckSection; @@ -681,7 +682,7 @@ public class AdvancedSearch { COMMANDER_DECK_AVERAGE_CMC("lblDeckAverageCMC", ConquestCommander.class, FilterOperator.NUMBER_OPS, new NumericEvaluator(0, 20) { @Override protected Integer getItemValue(ConquestCommander input) { - return DeckProxy.getAverageCMC(input.getDeck()); + return Deck.getAverageCMC(input.getDeck()); } }), COMMANDER_DECK_CONTENTS("lblDeckContents", ConquestCommander.class, FilterOperator.DECK_CONTENT_OPS, new DeckContentEvaluator() {