diff --git a/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java b/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java index 2ac54d49a8..8cf9ca61e1 100644 --- a/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java +++ b/Mage.Tests/src/test/java/org/mage/test/cards/cost/adventure/AdventureCardsTest.java @@ -41,6 +41,7 @@ public class AdventureCardsTest extends CardTestPlayerBase { castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Treats to Share"); setStopAt(1, PhaseStep.BEGIN_COMBAT); execute(); + assertActionsCount(playerA, 1); assertHandCount(playerA, 0); assertPermanentCount(playerA, "Food", 1); assertExileCount(playerA, "Curious Pair", 1); @@ -335,7 +336,7 @@ public class AdventureCardsTest extends CardTestPlayerBase { } @Test - public void testAdventurePermanentText() { + public void testRimrockKnightPermanentText() { setStrictChooseMode(true); addCard(Zone.BATTLEFIELD, playerA, "Mountain", 2); addCard(Zone.HAND, playerA, "Rimrock Knight"); @@ -354,4 +355,144 @@ public class AdventureCardsTest extends CardTestPlayerBase { Permanent rimrock = getPermanent("Rimrock Knight"); Assert.assertEquals(rimrock.getRules(currentGame).get(0), "{this} can't block."); } + + /* + * Tests for Rule 601.3e: + * 601.3e If a rule or effect states that only an alternative set of characteristics or a subset of characteristics + * are considered to determine if a card or copy of a card is legal to cast, those alternative characteristics + * replace the object’s characteristics prior to determining whether the player may begin to cast it. + * Example: Garruk’s Horde says, in part, “You may cast the top card of your library if it’s a creature card.” If + * you control Garruk’s Horde and the top card of your library is a noncreature card with morph, you may cast it + * using its morph ability. + * Example: Melek, Izzet Paragon says, in part, “You may cast the top card of your library if it’s an instant or + * sorcery card.” If you control Melek, Izzet Paragon and the top card of your library is Giant Killer, an + * adventurer creature card whose Adventure is an instant named Chop Down, you may cast Chop Down but not Giant + * Killer. If instead you control Garruk’s Horde and the top card of your library is Giant Killer, you may cast + * Giant Killer but not Chop Down. + */ + + @Test + public void testCastTreatsToShareWithMelek() { + /* + * Melek, Izzet Paragon {4}{U}{R} + * Legendary Creature — Weird Wizard + * Play with the top card of your library revealed. + * You may cast the top card of your library if it's an instant or sorcery card. + * Whenever you cast an instant or sorcery spell from your library, copy it. You may choose new targets for the copy. + * 2/4 + */ + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Melek, Izzet Paragon"); + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Curious Pair"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Treats to Share"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + assertHandCount(playerA, 0); + assertPermanentCount(playerA, 4); + assertPermanentCount(playerA, "Food", 2); + assertPermanentCount(playerA, "Curious Pair", 0); + assertExileCount(playerA, "Curious Pair", 1); + assertGraveyardCount(playerA, 0); + } + + @Test + public void testCantCastCuriousPairWithMelek() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Melek, Izzet Paragon"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Curious Pair"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Curious Pair"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertActionsCount(playerA, 1); + assertPermanentCount(playerA, "Curious Pair", 0); + assertLibraryCount(playerA, 1); + } + + @Test + public void testCastCuriousPairWithGarruksHorde() { + /* + * Garruk's Horde {5}{G}{G} + * Creature — Beast + * Trample + * Play with the top card of your library revealed. + * You may cast the top card of your library if it's a creature card. + * 7/7 + */ + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Garruk's Horde"); + addCard(Zone.BATTLEFIELD, playerA, "Forest", 2); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Curious Pair"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Curious Pair"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + assertHandCount(playerA, 0); + assertPermanentCount(playerA, "Food", 0); + assertPermanentCount(playerA, "Curious Pair", 1); + assertExileCount(playerA, 0); + assertGraveyardCount(playerA, 0); + } + + @Test + public void testCantCastTreatsToShareWithGarruksHorde() { + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Garruk's Horde"); + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Curious Pair"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Treats to Share"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertActionsCount(playerA, 1); + assertPermanentCount(playerA, "Food", 0); + assertLibraryCount(playerA, 1); + } + + @Test + public void testCastTreatsToShareWithWrennAndSixEmblem() { + /* + * Melek, Izzet Paragon {4}{U}{R} + * Legendary Creature — Weird Wizard + * Play with the top card of your library revealed. + * You may cast the top card of your library if it's an instant or sorcery card. + * Whenever you cast an instant or sorcery spell from your library, copy it. You may choose new targets for the copy. + * 2/4 + */ + setStrictChooseMode(true); + addCard(Zone.BATTLEFIELD, playerA, "Melek, Izzet Paragon"); + addCard(Zone.BATTLEFIELD, playerA, "Forest"); + removeAllCardsFromLibrary(playerA); + addCard(Zone.LIBRARY, playerA, "Curious Pair"); + + castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Treats to Share"); + + setStopAt(1, PhaseStep.BEGIN_COMBAT); + execute(); + + assertAllCommandsUsed(); + assertHandCount(playerA, 0); + assertPermanentCount(playerA, 4); + assertPermanentCount(playerA, "Food", 2); + assertPermanentCount(playerA, "Curious Pair", 0); + assertExileCount(playerA, "Curious Pair", 1); + assertGraveyardCount(playerA, 0); + } } diff --git a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java index 35e63cc361..446876b1a5 100644 --- a/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java +++ b/Mage/src/main/java/mage/abilities/effects/common/continuous/PlayTheTopCardEffect.java @@ -2,7 +2,10 @@ package mage.abilities.effects.common.continuous; import java.util.UUID; + +import mage.MageObject; import mage.abilities.Ability; +import mage.abilities.SpellAbility; import mage.abilities.effects.AsThoughEffectImpl; import mage.cards.Card; import mage.constants.AsThoughEffectType; @@ -47,12 +50,27 @@ public class PlayTheTopCardEffect extends AsThoughEffectImpl { @Override public boolean applies(UUID objectId, Ability source, UUID affectedControllerId, Game game) { + return applies(objectId, null, source, game, affectedControllerId); + } + + @Override + public boolean applies(UUID objectId, Ability affectedAbility, Ability source, Game game, UUID playerId) { Card cardOnTop = game.getCard(objectId); + Card cardToCheckProperties = cardOnTop; + + // Check each ability individually, as e.g. Adventures and associated creatures may get different results from the filter. + if (affectedAbility != null) { + MageObject sourceObject = affectedAbility.getSourceObject(game); + if (sourceObject != null && sourceObject instanceof Card) { + cardToCheckProperties = (Card) sourceObject; + } + } + if (cardOnTop != null - && affectedControllerId.equals(source.getControllerId()) + && playerId.equals(source.getControllerId()) && cardOnTop.isOwnedBy(source.getControllerId()) - && (!cardOnTop.getManaCost().isEmpty() || cardOnTop.isLand()) - && filter.match(cardOnTop, game)) { + && (!cardToCheckProperties.getManaCost().isEmpty() || cardToCheckProperties.isLand()) + && filter.match(cardToCheckProperties, game)) { Player player = game.getPlayer(cardOnTop.getOwnerId()); if (player != null && cardOnTop.equals(player.getLibrary().getFromTop(game))) { return true; diff --git a/Mage/src/main/java/mage/cards/AdventureCard.java b/Mage/src/main/java/mage/cards/AdventureCard.java index 5562205b8a..26c649ba16 100644 --- a/Mage/src/main/java/mage/cards/AdventureCard.java +++ b/Mage/src/main/java/mage/cards/AdventureCard.java @@ -21,7 +21,9 @@ public abstract class AdventureCard extends CardImpl { public AdventureCard(UUID ownerId, CardSetInfo setInfo, CardType[] types, CardType[] typesSpell, String costs, String adventureName, String costsSpell) { super(ownerId, setInfo, types, costs); spellCard = new AdventureCardSpellImpl(ownerId, setInfo, adventureName, typesSpell, costsSpell, this); - this.addAbility(spellCard.getSpellAbility()); + Ability adventureAbility = spellCard.getSpellAbility(); + this.addAbility(adventureAbility); + adventureAbility.setSourceId(spellCard.getId()); } public AdventureCard(AdventureCard card) { diff --git a/Mage/src/main/java/mage/game/command/emblems/WrennAndSixEmblem.java b/Mage/src/main/java/mage/game/command/emblems/WrennAndSixEmblem.java index 8ca6f0b212..4ae3d49a17 100644 --- a/Mage/src/main/java/mage/game/command/emblems/WrennAndSixEmblem.java +++ b/Mage/src/main/java/mage/game/command/emblems/WrennAndSixEmblem.java @@ -4,6 +4,7 @@ import mage.abilities.Ability; import mage.abilities.common.SimpleStaticAbility; import mage.abilities.effects.ContinuousEffectImpl; import mage.abilities.keyword.RetraceAbility; +import mage.cards.AdventureCard; import mage.cards.Card; import mage.constants.*; import mage.game.Game; @@ -45,6 +46,10 @@ class WrennAndSixEmblemEffect extends ContinuousEffectImpl { if (card == null || !card.isInstantOrSorcery()) { continue; } + if (card instanceof AdventureCard) { + // Adventure cards are castable per https://twitter.com/elishffrn/status/1179047911729946624 + card = ((AdventureCard) card).getSpellCard(); + } Ability ability = new RetraceAbility(card); ability.setSourceId(cardId); ability.setControllerId(card.getOwnerId()); diff --git a/Mage/src/main/java/mage/players/PlayerImpl.java b/Mage/src/main/java/mage/players/PlayerImpl.java index 77aa858ddc..e302a66c1d 100644 --- a/Mage/src/main/java/mage/players/PlayerImpl.java +++ b/Mage/src/main/java/mage/players/PlayerImpl.java @@ -3218,6 +3218,24 @@ public abstract class PlayerImpl implements Player, Serializable { } } + private List cardPlayableAbilities(Game game, Card card) { + List playable = new ArrayList(); + if (card != null) { + for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.HAND)) { + if (ability instanceof SpellAbility + && null != game.getContinuousEffects().asThough(card.getId(), + AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, ability, getId(), game)) { + playable.add(ability); + } else if (ability instanceof PlayLandAbility + && null != game.getContinuousEffects().asThough(card.getId(), + AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, card.getSpellAbility(), getId(), game)) { + playable.add(ability); + } + } + } + return playable; + } + @Override public List getPlayable(Game game, boolean hidden) { return getPlayable(game, hidden, Zone.ALL, true); @@ -3294,20 +3312,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll || fromZone == Zone.EXILED) { for (ExileZone exile : game.getExile().getExileZones()) { for (Card card : exile.getCards(game)) { - if (null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, this.getId(), game)) { - for (Ability ability : card.getAbilities()) { - if (ability.getZone().match(Zone.HAND)) { - ability.setControllerId(this.getId()); // controller must be set for case owner != caster - if (ability instanceof ActivatedAbility) { - if (((ActivatedAbility) ability).canActivate(playerId, game).canActivate()) { - playable.add(ability); - } - } - ability.setControllerId(card.getOwnerId()); - } - } - } + playable.addAll(cardPlayableAbilities(game, card)); } } } @@ -3316,14 +3321,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (fromAll) { for (Cards revealedCards : game.getState().getRevealed().values()) { for (Card card : revealedCards.getCards(game)) { - if (null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, null, this.getId(), game)) { - for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.HAND)) { - if (ability instanceof SpellAbility || ability instanceof PlayLandAbility) { - playable.add(ability); - } - } - } + playable.addAll(cardPlayableAbilities(game, card)); } } } @@ -3335,14 +3333,7 @@ public abstract class PlayerImpl implements Player, Serializable { if (player != null) { if (/*player.isTopCardRevealed() &&*/player.getLibrary().hasCards()) { Card card = player.getLibrary().getFromTop(game); - if (card != null && null != game.getContinuousEffects().asThough(card.getId(), - AsThoughEffectType.PLAY_FROM_NOT_OWN_HAND_ZONE, card.getSpellAbility(), getId(), game)) { - for (ActivatedAbility ability : card.getAbilities().getActivatedAbilities(Zone.HAND)) { - if (ability instanceof SpellAbility || ability instanceof PlayLandAbility) { - playable.add(ability); - } - } - } + playable.addAll(cardPlayableAbilities(game, card)); } } }