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.game.Game;
import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
@@ -43,7 +42,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellPermanent;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
@@ -78,9 +76,7 @@ public class AttachAi extends SpellAbilityAi {
}
}
if (!ai.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)
&& source.getType().isLegendary() && sa instanceof SpellPermanent
&& ai.isCardInPlay(source.getName())) {
if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
// 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

View File

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

View File

@@ -20,11 +20,13 @@ package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
@@ -37,6 +39,7 @@ import forge.game.player.Player;
import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
/**
* <p>
@@ -97,13 +100,19 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
return true;
} else {
// currently only Clockspinning
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
// logic to remove some counter
CardCollection countersList = CardLists.filter(list, CardPredicates.hasCounters());
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,
CardPredicates.nameEquals("Dark Depths"), CardPredicates.hasCounter(CounterEnumType.ICE));
@@ -198,9 +207,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
@Override
public CounterType chooseCounterType(List<CounterType> options, SpellAbility sa, Map<String, Object> params) {
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
Card tgt = (Card) params.get("Target");
@@ -229,7 +235,15 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
} else {
// this counters are treat first to be removed
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);
}
} 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) {
if (kindOfChoice.equals(BinaryChoiceType.AddOrRemove)) {
final Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
Card tgt = (Card) params.get("Target");
CounterType type = (CounterType) params.get("CounterType");
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
if (tgt.getController().isOpponentOf(ai)) {
if (type.is(CounterEnumType.LOYALTY) && tgt.isPlaneswalker()) {
return false;
@@ -274,7 +286,15 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
return ComputerUtil.isNegativeCounter(type, tgt);
} else {
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;
}
} 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.Map;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
@@ -11,7 +13,6 @@ import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -117,16 +118,21 @@ public class CountersRemoveAi extends SpellAbilityAi {
// Filter AI-specific targets if provided
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")) {
// Logic Part for Vampire Hexmage
// Break Dark Depths
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
if (maritEmpty) {
CardCollectionView depthsList = ai.getCardsIn(ZoneType.Battlefield, "Dark Depths");
depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa),
CardPredicates.hasCounter(CounterEnumType.ICE, 3));
if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst());
return true;
@@ -161,7 +167,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
// 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");
depthsList = CardLists.filter(depthsList, CardPredicates.isTargetableBy(sa),
CardPredicates.hasCounter(CounterEnumType.ICE));

View File

@@ -11,8 +11,6 @@ import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardType.Supertype;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
@@ -52,28 +50,26 @@ public class PermanentAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final Game game = ai.getGame();
// check on legendary
if (card.getType().isLegendary()
&& !game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule)) { // TODO check the risk we'd lose the effect with bad timing
if (ai.isCardInPlay(card.getName())) { // TODO check for keyword
if (!card.hasSVar("AILegendaryException")) {
// AiPlayDecision.WouldDestroyLegend
return false;
} else {
String specialRule = card.getSVar("AILegendaryException");
if ("TwoCopiesAllowed".equals(specialRule)) {
// One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
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
if (!card.ignoreLegendRule() && ai.isCardInPlay(card.getName())) {
// TODO check the risk we'd lose the effect with bad timing
// TODO check for keyword
if (!card.hasSVar("AILegendaryException")) {
// AiPlayDecision.WouldDestroyLegend
return false;
} else {
String specialRule = card.getSVar("AILegendaryException");
if ("TwoCopiesAllowed".equals(specialRule)) {
// One extra copy allowed on the battlefield, e.g. Brothers Yamazaki
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;
}
}
}

View File

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

View File

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