From 62279bbe6a1e740247b5f8341a50d1c235222dfd Mon Sep 17 00:00:00 2001 From: Susucre <34709007+Susucre@users.noreply.github.com> Date: Fri, 5 Apr 2024 00:17:14 +0200 Subject: [PATCH] [OTJ] Implement Satoru, the Infiltrator and Freestrider Commando (#12052) --- .../src/mage/cards/f/FreestriderCommando.java | 78 +++++ .../mage/cards/s/SatoruTheInfiltrator.java | 114 ++++++++ .../mage/sets/OutlawsOfThunderJunction.java | 2 + .../single/otj/FreestriderCommandoTest.java | 127 +++++++++ .../single/otj/SatoruTheInfiltratorTest.java | 268 ++++++++++++++++++ 5 files changed, 589 insertions(+) create mode 100644 Mage.Sets/src/mage/cards/f/FreestriderCommando.java create mode 100644 Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java create mode 100644 Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java diff --git a/Mage.Sets/src/mage/cards/f/FreestriderCommando.java b/Mage.Sets/src/mage/cards/f/FreestriderCommando.java new file mode 100644 index 00000000000..10141dd058c --- /dev/null +++ b/Mage.Sets/src/mage/cards/f/FreestriderCommando.java @@ -0,0 +1,78 @@ +package mage.cards.f; + +import mage.MageInt; +import mage.abilities.Ability; +import mage.abilities.common.EntersBattlefieldAbility; +import mage.abilities.condition.Condition; +import mage.abilities.decorator.ConditionalOneShotEffect; +import mage.abilities.effects.common.counter.AddCountersSourceEffect; +import mage.abilities.keyword.PlotAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.counters.CounterType; +import mage.game.Game; +import mage.game.permanent.Permanent; +import mage.game.stack.Spell; + +import java.util.UUID; + +/** + * @author Susucr + */ +public final class FreestriderCommando extends CardImpl { + + public FreestriderCommando(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{2}{G}"); + + this.subtype.add(SubType.CENTAUR); + this.subtype.add(SubType.MERCENARY); + this.power = new MageInt(3); + this.toughness = new MageInt(3); + + // Freestrider Commando enters the battlefield with two +1/+1 counters on it if it wasn't cast or no mana was spent to cast it. + this.addAbility(new EntersBattlefieldAbility( + new ConditionalOneShotEffect( + new AddCountersSourceEffect(CounterType.P1P1.createInstance(2)), + FreestriderCommandoCondition.instance, "" + ), "with two +1/+1 counters on it if it wasn't cast or no mana was spent to cast it" + )); + + // Plot {3}{G} + this.addAbility(new PlotAbility("{3}{G}")); + } + + private FreestriderCommando(final FreestriderCommando card) { + super(card); + } + + @Override + public FreestriderCommando copy() { + return new FreestriderCommando(this); + } +} + +enum FreestriderCommandoCondition implements Condition { + instance; + + @Override + public boolean apply(Game game, Ability source) { + Permanent permanent = game.getPermanentEntering(source.getSourceId()); + if (permanent == null) { + return false; + } + // Check if the spell exists on the stack + Spell spell = game.getStack().getSpell(source.getSourceId()); + if (spell == null) { + return true; // cannot find the spell, so it wasn't cast. + } + // spell was found, did it cost mana? + return 0 == spell.getStackAbility().getManaCostsToPay().getUsedManaToPay().count(); + } + + @Override + public String toString() { + return "if it wasn't cast or no mana was spent to cast it"; + } +} diff --git a/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java b/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java new file mode 100644 index 00000000000..c4d4de8d513 --- /dev/null +++ b/Mage.Sets/src/mage/cards/s/SatoruTheInfiltrator.java @@ -0,0 +1,114 @@ +package mage.cards.s; + +import mage.MageInt; +import mage.abilities.TriggeredAbilityImpl; +import mage.abilities.effects.common.DrawCardSourceControllerEffect; +import mage.abilities.keyword.MenaceAbility; +import mage.cards.CardImpl; +import mage.cards.CardSetInfo; +import mage.constants.CardType; +import mage.constants.SubType; +import mage.constants.SuperType; +import mage.constants.Zone; +import mage.game.Game; +import mage.game.events.GameEvent; +import mage.game.events.ZoneChangeBatchEvent; +import mage.game.events.ZoneChangeEvent; +import mage.game.permanent.Permanent; +import mage.game.permanent.PermanentToken; +import mage.game.stack.Spell; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * @author Susucr + */ +public final class SatoruTheInfiltrator extends CardImpl { + + public SatoruTheInfiltrator(UUID ownerId, CardSetInfo setInfo) { + super(ownerId, setInfo, new CardType[]{CardType.CREATURE}, "{U}{B}"); + + this.supertype.add(SuperType.LEGENDARY); + this.subtype.add(SubType.HUMAN); + this.subtype.add(SubType.NINJA); + this.subtype.add(SubType.ROGUE); + this.power = new MageInt(2); + this.toughness = new MageInt(3); + + // Menace + this.addAbility(new MenaceAbility()); + + // Whenever Satoru, the Infiltrator and/or one or more other nontoken creatures enter the battlefield under your control, if none of them were cast or no mana was spent to cast them, draw a card. + this.addAbility(new SatoruTheInfiltratorTriggeredAbility()); + } + + private SatoruTheInfiltrator(final SatoruTheInfiltrator card) { + super(card); + } + + @Override + public SatoruTheInfiltrator copy() { + return new SatoruTheInfiltrator(this); + } +} + +class SatoruTheInfiltratorTriggeredAbility extends TriggeredAbilityImpl { + + public SatoruTheInfiltratorTriggeredAbility() { + super(Zone.BATTLEFIELD, new DrawCardSourceControllerEffect(1), false); + this.setTriggerPhrase("Whenever {this} and/or one or more other nontoken creatures " + + "enter the battlefield under your control, if none of them were cast or no mana was spent to cast them, "); + } + + protected SatoruTheInfiltratorTriggeredAbility(final SatoruTheInfiltratorTriggeredAbility ability) { + super(ability); + } + + @Override + public SatoruTheInfiltratorTriggeredAbility copy() { + return new SatoruTheInfiltratorTriggeredAbility(this); + } + + @Override + public boolean checkEventType(GameEvent event, Game game) { + return event.getType() == GameEvent.EventType.ZONE_CHANGE_BATCH; + } + + // event is GameEvent.EventType.ENTERS_THE_BATTLEFIELD + @Override + public boolean checkTrigger(GameEvent event, Game game) { + ZoneChangeBatchEvent zEvent = (ZoneChangeBatchEvent) event; + List moved = zEvent.getEvents() + .stream() + .filter(e -> e.getToZone() == Zone.BATTLEFIELD) // Keep only to the battlefield + .filter(e -> { + Permanent permanent = e.getTarget(); + if (permanent == null) { + return false; + } + return permanent.isControlledBy(getControllerId()) // under your control + && (permanent.getId().equals(getSourceId()) // {this} + || (permanent.isCreature(game) && !(permanent instanceof PermanentToken)) // other nontoken Creature + ); + }) + .collect(Collectors.toList()); + if (moved.isEmpty()) { + return false; + } + // At this point, we have at least one event matching + // "Whenever {this} and/or one or more other nontoken creatures enter the battlefield under your control" + // Now to check that none were cast using mana + for (ZoneChangeEvent zce : moved) { + if (zce.getFromZone() != Zone.STACK) { + continue; // not from stack, we good for this one event + } + Spell spell = game.getSpellOrLKIStack(zce.getTargetId()); + if (spell != null && spell.getStackAbility().getManaCostsToPay().getUsedManaToPay().count() > 0) { + return false; // found one that did use mana, so no triggering. + } + } + return true; // all relevant permanents passed the spell mana check, so triggering. + } +} diff --git a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java index b2a6deacd0f..3c18e49b830 100644 --- a/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java +++ b/Mage.Sets/src/mage/sets/OutlawsOfThunderJunction.java @@ -113,6 +113,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Form a Posse", 204, Rarity.UNCOMMON, mage.cards.f.FormAPosse.class)); cards.add(new SetCardInfo("Forsaken Miner", 88, Rarity.UNCOMMON, mage.cards.f.ForsakenMiner.class)); cards.add(new SetCardInfo("Fortune, Loyal Steed", 12, Rarity.RARE, mage.cards.f.FortuneLoyalSteed.class)); + cards.add(new SetCardInfo("Freestrider Commando", 162, Rarity.COMMON, mage.cards.f.FreestriderCommando.class)); cards.add(new SetCardInfo("Freestrider Lookout", 163, Rarity.RARE, mage.cards.f.FreestriderLookout.class)); cards.add(new SetCardInfo("Frontier Seeker", 13, Rarity.UNCOMMON, mage.cards.f.FrontierSeeker.class)); cards.add(new SetCardInfo("Full Steam Ahead", 164, Rarity.UNCOMMON, mage.cards.f.FullSteamAhead.class)); @@ -235,6 +236,7 @@ public final class OutlawsOfThunderJunction extends ExpansionSet { cards.add(new SetCardInfo("Rustler Rampage", 27, Rarity.UNCOMMON, mage.cards.r.RustlerRampage.class)); cards.add(new SetCardInfo("Ruthless Lawbringer", 229, Rarity.UNCOMMON, mage.cards.r.RuthlessLawbringer.class)); cards.add(new SetCardInfo("Sandstorm Verge", 263, Rarity.UNCOMMON, mage.cards.s.SandstormVerge.class)); + cards.add(new SetCardInfo("Satoru, the Infiltrator", 230, Rarity.RARE, mage.cards.s.SatoruTheInfiltrator.class)); cards.add(new SetCardInfo("Scalestorm Summoner", 144, Rarity.UNCOMMON, mage.cards.s.ScalestormSummoner.class)); cards.add(new SetCardInfo("Scorching Shot", 145, Rarity.UNCOMMON, mage.cards.s.ScorchingShot.class)); cards.add(new SetCardInfo("Seize the Secrets", 64, Rarity.COMMON, mage.cards.s.SeizeTheSecrets.class)); diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java new file mode 100644 index 00000000000..f1fe11f3735 --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/FreestriderCommandoTest.java @@ -0,0 +1,127 @@ +package org.mage.test.cards.single.otj; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class FreestriderCommandoTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.f.FreestriderCommando Freestrider Commando} {2}{G} + * Creature — Centaur Mercenary + * Freestrider Commando enters the battlefield with two +1/+1 counters on it if it wasn’t cast or no mana was spent to cast it. + * Plot {3}{G} (You may pay {3}{G} and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery.) + * 3/3 + */ + private static final String commando = "Freestrider Commando"; + + @Test + public void test_RegularCast() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.HAND, playerA, commando); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // No +1/+1 as cast with mana + assertPowerToughness(playerA, commando, 3, 3); + } + + @Test + public void test_PlotCast() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, commando); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + // 2 +1/+1 as cast free with plot + assertPowerToughness(playerA, commando, 3 + 2, 3 + 2); + } + + @Test + public void test_OmniscienceCast() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + addCard(Zone.HAND, playerA, commando); + + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick? + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + // 2 +1/+1 as cast free with Omniscience + assertPowerToughness(playerA, commando, 3 + 2, 3 + 2); + } + + @Test + public void test_PlotCast_WithTax() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, commando); + addCard(Zone.BATTLEFIELD, playerA, "Sphere of Resistance"); // Spells cost {1} more to cast + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + // no +1/+1 as cast free with plot, but tax make mana being paid + assertPowerToughness(playerA, commando, 3, 3); + } + + @Test + public void test_Blink() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Savannah", 4); + addCard(Zone.HAND, playerA, commando); + addCard(Zone.HAND, playerA, "Ephemerate"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", "Freestrider Commando"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + // 2 +1/+1 as cast free with plot, as re-enter without being cast + assertPowerToughness(playerA, commando, 3 + 2, 3 + 2); + } + + @Test + public void test_DoubleMajor() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 6); + addCard(Zone.HAND, playerA, commando); + addCard(Zone.HAND, playerA, "Double Major"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", "Freestrider Commando"); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, 2); // resolve Double Major + the copy + checkPT("double major copy enters as a 5/5", 1, PhaseStep.PRECOMBAT_MAIN, playerA, commando, 3 + 2, 3 + 2); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 2); // real + copy + } +} diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java new file mode 100644 index 00000000000..6e6644d8d5e --- /dev/null +++ b/Mage.Tests/src/test/java/org/mage/test/cards/single/otj/SatoruTheInfiltratorTest.java @@ -0,0 +1,268 @@ +package org.mage.test.cards.single.otj; + +import mage.constants.PhaseStep; +import mage.constants.Zone; +import org.junit.Test; +import org.mage.test.serverside.base.CardTestPlayerBase; + +/** + * @author Susucr + */ +public class SatoruTheInfiltratorTest extends CardTestPlayerBase { + + /** + * {@link mage.cards.s.SatoruTheInfiltrator Satoru, the Infiltrator} {U}{B} + * Legendary Creature — Human Ninja Rogue + * Menace + * Whenever Satoru, the Infiltrator and/or one or more other nontoken creatures enter the battlefield under your control, if none of them were cast or no mana was spent to cast them, draw a card. + */ + private static final String satoru = "Satoru, the Infiltrator"; + + /** + * {@link mage.cards.f.FreestriderCommando Freestrider Commando} {2}{G} + * Creature — Centaur Mercenary + * Freestrider Commando enters the battlefield with two +1/+1 counters on it if it wasn’t cast or no mana was spent to cast it. + * Plot {3}{G} (You may pay {3}{G} and exile this card from your hand. Cast it as a sorcery on a later turn without paying its mana cost. Plot only as a sorcery.) + * 3/3 + */ + private static final String commando = "Freestrider Commando"; + + @Test + public void test_RegularCast_Other() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 3); + addCard(Zone.HAND, playerA, commando); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 1); + assertHandCount(playerA, 0); // no card draw. + } + + @Test + public void test_RegularCast_Other_Memnite() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.HAND, playerA, "Memnite"); // Is free! + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Memnite"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Memnite", 1); + assertHandCount(playerA, 1); + } + + @Test + public void test_RegularCast_Satoru() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Underground Sea", 2); + addCard(Zone.HAND, playerA, satoru); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, satoru, 1); + assertHandCount(playerA, 0); // no card draw. + } + + @Test + public void test_PlotCast() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, commando); + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 1); + assertHandCount(playerA, 1 + 1); // Drawn 1 from draw step + 1 from Satoru + } + + @Test + public void test_Omniscience_CastOther() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + addCard(Zone.HAND, playerA, commando); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick? + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 1); + assertHandCount(playerA, 1); // Drawn 1 from Satoru + } + + @Test + public void test_Omniscience_CastSatoru() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, "Omniscience"); + addCard(Zone.HAND, playerA, satoru); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru); + setChoice(playerA, true); // Omniscience asks for confirmation to cast to avoid missclick? + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, satoru, 1); + assertHandCount(playerA, 1); // Drawn 1 from Satoru + } + + @Test + public void test_PlotCast_WithTax() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 4); + addCard(Zone.HAND, playerA, commando); + addCard(Zone.BATTLEFIELD, playerA, "Sphere of Resistance"); // Spells cost {1} more to cast + + activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Plot"); + castSpell(3, PhaseStep.PRECOMBAT_MAIN, playerA, commando + " using Plot"); + + setStopAt(3, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 1); + assertHandCount(playerA, 1); // Drawn 1 from draw step, 0 from Satoru + } + + @Test + public void test_Blink() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Savannah", 4); + addCard(Zone.HAND, playerA, commando); + addCard(Zone.HAND, playerA, "Ephemerate"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Ephemerate", commando); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 1); + assertHandCount(playerA, 1); // Drawn 1 from Satoru + } + + @Test + public void test_MultipleOtherEnter() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Plains", 6); + addCard(Zone.GRAVEYARD, playerA, commando, 3); + addCard(Zone.HAND, playerA, "Storm of Souls"); // Return all creature cards from your graveyard to the battlefield. Each of them is a 1/1 Spirit with flying in addition to its other types. Exile Storm of Souls. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Storm of Souls"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 3); + assertHandCount(playerA, 1); // Drawn 1 from Satoru + } + + @Test + public void test_Saturo_AndAnother_Enter() { + setStrictChooseMode(true); + + addCard(Zone.GRAVEYARD, playerA, satoru); + addCard(Zone.GRAVEYARD, playerA, commando, 3); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5); + // Each player exiles all creature cards from their graveyard, then sacrifices all creatures they control, + // then puts all cards they exiled this way onto the battlefield. + addCard(Zone.HAND, playerA, "Living Death"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Living Death"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 3); + assertPermanentCount(playerA, satoru, 1); + assertHandCount(playerA, 1); // Drawn 1 from Satoru + } + + @Test + public void test_CopyOnStack() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.HAND, playerA, commando); + addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 5); + // Copy target creature spell you control, except it isn’t legendary if the spell is legendary + addCard(Zone.HAND, playerA, "Double Major"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, commando); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", commando); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, commando, 2); + assertPermanentCount(playerA, satoru, 1); + assertHandCount(playerA, 0); // Drawn 0 from Satoru, as token does not count. + } + + @Test + public void test_Tokens() { + setStrictChooseMode(true); + + addCard(Zone.BATTLEFIELD, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); + addCard(Zone.HAND, playerA, "Krenko's Command"); // Create two 1/1 red Goblin creature tokens. + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Krenko's Command"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, "Goblin Token", 2); + assertPermanentCount(playerA, satoru, 1); + assertHandCount(playerA, 0); // Drawn 0 from Satoru, as token does not count. + } + + @Test + public void test_CopySatoruOnStack() { + setStrictChooseMode(true); + + addCard(Zone.HAND, playerA, satoru); + addCard(Zone.BATTLEFIELD, playerA, "Tropical Island", 3); + addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); + // Copy target creature spell you control, except it isn’t legendary if the spell is legendary + addCard(Zone.HAND, playerA, "Double Major"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, satoru); + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Double Major", satoru); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertPermanentCount(playerA, satoru, 2); + assertHandCount(playerA, 1); // While Satoru does not trigger on other tokens, a copy of Satoru on the stack will trigger its own etb. + } +}