tests: added additional tests for Dryad Militant card and Madness abilities, added docs;

This commit is contained in:
Oleg Agafonov 2024-05-12 12:33:48 +04:00
parent 8c0ed8a749
commit d28b9e6d05
6 changed files with 283 additions and 43 deletions

View file

@ -45,12 +45,14 @@ public final class ChandraAblaze extends CardImpl {
ability.addEffect(new ChandraAblazeEffect2()); ability.addEffect(new ChandraAblazeEffect2());
ability.addTarget(new TargetAnyTarget()); ability.addTarget(new TargetAnyTarget());
this.addAbility(ability); this.addAbility(ability);
// -2: Each player discards their hand, then draws three cards. // -2: Each player discards their hand, then draws three cards.
ability = new LoyaltyAbility(new DiscardHandAllEffect(), -2); ability = new LoyaltyAbility(new DiscardHandAllEffect(), -2);
Effect effect = new DrawCardAllEffect(3); Effect effect = new DrawCardAllEffect(3);
effect.setText(", then draws three cards"); effect.setText(", then draws three cards");
ability.addEffect(effect); ability.addEffect(effect);
this.addAbility(ability); this.addAbility(ability);
// -7: Cast any number of red instant and/or sorcery cards from your graveyard without paying their mana costs. // -7: Cast any number of red instant and/or sorcery cards from your graveyard without paying their mana costs.
ability = new LoyaltyAbility(new ChandraAblazeEffect5(), -7); ability = new LoyaltyAbility(new ChandraAblazeEffect5(), -7);
this.addAbility(ability); this.addAbility(ability);
@ -84,6 +86,11 @@ class ChandraAblazeEffect1 extends OneShotEffect {
@Override @Override
public boolean apply(Game game, Ability source) { public boolean apply(Game game, Ability source) {
// If you activate Chandra Ablazes first ability, you dont discard a card until the ability resolves.
// You may activate the ability even if your hand is empty. You choose a target as you activate the ability
// even if you have no red cards in hand at that time.
// (2009-10-01)
Player player = game.getPlayer(source.getControllerId()); Player player = game.getPlayer(source.getControllerId());
if (player != null) { if (player != null) {
TargetDiscard target = new TargetDiscard(player.getId()); TargetDiscard target = new TargetDiscard(player.getId());

View file

@ -59,7 +59,7 @@ public class UnboundFlourishingTest extends CardTestPlayerBase {
} }
@Test @Test
public void test_OnCastInstantOrSourcery_MustCopy() { public void test_OnCastInstantOrSorcery_MustCopy() {
addCard(Zone.BATTLEFIELD, playerA, "Unbound Flourishing", 1); addCard(Zone.BATTLEFIELD, playerA, "Unbound Flourishing", 1);
// //
// Banefire deals X damage to any target. // Banefire deals X damage to any target.

View file

@ -1,17 +1,35 @@
package org.mage.test.cards.replacement; package org.mage.test.cards.replacement;
import mage.cards.Card;
import mage.constants.PhaseStep; import mage.constants.PhaseStep;
import mage.constants.Zone; import mage.constants.Zone;
import mage.watchers.common.CardsExiledThisTurnWatcher;
import mage.watchers.common.CardsPutIntoGraveyardWatcher;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase; import org.mage.test.serverside.base.CardTestPlayerBase;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
/** /**
* Dryad Militant * Dryad Militant oracle rules:
* Creature - Dryad Soldier 1/1 {G/W} * <p>
* If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead. * If an instant or sorcery spell destroys Dryad Militant directly (like Murder does), that instant or sorcery
* card will be put into its owners graveyard. However, if an instant or sorcery card deals lethal damage to
* Dryad Militant, Dryad Militant will remain on the battlefield until the next time state-based actions are
* checked, which is after the instant or sorcery finishes resolving.
* The instant or sorcery will be exiled.
* (2012-10-01) - supported, has tests
* <p>
* If an instant or sorcery card is discarded while Dryad Militant is on the battlefield, abilities that
* function when a card is discarded (such as madness) still work, even though that card never reaches a
* graveyard. In addition, spells or abilities that check the characteristics of a discarded card (such as
* Chandra Ablazes first ability) can find that card in exile.
* (2012-10-01) - supported, has tests
* *
* @author LevelX2 * @author LevelX2, JayDi85
*/ */
public class DryadMilitantTest extends CardTestPlayerBase { public class DryadMilitantTest extends CardTestPlayerBase {
@ -19,60 +37,251 @@ public class DryadMilitantTest extends CardTestPlayerBase {
* Tests that an instant or sorcery card is moved to exile * Tests that an instant or sorcery card is moved to exile
*/ */
@Test @Test
public void testNormalCase() { public void test_OnResolve_MustWork() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant"); addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
//
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", playerB);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); execute();
assertLife(playerB, 17); assertLife(playerB, 17);
assertExileCount("Lightning Bolt", 1); assertExileCount("Lightning Bolt", 1);
} }
/** /**
* Tests if Dryad Militant dies by damage spell, the * Tests if Dryad Militant dies by damage spell, the
* spell gets exiled * spell gets exiled (see oracle rules)
*/ */
@Test @Test
public void testDiesByDamage() { public void test_DiesByDamage_MustWork() {
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); // If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant"); addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
//
addCard(Zone.HAND, playerA, "Lightning Bolt");
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Dryad Militant"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Lightning Bolt", "Dryad Militant");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); execute();
assertLife(playerB, 20); assertLife(playerB, 20);
assertExileCount("Lightning Bolt", 1); assertExileCount("Lightning Bolt", 1);
} }
/** /**
* Tests if Dryad Militant dies by destroy spell, the * Tests if Dryad Militant dies by destroy spell, the
* spell don't get exiled * spell don't get exiled (see oracle rules)
*/ */
@Test @Test
public void testDiesByDestroy() { public void test_DiesByDestroy_MustNotWork() {
// If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
//
// Destroy target creature. It can't be regenerated.
addCard(Zone.HAND, playerA, "Terminate"); // {B}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1); addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
addCard(Zone.HAND, playerA, "Terminate");
addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Terminate", "Dryad Militant"); castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Terminate", "Dryad Militant");
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT); setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute(); execute();
assertLife(playerB, 20); assertLife(playerB, 20);
assertHandCount(playerA, "Terminate", 0); assertHandCount(playerA, "Terminate", 0);
assertGraveyardCount(playerA, "Terminate", 1); assertGraveyardCount(playerA, "Terminate", 1);
} }
private void prepareGraveyardAndExileWatchers() {
currentGame.getState().addWatcher(new CardsPutIntoGraveyardWatcher());
currentGame.getState().addWatcher(new CardsExiledThisTurnWatcher());
} }
private void assertCardsList(String info, Collection<Card> currentList, Collection<String> needList) {
String current = currentList.stream().map(Card::getName).sorted().collect(Collectors.joining("; "));
String need = needList.stream().sorted().collect(Collectors.joining("; "));
Assert.assertEquals(info, need, current);
}
private void assertReachesGraveyard(Collection<String> needList) {
CardsPutIntoGraveyardWatcher graveyardWatcher = currentGame.getState().getWatcher(CardsPutIntoGraveyardWatcher.class);
assertCardsList(
"graveyard must reaches " + needList.size() + " cards this turn",
graveyardWatcher.getCardsPutIntoGraveyardFromAnywhere(currentGame),
needList
);
}
private void assertReachesExile(Collection<String> needList) {
CardsExiledThisTurnWatcher exileWatcher = currentGame.getState().getWatcher(CardsExiledThisTurnWatcher.class);
assertCardsList(
"exile must reaches " + needList.size() + " cards this turn",
exileWatcher.getCardsExiledThisTurn(currentGame),
needList
);
}
@Test
public void test_MadnessCreature_OnDiscard() {
prepareGraveyardAndExileWatchers();
// Madness {B}{B} (If you discard this card, discard it into exile. When you do, cast it for its madness cost
// or put it into your graveyard.)
addCard(Zone.HAND, playerA, "Gorgon Recluse"); // {3}{B}{B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 2);
//
// Target player discards a card.
addCard(Zone.HAND, playerA, "Raven's Crime"); // {B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
// discard
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Raven's Crime", playerA);
setChoice(playerA, true); // use madness
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
// raven on cast go to grave
// gorgon on madness go to exile and cast instead grave
assertReachesGraveyard(Arrays.asList("Raven's Crime"));
assertReachesExile(Arrays.asList("Gorgon Recluse")); // was cast from exile
}
@Test
public void test_MadnessSorcery_OnDiscard() {
prepareGraveyardAndExileWatchers();
// Alchemists Greeting deals 4 damage to target creature.
// Madness {1}{R} (If you discard this card, discard it into exile. When you do, cast it for its madness
// cost or put it into your graveyard.)
addCard(Zone.HAND, playerA, "Alchemist's Greeting"); // {4}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
//
// Target player discards a card.
addCard(Zone.HAND, playerA, "Raven's Crime"); // {B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
// discard
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Raven's Crime", playerA);
setChoice(playerA, true); // use madness
addTarget(playerA, "Grizzly Bears"); // target of madness card
showGraveyard("after grave", 1, PhaseStep.END_TURN, playerA);
showExile("after exile", 1, PhaseStep.END_TURN, playerA);
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, "Grizzly Bears", 1);
assertGraveyardCount(playerA, "Alchemist's Greeting", 1); // go to grave after madness cast resolve
// raven on cast go to grave
// alchemist on discard with madness go to exile and grave
// bears on damage go to grave
assertReachesGraveyard(Arrays.asList("Raven's Crime", "Alchemist's Greeting", "Grizzly Bears"));
assertReachesExile(Arrays.asList("Alchemist's Greeting"));
}
/**
* Make sure no exile zone reaches due replacement effects (see oracle rules)
*/
@Test
public void test_MadnessSorcery_OnDiscardWithDryadMilitant_BySpell() {
prepareGraveyardAndExileWatchers();
// checking rule:
// If an instant or sorcery card is discarded while Dryad Militant is on the battlefield, abilities that
// function when a card is discarded (such as madness) still work, even though that card never reaches a
// graveyard.
// If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
//
// Alchemists Greeting deals 4 damage to target creature.
// Madness {1}{R} (If you discard this card, discard it into exile. When you do, cast it for its madness
// cost or put it into your graveyard.)
addCard(Zone.HAND, playerA, "Alchemist's Greeting"); // {4}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
//
// Target player discards a card.
addCard(Zone.HAND, playerA, "Raven's Crime"); // {B}
addCard(Zone.BATTLEFIELD, playerA, "Swamp", 1);
// discard
activateManaAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "{T}: Add {B}", 1);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Raven's Crime", playerA);
setChoice(playerA, "Alchemist's Greeting"); // choose replacement effects (madness first)
setChoice(playerA, true); // use madness
addTarget(playerA, "Grizzly Bears"); // target of madness card
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, "Grizzly Bears", 1);
assertExileCount(playerA, "Alchemist's Greeting", 1); // discard -> exile and cast madness -> exile instead grave
// raven on cast go to exile instead grave due dryad effect
// alchemist on discard with madness go to exile, after cast go to exile again instead grave due dryad effect
// bears on damage go to grave (ignored by dryad effect due it's not a instant/sorcery)
assertReachesGraveyard(Arrays.asList("Grizzly Bears"));
assertReachesExile(Arrays.asList("Raven's Crime", "Alchemist's Greeting", "Alchemist's Greeting"));
}
@Test
public void test_MadnessSorcery_OnDiscardWithDryadMilitant_ByChandraAblaze() {
prepareGraveyardAndExileWatchers();
// checking rule:
// In addition, spells or abilities that check the characteristics of a discarded card (such as
// Chandra Ablazes first ability) can find that card in exile.
// If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
//
// Alchemists Greeting deals 4 damage to target creature.
// Madness {1}{R} (If you discard this card, discard it into exile. When you do, cast it for its madness
// cost or put it into your graveyard.)
addCard(Zone.HAND, playerA, "Alchemist's Greeting"); // {4}{R}
addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2);
addCard(Zone.BATTLEFIELD, playerA, "Grizzly Bears", 1);
//
// +1: Discard a card. If a red card is discarded this way, Chandra Ablaze deals 4 damage to any target.
addCard(Zone.BATTLEFIELD, playerA, "Chandra Ablaze");
// discard
activateAbility(1, PhaseStep.PRECOMBAT_MAIN, playerA, "+1");
addTarget(playerA, playerB); // x4 damage by chandra
setChoice(playerA, "Alchemist's Greeting"); // discard by chandra
setChoice(playerA, "Alchemist's Greeting"); // choose replacement effects (madness first)
setChoice(playerA, true); // use madness
addTarget(playerA, "Grizzly Bears"); // target of madness card
setStrictChooseMode(true);
setStopAt(1, PhaseStep.END_TURN);
execute();
assertGraveyardCount(playerA, "Grizzly Bears", 1);
assertExileCount(playerA, "Alchemist's Greeting", 1); // discard -> exile and cast madness -> exile instead grave
assertLife(playerB, 20 - 4); // by chandra
// alchemist on discard with madness go to exile, after cast go to exile again instead grave due dryad effect
// bears on damage go to grave (ignored by dryad effect due it's not a instant/sorcery)
assertReachesGraveyard(Arrays.asList("Grizzly Bears"));
assertReachesExile(Arrays.asList("Alchemist's Greeting", "Alchemist's Greeting"));
}
}

View file

@ -131,12 +131,15 @@ class MadnessReplacementEffect extends ReplacementEffectImpl {
return false; return false;
} }
// TODO, deal with deprecated call if (!controller.moveCards(card, Zone.EXILED, source, game)) {
if (controller.moveCards(card, Zone.EXILED, source, game)) { return false;
game.applyEffects(); // needed to add Madness ability to cards (e.g. by Falkenrath Gorger) }
// needed to add Madness ability to cards (e.g. by Falkenrath Gorger)
game.getState().processAction(game);
GameEvent gameEvent = new MadnessCardExiledEvent(card.getId(), source, controller.getId()); GameEvent gameEvent = new MadnessCardExiledEvent(card.getId(), source, controller.getId());
game.fireEvent(gameEvent); game.fireEvent(gameEvent);
}
return true; return true;
} }

View file

@ -1,5 +1,6 @@
package mage.watchers.common; package mage.watchers.common;
import mage.cards.Card;
import mage.constants.WatcherScope; import mage.constants.WatcherScope;
import mage.constants.Zone; import mage.constants.Zone;
import mage.game.Game; import mage.game.Game;
@ -7,12 +8,22 @@ import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent; import mage.game.events.ZoneChangeEvent;
import mage.watchers.Watcher; import mage.watchers.Watcher;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/** /**
* @author Susucr * Counts cards that was moved to exile zone by any way
* <p>
* Can contain multiple instances of the same card (if it was moved multiple times per turn)
*
* @author Susucr, JayDi85
*/ */
public class CardsExiledThisTurnWatcher extends Watcher { public class CardsExiledThisTurnWatcher extends Watcher {
private int countExiled = 0; private final List<UUID> exiledCards = new ArrayList<>();
public CardsExiledThisTurnWatcher() { public CardsExiledThisTurnWatcher() {
super(WatcherScope.GAME); super(WatcherScope.GAME);
@ -22,17 +33,24 @@ public class CardsExiledThisTurnWatcher extends Watcher {
public void watch(GameEvent event, Game game) { public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.ZONE_CHANGE if (event.getType() == GameEvent.EventType.ZONE_CHANGE
&& ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) { && ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) {
countExiled++; this.exiledCards.add(event.getTargetId());
} }
} }
public int getCountCardsExiledThisTurn() { public int getCountCardsExiledThisTurn() {
return countExiled; return this.exiledCards.size();
}
public List<Card> getCardsExiledThisTurn(Game game) {
return this.exiledCards.stream()
.map(game::getCard)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} }
@Override @Override
public void reset() { public void reset() {
super.reset(); super.reset();
countExiled = 0; this.exiledCards.clear();
} }
} }

View file

@ -16,6 +16,8 @@ import java.util.stream.Collectors;
* Counts how many cards are put into each player's graveyard this turn. * Counts how many cards are put into each player's graveyard this turn.
* Keeps track of the UUIDs of the cards that went to graveyard this turn. * Keeps track of the UUIDs of the cards that went to graveyard this turn.
* from the battlefield, from anywhere other both from anywhere and from only the battlefield. * from the battlefield, from anywhere other both from anywhere and from only the battlefield.
* <p>
* Can contain multiple instances of the same card (if it was moved multiple times per turn)
* *
* @author LevelX2 * @author LevelX2
*/ */
@ -40,7 +42,8 @@ public class CardsPutIntoGraveyardWatcher extends Watcher {
} }
UUID playerId = event.getPlayerId(); UUID playerId = event.getPlayerId();
if (playerId == null || game.getCard(event.getTargetId()) == null) { Card card = game.getCard(event.getTargetId());
if (playerId == null || card == null) {
return; return;
} }