From 60112c6be5c74dd65e707d5bc97a7e98a68fee44 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Mon, 30 Dec 2024 15:09:02 +0400 Subject: [PATCH] test framework: added commands to check declared attackers and blockers creatures (useful for AI tests, see checkAttackers and checkBlockers) --- .../test/AI/basic/AttackAndBlockByAITest.java | 32 ++++++++ .../test/AI/basic/BlockSimulationAITest.java | 10 ++- .../java/org/mage/test/player/TestPlayer.java | 78 ++++++++++++++++++- .../base/impl/CardTestPlayerAPIImpl.java | 30 ++++++- Mage/src/main/java/mage/game/GameImpl.java | 3 +- 5 files changed, 146 insertions(+), 7 deletions(-) diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java index b294eb738b3..a27502ad4f6 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/AttackAndBlockByAITest.java @@ -6,6 +6,9 @@ import org.junit.Ignore; import org.junit.Test; import org.mage.test.serverside.base.CardTestPlayerBaseAI; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + /** * @author JayDi85 */ @@ -21,6 +24,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { // 2 x 2/2 vs 0 - can't lose any attackers addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2 + checkAttackers("x2 attack", 1, playerA, "Balduvian Bears", "Balduvian Bears"); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -35,6 +40,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2 addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 1); // 1/1 + checkAttackers("x2 attack", 1, playerA, "Balduvian Bears", "Balduvian Bears"); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -49,6 +56,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 2); // 1/1 + checkAttackers("x1 attack", 1, playerA, "Balduvian Bears"); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -63,6 +72,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 2); // 2/2 addCard(Zone.BATTLEFIELD, playerB, "Arbor Elf", 2); // 1/1 + checkAttackers("x2 attack", 1, playerA, "Balduvian Bears", "Balduvian Bears"); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -75,6 +86,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { public void test_Attack_1_small_vs_0() { addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 + checkAttackers("x1 attack", 1, playerA, "Arbor Elf"); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -88,6 +101,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 1); // 1/1 addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 + checkAttackers("no attack", 1, playerA, ""); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -101,6 +116,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Arbor Elf", 2); // 1/1 addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 + checkAttackers("no attack", 1, playerA, ""); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); @@ -114,6 +131,11 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 15); // 2/2 addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 + String needAttackers = IntStream.rangeClosed(1, 15) + .mapToObj(x -> "Balduvian Bears") + .collect(Collectors.joining("^")); + checkAttackers("x15 attack", 1, playerA, needAttackers); + block(1, playerB, "Ancient Brontodon", "Balduvian Bears"); setStopAt(1, PhaseStep.END_TURN); @@ -131,6 +153,10 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 block(1, playerB, "Ancient Brontodon", "Balduvian Bears"); + String needAttackers = IntStream.rangeClosed(1, 10) + .mapToObj(x -> "Balduvian Bears") + .collect(Collectors.joining("^")); + checkAttackers("x10 attack", 1, playerA, needAttackers); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -146,6 +172,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerB, "Ancient Brontodon", 1); // 9/9 block(1, playerB, "Ancient Brontodon", "Goblin Brigand"); + checkAttackers("forced x1 attack", 1, playerA, "Goblin Brigand"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -163,6 +190,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { block(1, playerB, "Ancient Brontodon:0", "Goblin Brigand:0"); block(1, playerB, "Ancient Brontodon:1", "Goblin Brigand:1"); + checkAttackers("forced x2 attack", 1, playerA, "Goblin Brigand", "Goblin Brigand"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -183,6 +211,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerB, "Trove of Temptation", 1); // 9/9 block(1, playerB, "Ancient Brontodon", "Arbor Elf"); + checkAttackers("forced x1 attack", 1, playerA, "Arbor Elf"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -201,6 +230,7 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerB, "Seeker of Slaanesh", 1); // 3/3 block(1, playerB, "Seeker of Slaanesh", "Arbor Elf"); + checkAttackers("forced x1 attack", 1, playerA, "Arbor Elf"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -217,6 +247,8 @@ public class AttackAndBlockByAITest extends CardTestPlayerBaseAI { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); // 2/2 addCard(Zone.BATTLEFIELD, playerB, "Chainbreaker", 1); // 3/3, but with 2x -1/-1 counters + checkAttackers("x1 attack", 1, playerA, "Balduvian Bears"); + setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); execute(); 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 ef83f53f4ab..624a26a3341 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 @@ -19,6 +19,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must block aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 blocker", 1, playerB, "Balduvian Bears"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -37,8 +38,9 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { attack(1, playerA, "Arbor Elf"); - // ai must block + // ai must block by optimal blocker aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 optimal blocker", 1, playerB, "Balduvian Bears"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -58,6 +60,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must block aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 blocker", 1, playerB, "Arbor Elf"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -78,6 +81,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must not block aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("no blockers", 1, playerB, ""); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -100,6 +104,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must not block aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("no blockers", 1, playerB, ""); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -123,6 +128,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must block bigger attacker and survive (6/6 must block 5/5) aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 optimal blocker", 1, playerB, "Colossal Dreadmaw"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -151,6 +157,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must block bigger attacker and survive (3/3 must block 2/2) aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -175,6 +182,7 @@ public class BlockSimulationAITest extends CardTestPlayerBaseWithAIHelps { // ai must use smaller blocker and survive (3/3 must block 2/2) aiPlayStep(1, PhaseStep.DECLARE_BLOCKERS, playerB); + checkBlockers("x1 optimal blocker", 1, playerB, "Spectral Bears"); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java index ff6d8181f3d..b2433f4ff43 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/TestPlayer.java @@ -28,10 +28,7 @@ import mage.filter.common.*; import mage.filter.predicate.Predicates; import mage.filter.predicate.mageobject.NamePredicate; import mage.filter.predicate.permanent.SummoningSicknessPredicate; -import mage.game.Game; -import mage.game.GameImpl; -import mage.game.Graveyard; -import mage.game.Table; +import mage.game.*; import mage.game.combat.CombatGroup; import mage.game.command.CommandObject; import mage.game.draft.Draft; @@ -52,6 +49,7 @@ import mage.target.common.*; import mage.util.CardUtil; import mage.util.MultiAmountMessage; import mage.util.RandomUtil; +import mage.watchers.common.AttackedOrBlockedThisCombatWatcher; import org.apache.log4j.Logger; import org.junit.Assert; @@ -839,6 +837,20 @@ public class TestPlayer implements Player { wasProccessed = true; } + // check attacking: attackers list + if (params[0].equals(CHECK_COMMAND_ATTACKERS) && params.length == 2) { + assertAttackers(action, game, computerPlayer, params[1]); + actions.remove(action); + wasProccessed = true; + } + + // check attacking: attackers list + if (params[0].equals(CHECK_COMMAND_BLOCKERS) && params.length == 2) { + assertBlockers(action, game, computerPlayer, params[1]); + actions.remove(action); + wasProccessed = true; + } + // check playable ability: ability text, must have if (params[0].equals(CHECK_COMMAND_PLAYABLE_ABILITY) && params.length == 3) { assertPlayableAbility(action, game, computerPlayer, params[1], Boolean.parseBoolean(params[2])); @@ -1418,6 +1430,64 @@ public class TestPlayer implements Player { } } + private void assertAttackers(PlayerAction action, Game game, Player player, String attackers) { + AttackedOrBlockedThisCombatWatcher watcher = game.getState().getWatcher(AttackedOrBlockedThisCombatWatcher.class); + Assert.assertNotNull(watcher); + + List actualAttackers = watcher.getAttackedThisTurnCreatures().stream() + .map(mor -> game.getObject(mor.getSourceId()))// no needs in zcc/lki + .filter(Objects::nonNull) + .filter(o -> o instanceof ControllableOrOwnerable) + .map(o -> (ControllableOrOwnerable) o) + .filter(o -> o.getControllerOrOwnerId().equals(player.getId())) + .map(o -> ((MageObject) o).getName()) + .sorted() + .collect(Collectors.toList()); + List needAttackers = Arrays.stream(attackers.split("\\^")) + .filter(s -> !s.equals(TestPlayer.ATTACK_SKIP)) + .sorted() + .collect(Collectors.toList()); + + if (!actualAttackers.equals(needAttackers)) { + printStart(game, action.getActionName()); + System.out.println(String.format("Need attackers: %d", needAttackers.size())); + needAttackers.forEach(s -> System.out.println(" - " + s)); + System.out.println(String.format("Actual attackers: %d", actualAttackers.size())); + actualAttackers.forEach(s -> System.out.println(" - " + s)); + printEnd(); + Assert.fail("Found wrong attackers"); + } + } + + private void assertBlockers(PlayerAction action, Game game, Player player, String blockers) { + AttackedOrBlockedThisCombatWatcher watcher = game.getState().getWatcher(AttackedOrBlockedThisCombatWatcher.class); + Assert.assertNotNull(watcher); + + List actualBlockers = watcher.getBlockedThisTurnCreatures().stream() + .map(mor -> game.getObject(mor.getSourceId()))// no needs in zcc/lki + .filter(Objects::nonNull) + .filter(o -> o instanceof ControllableOrOwnerable) + .map(o -> (ControllableOrOwnerable) o) + .filter(o -> o.getControllerOrOwnerId().equals(player.getId())) + .map(o -> ((MageObject) o).getName()) + .sorted() + .collect(Collectors.toList()); + List needBlockers = Arrays.stream(blockers.split("\\^")) + .filter(s -> !s.equals(TestPlayer.BLOCK_SKIP)) + .sorted() + .collect(Collectors.toList()); + + if (!actualBlockers.equals(needBlockers)) { + printStart(game, action.getActionName()); + System.out.println(String.format("Need blockers: %d", needBlockers.size())); + needBlockers.forEach(s -> System.out.println(" - " + s)); + System.out.println(String.format("Actual blockers: %d", actualBlockers.size())); + actualBlockers.forEach(s -> System.out.println(" - " + s)); + printEnd(); + Assert.fail("Found wrong blockers"); + } + } + private void assertMayAttackDefender(PlayerAction action, Game game, Player controller, String permanentName, Player defender, boolean expectedMayAttack) { Permanent attackingPermanent = findPermanentWithAssert(action, game, controller, permanentName); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index f13f18f145f..c78ea5c0fd7 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -87,6 +87,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String CHECK_COMMAND_LIFE = "LIFE"; public static final String CHECK_COMMAND_ABILITY = "ABILITY"; public static final String CHECK_COMMAND_PLAYABLE_ABILITY = "PLAYABLE_ABILITY"; + public static final String CHECK_COMMAND_ATTACKERS = "ATTACKERS"; + public static final String CHECK_COMMAND_BLOCKERS = "BLOCKERS"; public static final String CHECK_COMMAND_MAY_ATTACK_DEFENDER = "MAY_ATTACK_DEFENDER"; public static final String CHECK_COMMAND_PERMANENT_COUNT = "PERMANENT_COUNT"; public static final String CHECK_COMMAND_PERMANENT_TAPPED = "PERMANENT_TAPPED"; @@ -428,7 +430,33 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } /** - * Checks whether or not a creature can attack on a given turn a defender (player only for now, could be extended to permanents) + * Make sure in last declared attackers + * + * @param attackers in any order, use empty string or params for no attackers check + */ + public void checkAttackers(String checkName, int turnNum, TestPlayer player, String... attackers) { + String list = String.join("^", attackers); + if (list.isEmpty()) { + list = TestPlayer.ATTACK_SKIP; + } + check(checkName, turnNum, PhaseStep.DECLARE_ATTACKERS, player, CHECK_COMMAND_ATTACKERS, list); + } + + /** + * Make sure in last declared blockers + * + * @param blockers in any order, use empty string or params for no blockers check + */ + public void checkBlockers(String checkName, int turnNum, TestPlayer player, String... blockers) { + String list = String.join("^", blockers); + if (list.isEmpty()) { + list = TestPlayer.BLOCK_SKIP; + } + check(checkName, turnNum, PhaseStep.DECLARE_BLOCKERS, player, CHECK_COMMAND_BLOCKERS, list); + } + + /** + * Checks whether a creature can attack on a given turn a defender (player only for now, could be extended to permanents) * * @param checkName String to show up if the check fails, for display purposes only. * @param turnNum The turn number to check on. diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 0f82f0ae2e7..66ea48734b6 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1423,6 +1423,7 @@ public abstract class GameImpl implements Game { newWatchers.add(new CreaturesDiedWatcher()); newWatchers.add(new TemptedByTheRingWatcher()); newWatchers.add(new SpellsCastWatcher()); + newWatchers.add(new AttackedOrBlockedThisCombatWatcher()); // required for tests // runtime check - allows only GAME scope (one watcher per game) newWatchers.forEach(watcher -> { @@ -3635,7 +3636,7 @@ public abstract class GameImpl implements Game { /** * Reset objects stored for Last Known Information. (Happens if all effects - * are applied und stack is empty) + * are applied and stack is empty) */ @Override public void resetLKI() {