implement [MKM] Deadly Cover-Up; refactor to common class (#12500)

* Genericize most variants of SearchTargetGraveyardHandLibraryForCardNameAndExileEffect

* Implement Deadly Cover-Up

* int maxAmount instead of boolean maxFour, use player.chooseTarget

* Fix Surgical Extraction test, use withNotTarget for exile choices

* Add tests, fix MDFC's back sides' names from being matched against
This commit is contained in:
ssk97 2024-06-24 20:58:57 -07:00 committed by GitHub
parent 0a23521ece
commit f53954d56a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 349 additions and 662 deletions

View file

@ -1,13 +1,14 @@
package mage.cards.c;
import java.util.UUID;
import mage.abilities.effects.common.CounterTargetAndSearchGraveyardHandLibraryEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.target.TargetSpell;
import java.util.UUID;
/**
*
* @author LevelX2
@ -21,7 +22,7 @@ public final class Counterbore extends CardImpl {
// Counter target spell.
// Search its controller's graveyard, hand, and library for all cards with the same name as that spell and exile them. Then that player shuffles their library.
this.getSpellAbility().addTarget(new TargetSpell());
this.getSpellAbility().addEffect(new CounterTargetAndSearchGraveyardHandLibraryEffect().concatBy("."));
this.getSpellAbility().addEffect(new CounterTargetAndSearchGraveyardHandLibraryEffect());
}
private Counterbore(final Counterbore card) {

View file

@ -1,7 +1,6 @@
package mage.cards.c;
import java.util.UUID;
import mage.abilities.effects.common.ExileTargetAndSearchGraveyardHandLibraryEffect;
import mage.abilities.keyword.DevoidAbility;
import mage.cards.CardImpl;
@ -9,6 +8,8 @@ import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.target.common.TargetNonBasicLandPermanent;
import java.util.UUID;
/**
*
* @author fireshoes
@ -23,7 +24,7 @@ public final class CrumbleToDust extends CardImpl {
// Exile target nonbasic land. Search its controller's graveyard, hand, and library for any number of cards with the same name as that land and exile them. Then that player shuffles their library.
this.getSpellAbility().addTarget(new TargetNonBasicLandPermanent());
this.getSpellAbility().addEffect(new ExileTargetAndSearchGraveyardHandLibraryEffect(false, "its controller's", "any number of cards with the same name as that land"));
this.getSpellAbility().addEffect(new ExileTargetAndSearchGraveyardHandLibraryEffect(true, "its controller's", "any number of cards with the same name as that land"));
}
private CrumbleToDust(final CrumbleToDust card) {

View file

@ -0,0 +1,85 @@
package mage.cards.d;
import mage.abilities.Ability;
import mage.abilities.condition.common.CollectedEvidenceCondition;
import mage.abilities.decorator.ConditionalOneShotEffect;
import mage.abilities.effects.common.DestroyAllEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.abilities.keyword.CollectEvidenceAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.players.Player;
import mage.target.common.TargetCardInOpponentsGraveyard;
import java.util.UUID;
/**
* @author notgreat
*/
public final class DeadlyCoverUp extends CardImpl {
public DeadlyCoverUp(UUID ownerId, CardSetInfo setInfo) {
super(ownerId, setInfo, new CardType[]{CardType.SORCERY}, "{3}{B}{B}");
// As an additional cost to cast this spell, you may collect evidence 6.
this.addAbility(new CollectEvidenceAbility(8));
// Destroy all creatures. If evidence was collected, exile a card from an opponent's graveyard. Then search its owner's graveyard, hand, and library for any number of cards with that name and exile them. That player shuffles, then draws a card for each card exiled from their hand this way.
this.getSpellAbility().addEffect(new DestroyAllEffect(StaticFilters.FILTER_PERMANENT_CREATURES));
this.getSpellAbility().addEffect(new ConditionalOneShotEffect(new DeadlyCoverUpEffect(), CollectedEvidenceCondition.instance,
"If evidence was collected, exile a card from an opponent's graveyard. Then search its owner's "
+ "graveyard, hand, and library for any number of cards with that name and exile them. "
+ "That player shuffles, then draws a card for each card exiled from their hand this way."));
}
private DeadlyCoverUp(final DeadlyCoverUp card) {
super(card);
}
@Override
public DeadlyCoverUp copy() {
return new DeadlyCoverUp(this);
}
}
class DeadlyCoverUpEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
DeadlyCoverUpEffect() {
super(true, "its owner's", "any number of cards with the same name as that card", true);
this.staticText = "exile a card from an opponent's graveyard. Then search its owner's "
+ "graveyard, hand, and library for any number of cards with that name and exile them. "
+ "That player shuffles, then draws a card for each card exiled from their hand this way.";
}
private DeadlyCoverUpEffect(final DeadlyCoverUpEffect effect) {
super(effect);
}
@Override
public DeadlyCoverUpEffect copy() {
return new DeadlyCoverUpEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Player player = game.getPlayer(source.getControllerId());
TargetCardInOpponentsGraveyard target = new TargetCardInOpponentsGraveyard(StaticFilters.FILTER_CARD);
target.withNotTarget(true);
if (player != null && player.chooseTarget(Outcome.Exile, target, source, game)) {
Card cardToExile = game.getCard(target.getFirstTarget());
if (cardToExile == null) {
return false;
}
player.moveCards(cardToExile, Zone.EXILED, source, game);
this.applySearchAndExile(game, source, cardToExile.getName(), cardToExile.getOwnerId());
}
return true;
}
}

View file

@ -1,27 +1,19 @@
package mage.cards.e;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.abilities.keyword.SplitSecondAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInGraveyard;
import mage.target.common.TargetCardInLibrary;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
@ -57,10 +49,10 @@ public final class Extirpate extends CardImpl {
}
}
class ExtirpateEffect extends OneShotEffect {
class ExtirpateEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
ExtirpateEffect() {
super(Outcome.Exile);
super(false, "its owner's", "all cards with the same name as that card");
this.staticText = "Choose target card in a graveyard other than "
+ "a basic land card. Search its owner's graveyard, hand, "
+ "and library for all cards with the same name "
@ -78,60 +70,10 @@ class ExtirpateEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
Player controller = game.getPlayer(source.getControllerId());
MageObject sourceObject = game.getObject(source);
Card chosenCard = game.getCard(getTargetPointer().getFirst(game, source));
if (chosenCard != null && sourceObject != null && controller != null) {
Player owner = game.getPlayer(chosenCard.getOwnerId());
if (owner == null) {
return false;
}
// Exile all cards with the same name
// Building a card filter with the name
FilterCard filterNamedCard = new FilterCard();
String nameToSearch = CardUtil.getCardNameForSameNameSearch(chosenCard);
filterNamedCard.add(new NamePredicate(nameToSearch));
// The cards you're searching for must be found and exiled if they're in the graveyard because it's a public zone.
// Finding those cards in the hand and library is optional, because those zones are hidden (even if the hand is temporarily revealed).
// search cards in graveyard
for (Card checkCard : owner.getGraveyard().getCards(game)) {
if (checkCard.getName().equals(chosenCard.getName())) {
controller.moveCardToExileWithInfo(checkCard, null, "", source, game, Zone.GRAVEYARD, true);
}
}
// search cards in hand
filterNamedCard.setMessage("card named " + chosenCard.getLogName() + " in the hand of " + owner.getLogName());
TargetCard targetCardInHand = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filterNamedCard);
targetCardInHand.withNotTarget(true);
if (controller.chooseTarget(Outcome.Exile, owner.getHand(), targetCardInHand, source, game)) {
List<UUID> targets = targetCardInHand.getTargets();
for (UUID targetId : targets) {
Card targetCard = owner.getHand().get(targetId, game);
if (targetCard != null) {
controller.moveCardToExileWithInfo(targetCard, null, "", source, game, Zone.HAND, true);
}
}
}
// search cards in Library
filterNamedCard.setMessage("card named " + chosenCard.getName() + " in the library of " + owner.getName());
TargetCardInLibrary targetCardInLibrary = new TargetCardInLibrary(0, Integer.MAX_VALUE, filterNamedCard);
if (controller.searchLibrary(targetCardInLibrary, source, game, owner.getId())) {
List<UUID> targets = targetCardInLibrary.getTargets();
for (UUID targetId : targets) {
Card targetCard = owner.getLibrary().getCard(targetId, game);
if (targetCard != null) {
controller.moveCardToExileWithInfo(targetCard, null, "", source, game, Zone.LIBRARY, true);
}
}
}
owner.shuffleLibrary(source, game);
return true;
if (chosenCard != null) {
return super.applySearchAndExile(game, source, CardUtil.getCardNameForSameNameSearch(chosenCard), chosenCard.getOwnerId());
}
return false;
}
}

View file

@ -2,20 +2,20 @@ package mage.cards.l;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.cards.*;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.TargetPlayer;
import mage.target.common.TargetCardInLibrary;
import mage.util.CardUtil;
import java.util.UUID;
@ -43,7 +43,7 @@ public final class Lobotomy extends CardImpl {
}
}
class LobotomyEffect extends OneShotEffect {
class LobotomyEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
private static final FilterCard filter = new FilterCard("card other than a basic land card");
@ -52,7 +52,7 @@ class LobotomyEffect extends OneShotEffect {
}
public LobotomyEffect() {
super(Outcome.Benefit);
super(false, "that player's", "all cards with the same name as the chosen card");
staticText = "Target player reveals their hand, then you choose a card other than a basic land card from it. Search that player's graveyard, hand, and library for all cards with the same name as the chosen card and exile them. Then that player shuffles";
}
@ -78,54 +78,7 @@ class LobotomyEffect extends OneShotEffect {
chosenCard = game.getCard(target.getFirstTarget());
}
// Exile all cards with the same name
// Building a card filter with the name
FilterCard filterNamedCards = new FilterCard();
String nameToSearch = "---";// so no card matches
if (chosenCard != null) {
nameToSearch = CardUtil.getCardNameForSameNameSearch(chosenCard);
filterNamedCards.setMessage("cards named " + nameToSearch);
}
filterNamedCards.add(new NamePredicate(nameToSearch));
Cards cardsToExile = new CardsImpl();
// The cards you're searching for must be found and exiled if they're in the graveyard because it's a public zone.
// Finding those cards in the hand and library is optional, because those zones are hidden (even if the hand is temporarily revealed).
// search cards in graveyard
if (chosenCard != null) {
for (Card checkCard : targetPlayer.getGraveyard().getCards(game)) {
if (checkCard.getName().equals(chosenCard.getName())) {
cardsToExile.add(checkCard);
}
}
// search cards in hand
TargetCard targetCardsHand = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filterNamedCards);
controller.chooseTarget(outcome, targetPlayer.getHand(), targetCardsHand, source, game);
for (UUID cardId : targetCardsHand.getTargets()) {
Card card = game.getCard(cardId);
if (card != null) {
cardsToExile.add(card);
}
}
}
// search cards in Library
// If the player has no nonland cards in their hand, you can still search that player's library and have that player shuffle it.
if (chosenCard != null || controller.chooseUse(outcome, "Search library anyway?", source, game)) {
TargetCardInLibrary targetCardsLibrary = new TargetCardInLibrary(0, Integer.MAX_VALUE, filterNamedCards);
controller.searchLibrary(targetCardsLibrary, source, game, targetPlayer.getId());
for (UUID cardId : targetCardsLibrary.getTargets()) {
Card card = game.getCard(cardId);
if (card != null) {
cardsToExile.add(card);
}
}
}
if (!cardsToExile.isEmpty()) {
controller.moveCards(cardsToExile, Zone.EXILED, source, game);
}
targetPlayer.shuffleLibrary(source, game);
return true;
return applySearchAndExile(game, source, CardUtil.getCardNameForSameNameSearch(chosenCard), getTargetPointer().getFirst(game, source));
}
return false;
}

View file

@ -1,20 +1,18 @@
package mage.cards.l;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.common.ChooseACardNameEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetPlayer;
import java.util.UUID;
/**
*
* @author LevelX2
@ -43,8 +41,7 @@ public final class LostLegacy extends CardImpl {
class LostLegacyEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
LostLegacyEffect() {
super(true, "target player's", "any number of cards with that name");
this.staticText = "Search target player's graveyard, hand, and library for any number of cards with that name and exile them. That player shuffles, then draws a card for each card exiled from their hand this way";
super(true, "target player's", "any number of cards with that name", true);
}
private LostLegacyEffect(final LostLegacyEffect effect) {
@ -56,15 +53,7 @@ class LostLegacyEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExi
String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY);
Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source));
if (targetPlayer != null && cardName != null && !cardName.isEmpty()) {
FilterCard filter = new FilterCard();
filter.add(new NamePredicate(cardName));
int cardsInHandBefore = targetPlayer.getHand().count(filter, game);
boolean result = super.applySearchAndExile(game, source, cardName, getTargetPointer().getFirst(game, source));
int cardsExiled = cardsInHandBefore - targetPlayer.getHand().count(filter, game);
if (cardsExiled > 0) {
targetPlayer.drawCards(cardsExiled, source, game);
}
return result;
return applySearchAndExile(game, source, cardName, getTargetPointer().getFirst(game, source));
}
return false;
}

View file

@ -50,6 +50,7 @@ public final class Necromentia extends CardImpl {
}
}
//Based on SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
class NecromentiaEffect extends OneShotEffect {
NecromentiaEffect() {

View file

@ -4,9 +4,8 @@ import mage.MageInt;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.common.DealsCombatDamageToAPlayerTriggeredAbility;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.abilities.keyword.FlyingAbility;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
@ -16,11 +15,9 @@ import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.common.FilterNonlandCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInLibrary;
import mage.util.CardUtil;
import java.util.UUID;
@ -57,7 +54,7 @@ public final class ShimianSpecter extends CardImpl {
}
}
class ShimianSpecterEffect extends OneShotEffect {
class ShimianSpecterEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
private static final FilterCard filter = new FilterCard("nonland card");
@ -66,7 +63,7 @@ class ShimianSpecterEffect extends OneShotEffect {
}
public ShimianSpecterEffect() {
super(Outcome.Benefit);
super(false, "that player's", "all cards with the same name as that card");
staticText = "that player reveals their hand. You choose a nonland card from it. "
+ "Search that player's graveyard, hand, and library for all cards "
+ "with the same name as that card and exile them. Then that "
@ -92,62 +89,10 @@ class ShimianSpecterEffect extends OneShotEffect {
// You choose a nonland card from it
TargetCard target = new TargetCard(Zone.HAND, new FilterNonlandCard());
target.withNotTarget(true);
Card chosenCard = null;
if (target.canChoose(controller.getId(), source, game)
&& controller.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), target, source, game)) {
chosenCard = game.getCard(target.getFirstTarget());
return applySearchAndExile(game, source, CardUtil.getCardNameForSameNameSearch(game.getCard(target.getFirstTarget())), getTargetPointer().getFirst(game, source));
}
// Exile all cards with the same name
// Building a card filter with the name
FilterCard filterNamedCards = new FilterCard();
String nameToSearch = "---";// so no card matches
if (chosenCard != null) {
nameToSearch = CardUtil.getCardNameForSameNameSearch(chosenCard);
}
filterNamedCards.add(new NamePredicate(nameToSearch));
// The cards you're searching for must be found and exiled if they're
// in the graveyard because it's a public zone.
// Finding those cards in the hand and library is optional, because
// those zones are hidden (even if the hand is temporarily revealed).
// search cards in graveyard
if (chosenCard != null) {
for (Card checkCard : targetPlayer.getGraveyard().getCards(game)) {
if (checkCard.getName().equals(chosenCard.getName())) {
controller.moveCardToExileWithInfo(checkCard, null, "",
source, game, Zone.GRAVEYARD, true);
}
}
// search cards in hand
TargetCard targetHandCards = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filterNamedCards);
controller.chooseTarget(Outcome.Benefit, targetPlayer.getHand(), targetHandCards, source, game);
for (UUID cardId : targetHandCards.getTargets()) {
Card card = game.getCard(cardId);
if (card != null) {
controller.moveCardToExileWithInfo(card, null, "",
source, game, Zone.HAND, true);
}
}
}
// search cards in Library
// If the player has no nonland cards in their hand, you can still search
// that player's library and have that player shuffle it.
if (chosenCard != null
|| controller.chooseUse(outcome, "Search library anyway?", source, game)) {
TargetCardInLibrary targetCardsLibrary = new TargetCardInLibrary(0, Integer.MAX_VALUE, filterNamedCards);
controller.searchLibrary(targetCardsLibrary, source, game, targetPlayer.getId());
for (UUID cardId : targetCardsLibrary.getTargets()) {
Card card = game.getCard(cardId);
if (card != null) {
controller.moveCardToExileWithInfo(card, null, "", source, game, Zone.LIBRARY, true);
}
}
targetPlayer.shuffleLibrary(source, game);
}
return true;
}
return false;
}

View file

@ -1,25 +1,18 @@
package mage.cards.s;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.Predicates;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInGraveyard;
import mage.target.common.TargetCardInLibrary;
import mage.util.CardUtil;
import java.util.List;
import java.util.UUID;
/**
@ -53,10 +46,10 @@ public final class SurgicalExtraction extends CardImpl {
}
}
class SurgicalExtractionEffect extends OneShotEffect {
class SurgicalExtractionEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
SurgicalExtractionEffect() {
super(Outcome.Detriment);
super(true, "its owner's", "any number of cards with the same name as that card");
this.staticText = "Choose target card in a graveyard other than a basic land card. "
+ "Search its owner's graveyard, hand, and library for any number of cards "
+ "with the same name as that card and exile them. Then that player shuffles";
@ -73,68 +66,10 @@ class SurgicalExtractionEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
// 6/1/2011 "Any number of cards" means just that. If you wish, you can choose to
// leave some or all of the cards with the same name as the targeted card,
// including that card, in the zone they're in.
Card chosenCard = game.getCard(source.getFirstTarget());
Player controller = game.getPlayer(source.getControllerId());
if (chosenCard != null && controller != null) {
Player owner = game.getPlayer(chosenCard.getOwnerId());
if (owner != null) {
String nameToSearch = CardUtil.getCardNameForSameNameSearch(chosenCard);
FilterCard filterNamedCard = new FilterCard("card named " + nameToSearch);
filterNamedCard.add(new NamePredicate(nameToSearch));
// cards in Graveyard
int cardsCount = owner.getGraveyard().count(filterNamedCard, game);
if (cardsCount > 0) {
filterNamedCard.setMessage("card named " + nameToSearch
+ " in the graveyard of " + owner.getName());
TargetCardInGraveyard target = new TargetCardInGraveyard(0, cardsCount, filterNamedCard);
if (controller.chooseTarget(Outcome.Exile, owner.getGraveyard(), target, source, game)) {
List<UUID> targets = target.getTargets();
for (UUID targetId : targets) {
Card targetCard = owner.getGraveyard().get(targetId, game);
if (targetCard != null) {
controller.moveCardToExileWithInfo(targetCard, null,
"", source, game, Zone.GRAVEYARD, true);
}
}
}
}
// cards in Hand
filterNamedCard.setMessage("card named " + nameToSearch + " in the hand of " + owner.getName());
TargetCard targetCardInHand = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filterNamedCard);
targetCardInHand.withNotTarget(true);
if (controller.chooseTarget(Outcome.Exile, owner.getHand(), targetCardInHand, source, game)) {
List<UUID> targets = targetCardInHand.getTargets();
for (UUID targetId : targets) {
Card targetCard = owner.getHand().get(targetId, game);
if (targetCard != null) {
controller.moveCardToExileWithInfo(targetCard, null, "", source, game, Zone.HAND, true);
}
}
}
// cards in Library
filterNamedCard.setMessage("card named " + nameToSearch + " in the library of " + owner.getName());
TargetCardInLibrary targetCardInLibrary = new TargetCardInLibrary(0, Integer.MAX_VALUE, filterNamedCard);
if (controller.searchLibrary(targetCardInLibrary, source, game, owner.getId())) {
List<UUID> targets = targetCardInLibrary.getTargets();
for (UUID targetId : targets) {
Card targetCard = owner.getLibrary().getCard(targetId, game);
if (targetCard != null) {
controller.moveCardToExileWithInfo(targetCard, null, "", source, game, Zone.LIBRARY, true);
}
}
}
owner.shuffleLibrary(source, game);
return true;
}
Card chosenCard = game.getCard(getTargetPointer().getFirst(game, source));
if (chosenCard != null) {
return applySearchAndExile(game, source, CardUtil.getCardNameForSameNameSearch(chosenCard), chosenCard.getOwnerId());
}
return false;
}
}

View file

@ -1,20 +1,12 @@
package mage.cards.t;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.Card;
import mage.abilities.effects.common.CounterTargetAndSearchGraveyardHandLibraryEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.filter.StaticFilters;
import mage.game.Game;
import mage.game.stack.StackObject;
import mage.players.Player;
import mage.target.TargetSpell;
import mage.util.CardUtil;
import java.util.Objects;
import java.util.UUID;
/**
@ -26,7 +18,7 @@ public final class TestOfTalents extends CardImpl {
super(ownerId, setInfo, new CardType[]{CardType.INSTANT}, "{1}{U}");
// Counter target instant or sorcery spell. Search its controller's graveyard, hand, and library for any number of cards with the same name as that spell and exile them. That player shuffles, then draws a card for each card exiled from their hand this way.
this.getSpellAbility().addEffect(new TestOfTalentsEffect());
this.getSpellAbility().addEffect(new CounterTargetAndSearchGraveyardHandLibraryEffect(true, "its controller's", "any number of cards with the same name as that spell", true));
this.getSpellAbility().addTarget(new TargetSpell(StaticFilters.FILTER_SPELL_INSTANT_OR_SORCERY));
}
@ -39,64 +31,3 @@ public final class TestOfTalents extends CardImpl {
return new TestOfTalents(this);
}
}
class TestOfTalentsEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
TestOfTalentsEffect() {
super(false, "its controller's", "all cards with the same name as that spell");
staticText = "counter target instant or sorcery spell. Search its controller's graveyard, hand, " +
"and library for any number of cards with the same name as that spell and exile them. " +
"That player shuffles, then draws a card for each card exiled from their hand this way";
}
private TestOfTalentsEffect(final TestOfTalentsEffect effect) {
super(effect);
}
@Override
public TestOfTalentsEffect copy() {
return new TestOfTalentsEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
StackObject stackObject = game.getStack().getStackObject(source.getFirstTarget());
if (stackObject == null) {
return false;
}
MageObject targetObject = game.getObject(stackObject.getSourceId());
String cardName;
if (targetObject instanceof Card) {
cardName = targetObject.getName();
} else {
cardName = "";
}
UUID searchPlayerId = stackObject.getControllerId();
Player player = game.getPlayer(searchPlayerId);
if (player == null) {
return false;
}
int previousCount = player
.getHand()
.getCards(game)
.stream()
.map(MageObject::getName)
.filter(Objects::nonNull)
.mapToInt(s -> CardUtil.haveSameNames(s, cardName) ? 1 : 0)
.sum();
game.getStack().counter(source.getFirstTarget(), source, game);
this.applySearchAndExile(game, source, cardName, searchPlayerId);
int newCount = player
.getHand()
.getCards(game)
.stream()
.map(MageObject::getName)
.filter(Objects::nonNull)
.mapToInt(s -> CardUtil.haveSameNames(s, cardName) ? 1 : 0)
.sum();
if (previousCount > newCount) {
player.drawCards(previousCount - newCount, source, game);
}
return true;
}
}

View file

@ -1,19 +1,13 @@
package mage.cards.t;
import mage.abilities.Ability;
import mage.abilities.common.SimpleStaticAbility;
import mage.abilities.condition.common.FatefulHourCondition;
import mage.abilities.effects.common.ExileTargetAndSearchGraveyardHandLibraryEffect;
import mage.abilities.effects.common.cost.SpellCostReductionSourceEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.game.permanent.Permanent;
import mage.players.Player;
import mage.target.common.TargetCreatureOrPlaneswalker;
import java.util.UUID;
@ -35,7 +29,7 @@ public final class TheEnd extends CardImpl {
).setRuleAtTheTop(true));
// Exile target creature or planeswalker. Search its controller's graveyard, hand, and library for any number of cards with the same name as that permanent and exile them. That player shuffles, then draws card for each card exiled from their hand this way.
this.getSpellAbility().addEffect(new TheEndEffect());
this.getSpellAbility().addEffect(new ExileTargetAndSearchGraveyardHandLibraryEffect(true, "its controller's", "any number of cards with the same name as that permanent"));
this.getSpellAbility().addTarget(new TargetCreatureOrPlaneswalker());
}
@ -48,48 +42,3 @@ public final class TheEnd extends CardImpl {
return new TheEnd(this);
}
}
class TheEndEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
TheEndEffect() {
super(true, "its controller's", "any number of cards with the same name as that permanent");
this.staticText = "Exile target creature or planeswalker. Search its controller's graveyard, hand, and library" +
" for any number of cards with the same name as that permanent and exile them." +
" That player shuffles, then draws a card for each card exiled from their hand this way";
}
private TheEndEffect(final TheEndEffect effect) {
super(effect);
}
@Override
public TheEndEffect copy() {
return new TheEndEffect(this);
}
@Override
public boolean apply(Game game, Ability source) {
Permanent permanent = game.getPermanent(source.getFirstTarget());
if (permanent == null) {
return false;
}
String name = permanent.getName();
Player player = game.getPlayer(permanent.getControllerId());
if (player == null) {
return false;
}
player.moveCards(permanent, Zone.EXILED, source, game);
FilterCard filter = new FilterCard();
filter.add(new NamePredicate(name));
int cardsInHandBefore = player.getHand().count(filter, game);
boolean result = super.applySearchAndExile(game, source, name, player.getId());
int cardsExiled = cardsInHandBefore - player.getHand().count(filter, game);
if (cardsExiled > 0) {
player.drawCards(cardsExiled, source, game);
}
return result;
}
}

View file

@ -5,22 +5,13 @@ import mage.abilities.common.ActivateAsSorceryActivatedAbility;
import mage.abilities.costs.common.ExileSourceCost;
import mage.abilities.costs.common.TapSourceCost;
import mage.abilities.costs.mana.GenericManaCost;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ChooseACardNameEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.SuperType;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInLibrary;
import mage.target.common.TargetOpponent;
import java.util.UUID;
@ -56,12 +47,10 @@ public final class TheStoneBrain extends CardImpl {
}
}
class TheStoneBrainEffect extends OneShotEffect {
class TheStoneBrainEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
TheStoneBrainEffect() {
super(Outcome.Benefit);
staticText = "Search target opponent's graveyard, hand, and library for up to four cards with that name " +
"and exile them. That player shuffles, then draws a card for each card exiled from their hand this way";
super(true, "target opponent's", "up to four cards with that name", true, 4);
}
private TheStoneBrainEffect(final TheStoneBrainEffect effect) {
@ -76,61 +65,6 @@ class TheStoneBrainEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY);
Player controller = game.getPlayer(source.getControllerId());
if (cardName != null && controller != null) {
int numberOfCardsStillToRemove = 4;
int numberOfCardsExiledFromHand = 0;
Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source));
if (targetPlayer != null) {
FilterCard filter = new FilterCard("card named " + cardName);
filter.add(new NamePredicate(cardName));
// cards in Graveyard
int cardsCount = (cardName.isEmpty() ? 0 : targetPlayer.getGraveyard().count(filter, game));
if (cardsCount > 0) {
filter.setMessage("card named " + cardName + " in the graveyard of " + targetPlayer.getName());
TargetCard target = new TargetCard(Math.min(cardsCount, numberOfCardsStillToRemove),
Math.min(cardsCount, numberOfCardsStillToRemove), Zone.GRAVEYARD, filter);
if (controller.choose(Outcome.Exile, targetPlayer.getGraveyard(), target, source, game)) {
numberOfCardsStillToRemove -= target.getTargets().size();
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
}
// cards in Hand
if (numberOfCardsStillToRemove > 0) {
cardsCount = (cardName.isEmpty() ? 0 : targetPlayer.getHand().count(filter, game));
filter.setMessage("card named " + cardName + " in the hand of " + targetPlayer.getName());
TargetCard target = new TargetCard(0, Math.min(cardsCount, numberOfCardsStillToRemove), Zone.HAND, filter);
if (controller.choose(Outcome.Exile, targetPlayer.getHand(), target, source, game)) {
numberOfCardsExiledFromHand = target.getTargets().size();
numberOfCardsStillToRemove -= target.getTargets().size();
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
}
// cards in Library
if (numberOfCardsStillToRemove > 0) {
Cards cardsInLibrary = new CardsImpl();
cardsInLibrary.addAllCards(targetPlayer.getLibrary().getCards(game));
cardsCount = (cardName.isEmpty() ? 0 : cardsInLibrary.count(filter, game));
filter.setMessage("card named " + cardName + " in the library of " + targetPlayer.getLogName());
TargetCardInLibrary targetLib = new TargetCardInLibrary(0, Math.min(cardsCount, numberOfCardsStillToRemove), filter);
if (controller.choose(Outcome.Exile, cardsInLibrary, targetLib, source, game)) {
controller.moveCards(new CardsImpl(targetLib.getTargets()), Zone.EXILED, source, game);
}
}
targetPlayer.shuffleLibrary(source, game);
if (numberOfCardsExiledFromHand > 0) {
game.processAction();
targetPlayer.drawCards(numberOfCardsExiledFromHand, source, game);
}
}
return true;
}
return false;
return applySearchAndExile(game, source, cardName, getTargetPointer().getFirst(game, source));
}
}

View file

@ -2,26 +2,18 @@ package mage.cards.t;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ChooseACardNameEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.Card;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetPlayer;
import mage.target.common.TargetCardInLibrary;
import mage.util.CardUtil;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import mage.target.TargetCard;
/**
* @author jeffwadsworth
@ -52,7 +44,7 @@ public final class ThoughtHemorrhage extends CardImpl {
}
}
class ThoughtHemorrhageEffect extends OneShotEffect {
class ThoughtHemorrhageEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
static final String rule = "Target player reveals their hand. "
+ "{this} deals 3 damage to that player for each card with "
@ -62,7 +54,7 @@ class ThoughtHemorrhageEffect extends OneShotEffect {
+ "Then that player shuffles";
public ThoughtHemorrhageEffect() {
super(Outcome.Exile);
super(false, "that player's", "all cards with that name");
staticText = rule;
}
@ -93,49 +85,8 @@ class ThoughtHemorrhageEffect extends OneShotEffect {
if (cardsFound > 0) {
targetPlayer.damage(3 * cardsFound, source.getSourceId(), source, game);
}
// Exile all cards with the same name
// Building a card filter with the name
FilterCard filterNamedCards = new FilterCard();
filterNamedCards.add(new NamePredicate(cardName));
Set<Card> toExile = new LinkedHashSet<>();
// The cards you're searching for must be found and exiled if
// they're in the graveyard because it's a public zone.
// Finding those cards in the hand and library is optional,
// because those zones are hidden (even if the hand is temporarily revealed).
// search cards in graveyard
for (Card checkCard : targetPlayer.getGraveyard().getCards(game)) {
if (checkCard.getName().equals(cardName)) {
toExile.add(checkCard);
}
}
// search cards in Hand
TargetCard targetCardInHand = new TargetCard(0, Integer.MAX_VALUE, Zone.HAND, filterNamedCards);
if (controller.chooseTarget(Outcome.Exile, targetPlayer.getHand(), targetCardInHand, source, game)) {
List<UUID> targets = targetCardInHand.getTargets();
for (UUID targetId : targets) {
Card targetCard = targetPlayer.getHand().get(targetId, game);
if (targetCard != null) {
toExile.add(targetCard);
}
}
}
// search cards in Library
// If the player has no nonland cards in their hand, you can still search
// that player's library and have that player shuffle it.
TargetCardInLibrary targetCardsLibrary = new TargetCardInLibrary(0, Integer.MAX_VALUE, filterNamedCards);
controller.searchLibrary(targetCardsLibrary, source, game, targetPlayer.getId());
for (UUID cardId : targetCardsLibrary.getTargets()) {
Card card = game.getCard(cardId);
if (card != null) {
toExile.add(card);
}
}
controller.moveCards(toExile, Zone.EXILED, source, game);
targetPlayer.shuffleLibrary(source, game);
return true;
return applySearchAndExile(game, source, cardName, source.getFirstTarget());
}
}
return false;

View file

@ -1,24 +1,16 @@
package mage.cards.u;
import java.util.UUID;
import mage.abilities.Ability;
import mage.abilities.effects.OneShotEffect;
import mage.abilities.effects.common.ChooseACardNameEffect;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.CardImpl;
import mage.cards.CardSetInfo;
import mage.cards.Cards;
import mage.cards.CardsImpl;
import mage.constants.CardType;
import mage.constants.Outcome;
import mage.constants.Zone;
import mage.filter.FilterCard;
import mage.filter.predicate.mageobject.NamePredicate;
import mage.game.Game;
import mage.players.Player;
import mage.target.TargetCard;
import mage.target.common.TargetCardInLibrary;
import mage.target.common.TargetOpponent;
import java.util.UUID;
/**
*
* @author LevelX2
@ -44,11 +36,10 @@ public final class UnmooredEgo extends CardImpl {
}
}
class UnmooredEgoEffect extends OneShotEffect {
class UnmooredEgoEffect extends SearchTargetGraveyardHandLibraryForCardNameAndExileEffect {
UnmooredEgoEffect() {
super(Outcome.Benefit);
this.staticText = "Search target opponent's graveyard, hand, and library for up to four cards with that name and exile them. That player shuffles, then draws a card for each card exiled from their hand this way";
super(false, "target opponent's", "up to four cards with that name", true, 4);
}
private UnmooredEgoEffect(final UnmooredEgoEffect effect) {
@ -63,62 +54,6 @@ class UnmooredEgoEffect extends OneShotEffect {
@Override
public boolean apply(Game game, Ability source) {
String cardName = (String) game.getState().getValue(source.getSourceId().toString() + ChooseACardNameEffect.INFO_KEY);
Player controller = game.getPlayer(source.getControllerId());
if (cardName != null && controller != null) {
int numberOfCardsStillToRemove = 4;
int numberOfCardsExiledFromHand = 0;
Player targetPlayer = game.getPlayer(getTargetPointer().getFirst(game, source));
if (targetPlayer != null) {
FilterCard filter = new FilterCard("card named " + cardName);
filter.add(new NamePredicate(cardName));
// cards in Graveyard
int cardsCount = (cardName.isEmpty() ? 0 : targetPlayer.getGraveyard().count(filter, game));
if (cardsCount > 0) {
filter.setMessage("card named " + cardName + " in the graveyard of " + targetPlayer.getName());
TargetCard target = new TargetCard(Math.min(cardsCount, numberOfCardsStillToRemove),
Math.min(cardsCount, numberOfCardsStillToRemove), Zone.GRAVEYARD, filter);
if (controller.choose(Outcome.Exile, targetPlayer.getGraveyard(), target, source, game)) {
numberOfCardsStillToRemove -= target.getTargets().size();
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
}
// cards in Hand
if (numberOfCardsStillToRemove > 0) {
cardsCount = (cardName.isEmpty() ? 0 : targetPlayer.getHand().count(filter, game));
filter.setMessage("card named " + cardName + " in the hand of " + targetPlayer.getName());
TargetCard target = new TargetCard(0, Math.min(cardsCount, numberOfCardsStillToRemove), Zone.HAND, filter);
if (controller.choose(Outcome.Exile, targetPlayer.getHand(), target, source, game)) {
numberOfCardsExiledFromHand = target.getTargets().size();
numberOfCardsStillToRemove -= target.getTargets().size();
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
}
// cards in Library
if (numberOfCardsStillToRemove > 0) {
Cards cardsInLibrary = new CardsImpl();
cardsInLibrary.addAllCards(targetPlayer.getLibrary().getCards(game));
cardsCount = (cardName.isEmpty() ? 0 : cardsInLibrary.count(filter, game));
filter.setMessage("card named " + cardName + " in the library of " + targetPlayer.getLogName());
TargetCardInLibrary targetLib = new TargetCardInLibrary(0, Math.min(cardsCount, numberOfCardsStillToRemove), filter);
if (controller.choose(Outcome.Exile, cardsInLibrary, targetLib, source, game)) {
controller.moveCards(new CardsImpl(targetLib.getTargets()), Zone.EXILED, source, game);
}
}
targetPlayer.shuffleLibrary(source, game);
if (numberOfCardsExiledFromHand > 0) {
game.processAction();
targetPlayer.drawCards(numberOfCardsExiledFromHand, source, game);
}
}
return true;
}
return false;
return applySearchAndExile(game, source, cardName, getTargetPointer().getFirst(game, source));
}
}

View file

@ -97,6 +97,7 @@ public final class MurdersAtKarlovManor extends ExpansionSet {
cards.add(new SetCardInfo("Curious Cadaver", 194, Rarity.UNCOMMON, mage.cards.c.CuriousCadaver.class));
cards.add(new SetCardInfo("Curious Inquiry", 51, Rarity.UNCOMMON, mage.cards.c.CuriousInquiry.class));
cards.add(new SetCardInfo("Deadly Complication", 195, Rarity.UNCOMMON, mage.cards.d.DeadlyComplication.class));
cards.add(new SetCardInfo("Deadly Cover-Up", 83, Rarity.RARE, mage.cards.d.DeadlyCoverUp.class));
cards.add(new SetCardInfo("Deduce", 52, Rarity.COMMON, mage.cards.d.Deduce.class));
cards.add(new SetCardInfo("Defenestrated Phantom", 11, Rarity.COMMON, mage.cards.d.DefenestratedPhantom.class));
cards.add(new SetCardInfo("Delney, Streetwise Lookout", 12, Rarity.MYTHIC, mage.cards.d.DelneyStreetwiseLookout.class));

View file

@ -0,0 +1,153 @@
package org.mage.test.cards.abilities.oneshot.exile;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* {@link mage.cards.s.SurgicalExtraction Surgical Extraction}
* {B/P}
* Instant
* ({B/P} can be paid with either {B} or 2 life.)
* Choose target card in a graveyard other than a basic land card.
* Search its owners graveyard, hand, and library for any number of cards with the same name as that card and exile them.
* Then that player shuffles.
*
* @author LevelX2
*/
public class SearchNameExileTests extends CardTestPlayerBase {
/**
* I noticed that surgical extraction did not allow me to select any cards
* to exile when I targeted breaking // entering. It did however allow my
* opponent to target lingering souls so it could be a split card
* interaction or just a random glitch.
*/
@Test
public void testSearchAndExileSplitCards() {
addCard(Zone.HAND, playerA, "Surgical Extraction", 1); // Instant {B/P}
addCard(Zone.GRAVEYARD, playerB, "Breaking // Entering", 2);
addCard(Zone.HAND, playerB, "Breaking // Entering", 1);
addCard(Zone.LIBRARY, playerB, "Breaking // Entering", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Surgical Extraction", "Breaking // Entering");
setChoice(playerA, "Yes"); // Pay 2 life to cast instead of {B}
setChoice(playerA, "Breaking // Entering^Breaking // Entering"); // Graveyard
setChoice(playerA, "Breaking // Entering"); // Hand
setChoice(playerA, "Breaking // Entering"); // Library
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Surgical Extraction", 1);
assertGraveyardCount(playerB, "Breaking // Entering", 0);
assertLibraryCount(playerB, "Breaking // Entering", 0);
assertHandCount(playerB, "Breaking // Entering", 0);
assertHandCount(playerB, 0);
assertExileCount(playerB, "Breaking // Entering", 4);
}
/**
* {@link mage.cards.t.TestOfTalents Test Of Talents} 1U Instant
* Counter target instant or sorcery spell. Search its controllers graveyard, hand, and library for
* any number of cards with the same name as that spell and exile them. That player shuffles,
* then draws a card for each card exiled from their hand this way.
*/
@Test
public void testSearchAndExileSplitSpell() {
addCard(Zone.HAND, playerA, "Test of Talents", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.GRAVEYARD, playerB, "Ready // Willing", 1);
addCard(Zone.HAND, playerB, "Ready // Willing", 2);
addCard(Zone.LIBRARY, playerB, "Ready // Willing", 1);
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "fused Ready // Willing");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Test of Talents", "Ready // Willing", "Ready // Willing");
setChoice(playerA, "Ready // Willing^Ready // Willing"); // Should be 2 in Graveyard now, take both
setChoice(playerA, "Ready // Willing"); // Hand
setChoice(playerA, "Ready // Willing"); // Library
setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Test of Talents", 1);
assertGraveyardCount(playerB, "Ready // Willing", 0);
assertLibraryCount(playerB, "Ready // Willing", 0);
assertHandCount(playerB, "Ready // Willing", 0);
assertHandCount(playerB, 1); //add 2, cast 1, last is exiled+redrawn
assertExileCount(playerB, "Ready // Willing", 4);
}
@Test
public void testFailSearchAndExileMDFCSpell() {
addCard(Zone.HAND, playerA, "Test of Talents", 1); // Instant 1U
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.GRAVEYARD, playerB, "Flamescroll Celebrant", 1);
addCard(Zone.HAND, playerB, "Flamescroll Celebrant", 2);
addCard(Zone.LIBRARY, playerB, "Flamescroll Celebrant", 1);
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "Revel in Silence");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Test of Talents", "Revel in Silence", "Revel in Silence");
// Should be no choices in the graveyard available since the back side spell doesn't match the front side name
// Non-strict mode tries to select all possible targets, no current method to check if choosing is impossible
// setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerB, "Flamescroll Celebrant", 2);
assertLibraryCount(playerB, "Flamescroll Celebrant", 1);
assertHandCount(playerB, "Flamescroll Celebrant", 1);
assertHandCount(playerB, 1); //add 2, cast 1
assertExileCount(playerB, "Flamescroll Celebrant", 0);
}
//Asserts "exile all possible" behavior for MDFC test above
@Test
public void testSearchAndExileSplitSpellNonstrict() {
addCard(Zone.HAND, playerA, "Test of Talents", 1);
addCard(Zone.BATTLEFIELD, playerA, "Island", 2);
addCard(Zone.GRAVEYARD, playerB, "Ready // Willing", 1);
addCard(Zone.HAND, playerB, "Ready // Willing", 2);
addCard(Zone.LIBRARY, playerB, "Ready // Willing", 1);
addCard(Zone.BATTLEFIELD, playerB, "Plains", 2);
addCard(Zone.BATTLEFIELD, playerB, "Forest", 2);
addCard(Zone.BATTLEFIELD, playerB, "Swamp", 2);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerB, "fused Ready // Willing");
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Test of Talents", "Ready // Willing", "Ready // Willing");
// setStrictChooseMode(true);
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Test of Talents", 1);
assertGraveyardCount(playerB, "Ready // Willing", 0);
assertLibraryCount(playerB, "Ready // Willing", 0);
assertHandCount(playerB, "Ready // Willing", 0);
assertHandCount(playerB, 1); //add 2, cast 1, last is exiled+redrawn
assertExileCount(playerB, "Ready // Willing", 4);
}
}

View file

@ -1,53 +0,0 @@
package org.mage.test.cards.abilities.oneshot.exile;
import mage.constants.PhaseStep;
import mage.constants.Zone;
import org.junit.Test;
import org.mage.test.serverside.base.CardTestPlayerBase;
/**
* {@link mage.cards.s.SurgicalExtraction Surgical Extraction}
* {B/P}
* Instant
* ({B/P} can be paid with either {B} or 2 life.)
* Choose target card in a graveyard other than a basic land card.
* Search its owners graveyard, hand, and library for any number of cards with the same name as that card and exile them.
* Then that player shuffles.
*
* @author LevelX2
*/
public class SurgicalExtractionTest extends CardTestPlayerBase {
/**
* I noticed that surgical extraction did not allow me to select any cards
* to exile when I targeted breaking // entering. It did however allow my
* opponent to target lingering souls so it could be a split card
* interaction or just a random glitch.
*/
@Test
public void testSearchAndExileSplitCards() {
addCard(Zone.HAND, playerA, "Surgical Extraction", 1); // Instant {B/P}
addCard(Zone.GRAVEYARD, playerB, "Breaking // Entering", 2);
addCard(Zone.HAND, playerB, "Breaking // Entering", 1);
addCard(Zone.LIBRARY, playerB, "Breaking // Entering", 1);
setStrictChooseMode(true);
castSpell(1, PhaseStep.PRECOMBAT_MAIN, playerA, "Surgical Extraction", "Breaking // Entering");
setChoice(playerA, "Yes"); // Pay 2 life to cast instead of {B}
addTarget(playerA, "Breaking // Entering^Breaking // Entering"); // Graveyard
addTarget(playerA, "Breaking // Entering"); // Hand
addTarget(playerA, "Breaking // Entering"); // Library
setStopAt(1, PhaseStep.BEGIN_COMBAT);
execute();
assertGraveyardCount(playerA, "Surgical Extraction", 1);
assertGraveyardCount(playerB, "Breaking // Entering", 0);
assertLibraryCount(playerB, "Breaking // Entering", 0);
assertHandCount(playerB, "Breaking // Entering", 0);
assertExileCount(playerB, "Breaking // Entering", 4);
}
}

View file

@ -1,16 +1,15 @@
package mage.abilities.effects.common;
import java.util.UUID;
import mage.MageObject;
import mage.abilities.Ability;
import mage.abilities.Mode;
import mage.abilities.effects.common.search.SearchTargetGraveyardHandLibraryForCardNameAndExileEffect;
import mage.cards.Card;
import mage.game.Game;
import mage.game.stack.StackObject;
import mage.target.TargetSpell;
import mage.util.CardUtil;
import java.util.UUID;
/**
*
* @author LevelX2
@ -20,11 +19,15 @@ public class CounterTargetAndSearchGraveyardHandLibraryEffect extends SearchTarg
public CounterTargetAndSearchGraveyardHandLibraryEffect() {
this(false,"its controller's", "all cards with the same name as that spell" );
this(false, "its controller's", "all cards with the same name as that spell");
}
public CounterTargetAndSearchGraveyardHandLibraryEffect(boolean graveyardExileOptional, String searchWhatText, String searchForText) {
super(graveyardExileOptional, searchWhatText, searchForText);
this(graveyardExileOptional, searchWhatText, searchForText, false);
}
public CounterTargetAndSearchGraveyardHandLibraryEffect(boolean graveyardExileOptional, String searchWhatText, String searchForText, boolean drawForEachHandCard) {
super(graveyardExileOptional, searchWhatText, searchForText, drawForEachHandCard);
}
protected CounterTargetAndSearchGraveyardHandLibraryEffect(final CounterTargetAndSearchGraveyardHandLibraryEffect effect) {
@ -39,26 +42,19 @@ public class CounterTargetAndSearchGraveyardHandLibraryEffect extends SearchTarg
@Override
public boolean apply(Game game, Ability source) {
boolean result = false;
String cardName = "";
UUID searchPlayerId = null;
if (source.getTargets().get(0) instanceof TargetSpell) {
UUID objectId = source.getFirstTarget();
StackObject stackObject = game.getStack().getStackObject(objectId);
if (stackObject != null) {
MageObject targetObject = game.getObject(stackObject.getSourceId());
if (targetObject instanceof Card) {
cardName = targetObject.getName();
}
searchPlayerId = stackObject.getControllerId();
String cardName = stackObject.getName();
UUID searchPlayerId = stackObject.getControllerId();
result = game.getStack().counter(objectId, source, game);
// 5/1/2008: If the targeted spell can't be countered (it's Vexing Shusher, for example),
// that spell will remain on the stack. Counterbore will continue to resolve. You still
// get to search for and exile all other cards with that name.
this.applySearchAndExile(game, source, cardName, searchPlayerId);
}
}
// 5/1/2008: If the targeted spell can't be countered (it's Vexing Shusher, for example),
// that spell will remain on the stack. Counterbore will continue to resolve. You still
// get to search for and exile all other cards with that name.
this.applySearchAndExile(game, source, cardName, searchPlayerId);
return result;
}

View file

@ -18,6 +18,11 @@ public class ExileTargetAndSearchGraveyardHandLibraryEffect extends SearchTarget
this.staticText = ""; // since parent class overrides static text but we need to use a target
}
public ExileTargetAndSearchGraveyardHandLibraryEffect(boolean graveyardExileOptional, String searchWhatText, String searchForText, boolean drawForEachHandCard) {
super(graveyardExileOptional, searchWhatText, searchForText, drawForEachHandCard);
this.staticText = ""; // since parent class overrides static text but we need to use a target
}
private ExileTargetAndSearchGraveyardHandLibraryEffect(final ExileTargetAndSearchGraveyardHandLibraryEffect effect) {
super(effect);
}

View file

@ -30,13 +30,25 @@ public abstract class SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
* 2/1/2005: The copies must be found if they are in publicly viewable zones. Finding copies while searching private zones is optional.
*/
protected boolean graveyardExileOptional;
protected boolean drawForEachHandCard;
protected int maxAmount;
protected SearchTargetGraveyardHandLibraryForCardNameAndExileEffect(boolean graveyardExileOptional, String searchWhatText, String searchForText) {
this(graveyardExileOptional, searchWhatText, searchForText, false);
}
protected SearchTargetGraveyardHandLibraryForCardNameAndExileEffect(boolean graveyardExileOptional, String searchWhatText, String searchForText, boolean drawForEachHandCard) {
this(graveyardExileOptional, searchWhatText, searchForText, drawForEachHandCard, Integer.MAX_VALUE);
}
protected SearchTargetGraveyardHandLibraryForCardNameAndExileEffect(boolean graveyardExileOptional, String searchWhatText, String searchForText, boolean drawForEachHandCard, int maxAmount) {
super(Outcome.Exile);
this.searchWhatText = searchWhatText;
this.searchForText = searchForText;
this.graveyardExileOptional = graveyardExileOptional;
this.staticText = "search " + searchWhatText + " graveyard, hand, and library for " + searchForText + " and exile them. Then that player shuffles";
this.drawForEachHandCard = drawForEachHandCard;
this.maxAmount = maxAmount;
this.staticText = "search " + searchWhatText + " graveyard, hand, and library for " + searchForText + " and exile them. " +
(drawForEachHandCard ? "That player shuffles, then draws a card for each card exiled from their hand this way" : "Then that player shuffles");
}
protected SearchTargetGraveyardHandLibraryForCardNameAndExileEffect(final SearchTargetGraveyardHandLibraryForCardNameAndExileEffect effect) {
@ -44,6 +56,8 @@ public abstract class SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
this.searchWhatText = effect.searchWhatText;
this.searchForText = effect.searchForText;
this.graveyardExileOptional = effect.graveyardExileOptional;
this.drawForEachHandCard = effect.drawForEachHandCard;
this.maxAmount = effect.maxAmount;
}
/**
@ -59,37 +73,50 @@ public abstract class SearchTargetGraveyardHandLibraryForCardNameAndExileEffect
if (cardName != null && controller != null) {
Player targetPlayer = game.getPlayer(targetPlayerId);
if (targetPlayer != null) {
int handCards = 0;
int maxRemaining = maxAmount;
FilterCard filter = new FilterCard("card named \"" + cardName + "\"");
filter.add(new NamePredicate(cardName));
// cards in Graveyard
int cardsCount = targetPlayer.getGraveyard().count(filter, game);
int cardsCount = Math.min(targetPlayer.getGraveyard().count(filter, game), maxRemaining);
if (cardsCount > 0) {
filter.setMessage("card named " + cardName + " in the graveyard of " + targetPlayer.getName());
TargetCard target = new TargetCard((graveyardExileOptional ? 0 : cardsCount), cardsCount, Zone.GRAVEYARD, filter);
target.withNotTarget(true);
if (controller.choose(Outcome.Exile, targetPlayer.getGraveyard(), target, source, game)) {
maxRemaining -= target.getTargets().size();
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
}
// cards in Hand
cardsCount = targetPlayer.getHand().count(filter, game);
cardsCount = Math.min(targetPlayer.getHand().count(filter, game), maxRemaining);
filter.setMessage("card named " + cardName + " in the hand of " + targetPlayer.getName());
TargetCard target = new TargetCard(0, cardsCount, Zone.HAND, filter);
target.withNotTarget(true);
if (controller.choose(Outcome.Exile, targetPlayer.getHand(), target, source, game)) {
maxRemaining -= target.getTargets().size();
if (drawForEachHandCard) {
handCards = target.getTargets().size();
}
controller.moveCards(new CardsImpl(target.getTargets()), Zone.EXILED, source, game);
}
// cards in Library
Cards cardsInLibrary = new CardsImpl();
cardsInLibrary.addAllCards(targetPlayer.getLibrary().getCards(game));
cardsCount = cardsInLibrary.count(filter, game);
cardsCount = Math.min(cardsInLibrary.count(filter, game), maxRemaining);
filter.setMessage("card named " + cardName + " in the library of " + targetPlayer.getLogName());
TargetCardInLibrary targetLib = new TargetCardInLibrary(0, cardsCount, filter);
if (controller.choose(Outcome.Exile, cardsInLibrary, targetLib, source, game)) {
controller.moveCards(new CardsImpl(targetLib.getTargets()), Zone.EXILED, source, game);
}
targetPlayer.shuffleLibrary(source, game);
if (handCards > 0) {
targetPlayer.drawCards(handCards, source, game);
}
}
return true;

View file

@ -44,8 +44,14 @@ public class NamePredicate implements Predicate<MageObject> {
}
// If a player names a card, the player may name either half of a split card, but not both.
// A split card has the chosen name if one of its two names matches the chosen name.
// Same for modal double faces cards
if (input instanceof CardWithHalves) {
// This is NOT the same for double faced cards, where only the front side matches
// Test of Talents ruling:
// If the back face of a modal double-faced card is countered, you will not be able to exile any cards,
// including the one that you countered, because those cards have only their front-face characteristics
// (including name) in the graveyard, hand, and library. (2021-04-16)
if (input instanceof SplitCard) {
return CardUtil.haveSameNames(name, ((CardWithHalves) input).getLeftHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, ((CardWithHalves) input).getRightHalfCard().getName(), this.ignoreMtgRuleForEmptyNames) ||
CardUtil.haveSameNames(name, input.getName(), this.ignoreMtgRuleForEmptyNames);