diff --git a/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java b/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java new file mode 100644 index 00000000000..2f4bea22323 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/ShareTheSpoils.java @@ -0,0 +1,372 @@ +package mage.cards.s; + +import mage.MageIdentifier; +import mage.abilities.Ability; +import mage.abilities.TriggeredAbility; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.common.SimpleStaticAbility; +import mage.abilities.effects.*; +import mage.cards.*; +import mage.constants.*; +import mage.game.ExileZone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.players.ManaPoolItem; +import mage.players.Player; +import mage.players.PlayerList; +import mage.target.targetpointer.FixedTarget; +import mage.util.CardUtil; +import mage.watchers.Watcher; + +import java.util.HashSet; +import java.util.Set; +import java.util.UUID; + +/** + * @author Alex-Vasile + */ +public final class ShareTheSpoils extends CardImpl { + + public ShareTheSpoils(UUID ownderId, CardSetInfo setInfo) { + super(ownderId, setInfo, new CardType[]{CardType.ENCHANTMENT}, "{1}{R}"); + + // When Share the Spoils enters the battlefield or an opponent loses the game, + // exile the top card of each player’s library.exile the top card of each player’s library. + this.addAbility(new ShareTheSpoilsExileETBAndPlayerLossAbility()); + + // During each player’s turn, that player may play a land or cast a spell from among cards exiled with Share the Spoils, + // and they may spend mana as though it were mana of any color to cast that spell. + Ability castAbility = new SimpleStaticAbility(new ShareTheSpoilsPlayExiledCardEffect()); + castAbility.setIdentifier(MageIdentifier.ShareTheSpoilsWatcher); + this.addAbility(castAbility, new ShareTheSpoilsWatcher()); + + // When they do, exile the top card of their library. + Ability exileCardWhenPlayedACard = new ShareTheSpoilsExileCardWhenPlayACardAbility(); + this.addAbility(exileCardWhenPlayedACard); + } + + private ShareTheSpoils(final ShareTheSpoils card) { + super(card); + } + + @Override + public ShareTheSpoils copy() { + return new ShareTheSpoils(this); + } +} + +//-- Exile from Everyone --// +class ShareTheSpoilsExileETBAndPlayerLossAbility extends TriggeredAbilityImpl { + + ShareTheSpoilsExileETBAndPlayerLossAbility() { + super(Zone.BATTLEFIELD, new ShareTheSpoilsExileCardFromEveryoneEffect()); + } + + private ShareTheSpoilsExileETBAndPlayerLossAbility(final ShareTheSpoilsExileETBAndPlayerLossAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD || + event.getType() == GameEvent.EventType.LOST || // Player conceedes + event.getType() == GameEvent.EventType.LOSES; // Player loses by all other means + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + // The card that entered the battlefield was this one + if (event.getType() == GameEvent.EventType.ENTERS_THE_BATTLEFIELD) { + return event.getTargetId().equals(getSourceId()); + } + + // A player lost the game + return true; + } + + @Override + public TriggeredAbility copy() { + return new ShareTheSpoilsExileETBAndPlayerLossAbility(this); + } + + @Override + public String getRule() { + return "When {this} enters the battlefield or an opponent loses the game, " + + "exile the top card of each player's library."; + } +} + +class ShareTheSpoilsExileCardFromEveryoneEffect extends OneShotEffect { + + public ShareTheSpoilsExileCardFromEveryoneEffect() { + super(Outcome.Exile); + } + + public ShareTheSpoilsExileCardFromEveryoneEffect(final ShareTheSpoilsExileCardFromEveryoneEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + if (source == null) { + return false; + } + + PlayerList players = game.getState().getPlayersInRange(source.getControllerId(), game); + for (UUID playerId : players) { + Player player = game.getPlayer(playerId); + if (player == null) { + continue; + } + + Card topLibraryCard = player.getLibrary().getFromTop(game); + if (topLibraryCard == null) { + continue; + } + + boolean moved = player.moveCardsToExile( + topLibraryCard, + source, + game, + true, + CardUtil.getExileZoneId(game, source), + CardUtil.getSourceName(game, source) + ); + + if (moved) { + ShareTheSpoilsSpendAnyManaEffect effect = new ShareTheSpoilsSpendAnyManaEffect(); + effect.setTargetPointer(new FixedTarget(topLibraryCard, game)); + game.addEffect(effect, source); + } + } + return true; + } + + @Override + public ShareTheSpoilsExileCardFromEveryoneEffect copy() { + return new ShareTheSpoilsExileCardFromEveryoneEffect(this); + } +} + +//-- Play a card Exiled by Share the Spoils --// +class ShareTheSpoilsPlayExiledCardEffect extends AsThoughEffectImpl { + + ShareTheSpoilsPlayExiledCardEffect() { + super(AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, Duration.WhileOnBattlefield, Outcome.PutCardInPlay); + staticText = "During each player's turn, " + + "that player may play a land or cast a spell from among cards exiled with {this}, " + + "and they may spend mana as though it were mana of any color to cast that spell"; + } + + private ShareTheSpoilsPlayExiledCardEffect(final ShareTheSpoilsPlayExiledCardEffect effect) { + super(effect); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + // Have to play on your turn + if (!game.getActivePlayerId().equals(affectedControllerId)) { + return false; + } + + // Not in exile + if (game.getState().getZone(CardUtil.getMainCardId(game, sourceId)) != Zone.EXILED) { + return false; + } + + // TODO: This is a workaround for #8706, remove when that's fixed. + int zoneChangeCounter = game.getState().getZoneChangeCounter(source.getSourceId()); + // Not a card exiled with this Share the Spoils + ExileZone exileZone = game.getExile().getExileZone(CardUtil.getExileZoneId(game, source.getSourceId(), zoneChangeCounter)); + if (exileZone == null) { + return false; + } + if (!exileZone.contains(CardUtil.getMainCardId(game, sourceId))) { + return false; + } + + ShareTheSpoilsWatcher watcher = game.getState().getWatcher(ShareTheSpoilsWatcher.class); + if (watcher == null) { + return false; + } + + return watcher.hasNotUsedAbilityThisTurn(); + } + + @Override + public ShareTheSpoilsPlayExiledCardEffect copy() { + return new ShareTheSpoilsPlayExiledCardEffect(this); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } +} + +//-- Spend mana as any color --// +class ShareTheSpoilsSpendAnyManaEffect extends AsThoughEffectImpl implements AsThoughManaEffect { + + ShareTheSpoilsSpendAnyManaEffect() { + super(AsThoughEffectType.SPEND_OTHER_MANA, Duration.WhileOnBattlefield, Outcome.Benefit); + } + + private ShareTheSpoilsSpendAnyManaEffect(final ShareTheSpoilsSpendAnyManaEffect effect) { + super(effect); + } + + @Override + public boolean apply(Game game, Ability source) { + return true; + } + + @Override + public ShareTheSpoilsSpendAnyManaEffect copy() { + return new ShareTheSpoilsSpendAnyManaEffect(this); + } + + @Override + public boolean applies(UUID sourceId, Ability source, UUID affectedControllerId, Game game) { + UUID mainCardId = CardUtil.getMainCardId(game, sourceId); + + if (getTargetPointer() == null) { + return false; + } + UUID targetUUID = ((FixedTarget) getTargetPointer()).getTarget(); + + // Not the right card + if (mainCardId != targetUUID) { + return false; + } + + int mainCardZCC = game.getState().getZoneChangeCounter(mainCardId); + int targetZCC = game.getState().getZoneChangeCounter(targetUUID); + + if (mainCardZCC <= targetZCC + 1) { + // card moved one zone, from exile to being cast + return true; + } else { + // card moved zones, effect can be discarded + this.discard(); + return false; + } + } + + @Override + public ManaType getAsThoughManaType(ManaType manaType, ManaPoolItem mana, UUID affectedControllerId, Ability source, Game game) { + return mana.getFirstAvailable(); + } +} + +//-- Exile another card when a card is played that was exiled with Share the Spoils --// +class ShareTheSpoilsExileCardWhenPlayACardAbility extends TriggeredAbilityImpl { + + ShareTheSpoilsExileCardWhenPlayACardAbility() { + super(Zone.BATTLEFIELD, new ShareTheSpoilsExileSingleCardEffect()); + setRuleVisible(false); + } + + private ShareTheSpoilsExileCardWhenPlayACardAbility(final ShareTheSpoilsExileCardWhenPlayACardAbility ability) { + super(ability); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.SPELL_CAST || event.getType() == GameEvent.EventType.LAND_PLAYED; + } + + @Override + public boolean checkTrigger(GameEvent event, Game game) { + return event.hasApprovingIdentifier(MageIdentifier.ShareTheSpoilsWatcher); + } + + @Override + public TriggeredAbility copy() { + return new ShareTheSpoilsExileCardWhenPlayACardAbility(this); + } + + @Override + public String getTriggerPhrase() { + return "When they do"; + } +} + +class ShareTheSpoilsExileSingleCardEffect extends OneShotEffect { + + ShareTheSpoilsExileSingleCardEffect() { + super(Outcome.Exile); + staticText = ", exile the top card of their library"; + } + + @Override + public boolean apply(Game game, Ability source) { + if (source == null) { + return false; + } + + Player player = game.getPlayer(source.getControllerId()); + if (player == null) { + return false; + } + + Card topLibraryCard = player.getLibrary().getFromTop(game); + if (topLibraryCard == null) { + return false; + } + + boolean moved = player.moveCardsToExile( + topLibraryCard, + source, + game, + true, + CardUtil.getExileZoneId(game, source), + CardUtil.getSourceName(game, source) + ); + + if (moved) { + ShareTheSpoilsSpendAnyManaEffect effect = new ShareTheSpoilsSpendAnyManaEffect(); + effect.setTargetPointer(new FixedTarget(topLibraryCard, game)); + game.addEffect(effect, source); + } + return moved; + } + + private ShareTheSpoilsExileSingleCardEffect(final ShareTheSpoilsExileSingleCardEffect effect) { + super(effect); + } + + @Override + public ShareTheSpoilsExileSingleCardEffect copy() { + return new ShareTheSpoilsExileSingleCardEffect(this); + } +} + +class ShareTheSpoilsWatcher extends Watcher { + + private boolean usedThisTurn; + + public ShareTheSpoilsWatcher() { + super(WatcherScope.GAME); + } + + @Override + public void watch(GameEvent event, Game game) { + if (event.getType() != GameEvent.EventType.SPELL_CAST && event.getType() != GameEvent.EventType.LAND_PLAYED) { + return; + } + + if (event.hasApprovingIdentifier(MageIdentifier.ShareTheSpoilsWatcher)) { + usedThisTurn = true; + } + } + + @Override + public void reset() { + super.reset(); + usedThisTurn = false; + } + + public boolean hasNotUsedAbilityThisTurn() { + return !usedThisTurn; + } +} diff --git a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java index 540e2560846..faaf1f88efd 100644 --- a/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java +++ b/Mage.Sets/src/mage/sets/ForgottenRealmsCommander.java @@ -228,6 +228,8 @@ public final class ForgottenRealmsCommander extends ExpansionSet { cards.add(new SetCardInfo("Serum Visions", 94, Rarity.UNCOMMON, mage.cards.s.SerumVisions.class)); cards.add(new SetCardInfo("Shadowblood Ridge", 259, Rarity.RARE, mage.cards.s.ShadowbloodRidge.class)); cards.add(new SetCardInfo("Shamanic Revelation", 171, Rarity.RARE, mage.cards.s.ShamanicRevelation.class)); + cards.add(new SetCardInfo("Share the Spoils",34, Rarity.RARE, mage.cards.s.ShareTheSpoils.class, NON_FULL_USE_VARIOUS)); + cards.add(new SetCardInfo("Share the Spoils",303, Rarity.RARE, mage.cards.s.ShareTheSpoils.class, NON_FULL_USE_VARIOUS)); cards.add(new SetCardInfo("Shielding Plax", 192, Rarity.COMMON, mage.cards.s.ShieldingPlax.class)); cards.add(new SetCardInfo("Shiny Impetus", 138, Rarity.UNCOMMON, mage.cards.s.ShinyImpetus.class)); cards.add(new SetCardInfo("Shivan Hellkite", 139, Rarity.RARE, mage.cards.s.ShivanHellkite.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java new file mode 100644 index 00000000000..3223c775e33 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/afc/ShareTheSpoilsTest.java @@ -0,0 +1,526 @@ +package org.mage.test.cards.single.afc; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestCommander4Players; + + +public class ShareTheSpoilsTest extends CardTestCommander4Players { + + private static final String shareTheSpoils = "Share the Spoils"; + + /** + * When Share the Spoils enters the battlefield every player exiles one card. + */ + @Test + public void enterTheBattleField() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * When an opponent loses, their exiled cards are removed from the game and all other players exile a new card. + */ + @Test + public void nonOwnerLoses() { + setLife(playerD, 1); + addCard(Zone.BATTLEFIELD, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Banehound", 1); + + attack(1, playerA, "Banehound", playerD); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 0); + } + + /** + * When an opponent loses, their exiled cards are removed from the game and all other players exile a new card. + */ + @Test + public void nonOwnerConcedes() { + addCard(Zone.BATTLEFIELD, playerA, shareTheSpoils); + concede(1, PhaseStep.PRECOMBAT_MAIN, playerD); + + setStopAt(1, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 0); + } + + /** + * When owner loses, no new cards should be exiled and owner's exiled cards are removed from the game. + */ + @Test + public void ownerConcedes() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + concede(1, PhaseStep.POSTCOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + assertExileCount(playerA, 0); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * When owner loses, no new cards should be exiled and owner's exiled cards are removed from the game. + */ + @Test + public void ownerLoses() { + setLife(playerA, 1); + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.BATTLEFIELD, playerD, "Banehound", 1); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + attack(2, playerD, "Banehound", playerA); + + setStopAt(2, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + assertExileCount(playerA, 0); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * During your turn, you may cast A card from the cards exiled with share the spoils. + */ + @Test + public void canCastOnOwnTurn() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // 3rd from the top, exiled when Tana is cast with Share the Spoils + addCard(Zone.LIBRARY, playerA, "Reliquary Tower"); + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Tana, the Bloodsower", 1); // {2}{R}{G} + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Tana, the Bloodsower"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Tana, the Bloodsower", 1); + + assertExileCount(playerA, "Tana, the Bloodsower", 0); + assertExileCount(playerA, "Reliquary Tower", 1); + + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * During your turn, you may play A land from the cards exiled with share the spoils. + */ + @Test + public void playLandOnOwnTurn() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // 3rd from the top, exiled when Exotic Orchard is played with Share the Spoils + addCard(Zone.LIBRARY, playerA, "Reliquary Tower"); + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Tana, the Bloodsower", 1); // {2}{R}{G} + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exotic Orchard"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(1, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Exotic Orchard", 1); + + assertExileCount(playerA, "Exotic Orchard", 0); + assertExileCount(playerA, "Reliquary Tower", 1); + + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * You cannot play a card or cast a spell when it's not your turn. + */ + @Test + public void cannotCastWhenNotYourTurn() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 1); // {R} + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + + addCard(Zone.LIBRARY, playerB, "Reliquary Tower"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + checkPlayableAbility("normal cast", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + checkPlayableAbility("before play", 2, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Reliquary Tower", false); + + setStopAt(2, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertExileCount(playerA, "Lightning Bolt", 1); + assertExileCount(playerA, "Reliquary Tower", 0); + + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + + assertGraveyardCount(playerA, 0); + } + + /** + * You may cast A spell or play A card, you cannot do both, or multiple of either. + */ + @Test + public void tryToCastOrPlayASecondCard() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // 3rd from the top, exiled when card is played with Share the Spoils + addCard(Zone.LIBRARY, playerA, "Lightning Bolt", 1); // {R} + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Reliquary Tower"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + playLand(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Exotic Orchard"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + // Abilities available after casting with Share the Spoils + checkPlayableAbility("Available Abilities", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Lightning Bolt", false); + + setStopAt(1, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertExileCount(playerA, "Exotic Orchard", 0); + assertExileCount(playerA, "Lightning Bolt", 1); + + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * Ensure that the spending mana as if it were any color only works for cards exiled with Share the Spoils. + */ + @Test + public void checkManaSpendingForOtherExileSource() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.HAND, playerA, "Augury Raven"); + addCard(Zone.BATTLEFIELD, playerA, "Prosper, Tome-Bound"); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 1); + + // 3rd from the top, exile at end of turn with propser + addCard(Zone.LIBRARY, playerA, "Tana, the Bloodsower", 1); // {2}{R}{G} + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Ardenvale Tactician"); // Adventure part is "Dizzying Swoop" + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + // Foretell Augury Raven + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + + // Try to cast Tana, you should not be able to since there isn't the {G} for it since she was exiled by Proser + checkPlayableAbility("normal cast", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Tana, the Bloodsower", false); + + // Try to activate the foretell on Augury Raven, but we can't since we don't have the {U} for it. + checkPlayableAbility("foretell creature cast", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Foretell {1}{U}", false); + + // Cast an adventure card from hand + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Dizzying Swoop"); + addTarget(playerA, "Prosper, Tome-Bound"); + waitStackResolved(5, PhaseStep.PRECOMBAT_MAIN); + + // Make sure the creature card can't be played from exile since there isn't the {W}{W} for it + checkPlayableAbility("creature cast", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ardenvale Tactician", false); + + setStopAt(5, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + // 1 exiled with Share the Spoils + // 1 exiled Prosper (he only exiles one since we stop before the end step of playerA's second turn) + // 1 for the foretold Augury Raven + // 1 for the Dizzying Swoop Adventure + assertExileCount(playerA, 4); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + } + + /** + * Make sure that the more difficult types cards work properly. + */ + @Test + public void checkDifficultCards() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // 3rd from the top, exile Lovestruck Beast is cast + addCard(Zone.LIBRARY, playerA, "Ardenvale Tactician"); // Adventure, creature half {1}{W}{W} + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Lovestruck Beast"); // Adventure, adventure half "Heart's Desire" {G} + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Mountain"); + + // Modal dual face, cast front + addCard(Zone.LIBRARY, playerB, "Alrund, God of the Cosmos"); // Backside is "Hakka, Whispering Raven" + // Modal fual face, cast back + addCard(Zone.LIBRARY, playerC, "Esika, God of the Tree"); // Backside is "The Prismatic Bridge" + // Split card + addCard(Zone.LIBRARY, playerD, "Fire // Ice"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + // Cast the Adventure half of an Adventure card + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Heart's Desire"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // Cast the Creature half of an Adventure card + castSpell(5, PhaseStep.PRECOMBAT_MAIN, playerA, "Ardenvale Tactician"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // Cast split card + castSpell(9, PhaseStep.PRECOMBAT_MAIN, playerA, "Ice"); + addTarget(playerA, "Mountain"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // Cast front side of Modal dual face card + castSpell(13, PhaseStep.PRECOMBAT_MAIN, playerA, "Alrund, God of the Cosmos"); + setChoice(playerA, "Land"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // Cast back side of Modal dual face card + castSpell(17, PhaseStep.PRECOMBAT_MAIN, playerA, "The Prismatic Bridge"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + setStopAt(17, PhaseStep.POSTCOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertPermanentCount(playerA, "Ardenvale Tactician", 1); + + assertExileCount(playerA, "Lovestruck Beast", 1); + + // 1 from Share the Spoils exiling a card for ETB + // 1 from casting the Adventur half of a card (Heart's Desire) and leaving it in exile + // 1 from casting "Ice" + // 1 from casting "Alrund, God of the Cosmos" + // 1 from casting "The Prismatic Bridge + assertExileCount(playerA, 5); + // 0 since playerA cast their card and replaced it with one of their own + assertExileCount(playerB, 0); + // 0 since playerA cast their card and replaced it with one of their own + assertExileCount(playerC, 0); + // 0 since playerA cast their card and replaced it with one of their own + assertExileCount(playerD, 0); + + // Ice is the only card that went in here + assertGraveyardCount(playerD, 1); + } + + /** + * When Share the Spoils leaves the battlefield, the exiled cards are no longer playable. + */ + @Test + public void ensureCardsNotPlayable() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 8); + + // 3rd from the top, exiled when card is played with Share the Spoils + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Aether Helix", 1); // {3}{G}{U} + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Reliquary Tower"); + + // Target in graveyard for Aether Helix + addCard(Zone.GRAVEYARD, playerA, "Aether Spellbomb"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aether Helix"); + addTarget(playerA, shareTheSpoils); + addTarget(playerA, "Aether Spellbomb"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + checkPlayableAbility("before play", 5, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Exotic Orchard", false); + + setStopAt(5, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertExileCount(playerA, "Aether Helix", 0); + assertExileCount(playerA, "Exotic Orchard", 1); + + assertExileCount(playerA, 1); + assertExileCount(playerB, 1); + assertExileCount(playerC, 1); + assertExileCount(playerD, 1); + + assertGraveyardCount(playerA, 1); + } + + /** + * When this share the spoils is destroyed and it is somehow recast, it will create a new pool of cards. + * The previous cards are no longer playable. + */ + @Test + public void checkDifferentCardPools() { + addCard(Zone.HAND, playerA, shareTheSpoils); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 9); + + // 3rd from the top, exiled when card is played with Share the Spoils + addCard(Zone.LIBRARY, playerA, "Exotic Orchard"); + // 2nd from the top, exile when Share the Spoils is cast + addCard(Zone.LIBRARY, playerA, "Aether Helix", 1); // {3}{G}{U} + // Topmost, draw at beginning of turn + addCard(Zone.LIBRARY, playerA, "Reliquary Tower"); + + // Target in graveyard for Aether Helix + addCard(Zone.GRAVEYARD, playerA, "Aether Spellbomb"); + + skipInitShuffling(); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + // Casting Aether Helix from exile with Share the spoils. + // Doing so exiles Exotic Orchard. + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Aether Helix"); + addTarget(playerA, shareTheSpoils); + addTarget(playerA, "Aether Spellbomb"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + // Recast, exile a new set of cards + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, shareTheSpoils); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, playerA); + + // Exotic Orchard was exile by the first Share the Spoils, so can't be cast again with the new one + checkPlayableAbility("before play", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Play Exotic Orchard", false); + + setStopAt(1, PhaseStep.END_TURN); + + setStrictChooseMode(true); + execute(); + + assertAllCommandsUsed(); + + assertExileCount(playerA, "Aether Helix", 0); + assertExileCount(playerA, "Exotic Orchard", 1); + + assertExileCount(playerA, 2); + assertExileCount(playerB, 2); + assertExileCount(playerC, 2); + assertExileCount(playerD, 2); + + assertGraveyardCount(playerA, 1); + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java index ffd9f92a1b9..67251890204 100644 --- a/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java +++ b/Mage.Tests/src/test/java/org/mage/test/serverside/base/MageTestPlayerBase.java @@ -78,6 +78,7 @@ public abstract class MageTestPlayerBase { protected Map> graveyardCards = new HashMap<>(); protected Map> libraryCards = new HashMap<>(); protected Map> commandCards = new HashMap<>(); + protected Map> exiledCards = new HashMap<>(); protected Map> commands = new HashMap<>(); @@ -349,6 +350,15 @@ public abstract class MageTestPlayerBase { return res; } + protected List getExiledCards(TestPlayer player) { + if (exiledCards.containsKey(player)) { + return exiledCards.get(player); + } + List res = new ArrayList<>(); + exiledCards.put(player, res); + return res; + } + protected Map getCommands(TestPlayer player) { if (commands.containsKey(player)) { return commands.get(player); @@ -441,6 +451,9 @@ public abstract class MageTestPlayerBase { case COMMAND: getCommandCards(controllerPlayer).add(newCard); break; + case EXILED: + getExiledCards(controllerPlayer).add(newCard); + break; default: Assert.fail("Unsupported zone: " + putAtZone); } 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 8454fe95860..bd329162c2c 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 @@ -238,6 +238,7 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement getHandCards(testPlayer).clear(); getBattlefieldCards(testPlayer).clear(); getGraveCards(testPlayer).clear(); + getExiledCards(testPlayer).clear(); // Reset the turn counter for tests ((TestPlayer) player).setInitialTurns(0); } @@ -731,6 +732,8 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement return getLibraryCards(player); case COMMAND: return getCommandCards(player); + case EXILED: + return getExiledCards(player); default: break; } @@ -1442,7 +1445,21 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement } } } - Assert.assertEquals("(Exile " + owner.getName() + ") Card counts are not equal (" + cardName + ')', count, actualCount); + Assert.assertEquals("(Exile " + owner.getName() + ") Card counts are not equal (" + cardName + ").", count, actualCount); + } + + /** + * Assert card count in a specific exile zone. + * + * @param exileZoneName Name of the exile zone to be counted. + * @param count Expected count. + * @throws AssertionError + */ + public void assertExileZoneCount(String exileZoneName, int count) throws AssertionError { + ExileZone exileZone = currentGame.getExile().getExileZone(CardUtil.getExileZoneId(exileZoneName, currentGame)); + int actualCount = exileZone.getCards(currentGame).size(); + + Assert.assertEquals("(Exile \"" + exileZoneName + "\") Card counts are not equal.", count, actualCount); } /** diff --git a/Mage/src/main/java/mage/MageIdentifier.java b/Mage/src/main/java/mage/MageIdentifier.java index e2c4fc1c1c5..1948a8c407e 100644 --- a/Mage/src/main/java/mage/MageIdentifier.java +++ b/Mage/src/main/java/mage/MageIdentifier.java @@ -14,6 +14,7 @@ public enum MageIdentifier { KessDissidentMageWatcher, LurrusOfTheDreamDenWatcher, MuldrothaTheGravetideWatcher, + ShareTheSpoilsWatcher, WishWatcher, GlimpseTheCosmosWatcher }