AI: reworked blockers selections:

* now computer will use simplified 1 vs 1 combat damage simulations to choose better blockers (due better game score after combat);
* it's not a full combat simulation, but support many things like non-stack abilities, damage replacement effects and SBA -- much better than older PT compare (related to #13290);
* now AI correctly use a blockers with deathtouth, indestructible, first/double strike and other abilities;
* chump blocks also supported (chump logic implemented before in 92b7ed8efc, related to #4485);
This commit is contained in:
Oleg Agafonov 2025-02-06 06:28:17 +04:00
parent 7d229e511c
commit b4fa6ace66
3 changed files with 108 additions and 11 deletions

View file

@ -1,10 +1,13 @@
package mage.player.ai.util; package mage.player.ai.util;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.keyword.DoubleStrikeAbility; import mage.abilities.keyword.DoubleStrikeAbility;
import mage.abilities.keyword.InfectAbility; import mage.abilities.keyword.InfectAbility;
import mage.counters.CounterType; import mage.counters.CounterType;
import mage.game.Game; import mage.game.Game;
import mage.game.combat.Combat; import mage.game.combat.Combat;
import mage.game.combat.CombatGroup;
import mage.game.events.GameEvent; import mage.game.events.GameEvent;
import mage.game.permanent.Permanent; import mage.game.permanent.Permanent;
import mage.game.turn.CombatDamageStep; import mage.game.turn.CombatDamageStep;
@ -203,7 +206,7 @@ public final class CombatUtil {
blockedCount++; blockedCount++;
} }
// find good sacrifices // find good sacrifices (chump blocks also supported due bad game score on loose)
// TODO: add chump blocking support here? // TODO: add chump blocking support here?
// TODO: there are many triggers on damage, attack, etc - it can't be processed without real game simulations // TODO: there are many triggers on damage, attack, etc - it can't be processed without real game simulations
if (blocker == null) { if (blocker == null) {
@ -339,13 +342,38 @@ public final class CombatUtil {
// so blocker will block same creature with same score without penalty // so blocker will block same creature with same score without penalty
int startScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore(); int startScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore();
// fake game simulation (manual PT calculation) // fake combat simulation (simple damage simulation)
Permanent simAttacker = sim.getPermanent(attacker.getId());
Permanent simBlocker = sim.getPermanent(blocker.getId());
if (simAttacker == null || simBlocker == null) {
throw new IllegalArgumentException("Broken sim game, can't find attacker or blocker");
}
// don't ask about that hacks - just replace to real combat simulation someday (another hack but with full stack resolve)
// first damage step
simulateCombatDamage(sim, simBlocker, simAttacker, true);
simulateCombatDamage(sim, simAttacker, simBlocker, true);
simAttacker.applyDamage(sim);
simBlocker.applyDamage(sim);
sim.checkStateAndTriggered();
sim.processAction();
// second damage step
if (sim.getPermanent(simBlocker.getId()) != null && sim.getPermanent(simAttacker.getId()) != null) {
simulateCombatDamage(sim, simBlocker, simAttacker, false);
simulateCombatDamage(sim, simAttacker, simBlocker, false);
simAttacker.applyDamage(sim);
simBlocker.applyDamage(sim);
sim.checkStateAndTriggered();
sim.processAction();
}
/* old manual PT compare
if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) { if (attacker.getPower().getValue() >= blocker.getToughness().getValue()) {
sim.getBattlefield().removePermanent(blocker.getId()); sim.getBattlefield().removePermanent(blocker.getId());
} }
if (attacker.getToughness().getValue() <= blocker.getPower().getValue()) { if (attacker.getToughness().getValue() <= blocker.getPower().getValue()) {
sim.getBattlefield().removePermanent(attacker.getId()); sim.getBattlefield().removePermanent(attacker.getId());
} }
*/
// fake non-block simulation // fake non-block simulation
simNonBlocking.getPlayer(defendingPlayerId).damage( simNonBlocking.getPlayer(defendingPlayerId).damage(
@ -356,6 +384,7 @@ public final class CombatUtil {
true, true,
true true
); );
simNonBlocking.checkStateAndTriggered();
simNonBlocking.processAction(); simNonBlocking.processAction();
int endBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore(); int endBlockingScore = GameStateEvaluator2.evaluate(defendingPlayerId, sim, false).getTotalScore();
@ -368,6 +397,15 @@ public final class CombatUtil {
); );
} }
private static void simulateCombatDamage(Game sim, Permanent fromCreature, Permanent toCreature, boolean isFirstDamageStep) {
Ability fakeAbility = new SimpleStaticAbility(null);
if (CombatGroup.dealsDamageThisStep(fromCreature, isFirstDamageStep, sim)) {
fakeAbility.setSourceId(fromCreature.getId());
fakeAbility.setControllerId(fromCreature.getControllerId());
toCreature.damage(fromCreature.getPower().getValue(), fromCreature.getId(), fakeAbility, sim, true, true);
}
}
private static void simulateStep(Game sim, Step step) { private static void simulateStep(Game sim, Step step) {
sim.getPhase().setStep(step); sim.getPhase().setStep(step);
if (!step.skipStep(sim, sim.getActivePlayerId())) { if (!step.skipStep(sim, sim.getActivePlayerId())) {

View file

@ -6,6 +6,22 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps;
/** /**
* Combat's blocking tests
* <p>
* TODO: add tests with multi blocker requirement effects
* <p>
* Supported abilities:
* - DeathtouchAbility - supported, has tests
* - FirstStrikeAbility - supported, has tests
* - DoubleStrikeAbility - ?
* - IndestructibleAbility - supported, need tests
* - FlyingAbility - ?
* - ReachAbility - ?
* - ExaltedAbility - ?
* - Trample + Deathtouch
* - combat damage and die triggers - need to implement full combat simulation with stack resolve, see CombatUtil->willItSurviveSimple
* - other use cases, see https://github.com/magefree/mage/issues/4485
*
* @author JayDi85 * @author JayDi85
*/ */
public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
@ -242,12 +258,55 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
assertDamageReceived(playerB, "Spectral Bears", 2); assertDamageReceived(playerB, "Spectral Bears", 2);
} }
// TODO: add tests with multi blocker requirement effects @Test
// TODO: add tests for DeathtouchAbility public void test_Block_1_attacker_vs_first_strike() {
// TODO: add tests for FirstStrikeAbility addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
// TODO: add tests for DoubleStrikeAbility //
// TODO: add tests for IndestructibleAbility addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
// TODO: add tests for FlyingAbility addCard(Zone.BATTLEFIELD, playerB, "White Knight", 1); // 2/2 with first strike
// TODO: add tests for ReachAbility addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
// TODO: add tests for ExaltedAbility??? addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6
attack(1, playerA, "Balduvian Bears");
// ai must use smaller blocker and survive (2/2 with first strike must block 2/2)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 optimal blocker", 1, playerB, "White Knight");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Balduvian Bears", 1);
assertDamageReceived(playerB, "White Knight", 0); // due first strike
}
@Test
public void test_Block_1_attacker_vs_deathtouch() {
addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
//
addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
addCard(Zone.BATTLEFIELD, playerB, "Arashin Cleric", 1); // 1/3
addCard(Zone.BATTLEFIELD, playerB, "Graveblade Marauder", 1); // 1/4 with deathtouch
addCard(Zone.BATTLEFIELD, playerB, "Deadbridge Goliath", 1); // 5/5
addCard(Zone.BATTLEFIELD, playerB, "Colossal Dreadmaw", 1); // 6/6
attack(1, playerA, "Balduvian Bears");
// ai must use smaller blocker to kill and survive (1/4 with deathtouch must block 2/2 -- not a smaller 1/3)
aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB);
checkBlockers("x1 optimal blocker", 1, playerB, "Graveblade Marauder");
setStopAt(1, PhaseStep.END_TURN);
setStrictChooseMode(true);
execute();
assertLife(playerA, 20);
assertLife(playerB, 20);
assertGraveyardCount(playerA, "Balduvian Bears", 1);
assertDamageReceived(playerB, "Graveblade Marauder", 2);
}
} }

View file

@ -243,7 +243,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
* @param first true for first strike damage step, false for normal damage step * @param first true for first strike damage step, false for normal damage step
* @return true if permanent should deal damage this step * @return true if permanent should deal damage this step
*/ */
private boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) { public static boolean dealsDamageThisStep(Permanent perm, boolean first, Game game) {
if (perm == null) { if (perm == null) {
return false; return false;
} }