From 6e913cf24bb51b2ed27f7f012ad952990065e5bb Mon Sep 17 00:00:00 2001 From: magenoxx Date: Mon, 28 Feb 2011 14:23:59 +0300 Subject: [PATCH] Migrated minimax hybrid AI to Mad AI. Reproduced AI not attacking, added 2 new test scenarios that reproduce it. --- .../src/mage/player/ai/ComputerPlayer6.java | 28 +- .../src/mage/player/ai/ComputerPlayer7.java | 545 ++++++++++++++++++ Mage.Tests/config/config.xml | 2 +- Mage.Tests/plugins/mage-player-ai-ma.jar | Bin 34248 -> 43082 bytes Mage.Tests/plugins/mage-player-aiminimax.jar | Bin 38388 -> 38392 bytes Mage.Tests/scenario1.txt | 4 +- Mage.Tests/scenario2.txt | 30 +- Mage.Tests/scenario3.txt | 2 + Mage.Tests/scenario4.txt | 2 + Mage.Tests/scenario8.txt | 36 ++ .../mage/test/serverside/PlayGameTest.java | 26 +- .../test/serverside/base/MageTestBase.java | 2 +- .../src/test/resources/log4j.properties | 2 +- Mage/src/mage/game/GameImpl.java | 1 - 14 files changed, 657 insertions(+), 23 deletions(-) create mode 100644 Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java create mode 100644 Mage.Tests/scenario8.txt 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 4a945e05f0..ed298bbd01 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 @@ -160,7 +160,7 @@ public class ComputerPlayer6 extends ComputerPlayer implements } } - private void printOutState(Game game, UUID playerId) { + protected void printOutState(Game game, UUID playerId) { Player player = game.getPlayer(playerId); System.out.println("Turn::"+game.getTurnNum()); System.out.println("[" + game.getPlayer(playerId).getName() + "] " + game.getTurn().getStepType().name() +", life=" + player.getLife()); @@ -239,6 +239,7 @@ public class ComputerPlayer6 extends ComputerPlayer implements logger.info("simlating -- game value:" + game.getState().getValue() + " test value:" + test.gameValue); if (root.playerId.equals(playerId) && root.abilities != null && game.getState().getValue() == test.gameValue) { + /* // Try to fix horizon effect if (root.combat == null || root.combat.getAttackers().size() == 0) { FilterCreatureForAttack attackFilter = new FilterCreatureForAttack(); @@ -250,6 +251,7 @@ public class ComputerPlayer6 extends ComputerPlayer implements return false; } } + */ logger.info("simulating -- continuing previous action chain"); actions = new LinkedList(root.abilities); @@ -286,6 +288,11 @@ public class ComputerPlayer6 extends ComputerPlayer implements bestChild.setCombat(_combat); } } + // no need to check other actions + if (val == GameStateEvaluator2.LOSE_GAME_SCORE) { + logger.debug("lose - break"); + break; + } } else { if (val > alpha) { @@ -296,6 +303,11 @@ public class ComputerPlayer6 extends ComputerPlayer implements bestChild.setCombat(_combat); } } + // no need to check other actions + if (val == GameStateEvaluator2.WIN_GAME_SCORE) { + logger.debug("win - break"); + break; + } } } node.children.clear(); @@ -467,6 +479,12 @@ public class ComputerPlayer6 extends ComputerPlayer implements bestNode.setScore(val); node.setCombat(newNode.getCombat()); } + + // no need to check other actions + if (val == GameStateEvaluator2.LOSE_GAME_SCORE) { + logger.debug("lose - break"); + break; + } } else { if (val > alpha) { @@ -478,7 +496,7 @@ public class ComputerPlayer6 extends ComputerPlayer implements targets = node.getTargets(); if (node.getChoices().size() > 0) choices = node.getChoices(); - if (depth == 20) { + if (depth == Config2.maxDepth) { logger.info("saved"); node.children.clear(); node.children.add(bestNode); @@ -488,7 +506,7 @@ public class ComputerPlayer6 extends ComputerPlayer implements // no need to check other actions if (val == GameStateEvaluator2.WIN_GAME_SCORE) { - logger.info("win - break"); + logger.debug("win - break"); break; } } @@ -710,10 +728,12 @@ public class ComputerPlayer6 extends ComputerPlayer implements logger.debug("selectAttackers"); if (combat != null) { UUID opponentId = game.getCombat().getDefenders().iterator().next(); + String attackers = ""; for (UUID attackerId: combat.getAttackers()) { - logger.debug("declare attacker: " + game.getCard(attackerId).getName()); + attackers = "[" + game.getCard(attackerId).getName() + "]"; this.declareAttacker(attackerId, opponentId, game); } + logger.info("declare attackers: " + (attackers.isEmpty() ? "none" : attackers)); } } diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java new file mode 100644 index 0000000000..49d6b409a2 --- /dev/null +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/ComputerPlayer7.java @@ -0,0 +1,545 @@ +/* + * Copyright 2010 BetaSteward_at_googlemail.com. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without modification, are + * permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this list of + * conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright notice, this list + * of conditions and the following disclaimer in the documentation and/or other materials + * provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY BetaSteward_at_googlemail.com ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND + * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BetaSteward_at_googlemail.com OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * The views and conclusions contained in the software and documentation are those of the + * authors and should not be interpreted as representing official policies, either expressed + * or implied, of BetaSteward_at_googlemail.com. + */ + +package mage.player.ai; + +import mage.Constants.AbilityType; +import mage.Constants.PhaseStep; +import mage.Constants.RangeOfInfluence; +import mage.Constants.Zone; +import mage.abilities.Ability; +import mage.filter.FilterAbility; +import mage.game.Game; +import mage.game.combat.Combat; +import mage.game.combat.CombatGroup; +import mage.game.events.GameEvent; +import mage.game.turn.*; +import mage.players.Player; +import org.apache.log4j.Logger; + +import java.util.LinkedList; +import java.util.UUID; + +/** + * + * @author ayratn + */ +public class ComputerPlayer7 extends ComputerPlayer6 implements Player { + + private static final transient Logger logger = Logger.getLogger(ComputerPlayer7.class); + + private static FilterAbility filterLand = new FilterAbility(); + private static FilterAbility filterNotLand = new FilterAbility(); + + static { + filterLand.getTypes().add(AbilityType.PLAY_LAND); + filterLand.setZone(Zone.HAND); + + filterNotLand.getTypes().add(AbilityType.PLAY_LAND); + filterNotLand.setZone(Zone.HAND); + filterNotLand.setNotFilter(true); + + } + + public ComputerPlayer7(String name, RangeOfInfluence range) { + super(name, range); + maxDepth = Config2.maxDepth; + maxNodes = Config2.maxNodes; + } + + public ComputerPlayer7(final ComputerPlayer7 player) { + super(player); + } + + @Override + public ComputerPlayer7 copy() { + return new ComputerPlayer7(this); + } + + @Override + public void priority(Game game) { + logState(game); + if (logger.isDebugEnabled()) + logger.debug("Game State: Turn-" + game.getTurnNum() + " Step-" + game.getTurn().getStepType() + " ActivePlayer-" + game.getPlayer(game.getActivePlayerId()).getName() + " PriorityPlayer-" + name); + game.firePriorityEvent(playerId); + switch (game.getTurn().getStepType()) { + case UPKEEP: + case DRAW: + pass(); + break; + case PRECOMBAT_MAIN: + if (game.getActivePlayerId().equals(playerId)) { + System.out.println("Computer7:"); + printOutState(game, playerId); + printOutState(game, game.getOpponents(playerId).iterator().next()); + if (actions.size() == 0) { + calculatePreCombatActions(game); + } + act(game); + } + else + pass(); + break; + case BEGIN_COMBAT: + pass(); + break; + case DECLARE_ATTACKERS: + if (!game.getActivePlayerId().equals(playerId)) { + printOutState(game, playerId); + printOutState(game, game.getOpponents(playerId).iterator().next()); + if (actions.size() == 0) { + calculatePreCombatActions(game); + } + act(game); + } + else + pass(); + break; + case DECLARE_BLOCKERS: + case COMBAT_DAMAGE: + case END_COMBAT: + pass(); + break; + case POSTCOMBAT_MAIN: + if (game.getActivePlayerId().equals(playerId)) { + printOutState(game, playerId); + printOutState(game, game.getOpponents(playerId).iterator().next()); + if (actions.size() == 0) { + calculatePostCombatActions(game); + } + act(game); + } + else + pass(); + break; + case END_TURN: + case CLEANUP: + pass(); + break; + } + } + + protected void calculatePreCombatActions(Game game) { + if (!getNextAction(game)) { + currentScore = GameStateEvaluator2.evaluate(playerId, game); + Game sim = createSimulation(game); + SimulationNode2.resetCount(); + root = new SimulationNode2(null, sim, maxDepth, playerId); + logger.debug("simulating pre combat actions -----------------------------------------------------------------------------------------"); + + addActionsTimed(new FilterAbility()); + if (root.children.size() > 0) { + root = root.children.get(0); + int bestScore = root.getScore(); + if (bestScore > currentScore) { + actions = new LinkedList(root.abilities); + combat = root.combat; + } + } + } + } + + protected void calculatePostCombatActions(Game game) { + if (!getNextAction(game)) { + currentScore = GameStateEvaluator2.evaluate(playerId, game); + Game sim = createSimulation(game); + SimulationNode2.resetCount(); + root = new SimulationNode2(null, sim, maxDepth, playerId); + logger.debug("simulating post combat actions ----------------------------------------------------------------------------------------"); + + addActionsTimed(new FilterAbility()); + if (root.children.size() > 0) { + root = root.children.get(0); + int bestScore = root.getScore(); + if (bestScore > currentScore) { + actions = new LinkedList(root.abilities); + combat = root.combat; + } + } + } + } + + @Override + protected int addActions(SimulationNode2 node, FilterAbility filter, int depth, int alpha, int beta) { + boolean stepFinished = false; + int val; + Game game = node.getGame(); + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.isGameOver()) { + logger.debug("simulating -- reached end state"); + val = GameStateEvaluator2.evaluate(playerId, game); + } + else if (node.getChildren().size() > 0) { + logger.debug("simulating -- somthing added children:" + node.getChildren().size()); + val = minimaxAB(node, filter, depth-1, alpha, beta); + } + else { + if (logger.isDebugEnabled()) + logger.debug("simulating -- alpha: " + alpha + " beta: " + beta + " depth:" + depth + " step:" + game.getTurn().getStepType() + " for player:" + game.getPlayer(game.getPlayerList().get()).getName()); + if (allPassed(game)) { + if (!game.getStack().isEmpty()) { + resolve(node, depth, game); + } + else { + stepFinished = true; + } + } + + if (game.isGameOver()) { + val = GameStateEvaluator2.evaluate(playerId, game); + } + else if (stepFinished) { + logger.debug("step finished"); + int testScore = GameStateEvaluator2.evaluate(playerId, game); + if (game.getActivePlayerId().equals(playerId)) { + if (testScore < currentScore) { + // if score at end of step is worse than original score don't check further + logger.debug("simulating -- abandoning check, no immediate benefit"); + val = testScore; + } + else { + switch (game.getTurn().getStepType()) { + case PRECOMBAT_MAIN: + val = simulateCombat(game, node, depth-1, alpha, beta, false); + break; + case POSTCOMBAT_MAIN: + val = simulateCounterAttack(game, node, depth-1, alpha, beta); + break; + default: + val = GameStateEvaluator2.evaluate(playerId, game); + break; + } + } + } + else { + if (game.getTurn().getStepType() == PhaseStep.DECLARE_ATTACKERS) + val = simulateBlockers(game, node, playerId, depth-1, alpha, beta, true); + else + val = GameStateEvaluator2.evaluate(playerId, game); + } + } + else if (node.getChildren().size() > 0) { + logger.debug("simulating -- trigger added children:" + node.getChildren().size()); + val = minimaxAB(node, filter, depth, alpha, beta); + } + else { + val = simulatePriority(node, game, filter, depth, alpha, beta); + } + } + + if (logger.isDebugEnabled()) + logger.debug("returning -- score: " + val + " depth:" + depth + " step:" + game.getTurn().getStepType() + " for player:" + game.getPlayer(node.getPlayerId()).getName()); + return val; + + } + + protected int simulateCombat(Game game, SimulationNode2 node, int depth, int alpha, int beta, boolean counter) { + Integer val = null; + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + if (game.getTurn().getStepType() != PhaseStep.DECLARE_BLOCKERS) { + game.getTurn().setPhase(new CombatPhase()); + if (game.getPhase().beginPhase(game, game.getActivePlayerId())) { + simulateStep(game, new BeginCombatStep()); + game.getPhase().setStep(new DeclareAttackersStep()); + if (!game.getStep().skipStep(game, game.getActivePlayerId())) { + game.fireEvent(new GameEvent(GameEvent.EventType.DECLARE_ATTACKERS_STEP_PRE, null, null, game.getActivePlayerId())); + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_ATTACKERS, game.getActivePlayerId(), game.getActivePlayerId()))) { + val = simulateAttackers(game, node, game.getActivePlayerId(), depth, alpha, beta, counter); + } + } + else if (!counter) { + simulateToEnd(game); + val = simulatePostCombatMain(game, node, depth, alpha, beta); + } + } + } + else { + if (!game.getStep().skipStep(game, game.getActivePlayerId())) { + game.fireEvent(new GameEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_PRE, null, null, game.getActivePlayerId())); + if (!game.replaceEvent(GameEvent.getEvent(GameEvent.EventType.DECLARING_BLOCKERS, game.getActivePlayerId(), game.getActivePlayerId()))) { + //only suitable for two player games - only simulates blocks for 1st defender + val = simulateBlockers(game, node, game.getCombat().getDefenders().iterator().next(), depth, alpha, beta, counter); + } + } + else if (!counter) { + finishCombat(game); + val = simulateCounterAttack(game, node, depth, alpha, beta); + } + } + if (val == null) + val = GameStateEvaluator2.evaluate(playerId, game); + if (logger.isDebugEnabled()) + logger.debug("returning -- combat score: " + val + " depth:" + depth + " for player:" + game.getPlayer(node.getPlayerId()).getName()); + return val; + } + + + protected int simulateAttackers(Game game, SimulationNode2 node, UUID attackerId, int depth, int alpha, int beta, boolean counter) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + Integer val = null; + SimulationNode2 bestNode = null; + SimulatedPlayer2 attacker = (SimulatedPlayer2) game.getPlayer(attackerId); + + if (logger.isDebugEnabled()) + logger.debug(attacker.getName() + "'s possible attackers: " + attacker.getAvailableAttackers(game)); + for (Combat engagement: attacker.addAttackers(game)) { + if (alpha >= beta) { + logger.debug("simulating -- pruning attackers"); + break; + } + Game sim = game.copy(); + UUID defenderId = game.getOpponents(attackerId).iterator().next(); + for (CombatGroup group: engagement.getGroups()) { + for (UUID attackId: group.getAttackers()) { + sim.getPlayer(attackerId).declareAttacker(attackId, defenderId, sim); + } + } + sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_ATTACKERS, attackerId, attackerId)); + SimulationNode2 newNode = new SimulationNode2(node, sim, depth, attackerId); + if (logger.isDebugEnabled()) + logger.debug("simulating attack for player:" + game.getPlayer(attackerId).getName()); + sim.checkStateAndTriggered(); + while (!sim.getStack().isEmpty()) { + sim.getStack().resolve(sim); + logger.debug("resolving triggered abilities"); + sim.applyEffects(); + } + sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_ATTACKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); + Combat simCombat = sim.getCombat().copy(); + sim.getPhase().setStep(new DeclareBlockersStep()); + val = simulateCombat(sim, newNode, depth-1, alpha, beta, counter); + if (!attackerId.equals(playerId)) { + if (val < beta) { + beta = val; + bestNode = newNode; + bestNode.setScore(val); + node.setCombat(simCombat); + } + } + else { + if (val > alpha) { + alpha = val; + bestNode = newNode; + bestNode.setScore(val); + node.setCombat(simCombat); + } + } + } + if (val == null) + val = GameStateEvaluator2.evaluate(playerId, game); + if (bestNode != null) { + node.children.clear(); + node.children.add(bestNode); + node.setScore(bestNode.getScore()); + } + if (logger.isDebugEnabled()) + logger.debug("returning -- combat attacker score: " + val + " depth:" + depth + " for player:" + game.getPlayer(node.getPlayerId()).getName()); + return val; + } + + protected int simulateBlockers(Game game, SimulationNode2 node, UUID defenderId, int depth, int alpha, int beta, boolean counter) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + Integer val = null; + SimulationNode2 bestNode = null; + //check if defender is being attacked + if (game.getCombat().isAttacked(defenderId, game)) { + SimulatedPlayer2 defender = (SimulatedPlayer2) game.getPlayer(defenderId); + if (logger.isDebugEnabled()) + logger.debug(defender.getName() + "'s possible blockers: " + defender.getAvailableBlockers(game)); + for (Combat engagement: defender.addBlockers(game)) { + if (alpha >= beta) { + logger.debug("simulating -- pruning blockers"); + break; + } + Game sim = game.copy(); + for (CombatGroup group: engagement.getGroups()) { + if (group.getAttackers().size() > 0) { + UUID attackerId = group.getAttackers().get(0); + for (UUID blockerId: group.getBlockers()) { + sim.getPlayer(defenderId).declareBlocker(blockerId, attackerId, sim); + } + } + } + sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARED_BLOCKERS, defenderId, defenderId)); + SimulationNode2 newNode = new SimulationNode2(node, sim, depth, defenderId); + if (logger.isDebugEnabled()) + logger.debug("simulating block for player:" + game.getPlayer(defenderId).getName()); + sim.checkStateAndTriggered(); + while (!sim.getStack().isEmpty()) { + sim.getStack().resolve(sim); + logger.debug("resolving triggered abilities"); + sim.applyEffects(); + } + sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); + Combat simCombat = sim.getCombat().copy(); + finishCombat(sim); + if (sim.isGameOver()) { + val = GameStateEvaluator2.evaluate(playerId, sim); + } + else if (!counter) { + val = simulatePostCombatMain(sim, newNode, depth-1, alpha, beta); + } + else + val = GameStateEvaluator2.evaluate(playerId, sim); + if (!defenderId.equals(playerId)) { + if (val < beta) { + beta = val; + bestNode = newNode; + bestNode.setScore(val); + node.setCombat(simCombat); + } + } + else { + if (val > alpha) { + alpha = val; + bestNode = newNode; + node.setCombat(simCombat); + } + } + } + } + if (val == null) + val = GameStateEvaluator2.evaluate(playerId, game); + if (bestNode != null) { + node.children.clear(); + node.children.add(bestNode); + node.setScore(bestNode.getScore()); + } + if (logger.isDebugEnabled()) + logger.debug("returning -- combat blocker score: " + val + " depth:" + depth + " for player:" + game.getPlayer(node.getPlayerId()).getName()); + return val; + } + + protected int simulateCounterAttack(Game game, SimulationNode2 node, int depth, int alpha, int beta) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + Integer val = null; + if (!game.isGameOver()) { + simulateToEnd(game); + game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); + logger.debug("simulating -- counter attack for player " + game.getPlayer(game.getActivePlayerId()).getName()); + game.getTurn().setPhase(new BeginningPhase()); + if (game.getPhase().beginPhase(game, game.getActivePlayerId())) { + simulateStep(game, new UntapStep()); + simulateStep(game, new UpkeepStep()); + simulateStep(game, new DrawStep()); + game.getPhase().endPhase(game, game.getActivePlayerId()); + } + val = simulateCombat(game, node, depth-1, alpha, beta, true); + if (logger.isDebugEnabled()) + logger.debug("returning -- counter attack score: " + val + " depth:" + depth + " for player:" + game.getPlayer(node.getPlayerId()).getName()); + } + if (val == null) + val = GameStateEvaluator2.evaluate(playerId, game); + return val; + } + + protected void simulateStep(Game game, Step step) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return; + } + if (!game.isGameOver()) { + game.getPhase().setStep(step); + if (!step.skipStep(game, game.getActivePlayerId())) { + step.beginStep(game, game.getActivePlayerId()); + game.checkStateAndTriggered(); + while (!game.getStack().isEmpty()) { + game.getStack().resolve(game); + game.applyEffects(); + } + step.endStep(game, game.getActivePlayerId()); + } + } + } + + protected void finishCombat(Game game) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return; + } + simulateStep(game, new CombatDamageStep(true)); + simulateStep(game, new CombatDamageStep(false)); + simulateStep(game, new EndOfCombatStep()); + } + + protected int simulatePostCombatMain(Game game, SimulationNode2 node, int depth, int alpha, int beta) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return GameStateEvaluator2.evaluate(playerId, game); + } + logger.debug("simulating -- post combat main"); + game.getTurn().setPhase(new PostCombatMainPhase()); + if (game.getPhase().beginPhase(game, game.getActivePlayerId())) { + game.getPhase().setStep(new PostCombatMainStep()); + game.getStep().beginStep(game, playerId); + game.getPlayers().resetPassed(); + return addActions(node, new FilterAbility(), depth, alpha, beta); + } + return simulateCounterAttack(game, node, depth, alpha, beta); + } + + protected void simulateToEnd(Game game) { + if (Thread.interrupted()) { + Thread.currentThread().interrupt(); + logger.debug("interrupted"); + return; + } + if (!game.isGameOver()) { + game.getTurn().getPhase().endPhase(game, game.getActivePlayerId()); + game.getTurn().setPhase(new EndPhase()); + if (game.getTurn().getPhase().beginPhase(game, game.getActivePlayerId())) { + simulateStep(game, new EndStep()); + simulateStep(game, new CleanupStep()); + } + } + } + +} diff --git a/Mage.Tests/config/config.xml b/Mage.Tests/config/config.xml index 7af45388f8..ea8d028cd5 100644 --- a/Mage.Tests/config/config.xml +++ b/Mage.Tests/config/config.xml @@ -4,7 +4,7 @@ - + diff --git a/Mage.Tests/plugins/mage-player-ai-ma.jar b/Mage.Tests/plugins/mage-player-ai-ma.jar index 61b204cfa664d5a9d89937edc6810309a99320fd..849a57c7a3215d925039c145bfd5fb9f7b9e0354 100644 GIT binary patch delta 23085 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 delta 14264 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 delta 1117 zcmeydn(51GCf)#VW)?065XfeXn#ildo5dJqr+)nxQz}rXdZJy1s^3gYOM$wsb>UuS zCkl*hn=6!^LBF zI;R6>@R@5tM69jO%7R6>BfhbK87#4N++ap)Mg&CrgRET;#@yU)QLvPDvk@zpkRXTDbURvkEyx-tf=)D#c`~nLg#$_piO{cX*3D)iL&Ex4fsS zB%+-DDs|B=#pI~$=<0>He=NBb(Y>L2o#N4RA-ql3x74=p^nSIvb+5vPXCI!0+g+X; zXlGZ`G41-nRtd3xihmdbycwB9m|>w0bosA76V&jZY~62K4{|clrEtIsX9+O8b=(K! zLZ(mu7w9h%R`u!f}F(4)FQk(`57j^@8@CUp8Tg@4iwCjWhY28T1>W_pbO?@ zPLO7jWtrSCQGRk93+Lo@6L`SFdx65w*ucU+CxGK=vfxB%CM_Gy>|p9NI!0uB+> gY=Tg> implements Game, Serializa setZone(card.getId(), Zone.GRAVEYARD); player.getGraveyard().add(card); } - List permanents = new ArrayList(); for (Card card : battlefield) { card.setOwnerId(ownerId); PermanentCard permanent = new PermanentCard(card, ownerId);