Card: ignoreLegendRule as StaticAbility (#360)

* Card: ignoreLegendRule as StaticAbility
This commit is contained in:
Hans Mackowiak
2022-05-15 23:32:55 +02:00
committed by GitHub
parent 573e7dfb23
commit 24b2186e0d
17 changed files with 215 additions and 112 deletions

View File

@@ -22,7 +22,6 @@ import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -43,7 +42,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
@@ -78,9 +76,7 @@ public class AttachAi extends SpellAbilityAi {
} }
} }
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule) if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
&& source.getType().isLegendary() && sa instanceof SpellPermanent
&& ai.isCardInPlay(source.getName())) {
// Don't play the second copy of a legendary enchantment already in play // Don't play the second copy of a legendary enchantment already in play
// TODO: Add some extra checks for where the AI may want to cast a replacement aura // TODO: Add some extra checks for where the AI may want to cast a replacement aura

View File

@@ -11,7 +11,6 @@ import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityKey; import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -276,11 +275,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
//Ninjutsu //Ninjutsu
if (sa.hasParam("Ninjutsu")) { if (sa.hasParam("Ninjutsu")) {
if (source.getType().isLegendary() if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
&& !ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { return false;
if (ai.getZone(ZoneType.Battlefield).contains(CardPredicates.nameEquals(source.getName()))) {
return false;
}
} }
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) { if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return false; return false;
@@ -732,18 +728,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} }
// predict Legendary cards already present // predict Legendary cards already present
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { boolean nothingWillReturn = true;
boolean nothingWillReturn = true; for (final Card c : retrieval) {
for (final Card c : retrieval) { if (!(!c.ignoreLegendRule() && ai.isCardInPlay(c.getName()))) {
if (!(c.getType().isLegendary() && ai.isCardInPlay(c.getName()))) { nothingWillReturn = false;
nothingWillReturn = false; break;
break;
}
}
if (nothingWillReturn) {
return false;
} }
} }
if (nothingWillReturn) {
return false;
}
} }
} }

View File

@@ -20,11 +20,13 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
@@ -37,6 +39,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType; import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
/** /**
* <p> * <p>
@@ -97,13 +100,19 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
return true; return true;
} else { } else {
// currently only Clockspinning // currently only Clockspinning
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
// logic to remove some counter // logic to remove some counter
CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounters()); CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounters());
if (!countersList.isEmpty()) { if (!countersList.isEmpty()) {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
return input.ignoreLegendRule();
}
});
if (maritEmpty) {
CardCollectionView depthsList = CardLists.filter(countersList, CardCollectionView depthsList = CardLists.filter(countersList,
CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE)); CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE));
@@ -198,9 +207,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
@Override @Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, Map<String, Object> params) { public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, Map<String, Object> params) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
Card tgt = (Card) params.get("Target"); Card tgt = (Card) params.get("Target");
@@ -229,7 +235,15 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
} else { } else {
// this counters are treat first to be removed // this counters are treat first to be removed
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) { if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
return input.ignoreLegendRule();
}
});
if (maritEmpty) {
return CounterType.get(CounterEnumType.ICE); return CounterType.get(CounterEnumType.ICE);
} }
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) { } else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
@@ -260,12 +274,10 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) { public boolean chooseBinary(BinaryChoiceType kindOfChoice, SpellAbility sa, Map<String, Object> params) {
if (kindOfChoice.equals(BinaryChoiceType.AddOrRemove)) { if (kindOfChoice.equals(BinaryChoiceType.AddOrRemove)) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
Card tgt = (Card) params.get("Target"); Card tgt = (Card) params.get("Target");
CounterType type = (CounterType) params.get("CounterType"); CounterType type = (CounterType) params.get("CounterType");
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
if (tgt.getController().isOpponentOf(ai)) { if (tgt.getController().isOpponentOf(ai)) {
if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) { if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) {
return false; return false;
@@ -274,7 +286,15 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
return ComputerUtil.isNegativeCounter(type, tgt); return ComputerUtil.isNegativeCounter(type, tgt);
} else { } else {
if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) { if (type.is(CounterEnumType.ICE) && "Dark Depths".equals(tgt.getName())) {
if (!ai.isCardInPlay("Marit Lage") || noLegendary) { CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
return input.ignoreLegendRule();
}
});
if (maritEmpty) {
return false; return false;
} }
} else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) { } else if (type.is(CounterEnumType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {

View File

@@ -3,7 +3,9 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -11,7 +13,6 @@ import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
@@ -117,16 +118,21 @@ public class CountersRemoveAi extends SpellAbilityAi {
// Filter AI-specific targets if provided // Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false); list = ComputerUtil.filterAITgts(sa, ai, list, false);
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule); CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
return input.ignoreLegendRule();
}
});
if (type.matches("All")) { if (type.matches("All")) {
// Logic Part for Vampire Hexmage // Logic Part for Vampire Hexmage
// Break Dark Depths // Break Dark Depths
if (!ai.isCardInPlay("Marit Lage") || noLegendary) { if (maritEmpty) {
CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths");
depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa),
CardPredicates.hasCounter(CounterEnumType.ICE, 3)); CardPredicates.hasCounter(CounterEnumType.ICE, 3));
if (!depthsList.isEmpty()) { if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst()); sa.getTargets().add(depthsList.getFirst());
return true; return true;
@@ -161,7 +167,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
} }
// try to remove them from Dark Depths and Planeswalkers too // try to remove them from Dark Depths and Planeswalkers too
if (!ai.isCardInPlay("Marit Lage") || noLegendary) { if (maritEmpty) {
CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths"); CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths");
depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa), depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa),
CardPredicates.hasCounter(CounterEnumType.ICE)); CardPredicates.hasCounter(CounterEnumType.ICE));

View File

@@ -11,8 +11,6 @@ import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.card.CardType.Supertype; import forge.card.CardType.Supertype;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
@@ -52,28 +50,26 @@ public class PermanentAi extends SpellAbilityAi {
@Override @Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) { protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
final Game game = ai.getGame();
// check on legendary // check on legendary
if (card.getType().isLegendary() if (!card.ignoreLegendRule() && ai.isCardInPlay(card.getName())) {
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { // TODO check the risk we'd lose the effect with bad timing // TODO check the risk we'd lose the effect with bad timing
if (ai.isCardInPlay(card.getName())) { // TODO check for keyword // TODO check for keyword
if (!card.hasSVar("AILegendaryException")) { if (!card.hasSVar("AILegendaryException")) {
// AiPlayDecision.WouldDestroyLegend // AiPlayDecision.WouldDestroyLegend
return false; return false;
} else { } else {
String specialRule = card.getSVar("AILegendaryException"); String specialRule = card.getSVar("AILegendaryException");
if ("TwoCopiesAllowed".equals(specialRule)) { if ("TwoCopiesAllowed".equals(specialRule)) {
// One extra copy allowed on the battlefield, e.g. Brothers Yamazaki // One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())) > 1) { if (CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())) > 1) {
return false;
}
} else if ("AlwaysAllowed".equals(specialRule)) {
// Nothing to do here, check for Legendary is disabled
} else {
// Unknown hint, assume two copies not allowed
return false; return false;
} }
} else if ("AlwaysAllowed".equals(specialRule)) {
// Nothing to do here, check for Legendary is disabled
} else {
// Unknown hint, assume two copies not allowed
return false;
} }
} }
} }

View File

@@ -2,10 +2,8 @@ package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.*; import forge.ai.*;
import forge.game.GlobalRuleChange;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardPredicates; import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -54,16 +52,16 @@ public class RepeatAi extends SpellAbilityAi {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
if (logic.startsWith("CopyBestCreature")) { if (logic.startsWith("CopyBestCreature")) {
Card best = null; Card best = null;
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule) || logic.endsWith("IgnoreLegendary")) { Iterable<Card> targetableAi = Iterables.filter(ai.getCreaturesInPlay(), CardPredicates.isTargetableBy(sa));
best = ComputerUtilCard.getBestAI(Iterables.filter(ai.getCreaturesInPlay(), Predicates.and( if (!logic.endsWith("IgnoreLegendary")) {
CardPredicates.isTargetableBy(sa), new Predicate<Card>() { best = ComputerUtilCard.getBestAI(Iterables.filter(targetableAi, new Predicate<Card>() {
@Override @Override
public boolean apply(Card card) { public boolean apply(Card card) {
return !card.getType().isLegendary(); return card.ignoreLegendRule();
} }
}))); }));
} else { } else {
best = ComputerUtilCard.getBestAI(Iterables.filter(ai.getCreaturesInPlay(), CardPredicates.isTargetableBy(sa))); best = ComputerUtilCard.getBestAI(targetableAi);
} }
if (best == null && mandatory && sa.canTarget(sa.getHostCard())) { if (best == null && mandatory && sa.canTarget(sa.getHostCard())) {
best = sa.getHostCard(); best = sa.getHostCard();

View File

@@ -9,7 +9,6 @@ import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CardCollection; import forge.game.card.CardCollection;
@@ -238,7 +237,7 @@ public class SetStateAi extends SpellAbilityAi {
private boolean isSafeToTransformIntoLegendary(Player aiPlayer, Card source) { private boolean isSafeToTransformIntoLegendary(Player aiPlayer, Card source) {
// Prevent transform into legendary creature if copy already exists // Prevent transform into legendary creature if copy already exists
// Check first if Legend Rule does still apply // Check first if Legend Rule does still apply
if (!aiPlayer.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { if (!source.ignoreLegendRule()) {
if (!source.hasAlternateState()) { if (!source.hasAlternateState()) {
System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + "."); System.err.println("Warning: SetState without ALTERNATE on " + source.getName() + ".");
return false; return false;

View File

@@ -20,6 +20,8 @@ package forge.card;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.*; import com.google.common.collect.*;
import forge.StaticData;
import forge.card.CardEdition.CardInSet; import forge.card.CardEdition.CardInSet;
import forge.card.CardEdition.Type; import forge.card.CardEdition.Type;
import forge.deck.generation.IDeckGenPool; import forge.deck.generation.IDeckGenPool;
@@ -45,14 +47,16 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
private final Map<String, PaperCard> uniqueCardsByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final Map<String, PaperCard> uniqueCardsByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final Map<String, CardRules> rulesByName; private final Map<String, CardRules> rulesByName;
private final Map<String, ICardFace> facesByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final Map<String, ICardFace> facesByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private static Map<String, String> artPrefs = new HashMap<>(); private static Map<String, String> artPrefs = Maps.newHashMap();
private final Map<String, String> alternateName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER); private final Map<String, String> alternateName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final Map<String, Integer> artIds = new HashMap<>(); private final Map<String, Integer> artIds = Maps.newHashMap();
private final CardEdition.Collection editions; private final CardEdition.Collection editions;
private List<String> filtered; private List<String> filtered;
private Map<String, Boolean> nonLegendaryCreatureNames = Maps.newHashMap();
public enum CardArtPreference { public enum CardArtPreference {
LATEST_ART_ALL_EDITIONS(false, true), LATEST_ART_ALL_EDITIONS(false, true),
LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY(true, true), LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY(true, true),
@@ -489,7 +493,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return null; return null;
// 1. First off, try using all possible search parameters, to narrow down the actual cards looked for. // 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
String reqEditionCode = request.edition; String reqEditionCode = request.edition;
PaperCard result = null;
if (reqEditionCode != null && reqEditionCode.length() > 0) { if (reqEditionCode != null && reqEditionCode.length() > 0) {
// This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) - // This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) -
// MOST of the extensions have two short codes, 141 out of 221 (so far) // MOST of the extensions have two short codes, 141 out of 221 (so far)
@@ -843,6 +846,26 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return facesByName.get(getName(name)); return facesByName.get(getName(name));
} }
public boolean isNonLegendaryCreatureName(final String name) {
Boolean bool = nonLegendaryCreatureNames.get(name);
if (bool != null) {
return bool;
}
// check if the name is from a face
// in general token creatures does not have this
final ICardFace face = StaticData.instance().getCommonCards().getFaceByName(name);
if (face == null) {
nonLegendaryCreatureNames.put(name, false);
return false;
}
// TODO add check if face is legal in the format of the game
// name does need to be a non-legendary creature
final CardType type = face.getType();
bool = type.isCreature() && !type.isLegendary();
nonLegendaryCreatureNames.put(name, bool);
return bool;
}
@Override @Override
public Collection<PaperCard> getAllCards() { public Collection<PaperCard> getAllCards() {
return Collections.unmodifiableCollection(allCardsByName.values()); return Collections.unmodifiableCollection(allCardsByName.values());

View File

@@ -17,7 +17,6 @@
*/ */
package forge.game; package forge.game;
import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.Iterator; import java.util.Iterator;
@@ -31,12 +30,12 @@ import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ComparisonChain; import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets; import com.google.common.collect.Sets;
import forge.GameCommand; import forge.GameCommand;
@@ -1697,38 +1696,71 @@ public class GameAction {
} }
private boolean handleLegendRule(Player p, CardCollection noRegCreats) { private boolean handleLegendRule(Player p, CardCollection noRegCreats) {
final List<Card> a = CardLists.getType(p.getCardsIn(ZoneType.Battlefield), "Legendary"); final List<Card> a = Lists.newArrayList();
if (a.isEmpty() || game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) {
return false;
}
boolean recheck = false;
// TODO legend rule exception into static ability
List<Card> yamazaki = CardLists.getKeyword(a, "Legend rule doesn't apply to CARDNAME.");
a.removeAll(yamazaki);
Multimap<String, Card> uniqueLegends = ArrayListMultimap.create(); // check for ignore legend rule
for (Card c : a) { for (Card c : CardLists.getType(p.getCardsIn(ZoneType.Battlefield), "Legendary")) {
if (!c.isFaceDown()) { if (!c.ignoreLegendRule()) {
uniqueLegends.put(c.getName(), c); a.add(c);
} }
} }
// TODO handle Spy Kit if (a.isEmpty()) {
return false;
}
boolean recheck = false;
// Corner Case 1: Legendary with non legendary creature names
CardCollection nonLegendaryNames = new CardCollection(Iterables.filter(a, new Predicate<Card>() {
@Override
public boolean apply(Card input) {
return input.hasNonLegendaryCreatureNames();
}
}));
Multimap<String, Card> uniqueLegends = Multimaps.index(a, CardPredicates.Accessors.fnGetNetName);
CardCollection removed = new CardCollection();
for (String name : uniqueLegends.keySet()) { for (String name : uniqueLegends.keySet()) {
Collection<Card> cc = uniqueLegends.get(name); // skip the ones with empty names
if (name.isEmpty()) {
continue;
}
CardCollection cc = new CardCollection(uniqueLegends.get(name));
// check if it is a non legendary creature name
// if yes, then add the other legendary with Spy Kit too
if (!name.isEmpty() && StaticData.instance().getCommonCards().isNonLegendaryCreatureName(name)) {
cc.addAll(nonLegendaryNames);
}
if (cc.size() < 2) { if (cc.size() < 2) {
continue; continue;
} }
recheck = true; recheck = true;
Card toKeep = p.getController().chooseSingleEntityForEffect(new CardCollection(cc), new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, new Card(-1, game), p), Card toKeep = p.getController().chooseSingleEntityForEffect(cc, new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, new Card(-1, game), p),
"You have multiple legendary permanents named \""+name+"\" in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null); "You have multiple legendary permanents named \""+name+"\" in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null);
cc.remove(toKeep); cc.remove(toKeep);
noRegCreats.addAll(cc); removed.addAll(cc);
} }
// Corner Case 2: with all non legendary creature names
CardCollection emptyNameAllNonLegendary = new CardCollection(nonLegendaryNames);
// remove the ones that got already removed by other legend rule above
emptyNameAllNonLegendary.removeAll(removed);
if (emptyNameAllNonLegendary.size() > 1) {
recheck = true;
Card toKeep = p.getController().chooseSingleEntityForEffect(emptyNameAllNonLegendary, new SpellAbility.EmptySa(ApiType.InternalLegendaryRule, new Card(-1, game), p),
"You have multiple legendary permanents with non legendary creature names in play.\n\nChoose the one to stay on battlefield (the rest will be moved to graveyard)", null);
emptyNameAllNonLegendary.remove(toKeep);
removed.addAll(emptyNameAllNonLegendary);
}
noRegCreats.addAll(removed);
return recheck; return recheck;
} }

View File

@@ -28,7 +28,6 @@ public enum GlobalRuleChange {
noCreatureETBTriggers ("Creatures entering the battlefield don't cause abilities to trigger."), noCreatureETBTriggers ("Creatures entering the battlefield don't cause abilities to trigger."),
noCreatureDyingTriggers ("Creatures dying don't cause abilities to trigger."), noCreatureDyingTriggers ("Creatures dying don't cause abilities to trigger."),
noNight ("It can't become night."), noNight ("It can't become night."),
noLegendRule ("The legend rule doesn't apply."),
/* onlyOneAttackerATurn ("No more than one creature can attack each turn."), */ /* onlyOneAttackerATurn ("No more than one creature can attack each turn."), */
onlyOneAttackerACombat ("No more than one creature can attack each combat."), onlyOneAttackerACombat ("No more than one creature can attack each combat."),
onlyOneBlocker ("No more than one creature can block each combat."), onlyOneBlocker ("No more than one creature can block each combat."),

View File

@@ -60,6 +60,7 @@ import forge.game.staticability.StaticAbilityCantPutCounter;
import forge.game.staticability.StaticAbilityCantSacrifice; import forge.game.staticability.StaticAbilityCantSacrifice;
import forge.game.staticability.StaticAbilityCantTarget; import forge.game.staticability.StaticAbilityCantTarget;
import forge.game.staticability.StaticAbilityCantTransform; import forge.game.staticability.StaticAbilityCantTransform;
import forge.game.staticability.StaticAbilityIgnoreLegendRule;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -5189,15 +5190,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} else { } else {
// check if this card has a name from a face // check if this card has a name from a face
// in general token creatures does not have this // in general token creatures does not have this
final ICardFace face = StaticData.instance().getCommonCards().getFaceByName(getName());
if (face == null) {
return false;
}
// TODO add check if face is legal in the format of the game // TODO add check if face is legal in the format of the game
// name does need to be a non-legendary creature // name does need to be a non-legendary creature
final CardType type = face.getType(); return StaticData.instance().getCommonCards().isNonLegendaryCreatureName(getName());
if (type != null && type.isCreature() && !type.isLegendary())
return true;
} }
} }
return sharesNameWith(c1.getName()); return sharesNameWith(c1.getName());
@@ -5220,15 +5215,9 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
if (!shares && hasNonLegendaryCreatureNames()) { if (!shares && hasNonLegendaryCreatureNames()) {
// check if the name is from a face // check if the name is from a face
// in general token creatures does not have this // in general token creatures does not have this
final ICardFace face = StaticData.instance().getCommonCards().getFaceByName(name);
if (face == null) {
return false;
}
// TODO add check if face is legal in the format of the game // TODO add check if face is legal in the format of the game
// name does need to be a non-legendary creature // name does need to be a non-legendary creature
final CardType type = face.getType(); return StaticData.instance().getCommonCards().isNonLegendaryCreatureName(name);
if (type.isCreature() && !type.isLegendary())
return true;
} }
return shares; return shares;
} }
@@ -7107,4 +7096,16 @@ public class Card extends GameEntity implements Comparable<Card>, IHasSVars {
} }
return getGame().getCombat().isAttacking(this); return getGame().getCombat().isAttacking(this);
} }
public boolean ignoreLegendRule() {
// not legendary
if (!getType().isLegendary()) {
return true;
}
// empty name and no "has non legendary creature names"
if (this.getName().isEmpty() && !hasNonLegendaryCreatureNames()) {
return true;
}
return StaticAbilityIgnoreLegendRule.ignoreLegendRule(this);
}
} }

View File

@@ -760,6 +760,13 @@ public final class CardPredicates {
return a.getCMC(); return a.getCMC();
} }
}; };
public static final Function<Card, String> fnGetNetName = new Function<Card, String>() {
@Override
public String apply(Card a) {
return a.getName();
}
};
} }
} }

View File

@@ -0,0 +1,33 @@
package forge.game.staticability;
import forge.game.Game;
import forge.game.card.Card;
import forge.game.zone.ZoneType;
public class StaticAbilityIgnoreLegendRule {
static String MODE = "IgnoreLegendRule";
public static boolean ignoreLegendRule(final Card card) {
final Game game = card.getGame();
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
for (final StaticAbility stAb : ca.getStaticAbilities()) {
if (!stAb.getParam("Mode").equals(MODE) || stAb.isSuppressed() || !stAb.checkConditions()) {
continue;
}
if (applyIgnoreLegendRuleAbility(stAb, card)) {
return true;
}
}
}
return false;
}
public static boolean applyIgnoreLegendRuleAbility(final StaticAbility stAb, final Card card) {
if (!stAb.matchesValidParam("ValidCard", card)) {
return false;
}
return true;
}
}

View File

@@ -3,8 +3,7 @@ ManaCost:2 R
Types:Legendary Creature Human Samurai Types:Legendary Creature Human Samurai
PT:2/1 PT:2/1
K:Bushido:1 K:Bushido:1
S:Mode$ Continuous | Affected$ Permanent.namedBrothers Yamazaki | CheckSVar$ X | SVarCompare$ EQ2 | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ If there are exactly two permanents named Brothers Yamazaki on the battlefield, the "legend rule" doesn't apply to them. S:Mode$ IgnoreLegendRule | ValidCard$ Permanent.namedBrothers Yamazaki | IsPresent$ Permanent.namedBrothers Yamazaki | PresentCompare$ EQ2 | Description$ If there are exactly two permanents named Brothers Yamazaki on the battlefield, the "legend rule" doesn't apply to them.
SVar:X:Count$Valid Permanent.namedBrothers Yamazaki
S:Mode$ Continuous | Affected$ Creature.Other+namedBrothers Yamazaki | AddPower$ 2 | AddToughness$ 2 | AddKeyword$ Haste | Description$ Each other creature named CARDNAME gets +2/+2 and has haste. S:Mode$ Continuous | Affected$ Creature.Other+namedBrothers Yamazaki | AddPower$ 2 | AddToughness$ 2 | AddKeyword$ Haste | Description$ Each other creature named CARDNAME gets +2/+2 and has haste.
DeckHints:Name$Brothers Yamazaki DeckHints:Name$Brothers Yamazaki
SVar:AILegendaryException:TwoCopiesAllowed SVar:AILegendaryException:TwoCopiesAllowed

View File

@@ -1,7 +1,7 @@
Name:Mirror Box Name:Mirror Box
ManaCost:3 ManaCost:3
Types:Artifact Types:Artifact
S:Mode$ Continuous | Affected$ Permanent.YouCtrl | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ The "legend rule" doesn't apply to permanents you control. S:Mode$ IgnoreLegendRule | ValidCard$ Permanent.YouCtrl | Description$ The "legend rule" doesn't apply to permanents you control.
S:Mode$ Continuous | Affected$ Creature.Legendary+YouCtrl | AddPower$ 1 | AddToughness$ 1 | Description$ Each legendary creature you control gets +1/+1. S:Mode$ Continuous | Affected$ Creature.Legendary+YouCtrl | AddPower$ 1 | AddToughness$ 1 | Description$ Each legendary creature you control gets +1/+1.
S:Mode$ Continuous | Affected$ Creature.nonToken+YouCtrl | AddPower$ AffectedX | AddToughness$ AffectedX | Description$ Each nontoken creature you control gets +1/+1 for each other creature you control with the same name as that creature. S:Mode$ Continuous | Affected$ Creature.nonToken+YouCtrl | AddPower$ AffectedX | AddToughness$ AffectedX | Description$ Each nontoken creature you control gets +1/+1 for each other creature you control with the same name as that creature.
SVar:AffectedX:Count$Valid Creature.Other+sharesNameWith+YouCtrl SVar:AffectedX:Count$Valid Creature.Other+sharesNameWith+YouCtrl

View File

@@ -1,7 +1,7 @@
Name:Mirror Gallery Name:Mirror Gallery
ManaCost:5 ManaCost:5
Types:Artifact Types:Artifact
S:Mode$ Continuous | GlobalRule$ The legend rule doesn't apply. | Description$ The "legend rule" doesn't apply. S:Mode$ IgnoreLegendRule | Description$ The "legend rule" doesn't apply.
SVar:NonStackingEffect:True SVar:NonStackingEffect:True
AI:RemoveDeck:Random AI:RemoveDeck:Random
Oracle:The "legend rule" doesn't apply. Oracle:The "legend rule" doesn't apply.

View File

@@ -4,7 +4,7 @@ Types:Legendary Creature Human Rogue
PT:3/1 PT:3/1
K:ETBReplacement:Copy:DBCopy:Optional K:ETBReplacement:Copy:DBCopy:Optional
SVar:DBCopy:DB$ Clone | Choices$ Creature.YouCtrl+Other | AddKeywords$ Partner | AddStaticAbilities$ NoLegendRule | SpellDescription$ You may have CARDNAME enter the battlefield as a copy of another creature you control, except it has CARDNAME's other abilities. SVar:DBCopy:DB$ Clone | Choices$ Creature.YouCtrl+Other | AddKeywords$ Partner | AddStaticAbilities$ NoLegendRule | SpellDescription$ You may have CARDNAME enter the battlefield as a copy of another creature you control, except it has CARDNAME's other abilities.
SVar:NoLegendRule:Mode$ Continuous | Affected$ Permanent.YouCtrl | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ The "legend rule" doesn't apply to permanents you control. SVar:NoLegendRule:Mode$ IgnoreLegendRule | ValidCard$ Permanent.YouCtrl | Description$ The "legend rule" doesn't apply to permanents you control.
S:Mode$ Continuous | Affected$ Permanent.YouCtrl | AddHiddenKeyword$ Legend rule doesn't apply to CARDNAME. | Description$ The "legend rule" doesn't apply to permanents you control. S:Mode$ IgnoreLegendRule | ValidCard$ Permanent.YouCtrl | Description$ The "legend rule" doesn't apply to permanents you control.
K:Partner K:Partner
Oracle:You may have Sakashima of a Thousand Faces enter the battlefield as a copy of another creature you control, except it has Sakashima of a Thousand Faces's other abilities.\nThe "legend rule" doesn't apply to permanents you control.\nPartner (You can have two commanders if both have partner.) Oracle:You may have Sakashima of a Thousand Faces enter the battlefield as a copy of another creature you control, except it has Sakashima of a Thousand Faces's other abilities.\nThe "legend rule" doesn't apply to permanents you control.\nPartner (You can have two commanders if both have partner.)