* Copy spells - improved combo support with other abilities like Kicker or Entwine (#7192):

* Now ZCC of copied spells syncs with source card or coping spell (allows to keep ability settings that depends on ZCC);
  * Fixed bug that allows to lost kicked status in copied spells after counter the original spell or moves the original card (see #7192);
  * Test framework: improved support of targeting copy or non copy spells on stack;
This commit is contained in:
Oleg Agafonov 2020-12-15 20:06:53 +04:00
parent 936be75a66
commit 4d362d7edc
32 changed files with 522 additions and 302 deletions

View file

@ -9,7 +9,7 @@ import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* @author BetaSteward
* @author BetaSteward, LevelX2, JayDi85
*/
public class KickerTest extends CardTestPlayerBase {
@ -42,6 +42,7 @@ public class KickerTest extends CardTestPlayerBase {
* have those targets. See rule 601.2c.
*
*/
/**
* Aether Figment Creature Illusion 1/1, 1U (2) Kicker {3} (You may pay an
* additional {3} as you cast this spell.) Aether Figment can't be blocked.
@ -49,7 +50,7 @@ public class KickerTest extends CardTestPlayerBase {
* counters on it.
*/
@Test
public void testUseKicker_User() {
public void test_Use_Manual() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerA, "Aether Figment");
@ -69,7 +70,7 @@ public class KickerTest extends CardTestPlayerBase {
@Test
@Ignore
// TODO: enable test after replicate ability will be supported by AI (don't forget about multikicker support too)
public void testUseKicker_AI() {
public void test_Use_AI() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerA, "Aether Figment");
@ -87,7 +88,7 @@ public class KickerTest extends CardTestPlayerBase {
}
@Test
public void testDontUseKicker_User() {
public void test_DontUse_Manual() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
addCard(Zone.HAND, playerA, "Aether Figment");
@ -107,7 +108,7 @@ public class KickerTest extends CardTestPlayerBase {
@Test
@Ignore
// TODO: enable test after replicate ability will be supported by AI (don't forget about multikicker support too)
public void testDontUseKicker_AI() {
public void test_DontUse_AI() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 5 - 1); // haven't all mana
addCard(Zone.HAND, playerA, "Aether Figment");
@ -131,7 +132,7 @@ public class KickerTest extends CardTestPlayerBase {
* time it was kicked.
*/
@Test
public void testUseMultikickerOnce() {
public void test_Multikicker_UseOnce() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
addCard(Zone.HAND, playerA, "Apex Hawks");
@ -139,8 +140,10 @@ public class KickerTest extends CardTestPlayerBase {
setChoice(playerA, "Yes");
setChoice(playerA, "No");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Apex Hawks", 1);
assertCounterCount("Apex Hawks", CounterType.P1P1, 1);
@ -149,7 +152,7 @@ public class KickerTest extends CardTestPlayerBase {
}
@Test
public void testUseMultikickerTwice() {
public void test_Multikicker_UseTwice() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 7);
addCard(Zone.HAND, playerA, "Apex Hawks");
@ -158,39 +161,327 @@ public class KickerTest extends CardTestPlayerBase {
setChoice(playerA, "Yes");
setChoice(playerA, "No");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Apex Hawks", 1);
assertCounterCount("Apex Hawks", CounterType.P1P1, 2);
assertPowerToughness(playerA, "Apex Hawks", 4, 4);
}
@Test
public void testDontUseMultikicker() {
public void test_Multikicker_DontUse() {
addCard(Zone.BATTLEFIELD, playerA, "Plains", 7);
addCard(Zone.HAND, playerA, "Apex Hawks");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Apex Hawks");
setChoice(playerA, "No");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Apex Hawks", 1);
assertCounterCount("Apex Hawks", CounterType.P1P1, 0);
assertPowerToughness(playerA, "Apex Hawks", 2, 2);
}
/**
* When I cast Orim's Chant with Kicker cost, the player can play spells
* anyway during the turn. It seems like the kicker cost trigger an
* "instead" creatures can't attack.
*/
@Test
public void testOrimsChantskicker() {
public void test_AndOr_UseOr() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
// Kicker {1}{G} and/or {2}{U}
// When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying.
// When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards.
addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage");
setChoice(playerA, "No"); // not use kicker {1}{G}
setChoice(playerA, "Yes"); // use kicker {2}{U}
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerA, "Sunscape Battlemage", 1);
assertHandCount(playerA, 2);
}
@Test
public void test_AndOr_UseAnd() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
// Kicker {1}{G} and/or {2}{U}
// When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying.
// When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards.
addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W}
addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage");
setChoice(playerA, "Yes"); // use kicker {1}{G}
setChoice(playerA, "Yes"); // use kicker {2}{U}
setChoice(playerA, "When "); // two triggers from two kicker options
addTarget(playerA, "Birds of Paradise");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, "Birds of Paradise", 1);
assertPermanentCount(playerA, "Sunscape Battlemage", 1);
assertHandCount(playerA, 2);
}
@Test
public void test_Conditional_MustWorkWithMultipleKickerOptions() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
// Kicker {1}{G} and/or {2}{U}
// When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying.
// When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards.
addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W}
addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 1);
addCard(Zone.BATTLEFIELD, playerB, "Island", 1);
// Counter target spell if it was kicked.
addCard(Zone.HAND, playerB, "Ertai's Trickery", 1);
// cast with kicker
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage");
setChoice(playerA, "Yes"); // use kicker {1}{G} - destroy target creature with flying
setChoice(playerA, "Yes"); // use kicker {2}{U} - draw two cards
// spell must be countered, so no chooses
//setChoice(playerA, "When "); // two triggers rised: When {this} enters the battlefield, if it was kicked...
//addTarget(playerA, "Birds of Paradise"); // target for {1}{G} trigger
// counter kicked spell
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Ertai's Trickery", "Sunscape Battlemage", "Sunscape Battlemage");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertPermanentCount(playerB, "Birds of Paradise", 1);
assertGraveyardCount(playerB, "Ertai's Trickery", 1);
assertGraveyardCount(playerA, "Sunscape Battlemage", 1);
}
@Test
public void test_ZCC_ReturnedPermanentMustNotBeKicked() {
// bug:
// If a creature is cast with kicker, dies, and is then returned to play
// from graveyard, it still behaves like it were kicked. I noticed this
// while testing some newly implemented cards, but it can be reproduced for
// example by Zombifying a Gatekeeper of Malakir.
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
// Kicker {B} (You may pay an additional {B} as you cast this spell.)
// When Gatekeeper of Malakir enters the battlefield, if it was kicked, target player sacrifices a creature.
addCard(Zone.HAND, playerA, "Gatekeeper of Malakir", 1); // 2/2 {B}{B}
addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2);
addCard(Zone.BATTLEFIELD, playerB, "Island", 2);
// Return target permanent to its owner's hand.
addCard(Zone.HAND, playerB, "Boomerang", 1);
// first cast with kicker
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gatekeeper of Malakir");
setChoice(playerA, "Yes"); // use kicker
addTarget(playerA, playerB); // trigger's target
addTarget(playerB, "Birds of Paradise"); // sacrifice
// return to hand
castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Boomerang", "Gatekeeper of Malakir");
// second cast without kicker
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gatekeeper of Malakir");
setChoice(playerA, "No"); // no kicker
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertGraveyardCount(playerB, "Boomerang", 1);
assertGraveyardCount(playerB, "Birds of Paradise", 1);
assertPermanentCount(playerB, "Birds of Paradise", 1);
assertPermanentCount(playerA, "Gatekeeper of Malakir", 1);
}
@Test
public void test_ZCC_CopiedSpellMustKeepKickerStatus() {
// https://github.com/magefree/mage/issues/7192
// bug: Krark, the thumbless and a copy of him are on the field, and I cast Rite of replication kicked.
// The first coinflip fails and returns it to my hand, and the second coinflip wins and copies it,
// but does not copy the kicked part. I believe I did this before in another game and the first flip
// won then it would be a kicked copy.
// Whenever you cast an instant or sorcery spell, you may copy that spell. You may choose new targets for the copy.
addCard(Zone.BATTLEFIELD, playerA, "Swarm Intelligence", 1);
//
// Kicker {1}{R}
// Destroy target nonblack creature. It can't be regenerated.
// If Agonizing Demise was kicked, it deals damage equal to that creature's power to the creature's controller.
addCard(Zone.HAND, playerA, "Agonizing Demise", 1); // {3}{B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear1", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear2", 1); // 2/2
// cast spell with kicker and copy it (kicker status must be saved)
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Agonizing Demise", "@bear1");
setChoice(playerA, "Yes"); // use kicker
checkStackSize("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + trigger
checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 1);
checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1);
//
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
setChoice(playerA, "Yes"); // copy spell
setChoice(playerA, "Yes"); // new target
addTarget(playerA, "@bear2");
checkStackSize("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + copy
checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 2);
checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 0);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
assertLife(playerA, 20 - 2 * 2);
}
@Test
public void test_ZCC_CopiedSpellMustHaveIndependentZCC_InSpell() {
// reason: copied spell must have access to kicker status
// Whenever you cast an instant or sorcery spell, you may copy that spell. You may choose new targets for the copy.
addCard(Zone.BATTLEFIELD, playerA, "Swarm Intelligence", 1);
//
// Kicker {1}{R}
// Destroy target nonblack creature. It can't be regenerated.
// If Agonizing Demise was kicked, it deals damage equal to that creature's power to the creature's controller.
addCard(Zone.HAND, playerA, "Agonizing Demise", 1); // {3}{B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 4);
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear1", 1); // 2/2
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears@bear2", 1); // 2/2
//
// Counter target spell. You gain 3 life.
addCard(Zone.HAND, playerA, "Absorb", 1); // {W}{U}{U}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
// cast spell with kicker and copy it (kicker status must be saved)
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 4);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Agonizing Demise", "@bear1");
setChoice(playerA, "Yes"); // use kicker
checkStackSize("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + trigger
checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 1);
checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1);
//
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
setChoice(playerA, "Yes"); // copy spell
setChoice(playerA, "Yes"); // new target
addTarget(playerA, "@bear2");
checkStackSize("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 2); // spell + copy
checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 2);
checkStackObject("after copy trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 0);
//
// counter first spell (non copy) to change original card's ZCC
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absorb", "Agonizing Demise[no copy]", "Agonizing Demise", StackClause.WHILE_COPY_ON_STACK);
checkStackSize("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 4); // spell + copy + trigger + counter
checkStackObject("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 2);
checkStackObject("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1);
checkStackObject("before counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Absorb", 1);
//
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); // trigger
setChoice(playerA, "No"); // do not copy counter spell
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true); // counter
checkStackSize("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // copy
checkStackObject("after counter", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Ago", 1);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
// cast spell - countered
// copied spell - resolved (2 damage)
// counter - resolved (3 life)
// possible bug: kicker status for copied spell was lost and no 2 damage
assertLife(playerA, 20 - 2 + 3);
}
@Test
public void test_ZCC_CopiedSpellMustHaveIndependentZCC_InStaticAbility() {
// reason: static ability from copied spell's permanent must have access to kicker status
// {4}, {T}: Copy target permanent spell you control.
addCard(Zone.BATTLEFIELD, playerA, "Lithoform Engine", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 4);
//
// Kicker {4}
// If Academy Drake was kicked, it enters the battlefield with two +1/+1 counters on it.
addCard(Zone.HAND, playerA, "Academy Drake", 1); // {2}{U}, 2/2
addCard(Zone.BATTLEFIELD, playerA, "Island", 4 + 4 + 3);
//
// Counter target spell. You gain 3 life.
addCard(Zone.HAND, playerA, "Absorb", 1); // {W}{U}{U}
addCard(Zone.BATTLEFIELD, playerA, "Plains", 5);
addCard(Zone.BATTLEFIELD, playerA, "Island", 5);
// cast spell with kicker
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 3);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Academy Drake");
setChoice(playerA, "Yes"); // use kicker
// copy spell
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {U}", 4);
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}", "Academy Drake", "Academy Drake");
checkStackObject("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Academy Drake", 1);
checkStackObject("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "{4}, {T}", 1);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
checkStackObject("after copy", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast Academy Drake", 2);
// counter first spell (non copy) to change original card's ZCC
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Absorb", "Academy Drake[no copy]", "Academy Drake", StackClause.WHILE_COPY_ON_STACK);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertAllCommandsUsed();
// cast spell - countered
// copied spell - +2 counters
// counter - resolved (3 life)
// possible bug: kicker status for copied spell was lost and no 2 counters added
assertPermanentCount(playerA, "Academy Drake", 1);
assertCounterCount(playerA, "Academy Drake", CounterType.P1P1, 2);
}
@Test
public void test_Single_OrimsChants() {
// bug:
// When I cast Orim's Chant with Kicker cost, the player can play spells
// anyway during the turn. It seems like the kicker cost trigger an
// "instead" creatures can't attack.
addCard(Zone.BATTLEFIELD, playerA, "Raging Goblin", 1); // Haste 1/1
addCard(Zone.BATTLEFIELD, playerA, "Plains", 2);
// Kicker {W} (You may pay an additional {W} as you cast this spell.)
@ -208,23 +499,25 @@ public class KickerTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerB, "Lightning Bolt", playerA);
// attack must be restricted, so no attack commands available
//setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
//assertAllCommandsUsed();
assertGraveyardCount(playerA, "Orim's Chant", 1);
assertGraveyardCount(playerB, "Lightning Bolt", 0);
assertLife(playerA, 20);
assertLife(playerB, 20);
}
/**
* Bloodhusk Ritualist's discard trigger does nothing if the Ritualist
* leaves the battlefield before the trigger resolves.
*/
@Test
public void testBloodhuskRitualist() {
public void test_Single_BloodhuskRitualist() {
// bug:
// Bloodhusk Ritualist's discard trigger does nothing if the Ritualist
// leaves the battlefield before the trigger resolves.
addCard(Zone.BATTLEFIELD, playerB, "Mountain", 1);
addCard(Zone.HAND, playerB, "Lightning Bolt");
addCard(Zone.HAND, playerB, "Fireball", 2);
@ -235,13 +528,15 @@ public class KickerTest extends CardTestPlayerBase {
// Multikicker (You may pay an additional {B} any number of times as you cast this spell.)
// When Bloodhusk Ritualist enters the battlefield, target opponent discards a card for each time it was kicked.
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Bloodhusk Ritualist");
setChoice(playerA, "Yes"); // 2 x Multikicker
setChoice(playerA, "Yes");
setChoice(playerA, "No");
setChoice(playerA, "Yes", 2); // 2 x Multikicker
setChoice(playerA, "No"); // stop the kicking
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Lightning Bolt", "Bloodhusk Ritualist");
addTarget(playerA, playerB); // target for kicker's trigger (discard cards)
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
Assert.assertEquals("All mana has to be used", "[]", playerA.getManaAvailable(currentGame).toString());
assertGraveyardCount(playerB, "Lightning Bolt", 1);
@ -251,138 +546,13 @@ public class KickerTest extends CardTestPlayerBase {
assertHandCount(playerB, 0);
}
/**
* Test and/or kicker costs
*/
@Test
public void testSunscapeBattlemage1() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
public void test_Single_MarshCasualties() {
// bug:
// Paying the Kicker on "Marsh Casualties" has no effect. Target player's
// creatures still only get -1/-1 instead of -2/-2. Was playing against AI.
// It was me who cast the spell.
// Kicker {1}{G} and/or {2}{U}
// When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying.
// When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards.
addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W}
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage");
setChoice(playerA, "No"); // no {1}{G}
setChoice(playerA, "Yes"); // but {2}{U}
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerA, "Sunscape Battlemage", 1);
assertHandCount(playerA, 2);
}
/**
* Test and/or kicker costs
*/
@Test
public void testSunscapeBattlemage2() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
// Kicker {1}{G} and/or {2}{U}
// When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying.
// When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards.
addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W}
addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage");
addTarget(playerA, "Birds of Paradise");
setChoice(playerA, "Yes"); // no {1}{G}
setChoice(playerA, "Yes"); // but {2}{U}
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerB, "Birds of Paradise", 1);
assertPermanentCount(playerA, "Sunscape Battlemage", 1);
assertHandCount(playerA, 2);
}
/**
* If a creature is cast with kicker, dies, and is then returned to play
* from graveyard, it still behaves like it were kicked. I noticed this
* while testing some newly implemented cards, but it can be reproduced for
* example by Zombifying a Gatekeeper of Malakir.
*/
@Test
public void testKickerGoneForRecast() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
// Kicker {B} (You may pay an additional {B} as you cast this spell.)
// When Gatekeeper of Malakir enters the battlefield, if it was kicked, target player sacrifices a creature.
addCard(Zone.HAND, playerA, "Gatekeeper of Malakir", 1); // 2/2 {B}{B}
addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 2);
addCard(Zone.BATTLEFIELD, playerB, "Island", 2);
addCard(Zone.HAND, playerB, "Boomerang", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Gatekeeper of Malakir");
addTarget(playerA, playerB);
setChoice(playerA, "Yes"); // Kicker
castSpell(1, PhaseStep.BEGIN_COMBAT, playerB, "Boomerang", "Gatekeeper of Malakir");
castSpell(1, PhaseStep.POSTCOMBAT_MAIN, playerA, "Gatekeeper of Malakir");
setChoice(playerA, "No"); // no Kicker
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerB, "Boomerang", 1);
assertGraveyardCount(playerB, "Birds of Paradise", 1);
assertPermanentCount(playerB, "Birds of Paradise", 1);
assertPermanentCount(playerA, "Gatekeeper of Malakir", 1);
}
/**
* Check that kicker condition does also work for kicker cards with multiple
* kicker options
*/
@Test
public void testKickerCondition() {
addCard(Zone.BATTLEFIELD, playerA, "Island", 3);
addCard(Zone.BATTLEFIELD, playerA, "Plains", 3);
addCard(Zone.BATTLEFIELD, playerA, "Forest", 2);
// Kicker {1}{G} and/or {2}{U}
// When {this} enters the battlefield, if it was kicked with its {1}{G} kicker, destroy target creature with flying.
// When {this} enters the battlefield, if it was kicked with its {2}{U} kicker, draw two cards.
addCard(Zone.HAND, playerA, "Sunscape Battlemage", 1); // 2/2 {2}{W}
addCard(Zone.BATTLEFIELD, playerB, "Birds of Paradise", 1);
addCard(Zone.BATTLEFIELD, playerB, "Island", 1);
// Counter target spell if it was kicked.
addCard(Zone.HAND, playerB, "Ertai's Trickery", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Sunscape Battlemage");
addTarget(playerA, "Birds of Paradise");
setChoice(playerA, "Yes"); // {1}{G} destroy target creature with flying
setChoice(playerA, "Yes"); // {2}{U} draw two cards
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Ertai's Trickery", "Sunscape Battlemage");
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertPermanentCount(playerB, "Birds of Paradise", 1);
assertGraveyardCount(playerB, "Ertai's Trickery", 1);
assertGraveyardCount(playerA, "Sunscape Battlemage", 1);
}
/**
* Paying the Kicker on "Marsh Casualties" has no effect. Target player's
* creatures still only get -1/-1 instead of -2/-2. Was playing against AI.
* It was me who cast the spell.
*/
@Test
public void testMarshCasualties() {
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 5);
// Kicker {3}
@ -394,13 +564,13 @@ public class KickerTest extends CardTestPlayerBase {
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Marsh Casualties", playerB);
setChoice(playerA, "Yes"); // Pay Kicker
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertAllCommandsUsed();
assertTappedCount("Swamp", true, 5);
assertGraveyardCount(playerA, "Marsh Casualties", 1);
assertPowerToughness(playerB, "Centaur Courser", 1, 1);
}
}

View file

@ -37,18 +37,20 @@ public class KrarkTheThumblessTest extends CardTestPlayerBase {
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN);
// FIRST BOLT CAST
// cast bolt and generate 3 triggers
// cast bolt and generate 2 triggers
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
setChoice(playerA, "Whenever"); // 2x triggers raise
checkStackSize("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 3); // bolt + 2x triggers
checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast L", 1);
checkStackObject("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 2);
checkHandCardCount("after cast", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 0);
// resolve first trigger and lose (move spell to hand)
setFlipCoinResult(playerA, false);
waitStackResolved(1, PhaseStep.PRECOMBAT_MAIN, true);
checkStackSize("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // second trigger
checkStackSize("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // one trigger
checkStackObject("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast L", 0);
checkStackObject("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 1);
checkHandCardCount("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); // moved to hand
// resolve second trigger and win (copy the spell)
@ -57,6 +59,7 @@ public class KrarkTheThumblessTest extends CardTestPlayerBase {
setChoice(playerA, "No"); // do not change a target of the copy
checkStackSize("after win trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, 1); // copied bolt
checkStackObject("after lose trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Cast L", 1);
checkStackObject("after win trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Whenever you cast", 0);
checkHandCardCount("after win trigger", 1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", 1); // stil in hand
// SECOND BOLT CAST

View file

@ -43,9 +43,14 @@ public class PrizedAmalgamTest extends CardTestPlayerBase {
*/
@Test
public void testGravecrawlerCastFromGrave() {
// You may cast Gravecrawler from your graveyard as long as you control a Zombie.
addCard(Zone.GRAVEYARD, playerA, "Gravecrawler", 1);
//
// Whenever a creature enters the battlefield, if it entered from your graveyard or you cast it from your
// graveyard, return Prized Amalgam from your graveyard to the battlefield tapped at the beginning of the
// next end step.
addCard(Zone.GRAVEYARD, playerA, "Prized Amalgam", 1);
//
addCard(Zone.BATTLEFIELD, playerA, "Gnawing Zombie", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);

View file

@ -1695,14 +1695,26 @@ public abstract class CardTestPlayerAPIImpl extends MageTestPlayerBase implement
assertAliaseSupportInActivateCommand(cardName, true);
assertAliaseSupportInActivateCommand(targetName, true);
assertAliaseSupportInActivateCommand(spellOnStack, false);
if (StackClause.WHILE_ON_STACK == clause) {
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName)
+ "$spellOnStack=" + spellOnStack);
} else {
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName)
+ "$!spellOnStack=" + spellOnStack);
String targetInfo = (targetName != null && targetName.startsWith("target") ? targetName : "target=" + targetName);
switch (clause) {
case WHILE_ON_STACK:
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + targetInfo
+ "$spellOnStack=" + spellOnStack);
break;
case WHILE_COPY_ON_STACK:
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + targetInfo
+ "$spellCopyOnStack=" + spellOnStack);
break;
case WHILE_NOT_ON_STACK:
addPlayerAction(player, turnNum, step, ACTIVATE_CAST + cardName
+ '$' + targetInfo
+ "$!spellOnStack=" + spellOnStack);
break;
default:
throw new IllegalArgumentException("Unknown stack clause " + clause);
}
}

View file

@ -62,8 +62,8 @@ public class TestAliases extends CardTestPlayerBase {
// name with face down spells: face down spells don't have names, see https://github.com/magefree/mage/issues/6569
Card bearCard = CardRepository.instance.findCard("Balduvian Bears").getCard();
Spell normalSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND);
Spell faceDownSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND);
Spell normalSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND, currentGame);
Spell faceDownSpell = new Spell(bearCard, bearCard.getSpellAbility(), playerA.getId(), Zone.HAND, currentGame);
faceDownSpell.setFaceDown(true, currentGame);
// normal spell
Assert.assertFalse(CardUtil.haveSameNames(normalSpell, "", currentGame));