From ba662736884eab7c91fbb8e5daccc7272cde7a0d Mon Sep 17 00:00:00 2001 From: TRT <> Date: Thu, 24 Mar 2022 17:35:22 +0100 Subject: [PATCH] Improve Attack/Block costs decisions --- .../java/forge/ai/AiAttackController.java | 96 ++++++++++++++++--- .../src/main/java/forge/ai/ComputerUtil.java | 1 + 2 files changed, 85 insertions(+), 12 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index 319bb369fb5..89630290847 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -20,6 +20,8 @@ package forge.ai; import java.util.ArrayList; import java.util.List; +import org.apache.commons.lang3.tuple.Pair; + import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.Iterables; @@ -41,6 +43,7 @@ import forge.game.card.CounterEnumType; import forge.game.combat.Combat; import forge.game.combat.CombatUtil; import forge.game.combat.GlobalAttackRestrictions; +import forge.game.cost.Cost; import forge.game.keyword.Keyword; import forge.game.player.Player; import forge.game.spellability.SpellAbility; @@ -121,7 +124,7 @@ public class AiAttackController { } public static List getOpponentCreatures(final Player defender) { - List defenders = new ArrayList<>(defender.getCreaturesInPlay()); + List defenders = defender.getCreaturesInPlay(); Predicate canAnimate = new Predicate() { @Override public boolean apply(Card c) { @@ -475,7 +478,7 @@ public class AiAttackController { CardLists.sortByPowerDesc(this.attackers); - final CardCollection unblockedAttackers = new CardCollection(); + CardCollection unblockedAttackers = new CardCollection(); final CardCollection remainingAttackers = new CardCollection(this.attackers); final CardCollection remainingBlockers = new CardCollection(this.blockers); final CardCollection blockedAttackers = new CardCollection(); @@ -564,6 +567,7 @@ public class AiAttackController { int numExtraBlocks = blocker.canBlockAdditional(); if (numExtraBlocks > 0) { + // TODO should be limited to how much getBlockCost the opp can pay while (numExtraBlocks-- > 0 && !remainingAttackers.isEmpty()) { blockedAttackers.add(remainingAttackers.get(0)); remainingAttackers.remove(0); @@ -580,27 +584,69 @@ public class AiAttackController { } unblockedAttackers.addAll(remainingAttackers); - int trampleDamage = 0; - for (Card attacker : blockedAttackers) { - if (attacker.hasKeyword(Keyword.TRAMPLE)) { - int damage = ComputerUtilCombat.getAttack(attacker); - for (Card blocker : this.blockers) { - if (CombatUtil.canBlock(attacker, blocker)) { - damage -= ComputerUtilCombat.shieldDamage(attacker, blocker); + int totalCombatDamage = 0; + + // TODO might want to only calculate that if it's needed + // TODO might want to factor in isManaSourceReserved + int myFreeMana = ComputerUtilMana.getAvailableManaEstimate(ai, !nextTurn); + // skip attackers exceeding the attack tax + // (this prevents the AI from only making a partial attack that could backfire) + + Pair tramplerFirst = getDamageFromBlockingTramplers(blockedAttackers, remainingAttackers, myFreeMana); + int trampleDamage = tramplerFirst.getLeft(); + int tramplerTaxPaid = tramplerFirst.getRight(); + + // see how far we can get if paying for the unblockable first instead + if (tramplerTaxPaid > 0) { + int unblockableAttackTax = 0; + final CardCollection unblockableWithPaying = new CardCollection(); + final CardCollection unblockableCantPayFor = new CardCollection(); + final CardCollection unblockableWithoutCost = new CardCollection(); + // TODO also check poison + for (Card attacker : CardLists.getKeyword(unblockedAttackers, Keyword.TRAMPLE)) { + Cost tax = CombatUtil.getAttackCost(attacker.getGame(), attacker, defendingOpponent); + if (tax == null) { + unblockableWithoutCost.add(attacker); + } else { + int taxCMC = tax.getCostMana().getMana().getCMC(); + if (myFreeMana < unblockableAttackTax + taxCMC) { + unblockableCantPayFor.add(attacker); + continue; } + unblockableAttackTax += taxCMC; + unblockableWithPaying.add(attacker); } - if (damage > 0) { - trampleDamage += damage; + } + int dmgUnblockableAfterPaying = ComputerUtilCombat.sumDamageIfUnblocked(unblockableWithPaying, defendingOpponent); + if (dmgUnblockableAfterPaying > trampleDamage) { + unblockedAttackers.removeAll(unblockableCantPayFor); + unblockedAttackers.removeAll(unblockableWithPaying); + totalCombatDamage = dmgUnblockableAfterPaying; + // recalculate the trampler damage with the reduced mana available now + trampleDamage = getDamageFromBlockingTramplers(blockedAttackers, remainingAttackers, myFreeMana - unblockableAttackTax).getLeft(); + } else { + unblockedAttackers = unblockableWithoutCost; + myFreeMana -= tramplerTaxPaid; + // find out if we can still pay for some left + for (Card attacker : unblockableWithPaying) { + Cost tax = CombatUtil.getAttackCost(attacker.getGame(), attacker, defendingOpponent); + int taxCMC = tax.getCostMana().getMana().getCMC(); + if (myFreeMana < unblockableAttackTax + taxCMC) { + continue; + } + unblockableAttackTax += taxCMC; + unblockedAttackers.add(attacker); } } } - int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, defendingOpponent) + trampleDamage; + totalCombatDamage += ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, defendingOpponent) + trampleDamage; if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, defendingOpponent) >= defendingOpponent.getLife() && !((defendingOpponent.cantLoseForZeroOrLessLife() || ai.cantWin()) && defendingOpponent.getLife() < 1)) { return true; } + // TODO tramplers int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, defendingOpponent); if (totalPoisonDamage >= 10 - defendingOpponent.getPoisonCounters()) { return true; @@ -609,6 +655,32 @@ public class AiAttackController { return false; } + private final Pair getDamageFromBlockingTramplers(final List blockedAttackers, final List blockers, final int myFreeMana) { + int currentAttackTax = 0; + int trampleDamage = 0; + CardCollection remainingBlockers = new CardCollection(blockers); + for (Card attacker : CardLists.getKeyword(blockedAttackers, Keyword.TRAMPLE)) { + Cost tax = CombatUtil.getAttackCost(attacker.getGame(), attacker, defendingOpponent); + int taxCMC = tax != null ? tax.getCostMana().getMana().getCMC() : 0; + if (myFreeMana < currentAttackTax + taxCMC) { + continue; + } + currentAttackTax += taxCMC; + + int damage = ComputerUtilCombat.getAttack(attacker); + for (Card blocker : remainingBlockers.threadSafeIterable()) { + if (CombatUtil.canBlock(attacker, blocker) && damage > 0) { + damage -= ComputerUtilCombat.shieldDamage(attacker, blocker); + remainingBlockers.remove(blocker); + } + } + if (damage > 0) { + trampleDamage += damage; + } + } + return Pair.of(trampleDamage, currentAttackTax); + } + private final GameEntity chooseDefender(final Combat c, final boolean bAssault) { final FCollectionView defs = c.getDefenders(); if (defs.size() == 1) { diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index 4f739ee9fbf..5bf3b8527a5 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -3057,6 +3057,7 @@ public class ComputerUtil { // TODO !thisCombat should include cards that will phase in for (Card att : opp.getCreaturesInPlay()) { + // TODO should be limited based on how much getAttackCost the opp can pay if ((thisCombat && CombatUtil.canAttack(att, ai)) || (!thisCombat && ComputerUtilCombat.canAttackNextTurn(att, ai))) { // TODO need to copy the card // att = ComputerUtilCombat.applyPotentialAttackCloneTriggers(att);