From 9ef2e0bda73e7528d4b109d78e07589d955b7bd7 Mon Sep 17 00:00:00 2001 From: Oleg Agafonov Date: Sun, 28 Apr 2019 19:32:25 +0400 Subject: [PATCH] * No more continuous effects stay on battlefield after player leave the game; Test framework: added real time check for player in game or not; --- .../continuous/PlayerLeavesGameTest.java | 132 ++++++++++++++++++ .../duel/TeferiMageOfZhalfirTest.java | 41 +++--- .../java/org/mage/test/player/TestPlayer.java | 25 +++- .../base/impl/CardTestPlayerAPIImpl.java | 12 +- .../effects/ContinuousEffectsList.java | 59 +++++++- 5 files changed, 244 insertions(+), 25 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/PlayerLeavesGameTest.java diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/PlayerLeavesGameTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/PlayerLeavesGameTest.java new file mode 100644 index 000000000000..2d96966a04e7 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/PlayerLeavesGameTest.java @@ -0,0 +1,132 @@ +package org.mage.test.cards.continuous; + +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.common.continuous.BoostAllEffect; +import mage.constants.Duration; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBaseWithRangeAll; + +/** + * @author JayDi85 + */ +public class PlayerLeavesGameTest extends CardTestMultiPlayerBaseWithRangeAll { + + /* + 800.4a When a player leaves the game, all objects (see rule 109) owned by that player leave the game and any effects + which give that player control of any objects or players end. Then, if that player controlled any objects on the stack + not represented by cards, those objects cease to exist. Then, if there are any objects still controlled by that player, + those objects are exiled. This is not a state-based action. It happens as soon as the player leaves the game. + If the player who left the game had priority at the time he or she left, priority passes to the next player in turn + order who’s still in the game. + */ + + String cardBear2 = "Balduvian Bears"; // 2/2 + + @Test + public void test_PlayerLeaveGame() { + // Player order: A -> D -> C -> B + + // B must checks A for online status + checkPlayerInGame("turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPlayerInGame("turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPlayerInGame("turn 3 before", 3, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + + concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); + + checkPlayerInGame("turn 3 after", 3, PhaseStep.END_TURN, playerD, playerA, false); + checkPlayerInGame("turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, false); + + setStopAt(4, PhaseStep.CLEANUP); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_PlayerLeaveGameWithOwnPermanent() { + // Player order: A -> D -> C -> B + + addCard(Zone.BATTLEFIELD, playerA, cardBear2, 1); + + // B must checks A for online status + checkPlayerInGame("turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPermanentCount("turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 1); + checkPlayerInGame("turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPermanentCount("turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 1); + checkPlayerInGame("turn 3 before", 3, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPermanentCount("turn 3 before", 3, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 1); + + concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); + + checkPlayerInGame("turn 3 after", 3, PhaseStep.END_TURN, playerD, playerA, false); + checkPermanentCount("turn 3 after", 3, PhaseStep.END_TURN, playerD, playerA, cardBear2, 0); + checkPlayerInGame("turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, false); + checkPermanentCount("turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 0); + + setStopAt(4, PhaseStep.CLEANUP); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + } + + private void prepareAndRunLeaveGameWithEffectTest(Duration duration) { + // Player order: A -> D -> C -> B + addCard(Zone.BATTLEFIELD, playerA, cardBear2, 1); + addCard(Zone.BATTLEFIELD, playerD, cardBear2, 1); + addCustomCardWithAbility("effect", playerA, new SimpleStaticAbility(new BoostAllEffect(1, 1, duration))); + + // B must checks A for online status + checkPlayerInGame(duration.toString() + " - turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPermanentCount(duration.name() + " - turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 1); + checkPT(duration.name() + " - turn 1", 1, PhaseStep.PRECOMBAT_MAIN, playerD, cardBear2, 3, 3); + // + checkPlayerInGame(duration.name() + " - turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPermanentCount(duration.name() + " - turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 1); + checkPT(duration.name() + " - turn 2", 2, PhaseStep.PRECOMBAT_MAIN, playerD, cardBear2, 3, 3); + // + checkPlayerInGame(duration.name() + " - turn 3 before", 3, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, true); + checkPermanentCount(duration.name() + " - turn 3 before", 3, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 1); + checkPT(duration.name() + " - turn 3 before", 3, PhaseStep.PRECOMBAT_MAIN, playerD, cardBear2, 3, 3); + // + concede(3, PhaseStep.POSTCOMBAT_MAIN, playerA); + // + checkPlayerInGame(duration.name() + " - turn 3 after", 3, PhaseStep.END_TURN, playerD, playerA, false); + checkPermanentCount(duration.name() + " - turn 3 after", 3, PhaseStep.END_TURN, playerD, playerA, cardBear2, 0); + checkPT(duration.name() + " - turn 3 after", 3, PhaseStep.END_TURN, playerD, cardBear2, 2, 2); + // + checkPlayerInGame(duration.name() + " - turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, false); + checkPermanentCount(duration.name() + " - turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerD, playerA, cardBear2, 0); + checkPT(duration.name() + " - turn 4", 4, PhaseStep.PRECOMBAT_MAIN, playerD, cardBear2, 2, 2); + + setStopAt(4, PhaseStep.CLEANUP); + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + } + + @Test + public void test_PlayerLeaveGameWithOwnPermanentAndCustomEffect() { + prepareAndRunLeaveGameWithEffectTest(Duration.Custom); + } + + @Test + public void test_PlayerLeaveGameWithOwnPermanentAndWhileOnBattlefieldEffect() { + prepareAndRunLeaveGameWithEffectTest(Duration.WhileOnBattlefield); + } + + @Test + public void test_PlayerLeaveGameWithOwnPermanentAndEndOfGameEffect() { + prepareAndRunLeaveGameWithEffectTest(Duration.EndOfGame); + } + + @Test + public void test_PlayerLeaveGameWithOwnPermanentAndUntilSourceLeavesBattlefielEffect() { + prepareAndRunLeaveGameWithEffectTest(Duration.UntilSourceLeavesBattlefield); + } + + // TODO: add leave tests for end of step + // TODO: add leave tests for end of turn + // TODO: add leave tests for end of your turn +} diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/TeferiMageOfZhalfirTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/TeferiMageOfZhalfirTest.java index 03d175d19709..ea3929636402 100644 --- a/Mage.Tests/src/test/java/org/mage/test/commander/duel/TeferiMageOfZhalfirTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/TeferiMageOfZhalfirTest.java @@ -1,8 +1,6 @@ - package org.mage.test.commander.duel; -import java.io.FileNotFoundException; import mage.constants.PhaseStep; import mage.constants.Zone; import mage.game.Game; @@ -11,8 +9,9 @@ import org.junit.Test; import org.mage.test.serverside.base.CardTestCommanderDuelBase; +import java.io.FileNotFoundException; + /** - * * @author LevelX2 */ @@ -21,11 +20,11 @@ public class TeferiMageOfZhalfirTest extends CardTestCommanderDuelBase { @Override protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { setDecknamePlayerA("CommanderDuel_UW.dck"); // Commander = Daxos of Meletis - return super.createNewGameAndPlayers(); + return super.createNewGameAndPlayers(); } - + @Test - public void castCommanderWithFlash() { + public void castCommanderWithFlash() { addCard(Zone.BATTLEFIELD, playerA, "Plains", 2); addCard(Zone.BATTLEFIELD, playerA, "Island", 1); @@ -36,11 +35,13 @@ public void castCommanderWithFlash() { execute(); assertPermanentCount(playerA, "Daxos of Meletis", 1); - + assertAllCommandsUsed(); } - + @Test public void testCommanderDamage() { + setLife(playerA, 20); + setLife(playerB, 20); addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); addCard(Zone.BATTLEFIELD, playerA, "Island", 1); // Enchant creature @@ -49,24 +50,30 @@ public void testCommanderDamage() { addCard(Zone.HAND, playerA, "Angelic Destiny"); addCard(Zone.BATTLEFIELD, playerA, "Teferi, Mage of Zhalfir"); - + // Daxos of Meletis can't be blocked by creatures with power 3 or greater. - // Whenever Daxos of Meletis deals combat damage to a player, exile the top card of that player's library. You gain life equal to that card's converted mana cost. Until end of turn, you may cast that card and you may spend mana as though it were mana of any color to cast it. + // Whenever Daxos of Meletis deals combat damage to a player, exile the top card of that player's library. + // You gain life equal to that card's converted mana cost. Until end of turn, you may cast that card + // and you may spend mana as though it were mana of any color to cast it. castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis"); - castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angelic Destiny","Daxos of Meletis"); - + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Angelic Destiny", "Daxos of Meletis"); + attack(3, playerA, "Daxos of Meletis"); attack(5, playerA, "Daxos of Meletis"); attack(7, playerA, "Daxos of Meletis"); attack(9, playerA, "Daxos of Meletis"); - + checkPT("before lost", 9, PhaseStep.PRECOMBAT_MAIN, playerA, "Daxos of Meletis", 6, 6); + + setStrictChooseMode(true); setStopAt(9, PhaseStep.POSTCOMBAT_MAIN); execute(); + assertAllCommandsUsed(); assertPermanentCount(playerA, "Daxos of Meletis", 1); - assertPowerToughness(playerA, "Daxos of Meletis", 6, 6); - + assertPowerToughness(playerA, "Daxos of Meletis", 6, 6); // no effects removes after game over -- users and tests can get last game state with all affected effects + Assert.assertEquals("Player A has won because of commander damage", true, playerA.hasWon()); - Assert.assertEquals("Player A has lost because of commander damage", true, playerB.hasLost()); - } + Assert.assertEquals("Player B has lost because of commander damage", true, playerB.hasLost()); + } } \ No newline at end of file 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 87a675e32416..85fd011dc021 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 @@ -626,6 +626,13 @@ public boolean priority(Game game) { wasProccessed = true; } + // check player in game: target player, must be in game + if (params[0].equals(CHECK_COMMAND_PLAYER_IN_GAME) && params.length == 3) { + assertPlayerInGame(action, game, game.getPlayer(UUID.fromString(params[1])), Boolean.parseBoolean(params[2])); + actions.remove(action); + wasProccessed = true; + } + // check ability: card name, ability class, must have if (params[0].equals(CHECK_COMMAND_ABILITY) && params.length == 4) { assertAbility(action, game, computerPlayer, params[1], params[2], Boolean.parseBoolean(params[3])); @@ -633,9 +640,9 @@ public boolean priority(Game game) { wasProccessed = true; } - // check battlefield count: card name, count - if (params[0].equals(CHECK_COMMAND_PERMANENT_COUNT) && params.length == 3) { - assertPermanentCount(action, game, computerPlayer, params[1], Integer.parseInt(params[2])); + // check battlefield count: target player, card name, count + if (params[0].equals(CHECK_COMMAND_PERMANENT_COUNT) && params.length == 4) { + assertPermanentCount(action, game, game.getPlayer(UUID.fromString(params[1])), params[2], Integer.parseInt(params[3])); actions.remove(action); wasProccessed = true; } @@ -928,6 +935,18 @@ private void assertLife(PlayerAction action, Game game, Player player, int Life) Life, player.getLife()); } + private void assertPlayerInGame(PlayerAction action, Game game, Player targetPlayer, boolean mustBeInGame) { + Assert.assertNotNull("Can't find target player", targetPlayer); + + if (targetPlayer.isInGame() && !mustBeInGame) { + Assert.fail(action.getActionName() + " - player " + targetPlayer.getName() + " must NOT be in game"); + } + + if (!targetPlayer.isInGame() && mustBeInGame) { + Assert.fail(action.getActionName() + " - player " + targetPlayer.getName() + " must be in game"); + } + } + private void assertAbility(PlayerAction action, Game game, Player player, String permanentName, String abilityClass, boolean mustHave) { Permanent perm = findPermanentWithAssert(action, game, player, permanentName); diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java index 34962ecf00cc..b7ccdb1bfc61 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/impl/CardTestPlayerAPIImpl.java @@ -64,6 +64,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement public static final String CHECK_COMMAND_SUBTYPE = "SUBTYPE"; public static final String CHECK_COMMAND_MANA_POOL = "MANA_POOL"; public static final String CHECK_COMMAND_ALIAS_ZONE = "ALIAS_ZONE"; + public static final String CHECK_COMMAND_PLAYER_IN_GAME = "PLAYER_IN_GAME"; // TODO: add target player param to commands public static final String SHOW_COMMAND_LIBRARY = "LIBRARY"; @@ -300,6 +301,10 @@ public void checkLife(String checkName, int turnNum, PhaseStep step, TestPlayer check(checkName, turnNum, step, player, CHECK_COMMAND_LIFE, life.toString()); } + public void checkPlayerInGame(String checkName, int turnNum, PhaseStep step, TestPlayer player, TestPlayer targetPlayer, Boolean mustBeInGame) { + check(checkName, turnNum, step, player, CHECK_COMMAND_PLAYER_IN_GAME, targetPlayer.getId().toString(), mustBeInGame.toString()); + } + public void checkAbility(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Class abilityClass, Boolean mustHave) { //Assert.assertNotEquals("", permanentName); check(checkName, turnNum, step, player, CHECK_COMMAND_ABILITY, permanentName, abilityClass.getName(), mustHave.toString()); @@ -307,7 +312,12 @@ public void checkAbility(String checkName, int turnNum, PhaseStep step, TestPlay public void checkPermanentCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, Integer count) { //Assert.assertNotEquals("", permanentName); - check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNT, permanentName, count.toString()); + checkPermanentCount(checkName, turnNum, step, player, player, permanentName, count); + } + + public void checkPermanentCount(String checkName, int turnNum, PhaseStep step, TestPlayer player, TestPlayer targetPlayer, String permanentName, Integer count) { + //Assert.assertNotEquals("", permanentName); + check(checkName, turnNum, step, player, CHECK_COMMAND_PERMANENT_COUNT, targetPlayer.getId().toString(), permanentName, count.toString()); } public void checkPermanentCounters(String checkName, int turnNum, PhaseStep step, TestPlayer player, String permanentName, CounterType counterType, Integer count) { diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java index 8f3e11fad783..ac714d8e4e59 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java @@ -1,10 +1,13 @@ package mage.abilities.effects; +import mage.MageObject; import mage.abilities.Ability; import mage.abilities.MageSingleton; +import mage.cards.Card; import mage.constants.Duration; import mage.constants.Zone; import mage.game.Game; +import mage.players.Player; import org.apache.log4j.Logger; import java.util.*; @@ -93,6 +96,22 @@ public void incYourTurnNumPlayed(Game game) { } private boolean isInactive(T effect, Game game) { + // ends all inactive effects -- calls on player leave or apply new effect + if (game.getState().isGameOver()) { + // no need to remove effects after end -- users and tests must see last game state + return false; + } + + /* + 800.4a When a player leaves the game, all objects (see rule 109) owned by that player leave the game and any effects + which give that player control of any objects or players end. Then, if that player controlled any objects on the stack + not represented by cards, those objects cease to exist. Then, if there are any objects still controlled by that player, + those objects are exiled. This is not a state-based action. It happens as soon as the player leaves the game. + If the player who left the game had priority at the time he or she left, priority passes to the next player in turn + order who’s still in the game. + */ + // objects removes doing in player.leave() call... effects removes is here + Set set = effectAbilityMap.get(effect.getId()); if (set == null) { logger.debug("No abilities for effect found: " + effect.toString()); @@ -108,30 +127,62 @@ private boolean isInactive(T effect, Game game) { } else if (effect.isDiscarded()) { it.remove(); } else { + // 800.4k When a player leaves the game, any continuous effects with durations that last until that + // player’s next turn or until a specific point in that turn will last until that turn would have begun. + // They neither expire immediately nor last indefinitely. + MageObject object = game.getObject(ability.getSourceId()); + boolean isObjectInGame = ability.getSourceId() == null || object != null; // Commander effects have no sourceId + boolean isOwnerLeaveGame = false; + if (object instanceof Card) { + Player owner = game.getPlayer(((Card) object).getOwnerId()); + isOwnerLeaveGame = !owner.isInGame(); + } + switch (effect.getDuration()) { + // case WhileOnBattlefield: case WhileInGraveyard: case WhileOnStack: - if (ability.getSourceId() != null && game.getObject(ability.getSourceId()) == null) { // Commander effects have no sourceId - it.remove(); // if the related source object does no longer exist in game - the effect has to be removed + case EndOfStep: + case EndOfCombat: + case EndOfGame: + // if the related source object does no longer exist in game - the effect has to be removed + if (isOwnerLeaveGame || !isObjectInGame) { + it.remove(); } break; case OneUse: - if (effect.isUsed()) { + if (isOwnerLeaveGame || effect.isUsed()) { it.remove(); } break; case Custom: + // custom effects must process it's own inactive method (override), but can'be missied by devs + if (isOwnerLeaveGame || effect.isInactive(ability, game)) { + it.remove(); + } + break; + case EndOfTurn: + // end of turn discards on cleanup steps + // 514.2 + break; case UntilYourNextTurn: case UntilEndOfYourNextTurn: + // until your turn effects continue until real turn reached, their used it's own inactive method + // 514.2 Second, the following actions happen simultaneously: all damage marked on permanents + // (including phased-out permanents) is removed and all "until end of turn" and "this turn" effects end. + // This turn-based action doesn’t use the stack. if (effect.isInactive(ability, game)) { it.remove(); } break; case UntilSourceLeavesBattlefield: - if (Zone.BATTLEFIELD != game.getState().getZone(ability.getSourceId())) { + if (isOwnerLeaveGame || Zone.BATTLEFIELD != game.getState().getZone(ability.getSourceId())) { it.remove(); } + break; + default: + throw new IllegalStateException("Effects gets unknown duration " + effect.getDuration() + ", effect: " + effect.toString()); } } }