cleanup DelayedTriggeredAbility on player leave

This commit is contained in:
Susucre 2024-05-17 13:33:45 +02:00
parent 6cc3c5384a
commit 8db7a4d307
4 changed files with 88 additions and 35 deletions

View file

@ -0,0 +1,67 @@
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;
/**
* @author Susucr
*/
public class DelayedTriggerLastingAfterControllerLeavesTest 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);
}
}

View file

@ -27,11 +27,9 @@ public class DelayedTriggeredAbilities extends AbilitiesImpl<DelayedTriggeredAbi
// TODO: add same integrity checks as TriggeredAbilities?! // TODO: add same integrity checks as TriggeredAbilities?!
for (Iterator<DelayedTriggeredAbility> it = this.iterator(); it.hasNext(); ) { for (Iterator<DelayedTriggeredAbility> it = this.iterator(); it.hasNext(); ) {
DelayedTriggeredAbility ability = it.next(); DelayedTriggeredAbility ability = it.next();
if (ability.getDuration() == Duration.Custom) { if (ability.isInactive(game)) {
if (ability.isInactive(game)) { it.remove();
it.remove(); continue;
continue;
}
} }
if (!ability.checkEventType(event, game)) { if (!ability.checkEventType(event, game)) {
continue; continue;

View file

@ -4,6 +4,7 @@ import mage.abilities.effects.Effect;
import mage.constants.Duration; import mage.constants.Duration;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
import mage.players.Player;
/** /**
* @author BetaSteward_at_googlemail.com * @author BetaSteward_at_googlemail.com
@ -64,6 +65,11 @@ public abstract class DelayedTriggeredAbility extends TriggeredAbilityImpl {
} }
public boolean isInactive(Game game) { public boolean isInactive(Game game) {
return false; // discard as soon as possible for leaved player
// 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());
boolean canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
return canDelete;
} }
} }

View file

@ -310,22 +310,12 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
return false; return false;
} }
boolean canDelete;
Player player = game.getPlayer(startingControllerId); Player player = game.getPlayer(startingControllerId);
// discard on start of turn for leaved player // 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 // 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. // or until a specific point in that turn will last until that turn would have begun.
// They neither expire immediately nor last indefinitely. // They neither expire immediately nor last indefinitely.
switch (duration) { boolean canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
case UntilYourNextTurn:
case UntilEndOfYourNextTurn:
canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving());
break;
default:
canDelete = false;
}
if (canDelete) { if (canDelete) {
return true; return true;
} }
@ -333,28 +323,20 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu
// discard on another conditions (start of your turn) // discard on another conditions (start of your turn)
switch (duration) { switch (duration) {
case UntilYourNextTurn: case UntilYourNextTurn:
if (player != null && player.isInGame()) { return this.isYourNextTurn(game);
return this.isYourNextTurn(game);
}
break;
case UntilYourNextEndStep: case UntilYourNextEndStep:
if (player != null && player.isInGame()) { return this.isYourNextEndStep(game);
return this.isYourNextEndStep(game);
}
break;
case UntilEndCombatOfYourNextTurn: case UntilEndCombatOfYourNextTurn:
if (player != null && player.isInGame()) { return this.isEndCombatOfYourNextTurn(game);
return this.isEndCombatOfYourNextTurn(game);
}
break;
case UntilYourNextUpkeepStep: case UntilYourNextUpkeepStep:
if (player != null && player.isInGame()) { return this.isYourNextUpkeepStep(game);
return this.isYourNextUpkeepStep(game); case UntilEndOfYourNextTurn:
} // cleanup handled by ContinuousEffectsList::removeEndOfTurnEffects
break; // 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 @Override