From 58d3fc2328ae753845cc8dfcbabe429d78fbdd7f Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Sat, 21 Oct 2017 16:13:45 +0200 Subject: [PATCH] Fixed player leaving/conceding handling. --- .../src/mage/player/ai/ComputerPlayer6.java | 8 +- .../src/mage/player/ai/ComputerPlayer7.java | 10 +- .../mage/player/ai/GameStateEvaluator2.java | 2 +- .../player/ai/simulators/ActionSimulator.java | 2 +- .../src/mage/player/ai/MCTSNode.java | 6 +- .../src/mage/player/ai/ComputerPlayer2.java | 8 +- .../src/mage/player/ai/ComputerPlayer3.java | 12 +- .../mage/player/ai/GameStateEvaluator.java | 2 +- .../src/mage/player/human/HumanPlayer.java | 36 +- .../src/mage/player/human/PlayerResponse.java | 17 +- .../src/mage/cards/w/WatertrapWeaver.java | 2 +- .../multiplayer/PlayerLeftGameRange1Test.java | 746 +++++++++--------- .../java/org/mage/test/player/TestPlayer.java | 7 + .../java/org/mage/test/stub/PlayerStub.java | 10 +- Mage/src/main/java/mage/game/Game.java | 4 +- Mage/src/main/java/mage/game/GameImpl.java | 99 ++- .../main/java/mage/game/combat/Combat.java | 4 +- Mage/src/main/java/mage/game/turn/Phase.java | 20 +- Mage/src/main/java/mage/game/turn/Turn.java | 6 +- Mage/src/main/java/mage/players/Player.java | 2 + .../main/java/mage/players/PlayerImpl.java | 13 +- 21 files changed, 553 insertions(+), 463 deletions(-) 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 db6da80b74..f281d360e2 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 @@ -519,7 +519,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { logger.trace("interrupted - " + val); return val; } - if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.gameOver(null)) { + if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.checkIfGameIsOver()) { logger.trace("Add actions -- reached end state, node count=" + SimulationNode2.nodeCount + ", depth=" + depth); val = GameStateEvaluator2.evaluate(playerId, game); UUID currentPlayerId = node.getGame().getPlayerList().get(); @@ -540,7 +540,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, game); } else if (!node.getChildren().isEmpty()) { //declared attackers or blockers or triggered abilities @@ -588,7 +588,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { logger.debug("Sim Prio [" + depth + "] -- repeated action: " + action.toString()); continue; } - if (!sim.gameOver(null) && action.isUsesStack()) { + if (!sim.checkIfGameIsOver() && action.isUsesStack()) { // only pass if the last action uses the stack UUID nextPlayerId = sim.getPlayerList().get(); do { @@ -864,7 +864,7 @@ public class ComputerPlayer6 extends ComputerPlayer /*implements Player*/ { break; case CLEANUP: game.getPhase().getStep().beginStep(game, activePlayerId); - if (!game.checkStateAndTriggered() && !game.gameOver(null)) { + if (!game.checkStateAndTriggered() && !game.checkIfGameIsOver()) { game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); game.getTurn().setPhase(new BeginningPhase()); game.getPhase().setStep(new UntapStep()); 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 index 99bce93ce8..48f46b53f5 100644 --- 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 @@ -233,7 +233,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { return GameStateEvaluator2.evaluate(playerId, game); } // Condition to stop deeper simulation - if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.gameOver(null)) { + if (depth <= 0 || SimulationNode2.nodeCount > maxNodes || game.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, game); if (logger.isTraceEnabled()) { StringBuilder sb = new StringBuilder("Add Actions -- reached end state <").append(val).append('>'); @@ -267,7 +267,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, game); } else if (stepFinished) { logger.debug("Step finished"); @@ -481,7 +481,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); Combat simCombat = sim.getCombat().copy(); finishCombat(sim); - if (sim.gameOver(null)) { + if (sim.checkIfGameIsOver()) { val = GameStateEvaluator2.evaluate(playerId, sim); } else if (!counter) { val = simulatePostCombatMain(sim, newNode, depth - 1, alpha, beta); @@ -549,7 +549,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getPhase().setStep(step); if (!step.skipStep(game, game.getActivePlayerId())) { step.beginStep(game, game.getActivePlayerId()); @@ -598,7 +598,7 @@ public class ComputerPlayer7 extends ComputerPlayer6 { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getTurn().getPhase().endPhase(game, game.getActivePlayerId()); game.getTurn().setPhase(new EndPhase()); if (game.getTurn().getPhase().beginPhase(game, game.getActivePlayerId())) { diff --git a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java index 09f8b29123..580f47ba32 100644 --- a/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java +++ b/Mage.Server.Plugins/Mage.Player.AI.MA/src/mage/player/ai/GameStateEvaluator2.java @@ -33,7 +33,7 @@ public final class GameStateEvaluator2 { public static int evaluate(UUID playerId, Game game) { Player player = game.getPlayer(playerId); Player opponent = game.getPlayer(game.getOpponents(playerId).iterator().next()); - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { if (player.hasLost() || opponent.hasWon()) { return LOSE_GAME_SCORE; } diff --git a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java index b18c34dec5..e466891d3e 100644 --- a/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java +++ b/Mage.Server.Plugins/Mage.Player.AI/src/main/java/mage/player/ai/simulators/ActionSimulator.java @@ -61,7 +61,7 @@ public class ActionSimulator { public int evaluateState() { Player opponent = game.getPlayer(game.getOpponents(player.getId()).iterator().next()); - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { if (player.hasLost() || opponent.hasWon()) { return Integer.MIN_VALUE; } diff --git a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java index 7eef75f10e..86a6096798 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java +++ b/Mage.Server.Plugins/Mage.Player.AIMCTS/src/mage/player/ai/MCTSNode.java @@ -79,7 +79,7 @@ public class MCTSNode { this.game = game; this.stateValue = game.getState().getValue(game, targetPlayer); this.fullStateValue = game.getState().getValue(true, game); - this.terminal = game.gameOver(null); + this.terminal = game.checkIfGameIsOver(); setPlayer(); nodeCount = 1; // logger.info(this.stateValue); @@ -90,7 +90,7 @@ public class MCTSNode { this.game = game; this.stateValue = game.getState().getValue(game, targetPlayer); this.fullStateValue = game.getState().getValue(true, game); - this.terminal = game.gameOver(null); + this.terminal = game.checkIfGameIsOver(); this.parent = parent; this.action = action; setPlayer(); @@ -104,7 +104,7 @@ public class MCTSNode { this.combat = combat; this.stateValue = game.getState().getValue(game, targetPlayer); this.fullStateValue = game.getState().getValue(true, game); - this.terminal = game.gameOver(null); + this.terminal = game.checkIfGameIsOver(); this.parent = parent; setPlayer(); nodeCount++; diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java index 681fcac0fb..fa86b2205a 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer2.java @@ -330,7 +330,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { return GameStateEvaluator.evaluate(playerId, game); } int val; - if (node.depth > maxDepth || game.gameOver(null)) { + if (node.depth > maxDepth || game.checkIfGameIsOver()) { logger.debug(indent(node.depth) + "simulating -- reached end state"); val = GameStateEvaluator.evaluate(playerId, game); } @@ -357,7 +357,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator.evaluate(playerId, game); } else if (!node.getChildren().isEmpty()) { @@ -403,7 +403,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { logger.debug(indent(node.depth) + "found useless action: " + action); continue; } - if (!sim.gameOver(null) && action.isUsesStack()) { + if (!sim.checkIfGameIsOver() && action.isUsesStack()) { // only pass if the last action uses the stack sim.getPlayer(currentPlayer.getId()).pass(game); sim.getPlayerList().getNext(); @@ -588,7 +588,7 @@ public class ComputerPlayer2 extends ComputerPlayer implements Player { break; case CLEANUP: game.getPhase().getStep().beginStep(game, activePlayerId); - if (!game.checkStateAndTriggered() && !game.gameOver(null)) { + if (!game.checkStateAndTriggered() && !game.checkIfGameIsOver()) { game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); game.getTurn().setPhase(new BeginningPhase()); game.getPhase().setStep(new UntapStep()); diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java index b523360807..ec7c6f409d 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/ComputerPlayer3.java @@ -184,7 +184,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { logger.debug(indent(node.depth) + "interrupted"); return GameStateEvaluator.evaluate(playerId, game); } - if (node.depth > maxDepth || game.gameOver(null)) { + if (node.depth > maxDepth || game.checkIfGameIsOver()) { logger.debug(indent(node.depth) + "simulating -- reached end state"); val = GameStateEvaluator.evaluate(playerId, game); } @@ -204,7 +204,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { } } - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { val = GameStateEvaluator.evaluate(playerId, game); } else if (stepFinished) { @@ -408,7 +408,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { sim.fireEvent(GameEvent.getEvent(GameEvent.EventType.DECLARE_BLOCKERS_STEP_POST, sim.getActivePlayerId(), sim.getActivePlayerId())); Combat simCombat = sim.getCombat().copy(); finishCombat(sim); - if (sim.gameOver(null)) { + if (sim.checkIfGameIsOver()) { val = GameStateEvaluator.evaluate(playerId, sim); } else if (!counter) { @@ -450,7 +450,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { return GameStateEvaluator.evaluate(playerId, game); } Integer val = null; - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { logger.debug(indent(node.depth) + "simulating -- ending turn"); simulateToEnd(game); game.getState().setActivePlayerId(game.getState().getPlayerList(game.getActivePlayerId()).getNext()); @@ -478,7 +478,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getPhase().setStep(step); if (!step.skipStep(game, game.getActivePlayerId())) { step.beginStep(game, game.getActivePlayerId()); @@ -526,7 +526,7 @@ public class ComputerPlayer3 extends ComputerPlayer2 implements Player { logger.debug("interrupted"); return; } - if (!game.gameOver(null)) { + if (!game.checkIfGameIsOver()) { game.getTurn().getPhase().endPhase(game, game.getActivePlayerId()); game.getTurn().setPhase(new EndPhase()); if (game.getTurn().getPhase().beginPhase(game, game.getActivePlayerId())) { diff --git a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java index e45943dad8..df9e389d03 100644 --- a/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java +++ b/Mage.Server.Plugins/Mage.Player.AIMinimax/src/mage/player/ai/GameStateEvaluator.java @@ -70,7 +70,7 @@ public final class GameStateEvaluator { public static int evaluate(UUID playerId, Game game, boolean ignoreTapped) { Player player = game.getPlayer(playerId); Player opponent = game.getPlayer(game.getOpponents(playerId).iterator().next()); - if (game.gameOver(null)) { + if (game.checkIfGameIsOver()) { if (player.hasLost() || opponent.hasWon()) return LOSE_SCORE; if (opponent.hasLost() || player.hasWon()) diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java index 1e934e1915..06a4778646 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/HumanPlayer.java @@ -53,6 +53,7 @@ import mage.filter.common.FilterCreatureForCombat; import mage.filter.common.FilterCreatureForCombatBlock; import mage.filter.predicate.permanent.ControllerIdPredicate; import mage.game.Game; +import mage.game.GameImpl; import mage.game.combat.CombatGroup; import mage.game.draft.Draft; import mage.game.events.GameEvent; @@ -186,13 +187,25 @@ public class HumanPlayer extends PlayerImpl { response.clear(); logger.debug("Waiting response from player: " + getId()); game.resumeTimer(getTurnControlledBy()); - synchronized (response) { - try { - response.wait(); - } catch (InterruptedException ex) { - logger.error("Response error for player " + getName() + " gameId: " + game.getId(), ex); - } finally { - game.pauseTimer(getTurnControlledBy()); + boolean loop = true; + while (loop) { + loop = false; + synchronized (response) { + try { + response.wait(); + } catch (InterruptedException ex) { + logger.error("Response error for player " + getName() + " gameId: " + game.getId(), ex); + } finally { + game.pauseTimer(getTurnControlledBy()); + } + } + if (response.getResponseConcedeCheck()) { + ((GameImpl) game).checkConcede(); + if (game.hasEnded()) { + return; + } + response.clear(); + loop = true; } } if (recordingMacro && !macroTriggeredSelectionFlag) { @@ -1706,6 +1719,15 @@ public class HumanPlayer extends PlayerImpl { } } + @Override + public void signalPlayerConcede() { + synchronized (response) { + response.setResponseConcedeCheck(); + response.notifyAll(); + logger.debug("Set check concede for waiting player: " + getId()); + } + } + @Override public void skip() { synchronized (response) { diff --git a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java index 2f1e5871c2..b4e7b063bd 100644 --- a/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java +++ b/Mage.Server.Plugins/Mage.Player.Human/src/mage/player/human/PlayerResponse.java @@ -43,6 +43,7 @@ public class PlayerResponse implements Serializable { private Integer responseInteger; private ManaType responseManaType; private UUID responseManaTypePlayerId; + private Boolean responseConcedeCheck; public PlayerResponse() { clear(); @@ -55,7 +56,8 @@ public class PlayerResponse implements Serializable { + ',' + responseBoolean + ',' + responseInteger + ',' + responseManaType - + ',' + responseManaTypePlayerId; + + ',' + responseManaTypePlayerId + + ',' + responseConcedeCheck; } public PlayerResponse(PlayerResponse other) { @@ -69,6 +71,7 @@ public class PlayerResponse implements Serializable { responseInteger = other.responseInteger; responseManaType = other.responseManaType; responseManaTypePlayerId = other.responseManaTypePlayerId; + responseConcedeCheck = other.responseConcedeCheck; } public void clear() { @@ -78,6 +81,7 @@ public class PlayerResponse implements Serializable { responseInteger = null; responseManaType = null; responseManaTypePlayerId = null; + responseConcedeCheck = null; } public String getString() { @@ -104,6 +108,17 @@ public class PlayerResponse implements Serializable { this.responseBoolean = responseBoolean; } + public Boolean getResponseConcedeCheck() { + if (responseConcedeCheck == null) { + return false; + } + return responseConcedeCheck; + } + + public void setResponseConcedeCheck() { + this.responseConcedeCheck = true; + } + public Integer getInteger() { return responseInteger; } diff --git a/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java b/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java index 2820a85eee..703eb4ae9a 100644 --- a/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java +++ b/Mage.Sets/src/mage/cards/w/WatertrapWeaver.java @@ -61,7 +61,7 @@ public class WatertrapWeaver extends CardImpl { this.power = new MageInt(2); this.toughness = new MageInt(2); - // When Watertrap Weaver enters the battlefield, tap target creature an opponent controls. That creature doesn't untap during its controller's next untap step. + // When Watertrap Weaver enters the battlefield, tap target creature an opponent controls. That creature doesn't untap during its controller's next untap step. EntersBattlefieldTriggeredAbility ability = new EntersBattlefieldTriggeredAbility(new TapTargetEffect()); ability.addEffect(new DontUntapInControllersNextUntapStepTargetEffect("that creature")); ability.addTarget(new TargetCreaturePermanent(filter)); diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java index 69424f1425..1ac18334e5 100644 --- a/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/PlayerLeftGameRange1Test.java @@ -1,373 +1,373 @@ -/* - * 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 org.mage.test.multiplayer; - -import java.io.FileNotFoundException; -import mage.constants.MultiplayerAttackOption; -import mage.constants.PhaseStep; -import mage.constants.RangeOfInfluence; -import mage.constants.Zone; -import mage.counters.CounterType; -import mage.game.FreeForAll; -import mage.game.Game; -import mage.game.GameException; -import mage.game.permanent.Permanent; -import org.junit.Assert; -import org.junit.Test; -import org.mage.test.serverside.base.CardTestMultiPlayerBase; - -/** - * - * @author LevelX2 - */ -public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { - - @Override - protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { - // Start Life = 2 - Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, 0, 2); - // Player order: A -> D -> C -> B - playerA = createPlayer(game, playerA, "PlayerA"); - playerB = createPlayer(game, playerB, "PlayerB"); - playerC = createPlayer(game, playerC, "PlayerC"); - playerD = createPlayer(game, playerD, "PlayerD"); - return game; - } - - /** - * Tests Enchantment to control other permanent - */ - @Test - public void TestControlledByEnchantment() { - addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 4); - // Enchant creature - // You control enchanted creature. - addCard(Zone.HAND, playerA, "Control Magic"); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Control Magic", "Rootwater Commando"); - - attack(3, playerC, "Silvercoat Lion", playerB); - - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertLife(playerB, 0); - assertPermanentCount(playerB, 0); - assertPermanentCount(playerA, "Rootwater Commando", 0); - assertGraveyardCount(playerA, "Control Magic", 1); - - } - - /** - * Tests Sorcery to control other players permanent - */ - @Test - public void TestControlledBySorcery() { - addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 4); - // Exchange control of target artifact or creature and another target permanent that shares one of those types with it. - // (This effect lasts indefinitely.) - addCard(Zone.HAND, playerA, "Legerdemain"); // Sorcery - addCard(Zone.BATTLEFIELD, playerA, "Wall of Air"); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Legerdemain", "Rootwater Commando^Wall of Air"); - - attack(3, playerC, "Silvercoat Lion", playerB); - - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertLife(playerB, 0); - assertGraveyardCount(playerA, "Legerdemain", 1); - assertPermanentCount(playerB, 0); - assertPermanentCount(playerA, "Rootwater Commando", 0); // removed from game because player B left - assertPermanentCount(playerB, "Wall of Air", 0); - assertGraveyardCount(playerA, "Wall of Air", 0); - assertPermanentCount(playerA, "Wall of Air", 1); // Returned back to player A - - } - - /** - * Tests Instant to control other permanent - */ - @Test - public void TestOtherPlayerControllsCreature() { - addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); - - addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); - // Untap target nonlegendary creature and gain control of it until end of turn. That creature gains haste until end of turn. - addCard(Zone.HAND, playerA, "Blind with Anger"); // Instant - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); - - attack(3, playerC, "Silvercoat Lion", playerB); - - setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertLife(playerB, 0); - assertGraveyardCount(playerA, "Blind with Anger", 1); - assertPermanentCount(playerB, 0); - assertPermanentCount(playerA, "Rootwater Commando", 0); // Removed from game because player C left - assertPermanentCount(playerA, "Rootwater Commando", 0); // Returned back to player A - } - - /** - * Xmage throws an error involving an emblem unable to find the initial - * source if it has a proc. To reproduce, a Planeswalker was taken from an - * original player's control, such as using Scrambleverse to shuffle Jace, - * Unraveler of Secrets, to a second player and then the second player uses - * Jace's ability to create an emblem ("Whenever an opponent casts his or - * her first spell each turn, counter that spell."). Then the original - * player concedes the game and removes the Planeswalker. Once it becomes an - * opponent of the original player's turn and that opponent plays a spell, - * Xmage throws an error and rollsback the turn. - * - * I don't have the actual error report on my due to negligence, but what I - * can recollect is that the error message was along the lines of "The - * emblem cannot find the original source. This turn will be rolled back". - * This error message will always appear when an opponent tries to play a - * spell. Player order: A -> D -> C -> B - */ - @Test - public void TestOtherPlayerPlaneswalkerCreatedEmblem() { - // +1: Scry 1, then draw a card. - // -2: Return target creature to its owner's hand. - // -8: You get an emblem with "Whenever an opponent casts his or her first spell each turn, counter that spell." - addCard(Zone.BATTLEFIELD, playerB, "Jace, Unraveler of Secrets"); - addCounters(1, PhaseStep.DRAW, playerB, "Jace, Unraveler of Secrets", CounterType.LOYALTY, 8); - - addCard(Zone.BATTLEFIELD, playerA, "Island", 6); - // Enchant permanent (Target a permanent as you cast this. This card enters the battlefield attached to that permanent.) - // You control enchanted permanent. - addCard(Zone.HAND, playerA, "Confiscate"); // Enchantment Aura - - addCard(Zone.BATTLEFIELD, playerC, "Plains", 2); - addCard(Zone.HAND, playerC, "Silvercoat Lion"); - - addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); - addCard(Zone.HAND, playerD, "Silvercoat Lion"); - - addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Confiscate", "Jace, Unraveler of Secrets"); - activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-8: You get an emblem with"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); - - attack(3, playerC, "Silvercoat Lion", playerB); - castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerC, "Silvercoat Lion"); - - castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); - - setStopAt(5, PhaseStep.END_TURN); - execute(); - - assertLife(playerB, 0); - assertPermanentCount(playerB, 0); - assertGraveyardCount(playerA, "Confiscate", 1); - assertPermanentCount(playerA, "Jace, Unraveler of Secrets", 0); // Removed from game because player C left the game - assertEmblemCount(playerA, 1); - assertPermanentCount(playerC, "Silvercoat Lion", 2); // Emblem does not work yet on player C, because range 1 - assertGraveyardCount(playerD, "Silvercoat Lion", 1); // Emblem should counter the spell - } - - /** - * Situation: I attacked an opponent with some creatures with True - * Conviction in play. There were multiple "deals combat damage to a - * player"-triggers (Edric, Spymaster of Trest, Daxos of Meletis et al), - * then the opponent lost the game during the first strike combat - * damage-step . In the second combat damage step the triggers went on the - * stack again, although there was no player being dealt damage (multiplayer - * game, so the game wasn't over yet). I don't think these abilities should - * trigger again here. - */ - @Test - public void TestPlayerDiesDuringFirstStrikeDamageStep() { - // Creatures you control have double strike and lifelink. - addCard(Zone.BATTLEFIELD, playerD, "True Conviction"); - // Whenever a creature deals combat damage to one of your opponents, its controller may draw a card. - addCard(Zone.BATTLEFIELD, playerD, "Edric, Spymaster of Trest"); - addCard(Zone.BATTLEFIELD, playerD, "Dross Crocodile", 8); // Creature 5/1 - - attack(2, playerD, "Dross Crocodile", playerC); - - setStopAt(3, PhaseStep.END_TURN); - execute(); - - assertLife(playerC, -3); - assertLife(playerD, 7); - - assertHandCount(playerD, 2); // 1 (normal draw) + 1 from True Convition - assertPermanentCount(playerC, 0); - - } - - /** - * I've encountered a case today where someone conceded on their turn. The - * remaining phases were went through as normal, but my Luminarch Ascension - * did not trigger during the end step. - */ - // Player order: A -> D -> C -> B - @Test - public void TestTurnEndTrigger() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. - // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. - addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} - - addCard(Zone.HAND, playerC, "Lightning Bolt"); - addCard(Zone.BATTLEFIELD, playerC, "Mountain", 1); - - addCard(Zone.HAND, playerD, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); - castSpell(2, PhaseStep.BEGIN_COMBAT, playerC, "Lightning Bolt", playerD); - - setStopAt(3, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertPermanentCount(playerA, "Luminarch Ascension", 1); - assertGraveyardCount(playerC, "Lightning Bolt", 1); - - assertLife(playerD, -1); - Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); - - assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 - } - - @Test - public void TestTurnEndTriggerAfterConcede() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); - // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. - // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. - addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} - - addCard(Zone.HAND, playerD, "Silvercoat Lion"); - addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); - - castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); - - concede(2, PhaseStep.BEGIN_COMBAT, playerD); - - setStopAt(3, PhaseStep.PRECOMBAT_MAIN); - execute(); - - assertPermanentCount(playerA, "Luminarch Ascension", 1); - - assertLife(playerD, 2); - Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); - - assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 - } - - /** - * Pithing Needle keeps the named card's abilities disabled even after the - * player controlling the Needle loses the game. - * - * I saw it happen during a Commander game. A player cast Pithing Needle - * targeting my Proteus Staff. After I killed him, I still couldn't activate - * the Staff. - */ - @Test - public void TestPithingNeedle() { - addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); - // As Pithing Needle enters the battlefield, name a card. - // Activated abilities of sources with the chosen name can't be activated unless they're mana abilities. - addCard(Zone.HAND, playerA, "Pithing Needle"); // Artifact {1} - addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); - addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); - - addCard(Zone.BATTLEFIELD, playerD, "Island", 3); - // {2}{U}, {T}: Put target creature on the bottom of its owner's library. That creature's controller reveals cards from the - // top of his or her library until he or she reveals a creature card. The player puts that card onto the battlefield and the - // rest on the bottom of his or her library in any order. Activate this ability only any time you could cast a sorcery. - addCard(Zone.BATTLEFIELD, playerD, "Proteus Staff", 1); - - addCard(Zone.BATTLEFIELD, playerD, "Eager Cadet", 1); - addCard(Zone.LIBRARY, playerD, "Storm Crow", 2); - - addCard(Zone.BATTLEFIELD, playerC, "Island", 3); - addCard(Zone.BATTLEFIELD, playerC, "Proteus Staff", 1); - addCard(Zone.BATTLEFIELD, playerC, "Wall of Air", 1); - addCard(Zone.LIBRARY, playerC, "Wind Drake", 2); - - addCard(Zone.BATTLEFIELD, playerB, "Island", 3); - addCard(Zone.BATTLEFIELD, playerB, "Proteus Staff", 1); - - skipInitShuffling(); - - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pithing Needle"); - setChoice(playerA, "Proteus Staff"); - - activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerD, "{2}{U}", "Silvercoat Lion"); // not allowed - - activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerC, "{2}{U}", "Eager Cadet"); // allowed because Needle out of range - concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); - - activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}{U}", "Wall of Air"); // allowed because Needle lost game - - setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); - execute(); - - assertPermanentCount(playerA, 0); - - assertLife(playerA, 2); - Assert.assertFalse("Player A is no longer in the game", playerA.isInGame()); - - Permanent staffPlayerD = getPermanent("Proteus Staff", playerD); - Assert.assertFalse("Staff of player D could not be used", staffPlayerD.isTapped()); - - assertPermanentCount(playerD, "Eager Cadet", 0); - assertPermanentCount(playerD, "Storm Crow", 1); - - Permanent staffPlayerC = getPermanent("Proteus Staff", playerC); - Assert.assertTrue("Staff of player C could be used", staffPlayerC.isTapped()); - - assertPermanentCount(playerC, "Wall of Air", 0); - assertPermanentCount(playerC, "Wind Drake", 1); - - Permanent staffPlayerB = getPermanent("Proteus Staff", playerB); - Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped()); - - } -} +/* + * 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 org.mage.test.multiplayer; + +import java.io.FileNotFoundException; +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.counters.CounterType; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.permanent.Permanent; +import org.junit.Assert; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +/** + * + * @author LevelX2 + */ +public class PlayerLeftGameRange1Test extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // Start Life = 2 + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ONE, 0, 2); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, playerA, "PlayerA"); + playerB = createPlayer(game, playerB, "PlayerB"); + playerC = createPlayer(game, playerC, "PlayerC"); + playerD = createPlayer(game, playerD, "PlayerD"); + return game; + } + + /** + * Tests Enchantment to control other permanent + */ + @Test + public void TestControlledByEnchantment() { + addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // Enchant creature + // You control enchanted creature. + addCard(Zone.HAND, playerA, "Control Magic"); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Control Magic", "Rootwater Commando"); + + attack(3, playerC, "Silvercoat Lion", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 0); + assertPermanentCount(playerB, 0); + assertPermanentCount(playerA, "Rootwater Commando", 0); + assertGraveyardCount(playerA, "Control Magic", 1); + + } + + /** + * Tests Sorcery to control other players permanent + */ + @Test + public void TestControlledBySorcery() { + addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 4); + // Exchange control of target artifact or creature and another target permanent that shares one of those types with it. + // (This effect lasts indefinitely.) + addCard(Zone.HAND, playerA, "Legerdemain"); // Sorcery + addCard(Zone.BATTLEFIELD, playerA, "Wall of Air"); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Legerdemain", "Rootwater Commando^Wall of Air"); + + attack(3, playerC, "Silvercoat Lion", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 0); + assertGraveyardCount(playerA, "Legerdemain", 1); + assertPermanentCount(playerB, 0); + assertPermanentCount(playerA, "Rootwater Commando", 0); // removed from game because player B left + assertPermanentCount(playerB, "Wall of Air", 0); + assertGraveyardCount(playerA, "Wall of Air", 0); + assertPermanentCount(playerA, "Wall of Air", 1); // Returned back to player A + + } + + /** + * Tests Instant to control other permanent + */ + @Test + public void TestOtherPlayerControllsCreature() { + addCard(Zone.BATTLEFIELD, playerB, "Rootwater Commando"); + + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 4); + // Untap target nonlegendary creature and gain control of it until end of turn. That creature gains haste until end of turn. + addCard(Zone.HAND, playerA, "Blind with Anger"); // Instant + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); + + attack(3, playerC, "Silvercoat Lion", playerB); + + setStopAt(3, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 0); + assertGraveyardCount(playerA, "Blind with Anger", 1); + assertPermanentCount(playerB, 0); + assertPermanentCount(playerA, "Rootwater Commando", 0); // Removed from game because player C left + assertPermanentCount(playerA, "Rootwater Commando", 0); // Returned back to player A + } + + /** + * Xmage throws an error involving an emblem unable to find the initial + * source if it has a proc. To reproduce, a Planeswalker was taken from an + * original player's control, such as using Scrambleverse to shuffle Jace, + * Unraveler of Secrets, to a second player and then the second player uses + * Jace's ability to create an emblem ("Whenever an opponent casts his or + * her first spell each turn, counter that spell."). Then the original + * player concedes the game and removes the Planeswalker. Once it becomes an + * opponent of the original player's turn and that opponent plays a spell, + * Xmage throws an error and rollsback the turn. + * + * I don't have the actual error report on my due to negligence, but what I + * can recollect is that the error message was along the lines of "The + * emblem cannot find the original source. This turn will be rolled back". + * This error message will always appear when an opponent tries to play a + * spell. Player order: A -> D -> C -> B + */ + @Test + public void TestOtherPlayerPlaneswalkerCreatedEmblem() { + // +1: Scry 1, then draw a card. + // -2: Return target creature to its owner's hand. + // -8: You get an emblem with "Whenever an opponent casts his or her first spell each turn, counter that spell." + addCard(Zone.BATTLEFIELD, playerB, "Jace, Unraveler of Secrets"); + addCounters(1, PhaseStep.DRAW, playerB, "Jace, Unraveler of Secrets", CounterType.LOYALTY, 8); + + addCard(Zone.BATTLEFIELD, playerA, "Island", 6); + // Enchant permanent (Target a permanent as you cast this. This card enters the battlefield attached to that permanent.) + // You control enchanted permanent. + addCard(Zone.HAND, playerA, "Confiscate"); // Enchantment Aura + + addCard(Zone.BATTLEFIELD, playerC, "Plains", 2); + addCard(Zone.HAND, playerC, "Silvercoat Lion"); + + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + + addCard(Zone.BATTLEFIELD, playerC, "Silvercoat Lion"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Confiscate", "Jace, Unraveler of Secrets"); + activateAbility(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "-8: You get an emblem with"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerA, "Blind with Anger", "Rootwater Commando"); + + attack(3, playerC, "Silvercoat Lion", playerB); + castSpell(3, PhaseStep.POSTCOMBAT_MAIN, playerC, "Silvercoat Lion"); + + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + + setStopAt(5, PhaseStep.END_TURN); + execute(); + + assertLife(playerB, 0); + assertPermanentCount(playerB, 0); + assertGraveyardCount(playerA, "Confiscate", 1); + assertPermanentCount(playerA, "Jace, Unraveler of Secrets", 0); // Removed from game because player C left the game + assertEmblemCount(playerA, 1); + assertPermanentCount(playerC, "Silvercoat Lion", 2); // Emblem does not work yet on player C, because range 1 + assertGraveyardCount(playerD, "Silvercoat Lion", 1); // Emblem should counter the spell + } + + /** + * Situation: I attacked an opponent with some creatures with True + * Conviction in play. There were multiple "deals combat damage to a + * player"-triggers (Edric, Spymaster of Trest, Daxos of Meletis et al), + * then the opponent lost the game during the first strike combat + * damage-step . In the second combat damage step the triggers went on the + * stack again, although there was no player being dealt damage (multiplayer + * game, so the game wasn't over yet). I don't think these abilities should + * trigger again here. + */ + @Test + public void TestPlayerDiesDuringFirstStrikeDamageStep() { + // Creatures you control have double strike and lifelink. + addCard(Zone.BATTLEFIELD, playerD, "True Conviction"); + // Whenever a creature deals combat damage to one of your opponents, its controller may draw a card. + addCard(Zone.BATTLEFIELD, playerD, "Edric, Spymaster of Trest"); + addCard(Zone.BATTLEFIELD, playerD, "Dross Crocodile", 8); // Creature 5/1 + + attack(2, playerD, "Dross Crocodile", playerC); + + setStopAt(3, PhaseStep.END_TURN); + execute(); + + assertLife(playerC, -3); + assertLife(playerD, 7); + + assertHandCount(playerD, 2); // 1 (normal draw) + 1 from True Convition + assertPermanentCount(playerC, 0); + + } + + /** + * I've encountered a case today where someone conceded on their turn. The + * remaining phases were went through as normal, but my Luminarch Ascension + * did not trigger during the end step. + */ + // Player order: A -> D -> C -> B + @Test + public void TestTurnEndTrigger() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. + // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. + addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} + + addCard(Zone.HAND, playerC, "Lightning Bolt"); + addCard(Zone.BATTLEFIELD, playerC, "Mountain", 1); + + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + castSpell(2, PhaseStep.BEGIN_COMBAT, playerC, "Lightning Bolt", playerD); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Luminarch Ascension", 1); + assertGraveyardCount(playerC, "Lightning Bolt", 1); + + assertLife(playerD, -1); + Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); + + assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 + } + + @Test + public void TestTurnEndTriggerAfterConcede() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); + // At the beginning of each opponent's end step, if you didn't lose life this turn, you may put a quest counter on Luminarch Ascension. + // {1}{W}: Create a 4/4 white Angel creature token with flying. Activate this ability only if Luminarch Ascension has four or more quest counters on it.. + addCard(Zone.HAND, playerA, "Luminarch Ascension"); // Enchantment {1}{W} + + addCard(Zone.HAND, playerD, "Silvercoat Lion"); + addCard(Zone.BATTLEFIELD, playerD, "Plains", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Luminarch Ascension"); + + castSpell(2, PhaseStep.PRECOMBAT_MAIN, playerD, "Silvercoat Lion"); + + concede(2, PhaseStep.BEGIN_COMBAT, playerD); + + setStopAt(3, PhaseStep.PRECOMBAT_MAIN); + execute(); + + assertPermanentCount(playerA, "Luminarch Ascension", 1); + + assertLife(playerD, 2); + Assert.assertFalse("Player D is no longer in the game", playerD.isInGame()); + + assertCounterCount(playerA, "Luminarch Ascension", CounterType.QUEST, 1); // 1 from turn 2 + } + + /** + * Pithing Needle keeps the named card's abilities disabled even after the + * player controlling the Needle loses the game. + * + * I saw it happen during a Commander game. A player cast Pithing Needle + * targeting my Proteus Staff. After I killed him, I still couldn't activate + * the Staff. + */ + @Test + public void TestPithingNeedle() { + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + // As Pithing Needle enters the battlefield, name a card. + // Activated abilities of sources with the chosen name can't be activated unless they're mana abilities. + addCard(Zone.HAND, playerA, "Pithing Needle"); // Artifact {1} + addCard(Zone.BATTLEFIELD, playerA, "Silvercoat Lion", 1); + addCard(Zone.LIBRARY, playerA, "Pillarfield Ox", 1); + + addCard(Zone.BATTLEFIELD, playerD, "Island", 3); + // {2}{U}, {T}: Put target creature on the bottom of its owner's library. That creature's controller reveals cards from the + // top of his or her library until he or she reveals a creature card. The player puts that card onto the battlefield and the + // rest on the bottom of his or her library in any order. Activate this ability only any time you could cast a sorcery. + addCard(Zone.BATTLEFIELD, playerD, "Proteus Staff", 1); + + addCard(Zone.BATTLEFIELD, playerD, "Eager Cadet", 1); + addCard(Zone.LIBRARY, playerD, "Storm Crow", 2); + + addCard(Zone.BATTLEFIELD, playerC, "Island", 3); + addCard(Zone.BATTLEFIELD, playerC, "Proteus Staff", 1); + addCard(Zone.BATTLEFIELD, playerC, "Wall of Air", 1); + addCard(Zone.LIBRARY, playerC, "Wind Drake", 2); + + addCard(Zone.BATTLEFIELD, playerB, "Island", 3); + addCard(Zone.BATTLEFIELD, playerB, "Proteus Staff", 1); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Pithing Needle"); + setChoice(playerA, "Proteus Staff"); + + activateAbility(2, PhaseStep.PRECOMBAT_MAIN, playerD, "{2}{U}", "Silvercoat Lion"); // not allowed + + activateAbility(3, PhaseStep.PRECOMBAT_MAIN, playerC, "{2}{U}", "Eager Cadet"); // allowed because Needle out of range + concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); + + activateAbility(4, PhaseStep.PRECOMBAT_MAIN, playerB, "{2}{U}", "Wall of Air"); // allowed because Needle lost game + + setStopAt(4, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerA, 2); + Assert.assertFalse("Player A is no longer in the game", playerA.isInGame()); + + assertPermanentCount(playerA, 0); + + Permanent staffPlayerD = getPermanent("Proteus Staff", playerD); + Assert.assertFalse("Staff of player D could not be used", staffPlayerD.isTapped()); + + assertPermanentCount(playerD, "Eager Cadet", 0); + assertPermanentCount(playerD, "Storm Crow", 1); + + Permanent staffPlayerC = getPermanent("Proteus Staff", playerC); + Assert.assertTrue("Staff of player C could be used", staffPlayerC.isTapped()); + + assertPermanentCount(playerC, "Wall of Air", 0); + assertPermanentCount(playerC, "Wind Drake", 1); + + Permanent staffPlayerB = getPermanent("Proteus Staff", playerB); + Assert.assertTrue("Staff of player B could be used", staffPlayerB.isTapped()); + + } +} 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 74a9d87399..595a54d6e6 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 @@ -57,6 +57,7 @@ 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.combat.CombatGroup; @@ -519,6 +520,7 @@ public class TestPlayer implements Player { } if (groups[0].equals("Concede")) { game.concede(getId()); + ((GameImpl) game).checkConcede(); actions.remove(action); } } @@ -1182,6 +1184,11 @@ public class TestPlayer implements Player { computerPlayer.abort(); } + @Override + public void signalPlayerConcede() { + computerPlayer.signalPlayerConcede(); + } + @Override public void abortReset() { computerPlayer.abortReset(); diff --git a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java index 54fdb7ac45..e082f0a0b9 100644 --- a/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java +++ b/Mage.Tests/src/test/java/org/mage/test/stub/PlayerStub.java @@ -27,6 +27,8 @@ */ package org.mage.test.stub; +import java.io.Serializable; +import java.util.*; import mage.MageObject; import mage.abilities.*; import mage.abilities.costs.AlternativeSourceCosts; @@ -62,9 +64,6 @@ import mage.target.TargetAmount; import mage.target.TargetCard; import mage.target.common.TargetCardInLibrary; -import java.io.Serializable; -import java.util.*; - /** * * @author Quercitron @@ -702,6 +701,11 @@ public class PlayerStub implements Player { } + @Override + public void signalPlayerConcede() { + + } + @Override public void abortReset() { diff --git a/Mage/src/main/java/mage/game/Game.java b/Mage/src/main/java/mage/game/Game.java index 044e6a3ae7..3956dd24b7 100644 --- a/Mage/src/main/java/mage/game/Game.java +++ b/Mage/src/main/java/mage/game/Game.java @@ -173,7 +173,7 @@ public interface Game extends MageItem, Serializable { UUID getPriorityPlayerId(); - boolean gameOver(UUID playerId); + boolean checkIfGameIsOver(); boolean hasEnded(); @@ -347,6 +347,8 @@ public interface Game extends MageItem, Serializable { void concede(UUID playerId); + void setConcedingPlayer(UUID playerId); + void setManaPaymentMode(UUID playerId, boolean autoPayment); void setManaPaymentModeRestricted(UUID playerId, boolean autoPaymentRestricted); diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 768703d804..d371a119ed 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -161,6 +161,8 @@ public abstract class GameImpl implements Game, Serializable { private final LinkedList stackObjectsCheck = new LinkedList<>(); // used to check if different sources used the stack // used to set the counters a permanent adds the battlefield (if no replacement effect is used e.g. Persist) protected Map enterWithCounters = new HashMap<>(); + // used to proceed player conceding requests + private final LinkedList concedingPlayers = new LinkedList<>(); // used to handle asynchronous request of a player to leave the game public GameImpl(MultiplayerAttackOption attackOption, RangeOfInfluence range, int freeMulligans, int startLife) { this.id = UUID.randomUUID(); @@ -535,26 +537,58 @@ public abstract class GameImpl implements Game, Serializable { } } - /** - * Starts check if game is over or if playerId is given let the player - * concede. - * - * @param playerId - * @return - */ +// /** +// * Starts check if game is over or if playerId is given let the player +// * concede. +// * +// * @param playerId +// * @return +// */ +// @Override +// public synchronized boolean gameOver(UUID playerId) { +// if (playerId == null) { +// boolean result = checkIfGameIsOver(); +// return result; +// } else { +// logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); +// concedingPlayers.add(playerId); +// Player player = getPlayer(state.getPriorityPlayerId()); +// if (player != null && player.isHuman()) { +// player.signalPlayerConcede(); +// } else { +// checkConcede(); +// } +// return true; +// } +// } @Override - public synchronized boolean gameOver(UUID playerId) { - if (playerId == null) { - boolean result = checkIfGameIsOver(); - return result; + public void setConcedingPlayer(UUID playerId) { + Player player = getPlayer(state.getPriorityPlayerId()); + if (player != null) { + if (!player.hasLeft() && player.isHuman()) { + if (!concedingPlayers.contains(playerId)) { + logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); + concedingPlayers.add(playerId); + player.signalPlayerConcede(); + } + } else { + // no asynchronous action so check directly + checkConcede(); + } } else { - logger.debug("Game over for player Id: " + playerId + " gameId " + getId()); - leave(playerId); - return true; + checkConcede(); + checkIfGameIsOver(); } } - private boolean checkIfGameIsOver() { + public void checkConcede() { + while (!concedingPlayers.isEmpty()) { + leave(concedingPlayers.removeFirst()); + } + } + + @Override + public boolean checkIfGameIsOver() { if (state.isGameOver()) { return true; } @@ -578,7 +612,7 @@ public abstract class GameImpl implements Game, Serializable { } for (Player player : state.getPlayers().values()) { if (!player.hasLeft() && !player.hasLost()) { - logger.debug(new StringBuilder("Player ").append(player.getName()).append(" has won gameId: ").append(this.getId())); + logger.debug("Player " + player.getName() + " has won gameId: " + this.getId()); player.won(this); } } @@ -696,13 +730,13 @@ public abstract class GameImpl implements Game, Serializable { Player player = getPlayer(playerList.get()); boolean wasPaused = state.isPaused(); state.resume(); - if (!gameOver(null)) { + if (!checkIfGameIsOver()) { fireInformEvent("Turn " + state.getTurnNum()); if (checkStopOnTurnOption()) { return; } state.getTurn().resumePlay(this, wasPaused); - if (!isPaused() && !gameOver(null)) { + if (!isPaused() && !checkIfGameIsOver()) { endOfTurn(); player = playerList.getNext(this); state.setTurnNum(state.getTurnNum() + 1); @@ -712,11 +746,11 @@ public abstract class GameImpl implements Game, Serializable { } protected void play(UUID nextPlayerId) { - if (!isPaused() && !gameOver(null)) { + if (!isPaused() && !checkIfGameIsOver()) { playerList = state.getPlayerList(nextPlayerId); Player playerByOrder = getPlayer(playerList.get()); state.setPlayerByOrderId(playerByOrder.getId()); - while (!isPaused() && !gameOver(null)) { + while (!isPaused() && !checkIfGameIsOver()) { if (!playExtraTurns()) { break; } @@ -733,7 +767,7 @@ public abstract class GameImpl implements Game, Serializable { state.setPlayerByOrderId(playerByOrder.getId()); } } - if (gameOver(null) && !isSimulation()) { + if (checkIfGameIsOver() && !isSimulation()) { winnerId = findWinnersAndLosers(); StringBuilder sb = new StringBuilder("GAME END gameId: ").append(this.getId()).append(' '); int count = 0; @@ -816,7 +850,7 @@ public abstract class GameImpl implements Game, Serializable { skipTurn = state.getTurn().play(this, player); } while (executingRollback); - if (isPaused() || gameOver(null)) { + if (isPaused() || checkIfGameIsOver()) { return false; } if (!skipTurn) { @@ -854,7 +888,7 @@ public abstract class GameImpl implements Game, Serializable { saveState(false); - if (gameOver(null)) { + if (checkIfGameIsOver()) { return; } @@ -1245,7 +1279,7 @@ public abstract class GameImpl implements Game, Serializable { clearAllBookmarks(); try { applyEffects(); - while (!isPaused() && !gameOver(null) && !this.getTurn().isEndTurnRequested()) { + while (!isPaused() && !checkIfGameIsOver() && !this.getTurn().isEndTurnRequested()) { if (!resuming) { state.getPlayers().resetPassed(); state.getPlayerList().setCurrent(activePlayerId); @@ -1254,14 +1288,14 @@ public abstract class GameImpl implements Game, Serializable { } fireUpdatePlayersEvent(); Player player; - while (!isPaused() && !gameOver(null)) { + while (!isPaused() && !checkIfGameIsOver()) { try { if (bookmark == 0) { bookmark = bookmarkState(); } player = getPlayer(state.getPlayerList().get()); state.setPriorityPlayerId(player.getId()); - while (!player.isPassed() && player.canRespond() && !isPaused() && !gameOver(null)) { + while (!player.isPassed() && player.canRespond() && !isPaused() && !checkIfGameIsOver()) { if (!resuming) { // 603.3. Once an ability has triggered, its controller puts it on the stack as an object that's not a card the next time a player would receive priority checkStateAndTriggered(); @@ -1270,7 +1304,7 @@ public abstract class GameImpl implements Game, Serializable { resetLKI(); } saveState(false); - if (isPaused() || gameOver(null)) { + if (isPaused() || checkIfGameIsOver()) { return; } // resetPassed should be called if player performs any action @@ -1289,13 +1323,14 @@ public abstract class GameImpl implements Game, Serializable { } resetShortLivingLKI(); resuming = false; - if (isPaused() || gameOver(null)) { + if (isPaused() || checkIfGameIsOver()) { return; } if (allPassed()) { if (!state.getStack().isEmpty()) { //20091005 - 115.4 resolve(); + checkConcede(); applyEffects(); state.getPlayers().resetPassed(); fireUpdatePlayersEvent(); @@ -1609,11 +1644,11 @@ public abstract class GameImpl implements Game, Serializable { public boolean checkStateAndTriggered() { boolean somethingHappened = false; //20091005 - 115.5 - while (!isPaused() && !gameOver(null)) { + while (!isPaused() && !checkIfGameIsOver()) { if (!checkStateBasedActions()) { // nothing happened so check triggers state.handleSimultaneousEvent(this); - if (isPaused() || gameOver(null) || getTurn().isEndTurnRequested() || !checkTriggered()) { + if (isPaused() || checkIfGameIsOver() || getTurn().isEndTurnRequested() || !checkTriggered()) { break; } } @@ -1621,6 +1656,7 @@ public abstract class GameImpl implements Game, Serializable { applyEffects(); // needed e.g if boost effects end and cause creatures to die somethingHappened = true; } + checkConcede(); return somethingHappened; } @@ -1734,7 +1770,6 @@ public abstract class GameImpl implements Game, Serializable { } } - List planeswalkers = new ArrayList<>(); List legendary = new ArrayList<>(); List worldEnchantment = new ArrayList<>(); for (Permanent perm : getBattlefield().getAllActivePermanents()) { @@ -1781,7 +1816,6 @@ public abstract class GameImpl implements Game, Serializable { continue; } } - planeswalkers.add(perm); } if (perm.isWorld()) { worldEnchantment.add(perm); @@ -2288,7 +2322,6 @@ public abstract class GameImpl implements Game, Serializable { * @param playerId */ protected void leave(UUID playerId) { // needs to be executed from the game thread, not from the concede thread of conceding player! - Player player = getPlayer(playerId); if (player == null || player.hasLeft()) { logger.debug("Player already left " + (player != null ? player.getName() : playerId)); diff --git a/Mage/src/main/java/mage/game/combat/Combat.java b/Mage/src/main/java/mage/game/combat/Combat.java index 1ad1948a1b..135919b8ab 100644 --- a/Mage/src/main/java/mage/game/combat/Combat.java +++ b/Mage/src/main/java/mage/game/combat/Combat.java @@ -264,7 +264,7 @@ public class Combat implements Serializable, Copyable { player.selectAttackers(game, attackingPlayerId); } firstTime = false; - if (game.isPaused() || game.gameOver(null) || game.executingRollback()) { + if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) { return; } // because of possible undo during declare attackers it's neccassary to call here the methods with "game.getCombat()." to get the current combat object!!! @@ -461,7 +461,7 @@ public class Combat implements Serializable, Copyable { } while (choose) { controller.selectBlockers(game, defenderId); - if (game.isPaused() || game.gameOver(null) || game.executingRollback()) { + if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) { return; } if (!game.getCombat().checkBlockRestrictions(defender, game)) { diff --git a/Mage/src/main/java/mage/game/turn/Phase.java b/Mage/src/main/java/mage/game/turn/Phase.java index e4b276336e..500709b399 100644 --- a/Mage/src/main/java/mage/game/turn/Phase.java +++ b/Mage/src/main/java/mage/game/turn/Phase.java @@ -95,7 +95,7 @@ public abstract class Phase implements Serializable { } public boolean play(Game game, UUID activePlayerId) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } @@ -104,7 +104,7 @@ public abstract class Phase implements Serializable { if (beginPhase(game, activePlayerId)) { for (Step step : steps) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } if (game.getTurn().isEndTurnRequested() && step.getType()!=PhaseStep.CLEANUP) { @@ -122,7 +122,7 @@ public abstract class Phase implements Serializable { } } - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } count++; @@ -143,7 +143,7 @@ public abstract class Phase implements Serializable { } public boolean resumePlay(Game game, PhaseStep stepType, boolean wasPaused) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } @@ -157,7 +157,7 @@ public abstract class Phase implements Serializable { resumeStep(game, wasPaused); while (it.hasNext()) { step = it.next(); - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } currentStep = step; @@ -169,7 +169,7 @@ public abstract class Phase implements Serializable { } } - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } count++; @@ -206,13 +206,13 @@ public abstract class Phase implements Serializable { if (!currentStep.skipStep(game, activePlayerId)) { game.getState().increaseStepNum(); prePriority(game, activePlayerId); - if (!game.isPaused() && !game.gameOver(null) && !game.executingRollback()) { + if (!game.isPaused() && !game.checkIfGameIsOver() && !game.executingRollback()) { currentStep.priority(game, activePlayerId, false); if (game.executingRollback()) { return; } } - if (!game.isPaused() && !game.gameOver(null) && !game.executingRollback()) { + if (!game.isPaused() && !game.checkIfGameIsOver() && !game.executingRollback()) { postPriority(game, activePlayerId); } } @@ -233,11 +233,11 @@ public abstract class Phase implements Serializable { prePriority(game, activePlayerId); } case PRIORITY: - if (!game.isPaused() && !game.gameOver(null)) { + if (!game.isPaused() && !game.checkIfGameIsOver()) { currentStep.priority(game, activePlayerId, resuming); } case POST: - if (!game.isPaused() && !game.gameOver(null)) { + if (!game.isPaused() && !game.checkIfGameIsOver()) { postPriority(game, activePlayerId); } } diff --git a/Mage/src/main/java/mage/game/turn/Turn.java b/Mage/src/main/java/mage/game/turn/Turn.java index 4ae01ed55e..6b910ad1e6 100644 --- a/Mage/src/main/java/mage/game/turn/Turn.java +++ b/Mage/src/main/java/mage/game/turn/Turn.java @@ -127,7 +127,7 @@ public class Turn implements Serializable { public boolean play(Game game, Player activePlayer) { activePlayer.becomesActivePlayer(); this.setDeclareAttackersStepStarted(false); - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } @@ -143,7 +143,7 @@ public class Turn implements Serializable { resetCounts(); game.getPlayer(activePlayer.getId()).beginTurn(game); for (Phase phase : phases) { - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return false; } if (!isEndTurnRequested() || phase.getType() == TurnPhase.END) { @@ -189,7 +189,7 @@ public class Turn implements Serializable { } while (it.hasNext()) { phase = it.next(); - if (game.isPaused() || game.gameOver(null)) { + if (game.isPaused() || game.checkIfGameIsOver()) { return; } currentPhase = phase; diff --git a/Mage/src/main/java/mage/players/Player.java b/Mage/src/main/java/mage/players/Player.java index 261a24c22f..71478eb2f9 100644 --- a/Mage/src/main/java/mage/players/Player.java +++ b/Mage/src/main/java/mage/players/Player.java @@ -444,6 +444,8 @@ public interface Player extends MageItem, Copyable { void abortReset(); + void signalPlayerConcede(); + void skip(); // priority, undo, ... diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 1567246fda..1792900468 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -2039,9 +2039,9 @@ public abstract class PlayerImpl implements Player, Serializable { @Override public void concede(Game game) { - game.gameOver(playerId); + game.setConcedingPlayer(playerId); lost(game); - this.left = true; +// this.left = true; } @Override @@ -2136,7 +2136,7 @@ public abstract class PlayerImpl implements Player, Serializable { // for draw - first all players that have lost have to be set to lost if (!hasLeft()) { logger.debug("Game over playerId: " + playerId); - game.gameOver(playerId); + game.setConcedingPlayer(playerId); } } @@ -2197,7 +2197,7 @@ public abstract class PlayerImpl implements Player, Serializable { this.draws = true; game.fireEvent(GameEvent.getEvent(GameEvent.EventType.DRAW_PLAYER, null, null, playerId)); game.informPlayers("For " + this.getLogName() + " the game is a draw."); - game.gameOver(playerId); + game.setConcedingPlayer(playerId); } } @@ -3578,6 +3578,11 @@ public abstract class PlayerImpl implements Player, Serializable { abort = false; } + @Override + public void signalPlayerConcede() { + + } + @Override public boolean scry(int value, Ability source, Game game