From 6e65db284cf9b49f81f8557633c7cae76bdaf9c2 Mon Sep 17 00:00:00 2001 From: Evan Kranzler <theelk801@gmail.com> Date: Sun, 10 Apr 2022 17:57:58 -0400 Subject: [PATCH] Implemented "Until your next end step" duration (#8831) * initial implementation of until next end step duration * added test, reworked effect duration --- .../src/mage/cards/r/RiveteersCharm.java | 88 ++----------------- .../continuous/UntilNextEndStepTest.java | 77 ++++++++++++++++ .../abilities/effects/ContinuousEffect.java | 2 + .../effects/ContinuousEffectImpl.java | 35 ++++++-- .../effects/ContinuousEffectsList.java | 8 +- .../main/java/mage/constants/Duration.java | 1 + Mage/src/main/java/mage/game/GameImpl.java | 1 + .../watchers/common/EndStepCountWatcher.java | 38 ++++++++ 8 files changed, 160 insertions(+), 90 deletions(-) create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilNextEndStepTest.java create mode 100644 Mage/src/main/java/mage/watchers/common/EndStepCountWatcher.java diff --git a/Mage.Sets/src/mage/cards/r/RiveteersCharm.java b/Mage.Sets/src/mage/cards/r/RiveteersCharm.java index f81d402cc3..fbc1535837 100644 --- a/Mage.Sets/src/mage/cards/r/RiveteersCharm.java +++ b/Mage.Sets/src/mage/cards/r/RiveteersCharm.java @@ -1,26 +1,19 @@ package mage.cards.r; -import mage.abilities.Ability; import mage.abilities.Mode; -import mage.abilities.condition.Condition; -import mage.abilities.effects.OneShotEffect; import mage.abilities.effects.common.ExileGraveyardAllTargetPlayerEffect; +import mage.abilities.effects.common.ExileTopXMayPlayUntilEndOfTurnEffect; import mage.abilities.effects.common.SacrificeEffect; -import mage.cards.*; -import mage.constants.*; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.Duration; import mage.filter.FilterPermanent; import mage.filter.common.FilterCreatureOrPlaneswalkerPermanent; import mage.filter.predicate.permanent.MaxManaValueControlledPermanentPredicate; -import mage.game.Game; -import mage.game.events.GameEvent; -import mage.players.Player; import mage.target.TargetPlayer; import mage.target.common.TargetOpponent; -import mage.util.CardUtil; -import mage.watchers.Watcher; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; /** @@ -46,8 +39,9 @@ public final class RiveteersCharm extends CardImpl { this.getSpellAbility().addTarget(new TargetOpponent()); // • Exile the top three cards of your library. Until your next end step, you may play those cards. - this.getSpellAbility().addMode(new Mode(new RiveteersCharmEffect())); - this.getSpellAbility().addWatcher(new RiveteersCharmWatcher()); + this.getSpellAbility().addMode(new Mode(new ExileTopXMayPlayUntilEndOfTurnEffect( + 3, false, Duration.UntilYourNextEndStep + ))); // • Exile target player's graveyard. this.getSpellAbility().addMode(new Mode(new ExileGraveyardAllTargetPlayerEffect()).addTarget(new TargetPlayer())); @@ -62,69 +56,3 @@ public final class RiveteersCharm extends CardImpl { return new RiveteersCharm(this); } } - -class RiveteersCharmEffect extends OneShotEffect { - - RiveteersCharmEffect() { - super(Outcome.Benefit); - staticText = "exile the top three cards of your library. Until your next end step, you may play those cards"; - } - - private RiveteersCharmEffect(final RiveteersCharmEffect effect) { - super(effect); - } - - @Override - public RiveteersCharmEffect copy() { - return new RiveteersCharmEffect(this); - } - - @Override - public boolean apply(Game game, Ability source) { - Player player = game.getPlayer(source.getControllerId()); - if (player == null) { - return false; - } - Cards cards = new CardsImpl(player.getLibrary().getTopCards(game, 3)); - if (cards.isEmpty()) { - return false; - } - player.moveCards(cards, Zone.EXILED, source, game); - int count = RiveteersCharmWatcher.getCount(game, source); - Condition condition = (g, s) -> RiveteersCharmWatcher.getCount(g, s) == count; - for (Card card : cards.getCards(game)) { - CardUtil.makeCardPlayable(game, source, card, Duration.Custom, false, null, condition); - } - return true; - } -} - -class RiveteersCharmWatcher extends Watcher { - - private final Map<UUID, Integer> playerMap = new HashMap<>(); - - RiveteersCharmWatcher() { - super(WatcherScope.GAME); - } - - @Override - public void watch(GameEvent event, Game game) { - switch (event.getType()) { - case END_TURN_STEP_PRE: - playerMap.compute(game.getActivePlayerId(), CardUtil::setOrIncrementValue); - return; - case BEGINNING_PHASE_PRE: - if (game.getTurnNum() == 1) { - playerMap.clear(); - } - } - } - - static int getCount(Game game, Ability source) { - return game - .getState() - .getWatcher(RiveteersCharmWatcher.class) - .playerMap - .getOrDefault(source.getControllerId(), 0); - } -} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilNextEndStepTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilNextEndStepTest.java new file mode 100644 index 0000000000..8578b05557 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UntilNextEndStepTest.java @@ -0,0 +1,77 @@ +package org.mage.test.cards.continuous; + +import mage.abilities.common.SimpleActivatedAbility; +import mage.abilities.costs.mana.ManaCostsImpl; +import mage.abilities.effects.common.continuous.BoostSourceEffect; +import mage.constants.CardType; +import mage.constants.Duration; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author TheElk801 + */ +public class UntilNextEndStepTest extends CardTestPlayerBase { + + public void doTest(int startTurnNum, PhaseStep startPhaseStep, int endTurnNum, PhaseStep endPhaseStep, boolean stillActive) { + addCustomCardWithAbility( + "tester", playerA, + new SimpleActivatedAbility(new BoostSourceEffect( + 1, 1, Duration.UntilYourNextEndStep + ), new ManaCostsImpl<>("{0}")), null, + CardType.CREATURE, "", Zone.BATTLEFIELD + ); + + activateAbility(startTurnNum, startPhaseStep, playerA, "{0}"); + + setStrictChooseMode(true); + setStopAt(endTurnNum, endPhaseStep); + execute(); + assertAllCommandsUsed(); + + int powerToughness = stillActive ? 2 : 1; + assertPowerToughness(playerA, "tester", powerToughness, powerToughness); + } + + @Test + public void testSameTurnTrue() { + doTest(1, PhaseStep.PRECOMBAT_MAIN, 1, PhaseStep.POSTCOMBAT_MAIN, true); + } + + @Test + public void testSameTurnFalse() { + doTest(1, PhaseStep.PRECOMBAT_MAIN, 1, PhaseStep.END_TURN, false); + } + + @Test + public void testNextTurnTrue() { + doTest(1, PhaseStep.END_TURN, 2, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testNextTurnFalse() { + doTest(1, PhaseStep.PRECOMBAT_MAIN, 2, PhaseStep.PRECOMBAT_MAIN, false); + } + + @Test + public void testTurnCycleTrue() { + doTest(1, PhaseStep.END_TURN, 3, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testTurnCycleFalse() { + doTest(1, PhaseStep.END_TURN, 3, PhaseStep.END_TURN, false); + } + + @Test + public void testOpponentTurnTrue() { + doTest(2, PhaseStep.PRECOMBAT_MAIN, 3, PhaseStep.PRECOMBAT_MAIN, true); + } + + @Test + public void testOpponentTurnFalse() { + doTest(2, PhaseStep.PRECOMBAT_MAIN, 3, PhaseStep.END_TURN, false); + } +} diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java index 77faf553bf..11356d4fe7 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffect.java @@ -67,6 +67,8 @@ public interface ContinuousEffect extends Effect { boolean isYourNextTurn(Game game); + boolean isYourNextEndStep(Game game); + @Override void newId(); diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java index bde84fce90..172003483f 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectImpl.java @@ -4,7 +4,6 @@ import mage.MageObjectReference; import mage.abilities.Ability; import mage.abilities.CompoundAbility; import mage.abilities.MageSingleton; -import mage.abilities.costs.mana.ActivationManaAbilityStep; import mage.abilities.dynamicvalue.DynamicValue; import mage.abilities.dynamicvalue.common.DomainValue; import mage.abilities.dynamicvalue.common.SignInversionDynamicValue; @@ -19,6 +18,7 @@ import mage.game.stack.Spell; import mage.game.stack.StackObject; import mage.players.Player; import mage.target.targetpointer.TargetPointer; +import mage.watchers.common.EndStepCountWatcher; import java.util.*; @@ -61,6 +61,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu private UUID startingControllerId; // player to check for turn duration (can't different with real controller ability) private boolean startingTurnWasActive; // effect started during related players turn and related players turn was already active private int effectStartingOnTurn = 0; // turn the effect started + private int effectStartingEndStep = 0; public ContinuousEffectImpl(Duration duration, Outcome outcome) { super(outcome); @@ -91,6 +92,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu this.startingControllerId = effect.startingControllerId; this.startingTurnWasActive = effect.startingTurnWasActive; this.effectStartingOnTurn = effect.effectStartingOnTurn; + this.effectStartingEndStep = effect.effectStartingEndStep; this.dependencyTypes = effect.dependencyTypes; this.dependendToTypes = effect.dependendToTypes; this.characterDefining = effect.characterDefining; @@ -211,6 +213,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu this.startingTurnWasActive = activePlayerId != null && activePlayerId.equals(startingController); // you can't use "game" for active player cause it's called from tests/cheat too this.effectStartingOnTurn = game.getTurnNum(); + this.effectStartingEndStep = EndStepCountWatcher.getCount(startingController, game); } @Override @@ -219,6 +222,11 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu && game.isActivePlayer(startingControllerId); } + @Override + public boolean isYourNextEndStep(Game game) { + return EndStepCountWatcher.getCount(startingControllerId, game) > effectStartingEndStep; + } + @Override public boolean isInactive(Ability source, Game game) { // YOUR turn checks @@ -227,6 +235,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu switch (duration) { case UntilYourNextTurn: case UntilEndOfYourNextTurn: + case UntilYourNextEndStep: break; default: return false; @@ -237,7 +246,7 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu return false; } - boolean canDelete = false; + boolean canDelete; Player player = game.getPlayer(startingControllerId); // discard on start of turn for leaved player @@ -247,18 +256,26 @@ public abstract class ContinuousEffectImpl extends EffectImpl implements Continu switch (duration) { case UntilYourNextTurn: case UntilEndOfYourNextTurn: - canDelete = player == null - || (!player.isInGame() - && player.hasReachedNextTurnAfterLeaving()); + canDelete = player == null || (!player.isInGame() && player.hasReachedNextTurnAfterLeaving()); + break; + default: + canDelete = false; + } + + if (canDelete) { + return true; } // discard on another conditions (start of your turn) switch (duration) { case UntilYourNextTurn: - if (player != null - && player.isInGame()) { - canDelete = canDelete - || this.isYourNextTurn(game); + if (player != null && player.isInGame()) { + return this.isYourNextTurn(game); + } + break; + case UntilYourNextEndStep: + if (player != null && player.isInGame()) { + return this.isYourNextEndStep(game); } } diff --git a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java index 404401843c..e6ed614520 100644 --- a/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java +++ b/Mage/src/main/java/mage/abilities/effects/ContinuousEffectsList.java @@ -50,7 +50,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList // rules 514.2 for (Iterator<T> i = this.iterator(); i.hasNext(); ) { T entry = i.next(); - boolean canRemove = false; + boolean canRemove; switch (entry.getDuration()) { case EndOfTurn: canRemove = true; @@ -58,6 +58,11 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList case UntilEndOfYourNextTurn: canRemove = entry.isYourNextTurn(game); break; + case UntilYourNextEndStep: + canRemove = entry.isYourNextEndStep(game); + break; + default: + canRemove = false; } if (canRemove) { i.remove(); @@ -149,6 +154,7 @@ public class ContinuousEffectsList<T extends ContinuousEffect> extends ArrayList case Custom: case UntilYourNextTurn: case UntilEndOfYourNextTurn: + case UntilYourNextEndStep: // 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. diff --git a/Mage/src/main/java/mage/constants/Duration.java b/Mage/src/main/java/mage/constants/Duration.java index 243cb7b28e..84bf0aa1fb 100644 --- a/Mage/src/main/java/mage/constants/Duration.java +++ b/Mage/src/main/java/mage/constants/Duration.java @@ -12,6 +12,7 @@ public enum Duration { WhileInGraveyard("", false, false), EndOfTurn("until end of turn", true, true), UntilYourNextTurn("until your next turn", true, true), + UntilYourNextEndStep("until your next end step", true, true), UntilEndOfYourNextTurn("until the end of your next turn", true, true), UntilSourceLeavesBattlefield("until {this} leaves the battlefield", true, false), // supported for continuous layered effects EndOfCombat("until end of combat", true, true), diff --git a/Mage/src/main/java/mage/game/GameImpl.java b/Mage/src/main/java/mage/game/GameImpl.java index 078d9f2d25..7729961158 100644 --- a/Mage/src/main/java/mage/game/GameImpl.java +++ b/Mage/src/main/java/mage/game/GameImpl.java @@ -1299,6 +1299,7 @@ public abstract class GameImpl implements Game { newWatchers.add(new ManaSpentToCastWatcher()); newWatchers.add(new ManaPaidSourceWatcher()); newWatchers.add(new BlockingOrBlockedWatcher()); + newWatchers.add(new EndStepCountWatcher()); newWatchers.add(new CommanderPlaysCountWatcher()); // commander plays count uses in non commander games by some cards // runtime check - allows only GAME scope (one watcher per game) diff --git a/Mage/src/main/java/mage/watchers/common/EndStepCountWatcher.java b/Mage/src/main/java/mage/watchers/common/EndStepCountWatcher.java new file mode 100644 index 0000000000..2527cc48bf --- /dev/null +++ b/Mage/src/main/java/mage/watchers/common/EndStepCountWatcher.java @@ -0,0 +1,38 @@ +package mage.watchers.common; + +import mage.constants.WatcherScope; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * @author TheElk801 + */ +public class EndStepCountWatcher extends Watcher { + + private final Map<UUID, Integer> playerMap = new HashMap<>(); + + public EndStepCountWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() == GameEvent.EventType.END_TURN_STEP_PRE) { + playerMap.compute(game.getActivePlayerId(), CardUtil::setOrIncrementValue); + } + } + + public static int getCount(UUID playerId, Game game) { + return game + .getState() + .getWatcher(EndStepCountWatcher.class) + .playerMap + .getOrDefault(playerId, 0); + } +}