mirror of
https://github.com/magefree/mage.git
synced 2025-12-20 10:40:06 -08:00
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:
parent
7d229e511c
commit
b4fa6ace66
3 changed files with 108 additions and 11 deletions
|
|
@ -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())) {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue