From a9bdf2eb187d8fa96ef1211afe900c8fa80ae344 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sat, 26 Oct 2024 11:33:32 +0400 Subject: [PATCH] test framework: improved aiXXX commands support: - added more options for priority control (play single priority, play multiple priorities until stack resolved); - added more options for step control (play single step, play multiple steps); - improved compatibility with AI and real time commands (now check commands can be called inside AI controlled steps); - added tests for assign non-blocked damage; --- .../org/mage/test/AI/basic/CopyAITest.java | 8 +- .../AI/basic/TestFrameworkCanPlayAITest.java | 146 +++++++++++++++++- .../test/cards/damage/AssignDamageTest.java | 71 +++++++++ .../test/cards/rolldice/RollDiceTest.java | 1 + .../test/cards/single/tor/RadiateTest.java | 2 +- .../org/mage/test/player/PlayerAction.java | 10 +- .../java/org/mage/test/player/TestPlayer.java | 109 +++++++------ .../serverside/base/CardTestPlayerBaseAI.java | 3 +- .../base/impl/CardTestPlayerAPIImpl.java | 63 +++++--- .../main/java/mage/cards/mock/MockCard.java | 4 +- .../main/java/mage/constants/PhaseStep.java | 9 ++ .../java/mage/game/combat/CombatGroup.java | 4 +- 12 files changed, 342 insertions(+), 88 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/damage/AssignDamageTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java index 26de90fd89f..a7840516228 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CopyAITest.java @@ -48,8 +48,8 @@ public class CopyAITest extends CardTestPlayerBaseWithAIHelps { // addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 - // clone (AI must choose most valueable permanent - own) - aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // clone (AI must choose most valuable permanent - own) + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -70,8 +70,8 @@ public class CopyAITest extends CardTestPlayerBaseWithAIHelps { addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); // 2/2 addCard(Zone.BATTLEFIELD, playerB, "Spectral Bears", 1); // 3/3 - // clone (AI must choose most valueable permanent - opponent) - aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // clone (AI must choose most valuable permanent - opponent) + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java index e90f6bee0a7..01795ee2193 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/TestFrameworkCanPlayAITest.java @@ -15,14 +15,36 @@ import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; public class TestFrameworkCanPlayAITest extends CardTestPlayerBaseWithAIHelps { @Test - public void test_AI_PlayOnePriorityAction() { + public void test_AI_PlayOnePriority_FromSingle() { + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 3); + + // AI must play one time + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // make sure runtime commands can be called with same priority after stack resolve + checkGraveyardCount("after resolve", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertPermanentCount(playerB, "Balduvian Bears", 3 - 1); + } + + @Test + public void test_AI_PlayOnePriority_FromMultiple() { + // must choose only 1 spell + addCard(Zone.HAND, playerA, "Lightning Bolt", 3); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); // addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 5); // AI must play one time - aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA, false); // stop after first spell setStopAt(1, PhaseStep.END_TURN); setStrictChooseMode(true); @@ -33,7 +55,45 @@ public class TestFrameworkCanPlayAITest extends CardTestPlayerBaseWithAIHelps { } @Test - public void test_AI_PlayManyActionsInOneStep() { + public void test_AI_PlayOnePriority_WithChoicesOnCast() { + // 1/1 + // {2}{W}, Discard a card: Aven Trooper gets +1/+2 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Aven Trooper", 1); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 3); + addCard(Zone.HAND, playerA, "Mountain", 3); + + // AI must play one time and make all choices on cast + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + + assertPowerToughness(playerA, "Aven Trooper", 1 + 1, 1 + 2); + } + + @Test + public void test_AI_PlayOnePriority_WithChoicesOnResolve() { + // {2}{B}, {T}: Target player discards a card. Activate only as a sorcery. + addCard(Zone.BATTLEFIELD, playerA, "Cat Burglar", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 3); + // + addCard(Zone.HAND, playerB, "Mountain", 5); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{2}{B}, {T}: Target player", playerB); + + // AI must be able to use choices + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerB); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertGraveyardCount(playerB, "Mountain", 1); + } + + @Test + public void test_AI_PlayManyPrioritiesInOneStep() { addCard(Zone.HAND, playerA, "Lightning Bolt", 3); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 3); // @@ -50,6 +110,86 @@ public class TestFrameworkCanPlayAITest extends CardTestPlayerBaseWithAIHelps { assertPermanentCount(playerB, "Balduvian Bears", 5 - 3); } + @Test + public void test_AI_CleanStepCommands() { + // some step commands don't have priorities, so it must be clean another way, e.g. on start of the turn + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA); + + setStopAt(3, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); // on failed clean code it will raise error about not used command + } + + @Test + public void test_AI_PlayStep_CallRuntimeCommandsInsideAIControl() { + // make sure runtime check commands can be called under AI control + + runCode("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + Assert.assertFalse("must be non AI before", player.isComputer()); + }); + + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, PhaseStep.END_TURN, playerA); + + checkLife("on same start", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20); + runCode("on inner step", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> { + Assert.assertTrue("must be AI", player.isComputer()); + }); + // + checkLife("on inner step", 1, PhaseStep.DECLARE_ATTACKERS, playerA, 20); + runCode("on inner step", 1, PhaseStep.BEGIN_COMBAT, playerA, (info, player, game) -> { + Assert.assertTrue("must be AI", player.isComputer()); + }); + // + checkLife("on same end", 1, PhaseStep.END_TURN, playerA, 20); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + } + + @Test + public void test_AI_PlayPriority_CallRuntimeCommandsInsideAIControl() { + // make sure runtime check commands can be called under AI control + + runCode("before", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + Assert.assertFalse("must be non AI before", player.isComputer()); + }); + + aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + checkLife("on same start", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 20); + runCode("on same start", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + Assert.assertFalse("must be non AI after", player.isComputer()); + }); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + } + + @Test + public void test_AI_PlayManyPrioritiesInManySteps() { + addCard(Zone.HAND, playerA, "Lightning Bolt", 1); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + // + addCard(Zone.BATTLEFIELD, playerB, "Balduvian Bears", 1); + // + addCard(Zone.HAND, playerA, "Mountain", 1); + + // AI must play multiple actions in multiple steps: + // - cast bolt + // - play land on second main + aiPlayStep(1, PhaseStep.DECLARE_ATTACKERS, PhaseStep.POSTCOMBAT_MAIN, playerA); + + setStopAt(3, PhaseStep.END_TURN); // make sure no land plays on turn 1 + setStrictChooseMode(true); + execute(); + + assertGraveyardCount(playerA, "Lightning Bolt", 1); + assertPermanentCount(playerB, "Balduvian Bears", 0); + assertPermanentCount(playerA, "Mountain", 2 + 1); // must play land on second main + } + @Test public void test_AI_Attack() { addCard(Zone.BATTLEFIELD, playerA, "Balduvian Bears", 1); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/damage/AssignDamageTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/damage/AssignDamageTest.java new file mode 100644 index 00000000000..095eabbef23 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/damage/AssignDamageTest.java @@ -0,0 +1,71 @@ +package org.mage.test.cards.damage; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBaseWithAIHelps; + +/** + * @author JayDi85 + */ +public class AssignDamageTest extends CardTestPlayerBaseWithAIHelps { + + @Test + public void test_ThornElemental_Manual_DamageToBlock() { + // 7/7 + // You may have Thorn Elemental assign its combat damage as though it weren't blocked. + addCard(Zone.BATTLEFIELD, playerA, "Thorn Elemental", 1); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + + // attack and kill bear due block + attack(1, playerA, "Thorn Elemental"); + block(1, playerB, "Grizzly Bears", "Thorn Elemental"); + setChoice(playerA, false); // use blocked damage + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20); + assertGraveyardCount(playerB, "Grizzly Bears", 1); + } + + @Test + public void test_ThornElemental_Manual_DamageToPlayer() { + // 7/7 + // You may have Thorn Elemental assign its combat damage as though it weren't blocked. + addCard(Zone.BATTLEFIELD, playerA, "Thorn Elemental", 1); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + + // attack and ignore bear + attack(1, playerA, "Thorn Elemental"); + block(1, playerB, "Grizzly Bears", "Thorn Elemental"); + setChoice(playerA, true); // use ignored damage + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 7); + assertDamageReceived(playerB, "Grizzly Bears", 0); + } + + @Test + public void test_ThornElemental_AI() { + // 7/7 + // You may have Thorn Elemental assign its combat damage as though it weren't blocked. + addCard(Zone.BATTLEFIELD, playerA, "Thorn Elemental", 1); + addCard(Zone.BATTLEFIELD, playerB, "Grizzly Bears"); + + // AI must attack and decide to ignore block damage (e.g. damage a player) + aiPlayStep(1, PhaseStep.DECLARE_ATTACKERS, playerA); + block(1, playerB, "Grizzly Bears", "Thorn Elemental"); + + setStrictChooseMode(true); + setStopAt(1, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 20 - 7); + assertDamageReceived(playerB, "Grizzly Bears", 0); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java index 8bd7cff8cd0..e2b10e5a011 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/rolldice/RollDiceTest.java @@ -489,6 +489,7 @@ public class RollDiceTest extends CardTestPlayerBaseWithAIHelps { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Box of Free-Range Goblins"); setDieRollResult(playerA, 3); // normal roll setDieRollResult(playerA, 6); // additional roll + // AI must choose max value due good outcome aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java index 6eb3688ab08..c36233716c9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/tor/RadiateTest.java @@ -59,7 +59,7 @@ public class RadiateTest extends CardTestPlayerBaseWithAIHelps { addCard(Zone.BATTLEFIELD, playerB, "Kitesail Corsair", 2); // cast bolt and copy spell for each another target - // must call commands manually cause it's a bad scenario and AI don't cast it itself + // must call commands manually because it's a bad scenario and AI don't cast it itself castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Radiate", "Lightning Bolt", "Lightning Bolt"); aiPlayPriority(1, PhaseStep.PRECOMBAT_MAIN, playerA); // but AI can choose targets diff --git a/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java b/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java index c5debf2d59e..3e4c7405fdc 100644 --- a/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java +++ b/Mage.Tests/src/test/java/org/mage/test/player/PlayerAction.java @@ -49,12 +49,14 @@ public class PlayerAction { /** * Calls after action removed from commands queue later (for multi steps - * action, e.g.AI related) - * - * @param game - * @param player + * action, e.g. AI related) */ public void onActionRemovedLater(Game game, TestPlayer player) { // } + + @Override + public String toString() { + return "T" + this.turnNum + "." + this.step.getStepShortText() + ": " + this.action; + } } 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 df2f3cf3958..62231cb948e 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 @@ -54,7 +54,6 @@ import mage.util.MultiAmountMessage; import mage.util.RandomUtil; import org.apache.log4j.Logger; import org.junit.Assert; -import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; import java.io.Serializable; import java.util.*; @@ -62,6 +61,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static org.mage.test.serverside.base.impl.CardTestPlayerAPIImpl.*; + /** * Basic implementation of testable player * @@ -90,15 +91,16 @@ public class TestPlayer implements Player { // warning, test player do not restore own data by game rollback - // full playable AI, TODO: can be deleted? - private boolean AIPlayer; + // full playable AI + private boolean AIPlayer; // TODO: better rename // AI simulates a real game, e.g. ignores strict mode and play command/priority, see aiXXX commands // true - unit tests uses real AI logic (e.g. AI hints and AI workarounds in cards) - // false - unit tests uses Human logic and dialogs + // false - unit tests uses Human logic and dialogs (in non-strict mode AI replace miss target/choice commands) private boolean AIRealGameSimulation = false; + private PhaseStep AIRealGameControlUntil = null; // enable temporary AI control until some point in time private final List actions = new ArrayList<>(); - private final Map actionsToRemoveLater = new HashMap<>(); // remove actions later, on next step (e.g. for AI commands) + //private final Map actionsToRemoveLater = new HashMap<>(); // remove actions after some step (used for AI commands) private final Map>> rollbackActions = new HashMap<>(); // actions to add after a executed rollback private final List choices = new ArrayList<>(); // choices stack for choice private final List targets = new ArrayList<>(); // targets stack for choose (it's uses on empty direct target by cast command) @@ -140,6 +142,7 @@ public class TestPlayer implements Player { public TestPlayer(final TestPlayer testPlayer) { this.AIPlayer = testPlayer.AIPlayer; this.AIRealGameSimulation = testPlayer.AIRealGameSimulation; + this.AIRealGameControlUntil = testPlayer.AIRealGameControlUntil; this.foundNoAction = testPlayer.foundNoAction; this.actions.addAll(testPlayer.actions); this.choices.addAll(testPlayer.choices); @@ -573,17 +576,23 @@ public class TestPlayer implements Player { @Override public boolean priority(Game game) { - // later remove actions (ai commands related) - if (actionsToRemoveLater.size() > 0) { - List removed = new ArrayList<>(); - actionsToRemoveLater.forEach((action, step) -> { - if (game.getTurnStepType() != step) { - action.onActionRemovedLater(game, this); - actions.remove(action); - removed.add(action); - } - }); - removed.forEach(actionsToRemoveLater::remove); + boolean oldControl = AIRealGameSimulation; + if (AIPlayer) { + // full AI control + changeAIControl(game, true); + } else { + // temporary AI control + // after enabled on priority it must work until end of the priority + // e.g. AI can be called multiple times in complex choices + if (game.getTurnStepType().equals(PhaseStep.UPKEEP)) { + // reset + AIRealGameControlUntil = null; + changeAIControl(game, false); + } else { + // setup + boolean enable = AIRealGameControlUntil != null && game.getTurnStepType().getIndex() <= AIRealGameControlUntil.getIndex(); + changeAIControl(game, enable); + } } // fake test ability for triggers and events @@ -746,19 +755,31 @@ public class TestPlayer implements Player { String command = action.getAction(); command = command.substring(command.indexOf(AI_PREFIX) + AI_PREFIX.length()); - // play priority - if (command.equals(AI_COMMAND_PLAY_PRIORITY)) { - AIRealGameSimulation = true; // disable on action's remove + // play single priority, two modes support: + // - really single priority + // - multiple priorities until empty stack + if (command.startsWith(AI_COMMAND_PLAY_PRIORITY)) { + boolean needEmptyStack = Boolean.parseBoolean(command.split(AI_PARAM_DELIMETER)[1]); + changeAIControl(game, true); computerPlayer.priority(game); - actions.remove(action); + if (!needEmptyStack || game.getStack().isEmpty()) { + changeAIControl(game, false); + actions.remove(action); + computerPlayer.resetPassed(); // remove AI's pass, so runtime/check commands can be executed in same priority + } + // control will be disabled on next priority, not here + // (require to process triggers and other non-direct actions and choices) return true; } - // play step - if (command.equals(AI_COMMAND_PLAY_STEP)) { - AIRealGameSimulation = true; // disable on action's remove - actionsToRemoveLater.put(action, game.getTurnStepType()); + // play multiple priorities on one or multiple steps + if (command.startsWith(AI_COMMAND_PLAY_STEP)) { + PhaseStep endStep = PhaseStep.fromString(command.split(AI_PARAM_DELIMETER)[1]); + changeAIControl(game, true); // enable AI + AIRealGameControlUntil = endStep; // disable on end step computerPlayer.priority(game); + actions.remove(action); + computerPlayer.resetPassed(); // remove AI's pass, so runtime/check commands can be executed in same priority return true; } @@ -1087,6 +1108,7 @@ public class TestPlayer implements Player { } // turn/step } + // normal priority (by AI or pass) tryToPlayPriority(game); // check to prevent endless loops @@ -1103,6 +1125,16 @@ public class TestPlayer implements Player { return false; } + private void changeAIControl(Game game, boolean enable) { + if (AIRealGameSimulation != enable) { + LOGGER.info("AI control for " + getName() + + " " + (enable ? "ENABLED" : "DISABLED") + //+ " on T" + game.getTurnNum() + "." + game.getTurnStepType().getStepShortText()); + + " on " + game); + } + AIRealGameSimulation = enable; + } + /** * Adds actions to the player actions after an executed rollback Actions * have to be added after the rollback because otherwise the actions are @@ -1130,7 +1162,7 @@ public class TestPlayer implements Player { } private void tryToPlayPriority(Game game) { - if (AIPlayer) { + if (AIPlayer || AIRealGameSimulation) { computerPlayer.priority(game); } else { computerPlayer.pass(game); @@ -1788,16 +1820,6 @@ public class TestPlayer implements Player { boolean madeAttackByAction = false; for (Iterator it = actions.iterator(); it.hasNext(); ) { PlayerAction action = it.next(); - - // aiXXX commands - if (action.getTurnNum() == game.getTurnNum() && action.getAction().equals(AI_PREFIX + AI_COMMAND_PLAY_STEP)) { - mustAttackByAction = true; - madeAttackByAction = true; - this.computerPlayer.selectAttackers(game, attackingPlayerId); - // play step action will be removed on step end - continue; - } - if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("attack:")) { mustAttackByAction = true; String command = action.getAction(); @@ -1861,7 +1883,7 @@ public class TestPlayer implements Player { } // AI FULL play if no actions available - if (!mustAttackByAction && this.AIPlayer) { + if (!mustAttackByAction && (this.AIPlayer || this.AIRealGameSimulation)) { this.computerPlayer.selectAttackers(game, attackingPlayerId); } } @@ -1879,15 +1901,6 @@ public class TestPlayer implements Player { boolean mustBlockByAction = false; for (PlayerAction action : tempActions) { - - // aiXXX commands - if (action.getTurnNum() == game.getTurnNum() && action.getAction().equals(AI_PREFIX + AI_COMMAND_PLAY_STEP)) { - mustBlockByAction = true; - this.computerPlayer.selectBlockers(source, game, defendingPlayerId); - // play step action will be removed on step end - continue; - } - if (action.getTurnNum() == game.getTurnNum() && action.getAction().startsWith("block:")) { mustBlockByAction = true; String command = action.getAction(); @@ -1916,7 +1929,7 @@ public class TestPlayer implements Player { checkMultipleBlockers(game, blockedCreaturesList); // AI FULL play if no actions available - if (!mustBlockByAction && this.AIPlayer) { + if (!mustBlockByAction && (this.AIPlayer || this.AIRealGameSimulation)) { this.computerPlayer.selectBlockers(source, game, defendingPlayerId); } } @@ -4606,10 +4619,6 @@ public class TestPlayer implements Player { return computerPlayer; } - public void setAIRealGameSimulation(boolean AIRealGameSimulation) { - this.AIRealGameSimulation = AIRealGameSimulation; - } - public Map>> getRollbackActions() { return rollbackActions; } diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java index 206f37d13a4..3869bdde7c9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/CardTestPlayerBaseAI.java @@ -59,8 +59,7 @@ public abstract class CardTestPlayerBaseAI extends CardTestPlayerAPIImpl { protected TestPlayer createPlayer(String name, RangeOfInfluence rangeOfInfluence) { if (getFullSimulatedPlayers().contains(name)) { TestPlayer testPlayer = new TestPlayer(new TestComputerPlayer7(name, RangeOfInfluence.ONE, getSkillLevel())); - testPlayer.setAIPlayer(true); - testPlayer.setAIRealGameSimulation(true); // enable full AI support (game simulations) for all turns by default + testPlayer.setAIPlayer(true); // enable full AI support (game simulations) for all turns by default return testPlayer; } return super.createPlayer(name, rangeOfInfluence); 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 21a35cb89a9..03b7fd69013 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 @@ -27,14 +27,11 @@ import mage.player.ai.ComputerPlayerMCTS; import mage.players.ManaPool; import mage.players.Player; import mage.server.game.GameSessionPlayer; +import mage.util.CardUtil; import mage.util.ThreadUtils; import mage.utils.SystemUtil; -import mage.util.CardUtil; import mage.view.GameView; import org.junit.Assert; - -import static org.junit.Assert.assertTrue; - import org.junit.Before; import org.mage.test.player.PlayerAction; import org.mage.test.player.TestPlayer; @@ -48,6 +45,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import static org.junit.Assert.assertTrue; + /** * API for test initialization and asserting the test results. * @@ -61,10 +60,11 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement private static final boolean SHOW_EXECUTE_TIME_PER_TEST = false; public static final String ALIAS_PREFIX = "@"; // don't change -- it uses in user's tests - public static final String CHECK_PARAM_DELIMETER = "#"; public static final String CHECK_PREFIX = "check:"; // prefix for all check commands + public static final String CHECK_PARAM_DELIMETER = "#"; public static final String SHOW_PREFIX = "show:"; // prefix for all show commands public static final String AI_PREFIX = "ai:"; // prefix for all ai commands + public static final String AI_PARAM_DELIMETER = "#"; public static final String RUN_PREFIX = "run:"; // prefix for all run commands // prefix for activate commands @@ -1769,36 +1769,59 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement * AI play one PRIORITY with multi game simulations like real game * (calcs and play ONE best action, can be called with stack) * All choices must be made by AI (e.g.strict mode possible) - * - * @param turnNum - * @param step - * @param player + *

+ * Warning, by default it will take control until stack resolved */ public void aiPlayPriority(int turnNum, PhaseStep step, TestPlayer player) { + aiPlayPriority(turnNum, step, player, true); + } + + /** + * AI play one PRIORITY + * + * @param untilStackResolved use false to stop AI after first cast + */ + public void aiPlayPriority(int turnNum, PhaseStep step, TestPlayer player, boolean untilStackResolved) { assertAiPlayAndGameCompatible(player); - addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_PRIORITY)); + addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_PRIORITY + AI_PARAM_DELIMETER + untilStackResolved)); } /** * AI play STEP to the end with multi game simulations (calcs and play best - * actions until step ends, can be called in the middle of the step) All - * choices must be made by AI (e.g. strict mode possible) + * actions until step ends, can be called in the middle of the step) + * All choices must be made by AI (e.g. strict mode possible) *

* Can be used for AI's declare of attackers/blockers */ public void aiPlayStep(int turnNum, PhaseStep step, TestPlayer player) { + aiPlayStep(turnNum, step, step, player); + } + + public void aiPlayStep(int turnNum, PhaseStep startStep, PhaseStep endStep, TestPlayer player) { assertAiPlayAndGameCompatible(player); - addPlayerAction(player, createAIPlayerAction(turnNum, step, AI_COMMAND_PLAY_STEP)); + + // direct steps support removed to simplify AI enabling code + // (no needs in code duplicating in selectAttackers and selectBlockers methods anymore) + if (startStep == PhaseStep.DECLARE_ATTACKERS + || startStep == PhaseStep.DECLARE_BLOCKERS) { + PhaseStep newStartStep = PhaseStep.BEGIN_COMBAT; + PhaseStep newEndStep = endStep == startStep ? PhaseStep.END_COMBAT : endStep; + aiPlayStep(turnNum, newStartStep, newEndStep, player); + return; + } + + if (startStep == PhaseStep.UPKEEP + || startStep == PhaseStep.CLEANUP + || startStep == PhaseStep.FIRST_COMBAT_DAMAGE + || startStep == PhaseStep.COMBAT_DAMAGE) { + Assert.fail("AI can't be started from step without priority"); + } + + addPlayerAction(player, createAIPlayerAction(turnNum, startStep, AI_COMMAND_PLAY_STEP + AI_PARAM_DELIMETER + endStep.toString())); } public PlayerAction createAIPlayerAction(int turnNum, PhaseStep step, String aiCommand) { - // AI commands must disable and enable real game simulation and strict mode - return new PlayerAction("", turnNum, step, AI_PREFIX + aiCommand) { - @Override - public void onActionRemovedLater(Game game, TestPlayer player) { - player.setAIRealGameSimulation(false); - } - }; + return new PlayerAction("", turnNum, step, AI_PREFIX + aiCommand); } private void assertAiPlayAndGameCompatible(TestPlayer player) { diff --git a/Mage/src/main/java/mage/cards/mock/MockCard.java b/Mage/src/main/java/mage/cards/mock/MockCard.java index d76ad94c0d4..ae406a39dca 100644 --- a/Mage/src/main/java/mage/cards/mock/MockCard.java +++ b/Mage/src/main/java/mage/cards/mock/MockCard.java @@ -20,8 +20,8 @@ import java.util.List; */ public class MockCard extends CardImpl implements MockableCard { - static public String ADVENTURE_NAME_SEPARATOR = " // "; - static public String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // "; + public static String ADVENTURE_NAME_SEPARATOR = " // "; + public static String MODAL_DOUBLE_FACES_NAME_SEPARATOR = " // "; // Needs to be here, as it is normally calculated from the // PlaneswalkerEntersWithLoyaltyAbility of the card... but the MockCard diff --git a/Mage/src/main/java/mage/constants/PhaseStep.java b/Mage/src/main/java/mage/constants/PhaseStep.java index 2c489e556fd..5b71c6e09b5 100644 --- a/Mage/src/main/java/mage/constants/PhaseStep.java +++ b/Mage/src/main/java/mage/constants/PhaseStep.java @@ -1,5 +1,7 @@ package mage.constants; +import java.util.Arrays; + /** * @author North */ @@ -51,6 +53,13 @@ public enum PhaseStep { return text; } + public static PhaseStep fromString(String needText) { + return Arrays.stream(values()) + .filter(step -> step.toString().equals(needText)) + .findFirst() + .orElse(null); + } + public String getStepText() { return stepText; } diff --git a/Mage/src/main/java/mage/game/combat/CombatGroup.java b/Mage/src/main/java/mage/game/combat/CombatGroup.java index 32e09122066..22ad213ada8 100644 --- a/Mage/src/main/java/mage/game/combat/CombatGroup.java +++ b/Mage/src/main/java/mage/game/combat/CombatGroup.java @@ -966,7 +966,7 @@ public class CombatGroup implements Serializable, Copyable { return false; } - private static int getLethalDamage(Permanent damaged, Permanent damaging, Game game) { - return damaged.getLethalDamage(damaging.getId(), game); + private static int getLethalDamage(Permanent blocker, Permanent attacker, Game game) { + return blocker.getLethalDamage(attacker.getId(), game); } }