diff --git a/Mage.Sets/src/mage/cards/c/ChandraAblaze.java b/Mage.Sets/src/mage/cards/c/ChandraAblaze.java
index 4826388a400..813308966ed 100644
--- a/Mage.Sets/src/mage/cards/c/ChandraAblaze.java
+++ b/Mage.Sets/src/mage/cards/c/ChandraAblaze.java
@@ -45,12 +45,14 @@ public final class ChandraAblaze extends CardImpl {
ability.addEffect(new ChandraAblazeEffect2());
ability.addTarget(new TargetAnyTarget());
this.addAbility(ability);
+
// -2: Each player discards their hand, then draws three cards.
ability = new LoyaltyAbility(new DiscardHandAllEffect(), -2);
Effect effect = new DrawCardAllEffect(3);
effect.setText(", then draws three cards");
ability.addEffect(effect);
this.addAbility(ability);
+
// -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);
this.addAbility(ability);
@@ -84,6 +86,11 @@ class ChandraAblazeEffect1 extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
+ // If you activate Chandra Ablaze’s first ability, you don’t 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());
if (player != null) {
TargetDiscard target = new TargetDiscard(player.getId());
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UnboundFlourishingTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UnboundFlourishingTest.java
index 7fe96d2e752..be56cc73bd6 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UnboundFlourishingTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/continuous/UnboundFlourishingTest.java
@@ -59,7 +59,7 @@ public class UnboundFlourishingTest extends CardTestPlayerBase {
}
@Test
- public void test_OnCastInstantOrSourcery_MustCopy() {
+ public void test_OnCastInstantOrSorcery_MustCopy() {
addCard(Zone.BATTLEFIELD, playerA, "Unbound Flourishing", 1);
//
// Banefire deals X damage to any target.
diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DryadMilitantTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DryadMilitantTest.java
index ea904d27993..0057343ce95 100644
--- a/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DryadMilitantTest.java
+++ b/Mage.Tests/src/test/java/org/mage/test/cards/replacement/DryadMilitantTest.java
@@ -1,78 +1,287 @@
-
package org.mage.test.cards.replacement;
+import mage.cards.Card;
import mage.constants.PhaseStep;
import mage.constants.Zone;
+import mage.watchers.common.CardsExiledThisTurnWatcher;
+import mage.watchers.common.CardsPutIntoGraveyardWatcher;
+import org.junit.Assert;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.stream.Collectors;
+
/**
- * Dryad Militant
- * Creature - Dryad Soldier 1/1 {G/W}
- * If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
- *
- * @author LevelX2
+ * Dryad Militant oracle rules:
+ *
+ * If an instant or sorcery spell destroys Dryad Militant directly (like Murder does), that instant or sorcery
+ * card will be put into its owner’s 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
+ *
+ * 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 Ablaze’s first ability) can find that card in exile.
+ * (2012-10-01) - supported, has tests
+ *
+ * @author LevelX2, JayDi85
*/
public class DryadMilitantTest extends CardTestPlayerBase {
-
+
/**
* Tests that an instant or sorcery card is moved to exile
*/
@Test
- public void testNormalCase() {
- addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
- addCard(Zone.HAND, playerA, "Lightning Bolt");
+ public void test_OnResolve_MustWork() {
+ // If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
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);
+
+ setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
-
+
assertLife(playerB, 17);
assertExileCount("Lightning Bolt", 1);
- }
+ }
+
/**
* Tests if Dryad Militant dies by damage spell, the
- * spell gets exiled
+ * spell gets exiled (see oracle rules)
*/
@Test
- public void testDiesByDamage() {
- addCard(Zone.BATTLEFIELD, playerA, "Mountain", 1);
- addCard(Zone.HAND, playerA, "Lightning Bolt");
+ public void test_DiesByDamage_MustWork() {
+ // If an instant or sorcery card would be put into a graveyard from anywhere, exile it instead.
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");
+
+ setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
-
+
assertLife(playerB, 20);
-
assertExileCount("Lightning Bolt", 1);
- }
-
+ }
+
/**
* Tests if Dryad Militant dies by destroy spell, the
- * spell don't get exiled
+ * spell don't get exiled (see oracle rules)
*/
@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, "Swamp", 1);
- addCard(Zone.HAND, playerA, "Terminate");
- addCard(Zone.BATTLEFIELD, playerB, "Dryad Militant");
-
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Terminate", "Dryad Militant");
+
+ setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
-
- assertLife(playerB, 20);
+ assertLife(playerB, 20);
assertHandCount(playerA, "Terminate", 0);
assertGraveyardCount(playerA, "Terminate", 1);
- }
-}
+ }
+ private void prepareGraveyardAndExileWatchers() {
+ currentGame.getState().addWatcher(new CardsPutIntoGraveyardWatcher());
+ currentGame.getState().addWatcher(new CardsExiledThisTurnWatcher());
+ }
+ private void assertCardsList(String info, Collection currentList, Collection 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 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 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();
+
+ // Alchemist’s 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");
+ //
+ // Alchemist’s 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 Ablaze’s 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");
+ //
+ // Alchemist’s 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"));
+ }
+}
\ No newline at end of file
diff --git a/Mage/src/main/java/mage/abilities/keyword/MadnessAbility.java b/Mage/src/main/java/mage/abilities/keyword/MadnessAbility.java
index 347cf080648..646275ace2f 100644
--- a/Mage/src/main/java/mage/abilities/keyword/MadnessAbility.java
+++ b/Mage/src/main/java/mage/abilities/keyword/MadnessAbility.java
@@ -131,13 +131,16 @@ class MadnessReplacementEffect extends ReplacementEffectImpl {
return false;
}
- // TODO, deal with deprecated call
- if (controller.moveCards(card, Zone.EXILED, source, game)) {
- game.applyEffects(); // needed to add Madness ability to cards (e.g. by Falkenrath Gorger)
- GameEvent gameEvent = new MadnessCardExiledEvent(card.getId(), source, controller.getId());
- game.fireEvent(gameEvent);
+ if (!controller.moveCards(card, Zone.EXILED, source, game)) {
+ return false;
}
+ // 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());
+ game.fireEvent(gameEvent);
+
return true;
}
diff --git a/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java b/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java
index 783eb3bf0c0..42a02626ae8 100644
--- a/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java
+++ b/Mage/src/main/java/mage/watchers/common/CardsExiledThisTurnWatcher.java
@@ -1,5 +1,6 @@
package mage.watchers.common;
+import mage.cards.Card;
import mage.constants.WatcherScope;
import mage.constants.Zone;
import mage.game.Game;
@@ -7,12 +8,22 @@ import mage.game.events.GameEvent;
import mage.game.events.ZoneChangeEvent;
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
+ *
+ * Can contain multiple instances of the same card (if it was moved multiple times per turn)
+ *
+ * @author Susucr, JayDi85
*/
public class CardsExiledThisTurnWatcher extends Watcher {
- private int countExiled = 0;
+ private final List exiledCards = new ArrayList<>();
public CardsExiledThisTurnWatcher() {
super(WatcherScope.GAME);
@@ -22,17 +33,24 @@ public class CardsExiledThisTurnWatcher extends Watcher {
public void watch(GameEvent event, Game game) {
if (event.getType() == GameEvent.EventType.ZONE_CHANGE
&& ((ZoneChangeEvent) event).getToZone() == Zone.EXILED) {
- countExiled++;
+ this.exiledCards.add(event.getTargetId());
}
}
public int getCountCardsExiledThisTurn() {
- return countExiled;
+ return this.exiledCards.size();
+ }
+
+ public List getCardsExiledThisTurn(Game game) {
+ return this.exiledCards.stream()
+ .map(game::getCard)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
}
@Override
public void reset() {
super.reset();
- countExiled = 0;
+ this.exiledCards.clear();
}
}
diff --git a/Mage/src/main/java/mage/watchers/common/CardsPutIntoGraveyardWatcher.java b/Mage/src/main/java/mage/watchers/common/CardsPutIntoGraveyardWatcher.java
index a282318f028..08fa4ec9a3b 100644
--- a/Mage/src/main/java/mage/watchers/common/CardsPutIntoGraveyardWatcher.java
+++ b/Mage/src/main/java/mage/watchers/common/CardsPutIntoGraveyardWatcher.java
@@ -16,6 +16,8 @@ import java.util.stream.Collectors;
* 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.
* from the battlefield, from anywhere other both from anywhere and from only the battlefield.
+ *
+ * Can contain multiple instances of the same card (if it was moved multiple times per turn)
*
* @author LevelX2
*/
@@ -40,7 +42,8 @@ public class CardsPutIntoGraveyardWatcher extends Watcher {
}
UUID playerId = event.getPlayerId();
- if (playerId == null || game.getCard(event.getTargetId()) == null) {
+ Card card = game.getCard(event.getTargetId());
+ if (playerId == null || card == null) {
return;
}