diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java index 672de57a5a..2a17767268 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer6.java @@ -15,6 +15,8 @@ import mage.choices.Choice; import mage.constants.AbilityType; import mage.constants.Outcome; import mage.constants.RangeOfInfluence; +import mage.counters.CounterType; +import mage.filter.StaticFilters; import mage.game.Game; import mage.game.combat.Combat; import mage.game.events.GameEvent; @@ -40,8 +42,6 @@ import java.io.File; import java.util.*; import java.util.concurrent.*; import java.util.stream.Collectors; -import mage.counters.CounterType; -import mage.filter.StaticFilters; /** * @author nantuko @@ -215,7 +215,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { logger.trace("Add Action [" + depth + "] " + node.getAbilities().toString() + " a: " + alpha + " b: " + beta); } Game game = node.getGame(); - if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS + if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.interrupted()) { Thread.currentThread().interrupt(); logger.debug("interrupted"); @@ -435,7 +435,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { pool.execute(task); try { int maxSeconds = maxThink; - if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS) { + if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS) { maxSeconds = 3600; } logger.debug("maxThink: " + maxSeconds + " seconds "); @@ -460,7 +460,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } protected int simulatePriority(SimulationNode2 node, Game game, int depth, int alpha, int beta) { - if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS + if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.interrupted()) { Thread.currentThread().interrupt(); logger.info("interrupted"); @@ -480,7 +480,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { int bestValSubNodes = Integer.MIN_VALUE; for (Ability action : allActions) { counter++; - if (COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS + if (!COMPUTER_DISABLE_TIMEOUT_IN_GAME_SIMULATIONS && Thread.interrupted()) { Thread.currentThread().interrupt(); logger.info("Sim Prio [" + depth + "] -- interrupted"); @@ -492,7 +492,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { && sim.getPlayer(currentPlayer.getId()).activateAbility((ActivatedAbility) action.copy(), sim)) { sim.applyEffects(); if (checkForRepeatedAction(sim, node, action, currentPlayer.getId())) { - logger.debug("Sim Prio [" + depth + "] -- repeated action: " + action.toString()); + logger.debug("Sim Prio [" + depth + "] -- repeated action: " + action); continue; } if (!sim.checkIfGameIsOver() @@ -513,7 +513,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } else { val = addActions(newNode, depth - 1, alpha, beta); } - logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + counter + " <" + val + "> - (" + action.toString() + ") "); + logger.debug("Sim Prio " + BLANKS.substring(0, 2 + (maxDepth - depth) * 3) + '[' + depth + "]#" + counter + " <" + val + "> - (" + action + ") "); if (logger.isInfoEnabled() && depth >= maxDepth) { StringBuilder sb = new StringBuilder("Sim Prio [").append(depth).append("] #").append(counter) @@ -979,14 +979,15 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { protected Game createSimulation(Game game) { Game sim = game.copy(); sim.setSimulation(true); - for (Player copyPlayer : sim.getState().getPlayers().values()) { - Player origPlayer = game.getState().getPlayers().get(copyPlayer.getId()).copy(); + for (Player oldPlayer : sim.getState().getPlayers().values()) { + // replace original player by simulated player and find result (execute/resolve current action) + Player origPlayer = game.getState().getPlayers().get(oldPlayer.getId()).copy(); if (!suggestedActions.isEmpty()) { logger.debug(origPlayer.getName() + " suggested: " + suggestedActions); } - SimulatedPlayer2 newPlayer = new SimulatedPlayer2(copyPlayer.getId(), copyPlayer.getId().equals(playerId), suggestedActions); - newPlayer.restore(origPlayer); - sim.getState().getPlayers().put(copyPlayer.getId(), newPlayer); + SimulatedPlayer2 simPlayer = new SimulatedPlayer2(oldPlayer, oldPlayer.getId().equals(playerId), suggestedActions); + simPlayer.restore(origPlayer); + sim.getState().getPlayers().put(oldPlayer.getId(), simPlayer); } return sim; } @@ -1069,7 +1070,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { * * @param game * @param targets - * @param format example: my %s in data + * @param format example: my %s in data * @param emptyText default text for empty targets list * @return */ diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java index 3f7f220542..cd5b1a6430 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/SimulatedPlayer2.java @@ -31,14 +31,14 @@ import java.util.concurrent.ConcurrentLinkedQueue; public class SimulatedPlayer2 extends ComputerPlayer { private static final Logger logger = Logger.getLogger(SimulatedPlayer2.class); - private static PassAbility pass = new PassAbility(); + private static final PassAbility pass = new PassAbility(); private final boolean isSimulatedPlayer; private final List suggested; private transient ConcurrentLinkedQueue allActions; private boolean forced; - public SimulatedPlayer2(UUID id, boolean isSimulatedPlayer, List suggested) { - super(id); + public SimulatedPlayer2(Player originalPlayer, boolean isSimulatedPlayer, List suggested) { + super(originalPlayer.getId()); pass.setControllerId(playerId); this.isSimulatedPlayer = isSimulatedPlayer; this.suggested = suggested; @@ -435,8 +435,15 @@ public class SimulatedPlayer2 extends ComputerPlayer { @Override public boolean priority(Game game) { - //should never get here + // simulated player do nothing - it must pass until stack resolve to see final game score after action apply + + // it's a workaround for Karn Liberated restart ability (see CommandersGameRestartTest) + // reason: restarted game is broken (miss clear code of some game/player data?) and ai can't simulate it + // so game is freezes on non empty stack (last part of karn's restart ability) + if (game.getStack().isEmpty()) { + game.pause(); + } + pass(game); return false; } - } diff --git a/Mage.Sets/src/mage/cards/k/KarnLiberated.java b/Mage.Sets/src/mage/cards/k/KarnLiberated.java index 30d9ef48f0..b614f8a9b5 100644 --- a/Mage.Sets/src/mage/cards/k/KarnLiberated.java +++ b/Mage.Sets/src/mage/cards/k/KarnLiberated.java @@ -92,7 +92,11 @@ class KarnLiberatedEffect extends OneShotEffect { } } } - game.getState().clear(); + + // dirty hack for game restart, can cause bugs due strange clear code (some data like ZCC keeping on new game) + // see testCommanderRestoredToBattlefieldAfterKarnUltimate for more details + + game.getState().clearOnGameRestart(); // default watchers init, TODO: remove all restart/init code to game ((GameImpl) game).initGameDefaultWatchers(); diff --git a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java index db8796ba96..902af913aa 100644 --- a/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/AI/basic/CastCreaturesTest.java @@ -209,5 +209,4 @@ public class CastCreaturesTest extends CardTestPlayerBaseAI { assertPowerToughness(playerB, "Ammit Eternal", 4, 4); } - } diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersGameRestartTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersGameRestartTest.java new file mode 100644 index 0000000000..56d19fed39 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/CommandersGameRestartTest.java @@ -0,0 +1,103 @@ +package org.mage.test.cards.continuous; + +import mage.constants.CommanderCardType; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.watchers.common.CommanderPlaysCountWatcher; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4PlayersWithAIHelps; + +import java.util.UUID; + +/** + * @author JayDi85 + */ +public class CommandersGameRestartTest extends CardTestCommander4PlayersWithAIHelps { + + @Test + public void test_KarnLiberated_Manual() { + // Player order: A -> D -> C -> B + + addCard(Zone.COMMAND, playerA, "Balduvian Bears", 1); // {1}{G}, 2/2, commander + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + // + // -14: Restart the game, leaving in exile all non-Aura permanent cards exiled with Karn Liberated. + // Then put those cards onto the battlefield under your control. + addCard(Zone.BATTLEFIELD, playerA, "Karn Liberated", 1); + + // prepare commander + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + + // prepare karn + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Karn Liberated", CounterType.LOYALTY, 20); + + // check watcher before restart + runCode("before restart", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + UUID commanderId = game.getCommandersIds(player, CommanderCardType.ANY, false).stream().findFirst().orElse(null); + CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); + Assert.assertEquals("commander tax must be x1", 1, watcher.getPlaysCount(commanderId)); + }); + + // game restart + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "-14: "); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + // check watcher after restart + UUID commanderId = currentGame.getCommandersIds(playerA, CommanderCardType.ANY, false).stream().findFirst().orElse(null); + CommanderPlaysCountWatcher watcher = currentGame.getState().getWatcher(CommanderPlaysCountWatcher.class); + Assert.assertEquals("commander tax must be x0", 0, watcher.getPlaysCount(commanderId)); + + assertPermanentCount(playerA, 0); // no cards on battle after game restart + } + + @Test + public void test_KarnLiberated_AI() { + // Player order: A -> D -> C -> B + + addCard(Zone.COMMAND, playerA, "Balduvian Bears", 1); // {1}{G}, 2/2, commander + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + // + // -14: Restart the game, leaving in exile all non-Aura permanent cards exiled with Karn Liberated. + // Then put those cards onto the battlefield under your control. + addCard(Zone.BATTLEFIELD, playerA, "Karn Liberated", 1); + // + addCard(Zone.HAND, playerB, "Balduvian Bears", 5); + addCard(Zone.HAND, playerC, "Balduvian Bears", 5); + addCard(Zone.HAND, playerD, "Balduvian Bears", 5); + + // prepare commander + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + checkPermanentCount("prepare", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Balduvian Bears", 1); + + // prepare karn + addCounters(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Karn Liberated", CounterType.LOYALTY, 50); + + // check watcher before restart + runCode("before restart", 1, PhaseStep.PRECOMBAT_MAIN, playerA, (info, player, game) -> { + UUID commanderId = game.getCommandersIds(player, CommanderCardType.ANY, false).stream().findFirst().orElse(null); + CommanderPlaysCountWatcher watcher = game.getState().getWatcher(CommanderPlaysCountWatcher.class); + Assert.assertEquals("commander tax must be x1", 1, watcher.getPlaysCount(commanderId)); + }); + + // possible bug: ai can use restart in one of the simulations, so it can freeze the game (if bugged) + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerA); + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerB); + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerC); + aiPlayStep(1, PhaseStep.PRECOMBAT_MAIN, playerD); + + setStopAt(1, PhaseStep.END_TURN); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java index 924781998f..b3bf8e2bc1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/CastBRGCommanderTest.java @@ -1,6 +1,5 @@ package org.mage.test.commander.duel; -import java.io.FileNotFoundException; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.game.Game; @@ -10,8 +9,9 @@ import org.junit.Assert; import org.junit.Test; import org.mage.test.serverside.base.CardTestCommanderDuelBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { @@ -97,22 +97,36 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { addCard(Zone.BATTLEFIELD, playerB, "Plains", 2); addCard(Zone.BATTLEFIELD, playerB, "Island", 1); + // exile from hand 1 activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA); addTarget(playerA, "Silvercoat Lion"); + // prepare commander castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerB, "Daxos of Meletis"); + // exile from hand 2 activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA); addTarget(playerA, "Silvercoat Lion"); + // attack and get commander damage attack(4, playerB, "Daxos of Meletis"); + // exile commander activateAbility(5, PhaseStep.PRECOMBAT_MAIN, playerA, "-3: Exile target permanent", "Daxos of Meletis"); setChoice(playerB, "No"); // Move commander NOT to command zone + // exile from hand 3 activateAbility(7, PhaseStep.PRECOMBAT_MAIN, playerA, "+4: Target player", playerA); addTarget(playerA, "Silvercoat Lion"); + + // restart game and return to battlefield 1x commander and 3x lions activateAbility(9, PhaseStep.PRECOMBAT_MAIN, playerA, "-14: Restart"); + // warning: + // - karn restart code can clear some game data + // - current version ignores a card's ZCC + // - so ZCC are same after game restart and SBA can't react on commander new move + // - logic can be changed in the future, so game can ask commander move again here + //setChoice(playerB, "No"); // Move commander NOT to command zone setStopAt(9, PhaseStep.BEGIN_COMBAT); @@ -127,8 +141,7 @@ public class CastBRGCommanderTest extends CardTestCommanderDuelBase { assertPermanentCount(playerA, "Daxos of Meletis", 1); // Karn brings back the cards under the control of Karn's controller CommanderInfoWatcher watcher = currentGame.getState().getWatcher(CommanderInfoWatcher.class, playerB.getCommandersIds().iterator().next()); - Assert.assertEquals("Watcher is reset to 0 commander damage", 0, (int) watcher.getDamageToPlayer().size()); - + Assert.assertEquals("Watcher is reset to 0 commander damage", 0, watcher.getDamageToPlayer().size()); } /** diff --git a/Mage/src/main/java/mage/game/GameState.java b/Mage/src/main/java/mage/game/GameState.java index 8ff30e43cf..cdd0f975ab 100644 --- a/Mage/src/main/java/mage/game/GameState.java +++ b/Mage/src/main/java/mage/game/GameState.java @@ -193,6 +193,45 @@ public class GameState implements Serializable, Copyable { this.commandersToStay.addAll(state.commandersToStay); } + public void clearOnGameRestart() { + // special code for Karn Liberated + // must clear game data on restart, but also must keep some info (wtf, why?) + // if you catch freezes or bugs with Karn then research here + // test example: testCommanderRestoredToBattlefieldAfterKarnUltimate + // TODO: must be implemented as full data clear? + + battlefield.clear(); + effects.clear(); + triggers.clear(); + delayed.clear(); + triggered.clear(); + stack.clear(); + exile.clear(); + command.clear(); + designations.clear(); + seenPlanes.clear(); + isPlaneChase = false; + revealed.clear(); + lookedAt.clear(); + companion.clear(); + turnNum = 1; + stepNum = 0; + extraTurn = false; + legendaryRuleActive = true; + gameOver = false; + specialActions.clear(); + cardState.clear(); + combat.clear(); + turnMods.clear(); + watchers.clear(); + values.clear(); + zones.clear(); + simultaneousEvents.clear(); + copiedCards.clear(); + usePowerInsteadOfToughnessForDamageLethalityFilters.clear(); + permanentOrderNumber = 0; + } + public void restoreForRollBack(GameState state) { restore(state); this.turn = state.turn; @@ -1124,7 +1163,7 @@ public class GameState implements Serializable, Copyable { if (attachedTo instanceof PermanentCard) { throw new IllegalArgumentException("Error, wrong code usage. If you want to add new ability to the " + "permanent then use a permanent.addAbility(a, source, game): " - + ability.getClass().getCanonicalName() + " - " + ability.toString()); + + ability.getClass().getCanonicalName() + " - " + ability); } } @@ -1153,39 +1192,6 @@ public class GameState implements Serializable, Copyable { this.setManaBurn(false); } - public void clear() { - battlefield.clear(); - effects.clear(); - triggers.clear(); - delayed.clear(); - triggered.clear(); - stack.clear(); - exile.clear(); - command.clear(); - designations.clear(); - seenPlanes.clear(); - isPlaneChase = false; - revealed.clear(); - lookedAt.clear(); - companion.clear(); - turnNum = 0; - stepNum = 0; - extraTurn = false; - legendaryRuleActive = true; - gameOver = false; - specialActions.clear(); - cardState.clear(); - combat.clear(); - turnMods.clear(); - watchers.clear(); - values.clear(); - zones.clear(); - simultaneousEvents.clear(); - copiedCards.clear(); - usePowerInsteadOfToughnessForDamageLethalityFilters.clear(); - permanentOrderNumber = 0; - } - public void pause() { this.paused = true; }