Merge branch 'tamiyo_discard' into 'master'

Tamiyo: new logic for Discard effects

See merge request core-developers/forge!1540
This commit is contained in:
Michael Kamensky
2019-04-16 05:05:13 +00:00
8 changed files with 150 additions and 55 deletions

View File

@@ -150,9 +150,11 @@ public class DiscardAi extends SpellAbilityAi {
private boolean discardTargetAI(final Player ai, final SpellAbility sa) { private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ComputerUtil.getOpponentFor(ai); for (Player opp : ai.getOpponents()) {
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) { if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
return false; continue;
} else if (!opp.canDiscardBy(sa)) { // e.g. Tamiyo, Collector of Tales
continue;
} }
if (tgt != null) { if (tgt != null) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
@@ -160,6 +162,7 @@ public class DiscardAi extends SpellAbilityAi {
return true; return true;
} }
} }
}
return false; return false;
} // discardTargetAI() } // discardTargetAI()

View File

@@ -48,8 +48,11 @@ public class ChooseGenericEffect extends SpellAbilityEffect {
// Sac a permanent in presence of Sigarda, Host of Herons // Sac a permanent in presence of Sigarda, Host of Herons
// TODO: generalize this by testing if the unless cost can be paid // TODO: generalize this by testing if the unless cost can be paid
if (unlessCost.startsWith("Sac<")) { if (unlessCost.startsWith("Sac<")) {
if (saChoice.getActivatingPlayer().isOpponentOf(p) if (!p.canSacrificeBy(saChoice)) {
&& p.hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) { saToRemove.add(saChoice);
}
} else if (unlessCost.startsWith("Discard<")) {
if (!p.canDiscardBy(sa)) {
saToRemove.add(saChoice); saToRemove.add(saChoice);
} }
} }

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects; package forge.game.ability.effects;
import forge.game.Game;
import forge.game.GameActionUtil; import forge.game.GameActionUtil;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect; import forge.game.ability.SpellAbilityEffect;
@@ -7,6 +8,7 @@ import forge.game.card.*;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -29,9 +31,9 @@ public class DiscardEffect extends SpellAbilityEffect {
final String mode = sa.getParam("Mode"); final String mode = sa.getParam("Mode");
final StringBuilder sb = new StringBuilder(); final StringBuilder sb = new StringBuilder();
final List<Player> tgtPlayers = getTargetPlayers(sa); final Iterable<Player> tgtPlayers = Iterables.filter(getTargetPlayers(sa), PlayerPredicates.canDiscardBy(sa));
if (!tgtPlayers.isEmpty()) { if (!Iterables.isEmpty(tgtPlayers)) {
sb.append(Lang.joinHomogenous(tgtPlayers)).append(" "); sb.append(Lang.joinHomogenous(tgtPlayers)).append(" ");
if (mode.equals("RevealYouChoose")) { if (mode.equals("RevealYouChoose")) {
@@ -103,6 +105,7 @@ public class DiscardEffect extends SpellAbilityEffect {
public void resolve(SpellAbility sa) { public void resolve(SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String mode = sa.getParam("Mode"); final String mode = sa.getParam("Mode");
final Game game = source.getGame();
//final boolean anyNumber = sa.hasParam("AnyNumber"); //final boolean anyNumber = sa.hasParam("AnyNumber");
final List<Card> discarded = new ArrayList<Card>(); final List<Card> discarded = new ArrayList<Card>();
@@ -124,23 +127,26 @@ public class DiscardEffect extends SpellAbilityEffect {
final CardZoneTable table = new CardZoneTable(); final CardZoneTable table = new CardZoneTable();
for (final Player p : discarders) { for (final Player p : discarders) {
if ((mode.equals("RevealTgtChoose") && firstTarget != null) || !sa.usesTargeting() || p.canBeTargetedBy(sa)) { if ((mode.equals("RevealTgtChoose") && firstTarget != null) || !sa.usesTargeting() || p.canBeTargetedBy(sa)) {
if (sa.hasParam("RememberDiscarder")) { if (sa.hasParam("RememberDiscarder") && p.canDiscardBy(sa)) {
source.addRemembered(p); source.addRemembered(p);
} }
final int numCardsInHand = p.getCardsIn(ZoneType.Hand).size(); final int numCardsInHand = p.getCardsIn(ZoneType.Hand).size();
if (mode.equals("Defined")) { if (mode.equals("Defined")) {
if (!p.canDiscardBy(sa)) {
continue;
}
boolean runDiscard = !sa.hasParam("Optional") boolean runDiscard = !sa.hasParam("Optional")
|| p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage")); || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, sa.getParam("DiscardMessage"));
if (runDiscard) { if (runDiscard) {
CardCollectionView toDiscard = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa); CardCollectionView toDiscard = AbilityUtils.getDefinedCards(source, sa.getParam("DefinedCards"), sa);
if (toDiscard.size() > 1) { if (toDiscard.size() > 1) {
toDiscard = GameActionUtil.orderCardsByTheirOwners(p.getGame(), toDiscard, ZoneType.Graveyard); toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard);
} }
for (final Card c : toDiscard) { for (final Card c : toDiscard) {
boolean hasDiscarded = p.discard(c, sa, table) != null; if (p.discard(c, sa, table) != null) {
if (hasDiscarded) {
discarded.add(c); discarded.add(c);
} }
} }
@@ -155,11 +161,14 @@ public class DiscardEffect extends SpellAbilityEffect {
} }
if (mode.equals("Hand")) { if (mode.equals("Hand")) {
if (!p.canDiscardBy(sa)) {
continue;
}
boolean shouldRemember = sa.hasParam("RememberDiscarded"); boolean shouldRemember = sa.hasParam("RememberDiscarded");
CardCollectionView toDiscard = new CardCollection(Lists.newArrayList(p.getCardsIn(ZoneType.Hand))); CardCollectionView toDiscard = new CardCollection(Lists.newArrayList(p.getCardsIn(ZoneType.Hand)));
if (toDiscard.size() > 1) { if (toDiscard.size() > 1) {
toDiscard = GameActionUtil.orderCardsByTheirOwners(p.getGame(), toDiscard, ZoneType.Graveyard); toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard);
} }
for(Card c : toDiscard) { // without copying will get concurrent modification exception for(Card c : toDiscard) { // without copying will get concurrent modification exception
@@ -171,27 +180,31 @@ public class DiscardEffect extends SpellAbilityEffect {
} }
if (mode.equals("NotRemembered")) { if (mode.equals("NotRemembered")) {
if (!p.canDiscardBy(sa)) {
continue;
}
CardCollectionView dPHand = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), "Card.IsNotRemembered", p, source); CardCollectionView dPHand = CardLists.getValidCards(p.getCardsIn(ZoneType.Hand), "Card.IsNotRemembered", p, source);
if (dPHand.size() > 1) { if (dPHand.size() > 1) {
dPHand = GameActionUtil.orderCardsByTheirOwners(p.getGame(), dPHand, ZoneType.Graveyard); dPHand = GameActionUtil.orderCardsByTheirOwners(game, dPHand, ZoneType.Graveyard);
} }
for (final Card c : dPHand) { for (final Card c : dPHand) {
p.discard(c, sa, table); if (p.discard(c, sa, table) != null) {
discarded.add(c); discarded.add(c);
} }
} }
}
int numCards = 1; int numCards = 1;
if (sa.hasParam("NumCards")) { if (sa.hasParam("NumCards")) {
numCards = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa); numCards = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumCards"), sa);
if (!p.getCardsIn(ZoneType.Hand).isEmpty() && p.getCardsIn(ZoneType.Hand).size() < numCards) { numCards = Math.min(numCards, numCardsInHand);
// System.out.println("Scale down discard from " + numCards + " to " + p.getCardsIn(ZoneType.Hand).size());
numCards = p.getCardsIn(ZoneType.Hand).size();
}
} }
if (mode.equals("Random")) { if (mode.equals("Random")) {
if (!p.canDiscardBy(sa)) {
continue;
}
String message = "Would you like to discard " + numCards + " random card(s)?"; String message = "Would you like to discard " + numCards + " random card(s)?";
boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message); boolean runDiscard = !sa.hasParam("Optional") || p.getController().confirmAction(sa, PlayerActionConfirmMode.Random, message);
@@ -211,7 +224,7 @@ public class DiscardEffect extends SpellAbilityEffect {
CardCollectionView toDiscardView = toDiscard; CardCollectionView toDiscardView = toDiscard;
if (toDiscard.size() > 1) { if (toDiscard.size() > 1) {
toDiscardView = GameActionUtil.orderCardsByTheirOwners(p.getGame(), toDiscard, ZoneType.Graveyard); toDiscardView = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard);
} }
for (Card c : toDiscardView) { for (Card c : toDiscardView) {
@@ -222,13 +235,16 @@ public class DiscardEffect extends SpellAbilityEffect {
} }
} }
else if (mode.equals("TgtChoose") && sa.hasParam("UnlessType")) { else if (mode.equals("TgtChoose") && sa.hasParam("UnlessType")) {
if (!p.canDiscardBy(sa)) {
continue;
}
if( numCardsInHand > 0 ) { if( numCardsInHand > 0 ) {
CardCollectionView hand = p.getCardsIn(ZoneType.Hand); CardCollectionView hand = p.getCardsIn(ZoneType.Hand);
hand = CardLists.filter(hand, Presets.NON_TOKEN); hand = CardLists.filter(hand, Presets.NON_TOKEN);
CardCollectionView toDiscard = p.getController().chooseCardsToDiscardUnlessType(Math.min(numCards, numCardsInHand), hand, sa.getParam("UnlessType"), sa); CardCollectionView toDiscard = p.getController().chooseCardsToDiscardUnlessType(Math.min(numCards, numCardsInHand), hand, sa.getParam("UnlessType"), sa);
if (toDiscard.size() > 1) { if (toDiscard.size() > 1) {
toDiscard = GameActionUtil.orderCardsByTheirOwners(p.getGame(), toDiscard, ZoneType.Graveyard); toDiscard = GameActionUtil.orderCardsByTheirOwners(game, toDiscard, ZoneType.Graveyard);
} }
for (Card c : toDiscard) { for (Card c : toDiscard) {
@@ -240,10 +256,14 @@ public class DiscardEffect extends SpellAbilityEffect {
// Reveal // Reveal
final CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand); final CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
for (final Player opp : p.getOpponents()) { for (final Player opp : p.getAllOtherPlayers()) {
opp.getController().reveal(dPHand, ZoneType.Hand, p, "Reveal "); opp.getController().reveal(dPHand, ZoneType.Hand, p, "Reveal ");
} }
if (!p.canDiscardBy(sa)) {
continue;
}
String valid = sa.hasParam("DiscardValid") ? sa.getParam("DiscardValid") : "Card"; String valid = sa.hasParam("DiscardValid") ? sa.getParam("DiscardValid") : "Card";
if (valid.contains("X")) { if (valid.contains("X")) {
@@ -254,14 +274,15 @@ public class DiscardEffect extends SpellAbilityEffect {
CardCollectionView dPChHand = CardLists.getValidCards(dPHand, valid.split(","), source.getController(), source, sa); CardCollectionView dPChHand = CardLists.getValidCards(dPHand, valid.split(","), source.getController(), source, sa);
dPChHand = CardLists.filter(dPChHand, Presets.NON_TOKEN); dPChHand = CardLists.filter(dPChHand, Presets.NON_TOKEN);
if (dPChHand.size() > 1) { if (dPChHand.size() > 1) {
dPChHand = GameActionUtil.orderCardsByTheirOwners(p.getGame(), dPChHand, ZoneType.Graveyard); dPChHand = GameActionUtil.orderCardsByTheirOwners(game, dPChHand, ZoneType.Graveyard);
} }
// Reveal cards that will be discarded? // Reveal cards that will be discarded?
for (final Card c : dPChHand) { for (final Card c : dPChHand) {
p.discard(c, sa, table); if (p.discard(c, sa, table) != null) {
discarded.add(c); discarded.add(c);
} }
}
} else if (mode.equals("RevealYouChoose") || mode.equals("RevealTgtChoose") || mode.equals("TgtChoose")) { } else if (mode.equals("RevealYouChoose") || mode.equals("RevealTgtChoose") || mode.equals("TgtChoose")) {
CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand); CardCollectionView dPHand = p.getCardsIn(ZoneType.Hand);
dPHand = CardLists.filter(dPHand, Presets.NON_TOKEN); dPHand = CardLists.filter(dPHand, Presets.NON_TOKEN);
@@ -273,6 +294,7 @@ public class DiscardEffect extends SpellAbilityEffect {
int amount = StringUtils.isNumeric(amountString) ? Integer.parseInt(amountString) : CardFactoryUtil.xCount(source, source.getSVar(amountString)); int amount = StringUtils.isNumeric(amountString) ? Integer.parseInt(amountString) : CardFactoryUtil.xCount(source, source.getSVar(amountString));
dPHand = p.getController().chooseCardsToRevealFromHand(amount, amount, dPHand); dPHand = p.getController().chooseCardsToRevealFromHand(amount, amount, dPHand);
} }
final String valid = sa.hasParam("DiscardValid") ? sa.getParam("DiscardValid") : "Card"; final String valid = sa.hasParam("DiscardValid") ? sa.getParam("DiscardValid") : "Card";
String[] dValid = valid.split(","); String[] dValid = valid.split(",");
CardCollection validCards = CardLists.getValidCards(dPHand, dValid, source.getController(), source, sa); CardCollection validCards = CardLists.getValidCards(dPHand, dValid, source.getController(), source, sa);
@@ -284,8 +306,13 @@ public class DiscardEffect extends SpellAbilityEffect {
chooser = firstTarget; chooser = firstTarget;
} }
if (mode.startsWith("Reveal") && p != chooser) if (mode.startsWith("Reveal") && p != chooser) {
chooser.getGame().getAction().reveal(dPHand, p); game.getAction().reveal(dPHand, p);
}
if (!p.canDiscardBy(sa)) {
continue;
}
int min = sa.hasParam("AnyNumber") || sa.hasParam("Optional") ? 0 : Math.min(validCards.size(), numCards); int min = sa.hasParam("AnyNumber") || sa.hasParam("Optional") ? 0 : Math.min(validCards.size(), numCards);
int max = sa.hasParam("AnyNumber") ? validCards.size() : Math.min(validCards.size(), numCards); int max = sa.hasParam("AnyNumber") ? validCards.size() : Math.min(validCards.size(), numCards);
@@ -294,7 +321,7 @@ public class DiscardEffect extends SpellAbilityEffect {
if (toBeDiscarded != null) { if (toBeDiscarded != null) {
if (toBeDiscarded.size() > 1) { if (toBeDiscarded.size() > 1) {
toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(p.getGame(), toBeDiscarded, ZoneType.Graveyard); toBeDiscarded = GameActionUtil.orderCardsByTheirOwners(game, toBeDiscarded, ZoneType.Graveyard);
} }
if (mode.startsWith("Reveal") ) { if (mode.startsWith("Reveal") ) {
@@ -303,13 +330,14 @@ public class DiscardEffect extends SpellAbilityEffect {
} }
for (Card card : toBeDiscarded) { for (Card card : toBeDiscarded) {
if (card == null) { continue; } if (card == null) { continue; }
p.discard(card, sa, table); if (p.discard(card, sa, table) != null) {
discarded.add(card); discarded.add(card);
} }
} }
} }
} }
} }
}
if (sa.hasParam("RememberDiscarded")) { if (sa.hasParam("RememberDiscarded")) {
for (final Card c : discarded) { for (final Card c : discarded) {

View File

@@ -5480,8 +5480,7 @@ public class Card extends GameEntity implements Comparable<Card> {
} }
} }
if (getController().isOpponentOf(source.getActivatingPlayer()) if (!getController().canSacrificeBy(source)) {
&& getController().hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) {
return false; return false;
} }
@@ -5921,4 +5920,16 @@ public class Card extends GameEntity implements Comparable<Card> {
rE.setTemporarilySuppressed(false); rE.setTemporarilySuppressed(false);
} }
} }
public boolean canBeDiscardedBy(SpellAbility sa) {
if (!isInZone(ZoneType.Hand)) {
return false;
}
if (!getOwner().canDiscardBy(sa)) {
return false;
}
return true;
}
} }

View File

@@ -18,6 +18,7 @@
package forge.game.cost; package forge.game.cost;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
@@ -106,17 +107,20 @@ public class CostDiscard extends CostPartWithList {
public final boolean canPay(final SpellAbility ability, final Player payer) { public final boolean canPay(final SpellAbility ability, final Player payer) {
final Card source = ability.getHostCard(); final Card source = ability.getHostCard();
CardCollectionView handList = payer.getCardsIn(ZoneType.Hand); CardCollectionView handList = payer.canDiscardBy(ability) ? payer.getCardsIn(ZoneType.Hand) : CardCollection.EMPTY;
String type = this.getType(); String type = this.getType();
final Integer amount = this.convertAmount(); final Integer amount = this.convertAmount();
if (this.payCostFromSource()) { if (this.payCostFromSource()) {
if (!source.isInZone(ZoneType.Hand)) { if (!source.canBeDiscardedBy(ability)) {
return false; return false;
} }
} }
else { else {
if (type.equals("Hand")) { if (type.equals("Hand")) {
if (!payer.canDiscardBy(ability)) {
return false;
}
// this will always work // this will always work
} }
else if (type.equals("LastDrawn")) { else if (type.equals("LastDrawn")) {

View File

@@ -1555,6 +1555,10 @@ public class Player extends GameEntity implements Comparable<Player> {
} }
public final Card discard(final Card c, final SpellAbility sa, CardZoneTable table) { public final Card discard(final Card c, final SpellAbility sa, CardZoneTable table) {
if (!c.canBeDiscardedBy(sa)) {
return null;
}
// TODO: This line should be moved inside CostPayment somehow // TODO: This line should be moved inside CostPayment somehow
/*if (sa != null) { /*if (sa != null) {
sa.addCostToHashList(c, "Discarded"); sa.addCostToHashList(c, "Discarded");
@@ -2935,4 +2939,27 @@ public class Player extends GameEntity implements Comparable<Player> {
return CardLists.count(attachedCards, CardPredicates.Presets.CURSE) > 0; return CardLists.count(attachedCards, CardPredicates.Presets.CURSE) > 0;
} }
public boolean canDiscardBy(SpellAbility sa) {
if (sa == null) {
return true;
}
if (isOpponentOf(sa.getActivatingPlayer()) && hasKeyword("Spells and abilities your opponents control can't cause you to discard cards.")) {
return false;
}
return true;
}
public boolean canSacrificeBy(SpellAbility sa) {
if (sa == null) {
return true;
}
if (isOpponentOf(sa.getActivatingPlayer()) && hasKeyword("Spells and abilities your opponents control can't cause you to sacrifice permanents.")) {
return false;
}
return true;
}
} }

View File

@@ -22,6 +22,15 @@ public final class PlayerPredicates {
}; };
} }
public static final Predicate<Player> canDiscardBy(final SpellAbility source) {
return new Predicate<Player>() {
@Override
public boolean apply(final Player p) {
return p.canDiscardBy(source);
}
};
}
public static final Predicate<Player> isOpponentOf(final Player player) { public static final Predicate<Player> isOpponentOf(final Player player) {
return new Predicate<Player>() { return new Predicate<Player>() {
@Override @Override

View File

@@ -0,0 +1,10 @@
Name:Tamiyo, Collector of Tales
ManaCost:2 G U
Types:Legendary Planeswalker Tamiyo
Loyalty:5
S:Mode$ Continuous | Affected$ You | AddKeyword$ Spells and abilities your opponents control can't cause you to discard cards. & Spells and abilities your opponents control can't cause you to sacrifice permanents. | Description$ Spells and abilities your opponents control can't cause you to discard cards or sacrifice permanents.
A:AB$ NameCard | Cost$ AddCounter<1/LOYALTY> | Planeswalker$ True | Defined$ You | SubAbility$ DBDig | SpellDescription$ Choose a nonland card name, then reveal the top four cards of your library. Put all cards with the chosen name from among them into your hand and the rest into your graveyard.
SVar:DBDig:DB$Dig | DigNum$ 4 | Reveal$ True | ChangeNum$ All | ChangeValid$ Card.NamedCard | DestinationZone2$ Graveyard
A:AB$ ChangeZone | Cost$ SubCounter<3/LOYALTY> | Planeswalker$ True | TgtPrompt$ Choose target card in your graveyard | ValidTgts$ Card.YouCtrl | Origin$ Graveyard | Destination$ Hand | SpellDescription$ Return target card from your graveyard to your hand.
AI:RemoveDeck:All
Oracle:Spells and abilities your opponents control can't cause you to discard cards or sacrifice permanents.\n[+1]: Choose a nonland card name, then reveal the top four cards of your library. Put all cards with the chosen name from among them into your hand and the rest into your graveyard.\n[-3]: Return target card from your graveyard to your hand.