[AFC] Implemented Share the Spoils (#8583)

* Preliminary implementation of Share the Spoils.

* Added tracking of cards in exile to MageTestPlayerBase in order to help with testing of cards such as Share the Spoils which put and take cards out of exile a lot.

* Moved Share the Spoils to the correct set list.

* Fixed bug with using ’ instead of ' for card text.

* Add method to assert number of cards exiled into a specific exile zone.

* Further implementation of ShareTheSpoils and it's tests.

* Fixed a bug in the calcualtiong of exileNUmber. (I misunderstood what the copy() method was for)

* - Added a watcher which should limit player to only cast the spell once per turn.
- Fixed the tests to run properly (using .LOST instead of .LOSES for the GameEvent).
- A bit of cleaning up of the class

* Updated tests and card to capture both conceding and losing the game through other means.

* Further implementation. All parts are working (though not correctly).

* All pieces are now working. But, both the spend any mana and exile a card off the top of library activate when ANY card is played from exile, not just one exiled with Share the Spoils.

* Replaced changed how I move card from top of library to correct exile zone. Instead of using moveCards() and moveToAnotherZone(), I am not using moveCardsToExile().

* A new card is now properly exiled whenever one is played with Share the Spoils

* Moved inner class out and passing exileId as parameter instead

* Updated text based on comments

* Added several more tests

* Made changes to how exileId is handled based on comments

* Made changes based on comments

* Further changes based on comments

* Add a few more tests (will have to fix them once the exile zone issue is fixed)

* Added workaround for #8706

* Get most of the tests working

* Simplified cardId handling

* Reformat file based on comment

* - Added setStrictChooseMode to all tests
- Properly using checkPlayableAbility for all checks where things can't be played/cast.

* Fixed a missing getMainCardId call that made difficult cards unplayable

* - Simplified handling of casting only once per turn. I don't see the point to keeping the MageReference object. using a simple boolean instead
- This also fixes a bug where multiple cards could be played in the same turn.

* Fixed test for difficult cards.

* Reworked spend mana effect based on Dead Man's chest

* Added a check again a null watcher

* Fixed typo in checkManaSpendingForOtherExileSource

* Fixed tests to use proper checkPlayableAbility syntax.

Co-authored-by: Evan Kranzler <theelk801@gmail.com>
This commit is contained in:
Alex Vasile 2022-03-15 18:34:36 -04:00 committed by GitHub
parent 06eeb90b3d
commit b40bde5e12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 932 additions and 1 deletions

View file

@ -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 players library.exile the top card of each players library.
this.addAbility(new ShareTheSpoilsExileETBAndPlayerLossAbility());
// During each players 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;
}
}

View file

@ -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));

View file

@ -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);
}
}

View file

@ -78,6 +78,7 @@ public abstract class MageTestPlayerBase {
protected Map<TestPlayer, List<Card>> graveyardCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> libraryCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> commandCards = new HashMap<>();
protected Map<TestPlayer, List<Card>> exiledCards = new HashMap<>();
protected Map<TestPlayer, Map<Zone, String>> commands = new HashMap<>();
@ -349,6 +350,15 @@ public abstract class MageTestPlayerBase {
return res;
}
protected List<Card> getExiledCards(TestPlayer player) {
if (exiledCards.containsKey(player)) {
return exiledCards.get(player);
}
List<Card> res = new ArrayList<>();
exiledCards.put(player, res);
return res;
}
protected Map<Zone, String> 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);
}

View file

@ -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);
}
/**

View file

@ -14,6 +14,7 @@ public enum MageIdentifier {
KessDissidentMageWatcher,
LurrusOfTheDreamDenWatcher,
MuldrothaTheGravetideWatcher,
ShareTheSpoilsWatcher,
WishWatcher,
GlimpseTheCosmosWatcher
}