From 07386cce8d75effba60f322198c80ade16d5cfcb Mon Sep 17 00:00:00 2001 From: LevelX2 Date: Wed, 3 Jun 2020 15:07:52 +0200 Subject: [PATCH] * Shifting Shadows - Fixed not proper handling of gained triggered abilities during step resolution of Shifting Shadows effect (fixes #6571). --- .../src/mage/cards/m/MairsilThePretender.java | 6 +- .../src/mage/cards/s/ShiftingShadow.java | 9 +- Mage.Tests/CommanderDuel_Mairisil_UBR.dck | 90 ++++++++ .../duel/MairsilThePretenderTest.java | 203 ++++++++++++++++++ .../base/impl/CardTestPlayerAPIImpl.java | 9 + .../CommanderReplacementEffect.java | 6 +- 6 files changed, 316 insertions(+), 7 deletions(-) create mode 100644 Mage.Tests/CommanderDuel_Mairisil_UBR.dck create mode 100644 Mage.Tests/src/test/java/org/mage/test/commander/duel/MairsilThePretenderTest.java diff --git a/Mage.Sets/src/mage/cards/m/MairsilThePretender.java b/Mage.Sets/src/mage/cards/m/MairsilThePretender.java index 3a85125af0..3188b78a03 100644 --- a/Mage.Sets/src/mage/cards/m/MairsilThePretender.java +++ b/Mage.Sets/src/mage/cards/m/MairsilThePretender.java @@ -46,10 +46,12 @@ public final class MairsilThePretender extends CardImpl { this.power = new MageInt(4); this.toughness = new MageInt(4); - // When Mairsil, the Pretender enters the battlefield, you may exile an artifact or creature card from your hand or graveyard and put a cage counter on it. + // When Mairsil, the Pretender enters the battlefield, you may exile an artifact or creature card from your hand + // or graveyard and put a cage counter on it. this.addAbility(new EntersBattlefieldTriggeredAbility(new MairsilThePretenderExileEffect(), true)); - // Mairsil, the Pretender has all activated abilities of all cards you own in exile with cage counters on them. You may activate each of those abilities only once each turn. + // Mairsil, the Pretender has all activated abilities of all cards you own in exile with cage counters on them. + // You may activate each of those abilities only once each turn. Ability ability = new SimpleStaticAbility(Zone.BATTLEFIELD, new MairsilThePretenderGainAbilitiesEffect()); this.addAbility(ability); } diff --git a/Mage.Sets/src/mage/cards/s/ShiftingShadow.java b/Mage.Sets/src/mage/cards/s/ShiftingShadow.java index e181f37a9d..5f9f1e72ba 100644 --- a/Mage.Sets/src/mage/cards/s/ShiftingShadow.java +++ b/Mage.Sets/src/mage/cards/s/ShiftingShadow.java @@ -71,12 +71,12 @@ public final class ShiftingShadow extends CardImpl { class ShiftingShadowEffect extends OneShotEffect { - private UUID auraId; + private final UUID auraId; public ShiftingShadowEffect(UUID auraId) { super(Outcome.PutCreatureInPlay); this.staticText = "destroy this creature. Reveal cards from the top of your library until you reveal a creature card. " - + "Put that card onto the battlefield and attach Shifting Shadow to it, then put all other cards revealed this way on the bottom of your library in a random order"; + + "Put that card onto the battlefield and attach {this} to it, then put all other cards revealed this way on the bottom of your library in a random order"; this.auraId = auraId; } @@ -107,6 +107,11 @@ class ShiftingShadowEffect extends OneShotEffect { } if (aura != null) { enchanted.destroy(source.getSourceId(), game, false); + // Because this effect has two steps, we have to call the processAction method here, so that triggered effects of the target going to graveyard go to the stack + // If we don't do it here, gained triggered effects to the target will be removed from the following moveCards method and the applyEffcts done there. + // Example: {@link org.mage.test.commander.duel.MairsilThePretenderTest#MairsilThePretenderTest Test} + game.getState().processAction(game); + Cards revealed = new CardsImpl(); Cards otherCards = new CardsImpl(); for (Card card : controller.getLibrary().getCards(game)) { diff --git a/Mage.Tests/CommanderDuel_Mairisil_UBR.dck b/Mage.Tests/CommanderDuel_Mairisil_UBR.dck new file mode 100644 index 0000000000..e2368e5a05 --- /dev/null +++ b/Mage.Tests/CommanderDuel_Mairisil_UBR.dck @@ -0,0 +1,90 @@ +1 [DGM:11] Aetherling +1 [JUD:77] Anger +1 [10E:66] Arcanis the Omnipotent +1 [SOM:28] Argent Sphinx +1 [CHK:52] Azami, Lady of Scrolls +1 [AVR:47] Deadeye Navigator +1 [DRK:44] Eater of the Dead +1 [EXO:33] Ertai, Wizard Adept +1 [ALA:43] Fatestitcher +1 [SOM:64] Geth, Lord of the Vault +1 [EVE:55] Hateflayer +1 [DKA:139] Havengul Lich +1 [10E:87] Horseshoe Crab +1 [ICE:136] Infernal Denizen +1 [M12:59] Jace's Archivist +1 [SHM:42] Knacksaw Clique +1 [OGW:4] Kozilek, the Great Distortion +1 [PLC:43] Magus of the Bazaar +1 [ICE:150] Minion of Leshrac +1 [SOM:72] Necrotic Ooze +1 [SHM:258] Pili-Pala +1 [MRD:47] Quicksilver Elemental +1 [SOK:53] Sakashima the Impostor +1 [MIR:142] Shauku, Endbringer +1 [M15:231] Soul of New Phyrexia +1 [PLC:110] Torchling +1 [EMN:109] Tree of Perdition +1 [M20:64] Leyline of Anticipation +1 [9ED:152] Phyrexian Arena +1 [PCY:45] Rhystic Study +1 [RNA:245] Blood Crypt +1 [KTK:230] Bloodstained Mire +1 [WWK:132] Bojuka Bog +1 [ELD:333] Command Tower +1 [ALA:222] Crumbling Necropolis +1 [RAV:276] Dimir Aqueduct +1 [CON:142] Exotic Orchard +1 [CHK:277] Hall of the Bandit Lord +8 [IKO:263] Island +1 [GPT:159] Izzet Boilerworks +1 [CHK:279] Minamo, School at Water's Edge +3 [IKO:269] Mountain +1 [C20:298] Path of Ancestry +1 [KTK:239] Polluted Delta +1 [M19:254] Reliquary Tower +1 [ONS:322] Riptide Laboratory +1 [ORI:251] Shivan Reef +1 [GRN:257] Steam Vents +1 [10E:359] Sulfurous Springs +1 [BFZ:249] Sunken Hollow +4 [IKO:266] Swamp +1 [10E:362] Underground River +1 [GRN:259] Watery Grave +1 [ODY:118] Buried Alive +1 [3ED:105] Demonic Tutor +1 [USG:188] Gamble +1 [AVR:151] Reforge the Soul +1 [EMA:108] Toxic Deluge +1 [MB1:1603] Mana Crypt +1 [5ED:388] Mana Vault +1 [HOU:165] Mirage Mirror +1 [5ED:391] Nevinyrral's Disk +1 [CHK:268] Sensei's Divining Top +1 [3ED:274] Sol Ring +1 [5DN:156] Staff of Domination +1 [LRW:263] Thousand-Year Elixir +1 [UDS:139] Thran Dynamo +1 [5DN:163] Vedalken Orrery +1 [TMP:55] Capsize +1 [C20:146] Chaos Warp +1 [RTR:35] Cyclonic Rift +1 [ODY:132] Entomb +1 [INV:57] Fact or Fiction +1 [TMP:70] Intuition +1 [THS:65] Swan Song +1 [RTR:111] Vandalblast +1 [3ED:185] Wheel of Fortune +1 [GTC:207] Whispering Madness +1 [USG:111] Windfall +1 [SHM:248] Cauldron of Souls +1 [RAV:260] Dimir Signet +1 [9ED:297] Fellwar Stone +1 [DOM:215] Gilded Lotus +1 [ULG:126] Grim Monolith +1 [GTC:231] Illusionist's Bracers +1 [GPT:152] Izzet Signet +1 [MRD:199] Lightning Greaves +SB: 1 [C17:41] Mairsil, the Pretender +LAYOUT MAIN:(1,7)(CARD_TYPE,false,50)|([SHM:258],[M15:231])([SHM:248],[RAV:260],[9ED:297],[DOM:215],[ULG:126],[GTC:231],[GPT:152],[MRD:199],[MB1:1603],[5ED:388],[HOU:165],[5ED:391],[CHK:268],[3ED:274],[5DN:156],[LRW:263],[UDS:139],[5DN:163])([DGM:11],[JUD:77],[10E:66],[SOM:28],[CHK:52],[AVR:47],[DRK:44],[EXO:33],[ALA:43],[SOM:64],[EVE:55],[DKA:139],[10E:87],[ICE:136],[M12:59],[SHM:42],[OGW:4],[PLC:43],[ICE:150],[SOM:72],[MRD:47],[SOK:53],[MIR:142],[PLC:110],[EMN:109])([M20:64],[9ED:152],[PCY:45])([TMP:55],[C20:146],[RTR:35],[ODY:132],[INV:57],[TMP:70],[THS:65])([RNA:245],[KTK:230],[WWK:132],[ELD:333],[ALA:222],[RAV:276],[CON:142],[CHK:277],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[IKO:263],[GPT:159],[CHK:279],[IKO:269],[IKO:269],[IKO:269],[C20:298],[KTK:239],[M19:254],[ONS:322],[ORI:251],[GRN:257],[10E:359],[BFZ:249],[IKO:266],[IKO:266],[IKO:266],[IKO:266],[10E:362],[GRN:259])([ODY:118],[3ED:105],[USG:188],[AVR:151],[EMA:108],[RTR:111],[3ED:185],[GTC:207],[USG:111]) +LAYOUT SIDEBOARD:(1,1)(NONE,false,50)|([C17:41]) diff --git a/Mage.Tests/src/test/java/org/mage/test/commander/duel/MairsilThePretenderTest.java b/Mage.Tests/src/test/java/org/mage/test/commander/duel/MairsilThePretenderTest.java new file mode 100644 index 0000000000..d9e5370775 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/commander/duel/MairsilThePretenderTest.java @@ -0,0 +1,203 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package org.mage.test.commander.duel; + +import java.io.FileNotFoundException; +import mage.constants.PhaseStep; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.GameException; +import org.junit.Test; +import static org.mage.test.player.TestPlayer.NO_TARGET; +import org.mage.test.serverside.base.CardTestCommanderDuelBase; + +/** + * + * @author LevelX2 + */ +public class MairsilThePretenderTest extends CardTestCommanderDuelBase { + + @Override + protected Game createNewGameAndPlayers() throws GameException, FileNotFoundException { + // When Mairsil, the Pretender enters the battlefield, you may exile an artifact or creature card from your hand + // or graveyard and put a cage counter on it. + // Mairsil, the Pretender has all activated abilities of all cards you own in exile with cage counters on them. + // You may activate each of those abilities only once each turn. + setDecknamePlayerA("CommanderDuel_Mairisil_UBR.dck"); // Commander = Mairsil, the Pretender {1}{U}{B}{R} + return super.createNewGameAndPlayers(); + } + + /** + * Basic Test + */ + @Test + public void useShifitingShadowTest() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + + // Enchant creature + // Enchanted creature has haste and “At the beginning of your upkeep, destroy this creature. + // Reveal cards from the top of your library until you reveal a creature card. + // Put that card onto the battlefield and attach Shifting Shadow to it, + // then put all other cards revealed this way on the bottom of your library in a random order.” + addCard(Zone.HAND, playerA, "Shifting Shadow", 1); // {2}{R} + + // Tap: Draw three cards. + // {2}{U}{U}: Return Arcanis the Omnipotent to its owner's hand. + addCard(Zone.HAND, playerA, "Arcanis the Omnipotent", 1); // Creature {3}{U}{U}{U} + + setChoice(playerA, "Yes"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mairsil, the Pretender"); + setChoice(playerA, "Yes"); // Exile a card + setChoice(playerA, "Arcanis the Omnipotent"); + + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Shifting Shadow", "Mairsil, the Pretender"); + + setChoice(playerA, "Yes"); // Move commander to command zone + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 40); + assertLife(playerB, 40); + + assertExileCount("Arcanis the Omnipotent", 1); + + assertCommandZoneCount(playerA, "Mairsil, the Pretender", 1); + + assertPermanentCount(playerA, "Shifting Shadow", 1); + } + /** + * I tried playing it in Mairsil the Pretender commander deck and found 2 + * cases where the creature is not properly destroyed: + * + * Using Arcanis the Omnipotent ability to return my commander to hand while + * trigger is on the stack I got Mairsil to hand then got asked whether I + * want to put him in gy so I answered yes assuming it cannot be destroyed + * while in my hand. He got put in graveyard straight from my hand. + * According to Oracle rules I should get a creature from top of the deck + * and still have my commander in hand. After giving my commander undying + * with shifting shadow trigger on the stack he got put in gy with undying + * not triggering. + * + * All this points to the card being hardcoded to put the creature into + * graveyard instead of simply destroying it + */ + @Test + public void useShifitingShadowAndArcanisTest() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 3); + skipInitShuffling(); + // Enchant creature + // Enchanted creature has haste and “At the beginning of your upkeep, destroy this creature. + // Reveal cards from the top of your library until you reveal a creature card. + // Put that card onto the battlefield and attach Shifting Shadow to it, + // then put all other cards revealed this way on the bottom of your library in a random order.” + addCard(Zone.HAND, playerA, "Shifting Shadow", 1); // {2}{R} + + // Tap: Draw three cards. + // {2}{U}{U}: Return Arcanis the Omnipotent to its owner's hand. + addCard(Zone.HAND, playerA, "Arcanis the Omnipotent", 1); // Creature {3}{U}{U}{U} + + setChoice(playerA, "Yes"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mairsil, the Pretender"); + setChoice(playerA, "Yes"); // Exile a card + setChoice(playerA, "Arcanis the Omnipotent"); + + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Shifting Shadow", "Mairsil, the Pretender"); + + activateAbility(5, PhaseStep.UPKEEP, playerA, "{2}{U}{U}: Return", NO_TARGET, "At the beginning of your upkeep"); + setChoice(playerA, "No"); // Don't move commander to command zone because it goes to hand + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 40); + assertLife(playerB, 40); + + assertExileCount("Arcanis the Omnipotent", 1); + + assertCommandZoneCount(playerA, "Mairsil, the Pretender", 0); + assertHandCount(playerA, "Mairsil, the Pretender", 1); + + + assertGraveyardCount(playerA, "Shifting Shadow", 1); // Goes to graveyard because commander left battlefield to hand from Arcanis ability + assertPermanentCount(playerA, "Silvercoat Lion", 1); + } + + @Test + public void useShifitingShadowAndEndlingTest() { + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + addCard(Zone.BATTLEFIELD, playerA, "Island", 2); + addCard(Zone.LIBRARY, playerA, "Silvercoat Lion", 3); + skipInitShuffling(); + // Enchant creature + // Enchanted creature has haste and “At the beginning of your upkeep, destroy this creature. + // Reveal cards from the top of your library until you reveal a creature card. + // Put that card onto the battlefield and attach Shifting Shadow to it, + // then put all other cards revealed this way on the bottom of your library in a random order.” + addCard(Zone.HAND, playerA, "Shifting Shadow", 1); // {2}{R} + // Tap: Draw three cards. + // {2}{U}{U}: Return Arcanis the Omnipotent to its owner's hand. + addCard(Zone.HAND, playerA, "Arcanis the Omnipotent", 1); // Creature {3}{U}{U}{U} + // {B}: Endling gains menace until end of turn. + // {B}: Endling gains deathtouch until end of turn. + // {B}: Endling gains undying until end of turn. + // {1}: Endling gets +1/-1 or -1/+1 until end of turn. + addCard(Zone.HAND, playerA, "Endling", 1); // Creature {3}{U}{U}{U} + + + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Mairsil, the Pretender"); + setChoice(playerA, "Yes"); // Exile a card + setChoice(playerA, "Yes"); // Exile from Hand + setChoice(playerA, "Endling"); + + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, "Shifting Shadow", "Mairsil, the Pretender"); + + activateAbility(5, PhaseStep.UPKEEP, playerA, "{B}: {this} gains Undying", NO_TARGET, "At the beginning of your upkeep"); + setChoice(playerA, "No"); // Don't move commander to command zone because can come back by Undying + + setChoice(playerA, "Yes"); // Exile a card (Mairsil comes back from Undying) + setChoice(playerA, "Yes"); // Exile from hand + setChoice(playerA, "Arcanis the Omnipotent"); + + setStopAt(5, PhaseStep.PRECOMBAT_MAIN); + + setStrictChooseMode(true); + execute(); + assertAllCommandsUsed(); + + assertLife(playerA, 40); + assertLife(playerB, 40); + + assertExileCount("Endling", 1); + assertExileCount("Arcanis the Omnipotent", 1); + + assertCommandZoneCount(playerA, "Mairsil, the Pretender", 0); + assertGraveyardCount(playerA, "Mairsil, the Pretender", 0); + assertPermanentCount(playerA, "Mairsil, the Pretender", 1); // Returns from Undying + assertPowerToughness(playerA, "Mairsil, the Pretender", 5, 5); + + assertPermanentCount(playerA, "Shifting Shadow", 1); // Enchants Silvercoat Lion + assertPermanentCount(playerA, "Silvercoat Lion", 1); + } + +} 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 6d5b96b9cf..6a7250afee 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 @@ -1600,6 +1600,15 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement player.addAction(turnNum, step, ACTIVATE_ABILITY + ability + "$target=" + String.join("^", targetNames)); } + /** + * + * @param turnNum + * @param step + * @param player + * @param ability + * @param targetName use NO_TARGET if there is no target to set + * @param spellOnStack + */ public void activateAbility(int turnNum, PhaseStep step, TestPlayer player, String ability, String targetName, String spellOnStack) { // TODO: it's uses computerPlayer to execute, only ability target will work, but choices and targets commands aren't this.activateAbility(turnNum, step, player, ability, targetName, spellOnStack, StackClause.WHILE_ON_STACK); diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java index 399cd4c72a..e60ccca358 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/CommanderReplacementEffect.java @@ -91,9 +91,9 @@ public class CommanderReplacementEffect extends ReplacementEffectImpl { public boolean applies(GameEvent event, Ability source, Game game) { ZoneChangeEvent zEvent = (ZoneChangeEvent) event; - if (!game.isSimulation() && commanderId.equals(zEvent.getTargetId())) { - //System.out.println("applies " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); - } +// if (!game.isSimulation() && commanderId.equals(zEvent.getTargetId())) { +// System.out.println("applies " + game.getTurnNum() + ": " + game.getObject(event.getTargetId()).getName() + ": " + zEvent.getFromZone() + " -> " + zEvent.getToZone() + "; " + game.getObject(zEvent.getSourceId())); +// } if (zEvent.getToZone().equals(Zone.HAND) && !alsoHand) { return false;