diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java index e727b28855a..873b4c5bbcd 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/util/CombatUtil.java @@ -1,10 +1,13 @@ package mage.player.ai.util; +import mage.abilities.Ability; +import mage.abilities.common.SimpleStaticAbility; import mage.abilities.keyword.DoubleStrikeAbility; import mage.abilities.keyword.InfectAbility; import mage.counters.CounterType; import mage.game.Game; import mage.game.combat.Combat; +import mage.game.combat.CombatGroup; import mage.game.events.GameEvent; import mage.game.permanent.Permanent; import mage.game.turn.CombatDamageStep; @@ -203,7 +206,7 @@ public final class CombatUtil { blockedCount++; } - // find good sacrifices + // find good sacrifices (chump blocks also supported due bad game score on loose) // TODO: add chump blocking support here? // TODO: there are many triggers on damage, attack, etc - it can't be processed without real game simulations if (blocker == null) { @@ -339,13 +342,38 @@ public final class CombatUtil { // so blocker will block same creature with same score without penalty 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()) { sim.getBattlefield().removePermanent(blocker.getId()); } if (attacker.getToughness().getValue() <= blocker.getPower().getValue()) { sim.getBattlefield().removePermanent(attacker.getId()); } + */ // fake non-block simulation simNonBlocking.getPlayer(defendingPlayerId).damage( @@ -356,6 +384,7 @@ public final class CombatUtil { true, true ); + simNonBlocking.checkStateAndTriggered(); simNonBlocking.processAction(); 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) { sim.getPhase().setStep(step); if (!step.skipStep(sim, sim.getActivePlayerId())) { diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java index b2b2be9d5fb..af1e99a689e 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/BlockSimulationAITest.java @@ -6,6 +6,22 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; /** + * Combat's blocking tests + *
+ * TODO: add tests with multi blocker requirement effects + *
+ * 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
*/
public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
@@ -242,12 +258,55 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps {
assertDamageReceived(playerB, "Spectral Bears", 2);
}
- // TODO: add tests with multi blocker requirement effects
- // TODO: add tests for DeathtouchAbility
- // TODO: add tests for FirstStrikeAbility
- // TODO: add tests for DoubleStrikeAbility
- // TODO: add tests for IndestructibleAbility
- // TODO: add tests for FlyingAbility
- // TODO: add tests for ReachAbility
- // TODO: add tests for ExaltedAbility???
+ @Test
+ public void test_Block_1_attacker_vs_first_strike() {
+ addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2
+ //
+ addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1
+ addCard(Zone.BATTLEFIELD, playerB, "White Knight", 1); // 2/2 with first strike
+ addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3
+ 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);
+ }
}
diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java
index 9021beb2b83..c7caca995fa 100644
--- a/Mage/src/main/java/mage/game/combat/CombatGroup.java
+++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java
@@ -243,7 +243,7 @@ public class CombatGroup implements Serializable, Copyable