diff --git a/Mage.Tests/src/test/java/org/mage/test/multiplayer/DelayedTriggerAfterControllerLeavesTest.java b/Mage.Tests/src/test/java/org/mage/test/multiplayer/DelayedTriggerAfterControllerLeavesTest.java new file mode 100644 index 00000000000..11dee4bac20 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/multiplayer/DelayedTriggerAfterControllerLeavesTest.java @@ -0,0 +1,70 @@ +package org.mage.test.multiplayer; + +import mage.constants.MultiplayerAttackOption; +import mage.constants.PhaseStep; +import mage.constants.RangeOfInfluence; +import mage.constants.Zone; +import mage.game.FreeForAll; +import mage.game.Game; +import mage.game.GameException; +import mage.game.mulligan.MulliganType; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestMultiPlayerBase; + +import java.io.FileNotFoundException; + +/** + * 800.4d. If an object that would be owned by a player who has left the game would be created in any zone, it isn't created. + * If a triggered ability that would be controlled by a player who has left the game would be put onto the stack, it isn't put on the stack. + * + * @author Susucr + */ +public class DelayedTriggerAfterControllerLeavesTest extends CardTestMultiPlayerBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + Game game = new FreeForAll(MultiplayerAttackOption.MULTIPLE, RangeOfInfluence.ALL, MulliganType.GAME_DEFAULT.getMulligan(0), 40, 7); + // Player order: A -> D -> C -> B + playerA = createPlayer(game, "PlayerA"); + playerB = createPlayer(game, "PlayerB"); + playerC = createPlayer(game, "PlayerC"); + playerD = createPlayer(game, "PlayerD"); + return game; + } + + @Test + public void test_UntilYourNextTurn_AfterLeave() { + setStrictChooseMode(true); + + // +1: Until your next turn, whenever a creature an opponent controls attacks, it gets -1/-0 until end of turn. + addCard(Zone.BATTLEFIELD, playerA, "Jace, Architect of Thought"); + + addCard(Zone.BATTLEFIELD, playerD, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerD, "Elite Vanguard"); + addCard(Zone.BATTLEFIELD, playerC, "Grizzly Bears"); + addCard(Zone.BATTLEFIELD, playerC, "Elite Vanguard"); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1:"); + + attack(2, playerD, "Grizzly Bears", playerB); + attack(2, playerD, "Elite Vanguard", playerB); + setChoice(playerA, "Until your next turn"); // order trigger + + checkLife("2: after D attack affected by Delayed triggers", 2, PhaseStep.POSTCOMBAT_MAIN, playerB, 40 - 2); + concede(2, PhaseStep.END_TURN, playerA); + + attack(3, playerC, "Grizzly Bears", playerB); + attack(3, playerC, "Elite Vanguard", playerB); + + checkLife("3: after C attack affected by Delayed triggers", 3, PhaseStep.POSTCOMBAT_MAIN, playerB, 40 - 2 - 4); + // No trigger, as triggers from leaved players don't trigger + + attack(5, playerD, "Grizzly Bears", playerB); + attack(5, playerD, "Elite Vanguard", playerB); + + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + execute(); + + assertLife(playerB, 40 - 2 - 4 - 4); + } +} diff --git a/Mage/src/main/java/mage/abilities/DelayedTriggeredAbilities.java b/Mage/src/main/java/mage/abilities/DelayedTriggeredAbilities.java index d0a5a20dcd6..23af7412404 100644 --- a/Mage/src/main/java/mage/abilities/DelayedTriggeredAbilities.java +++ b/Mage/src/main/java/mage/abilities/DelayedTriggeredAbilities.java @@ -27,11 +27,9 @@ public class DelayedTriggeredAbilities extends AbilitiesImpl it = this.iterator(); it.hasNext(); ) { DelayedTriggeredAbility ability = it.next(); - if (ability.getDuration() == Duration.Custom) { - if (ability.isInactive(game)) { - it.remove(); - continue; - } + if (ability.isInactive(game)) { + it.remove(); + continue; } if (!ability.checkEventType(event, game)) { continue; diff --git a/Mage/src/main/java/mage/abilities/DelayedTriggeredAbility.java b/Mage/src/main/java/mage/abilities/DelayedTriggeredAbility.java index 4f8afbc5d97..e89c6a94eba 100644 --- a/Mage/src/main/java/mage/abilities/DelayedTriggeredAbility.java +++ b/Mage/src/main/java/mage/abilities/DelayedTriggeredAbility.java @@ -4,6 +4,7 @@ import mage.abilities.effects.Effect; import mage.constants.Duration; import mage.constants.Zone; import mage.game.Game; +import mage.players.Player; /** * @author BetaSteward_at_googlemail.com @@ -68,6 +69,16 @@ public abstract class DelayedTriggeredAbility extends TriggeredAbilityImpl { } public boolean isInactive(Game game) { - return false; + // discard on stack + // 800.4d. If an object that would be owned by a player who has left the game would be created in any zone, it isn't created. + // If a triggered ability that would be controlled by a player who has left the game would be put onto the stack, it isn't put on the stack. + Player player = game.getPlayer(getControllerId()); + if (player != null && player.isInGame()) { + return false; + } + // If using the stack, discard as soon as possible for leaved player + // If not using the stack (for instance return of "exile target player until {this} leaves the battlefield"), + // we wait till the player would have played a next turn to make sure they are debounced after the player leaves. + return usesStack || player.hasReachedNextTurnAfterLeaving(); } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index 1b282f109b0..d7db7059223 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -314,22 +314,12 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu return false; } - boolean canDelete; Player player = game.getPlayer(startingControllerId); - // discard on start of turn for leaved player // 800.4i 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. - switch (duration) { - case UntilYourNextTurn: - case UntilEndOfYourNextTurn: - canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving()); - break; - default: - canDelete = false; - } - + boolean canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving()); if (canDelete) { return true; } @@ -337,28 +327,20 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu // discard on another conditions (start of your turn) switch (duration) { case UntilYourNextTurn: - if (player != null && player.isInGame()) { - return this.isYourNextTurn(game); - } - break; + return this.isYourNextTurn(game); case UntilYourNextEndStep: - if (player != null && player.isInGame()) { - return this.isYourNextEndStep(game); - } - break; + return this.isYourNextEndStep(game); case UntilEndCombatOfYourNextTurn: - if (player != null && player.isInGame()) { - return this.isEndCombatOfYourNextTurn(game); - } - break; + return this.isEndCombatOfYourNextTurn(game); case UntilYourNextUpkeepStep: - if (player != null && player.isInGame()) { - return this.isYourNextUpkeepStep(game); - } - break; + return this.isYourNextUpkeepStep(game); + case UntilEndOfYourNextTurn: + // cleanup handled by ContinuousEffectsList::removeEndOfTurnEffects + // TODO: should those be aligned to all be handled in the same place? + return false; + default: // Should handle all the duration that do pass the first switch. + throw new IllegalStateException("Missing case for isInactive's Duration:" + duration); } - - return canDelete; } @Override