() {
@Override
public int compare(final Card o1, final Card o2) {
if (o1.hasSVar("MustBeBlocked") && !o2.hasSVar("MustBeBlocked")) {
@@ -252,8 +252,10 @@ private void makeGoodBlocks(final Combat combat) {
}
}
// 4.Blockers that have a big upside when dying
+ // 4a.Blockers that are profitable to sacrifice even in the event of an unfavorable block
for (Card b : blockers) {
- if (b.hasSVar("SacMe") && Integer.parseInt(b.getSVar("SacMe")) > 3) {
+ if ((b.hasSVar("SacMe") && Integer.parseInt(b.getSVar("SacMe")) > 3) ||
+ (b.hasSVar("SacMeAfterBlock") && !attacker.hasKeyword(Keyword.TRAMPLE) && !attacker.hasKeyword(Keyword.BANDING))) {
blocker = b;
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
blockedButUnkilled.add(attacker);
@@ -1355,8 +1357,8 @@ private boolean wouldLikeToRandomlyTrade(Card attacker, Card blocker, Combat com
}
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false);
- boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
- boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
+ boolean atkEmbalm = (attacker.hasKeyword(Keyword.EMBALM) || attacker.hasKeyword(Keyword.ETERNALIZE)) && !attacker.isToken();
+ boolean blkEmbalm = (blocker.hasKeyword(Keyword.EMBALM) || blocker.hasKeyword(Keyword.ETERNALIZE)) && !blocker.isToken();
if (atkEmbalm && !blkEmbalm) {
// The opponent will eventually get his creature back, while the AI won't
diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java
index 9cc68bea358..41c34b8b0b8 100644
--- a/forge-ai/src/main/java/forge/ai/AiController.java
+++ b/forge-ai/src/main/java/forge/ai/AiController.java
@@ -23,7 +23,6 @@
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ability.ChangeZoneAi;
-import forge.ai.ability.ExploreAi;
import forge.ai.ability.LearnAi;
import forge.ai.simulation.SpellAbilityPicker;
import forge.card.CardStateName;
@@ -69,7 +68,10 @@
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
-import java.util.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
/**
*
@@ -89,6 +91,7 @@ public class AiController {
private boolean useSimulation;
private SpellAbilityPicker simPicker;
private int lastAttackAggression;
+ private boolean useLivingEnd;
public AiController(final Player computerPlayer, final Game game0) {
player = computerPlayer;
@@ -189,13 +192,12 @@ private boolean checkCurseEffects(final SpellAbility sa) {
if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
&& !host.hasKeyword(Keyword.INDESTRUCTIBLE)) {
return true;
- } else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
- && CardFactoryUtil.isCounterable(host)) {
+ } else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment() && sa.isCounterableBy(null)) {
return true;
- } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
+ } else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && sa.isCounterableBy(null)
&& host.getCMC() == c.getCounters(CounterEnumType.CHARGE)) {
return true;
- } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
+ } else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && sa.isCounterableBy(null)) {
String hostName = host.getName();
for (Card card : ccvGameBattlefield) {
if (!card.isToken() && card.sharesNameWith(host)) {
@@ -361,28 +363,26 @@ private boolean checkETBEffectsPreparedCard(final Card card, final SpellAbility
}
}
- for (final Trigger tr : card.getTriggers()) {
- if (!card.hasStartOfKeyword("Saga") && !card.hasStartOfKeyword("Read ahead")) {
- break;
- }
+ if (card.isSaga()) {
+ for (final Trigger tr : card.getTriggers()) {
+ if (tr.getMode() != TriggerType.CounterAdded || !tr.isChapter()) {
+ continue;
+ }
- if (tr.getMode() != TriggerType.CounterAdded) {
- continue;
- }
+ SpellAbility exSA = tr.ensureAbility().copy(activator);
- SpellAbility exSA = tr.ensureAbility().copy(activator);
+ if (api != null && exSA.getApi() == api) {
+ rightapi = true;
+ }
- if (api != null && exSA.getApi() == api) {
- rightapi = true;
- }
+ if (exSA instanceof AbilitySub && !doTrigger(exSA, false)) {
+ // AI would not run this chapter if given the chance
+ // TODO eventually we'll want to consider playing it anyway, especially if Read ahead would still allow an immediate benefit
+ return false;
+ }
- if (exSA instanceof AbilitySub && !doTrigger(exSA, false)) {
- // AI would not run this chapter if given the chance
- // TODO eventually we'll want to consider playing it anyway, especially if Read ahead would still allow an immediate benefit
- return false;
+ break;
}
-
- break;
}
if (api != null && !rightapi) {
@@ -761,7 +761,7 @@ private AiPlayDecision canPlayAndPayFor(final SpellAbility sa) {
return decision;
}
-
+
// This is for playing spells regularly (no Cascade/Ripple etc.)
private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
final Card host = sa.getHostCard();
@@ -790,7 +790,7 @@ private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
}
int oldCMC = -1;
- boolean xCost = sa.costHasX() || host.hasStartOfKeyword("Strive");
+ boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE);
if (!xCost) {
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
@@ -812,7 +812,7 @@ private AiPlayDecision canPlayAndPayForFace(final SpellAbility sa) {
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
- if (!sa.isSpell() || CardFactoryUtil.isCounterable(host)) {
+ if (!sa.isSpell() || sa.isCounterableBy(null)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
// TODO some older cards don't use the keyword, so check for trigger instead
@@ -1088,7 +1088,6 @@ public CardCollection getCardsToDiscard(int min, final int max, final CardCollec
return discards;
}
}
-
}
// look for good discards
@@ -1096,9 +1095,7 @@ public CardCollection getCardsToDiscard(int min, final int max, final CardCollec
Card prefCard = null;
if (sa != null && sa.getActivatingPlayer() != null && sa.getActivatingPlayer().isOpponentOf(player)) {
for (Card c : validCards) {
- if (c.hasKeyword("If a spell or ability an opponent controls causes you to discard CARDNAME,"
- + " put it onto the battlefield instead of putting it into your graveyard.")
- || !c.getSVar("DiscardMeByOpp").isEmpty()) {
+ if (c.hasSVar("DiscardMeByOpp")) {
prefCard = c;
break;
}
@@ -1202,7 +1199,7 @@ public CardCollection getCardsToDiscard(int min, final int max, final CardCollec
}
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
- if (mode == PlayerActionConfirmMode.AlternativeDamageAssignment || mode == PlayerActionConfirmMode.ChangeZoneToAltDestination) {
+ if (mode == PlayerActionConfirmMode.ChangeZoneToAltDestination) {
System.err.printf("Overriding AI confirmAction decision for %s, defaulting to true.\n", mode);
return true;
}
@@ -1231,7 +1228,7 @@ public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, S
return false;
}
- public boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message) {
+ public boolean confirmStaticApplication(Card hostCard, String logic) {
return true;
}
@@ -1304,7 +1301,7 @@ public void declareBlockersFor(Player defender, Combat combat) {
public void declareAttackers(Player attacker, Combat combat) {
// 12/2/10(sol) the decision making here has moved to getAttackers()
- AiAttackController aiAtk = new AiAttackController(attacker);
+ AiAttackController aiAtk = new AiAttackController(attacker);
lastAttackAggression = aiAtk.declareAttackers(combat);
// Check if we can reinforce with Banding creatures
@@ -1390,7 +1387,7 @@ public List chooseSpellAbilityToPlay() {
// add mayPlay option
for (CardPlayOption o : land.mayPlay(player)) {
- la = new LandAbility(land, player, o.getAbility());
+ la = new LandAbility(land, player, o);
la.setCardState(land.getCurrentState());
if (la.canPlay()) {
abilities.add(la);
@@ -1537,7 +1534,13 @@ private final SpellAbility getSpellAbilityToPlay() {
top = game.getStack().peekAbility();
}
final boolean topOwnedByAI = top != null && top.getActivatingPlayer().equals(player);
- final boolean mustRespond = top != null && top.hasParam("AIRespondsToOwnAbility");
+
+ // Must respond: cases where the AI should respond to its own triggers or other abilities (need to add negative stuff to be countered here)
+ boolean mustRespond = false;
+ if (top != null) {
+ mustRespond = top.hasParam("AIRespondsToOwnAbility"); // Forced combos (currently defined for Sensei's Divining Top)
+ mustRespond |= top.isTrigger() && top.getTrigger().isKeyword(Keyword.EVOKE); // Evoke sacrifice trigger
+ }
if (topOwnedByAI) {
// AI's own spell: should probably let my stuff resolve first, but may want to copy the SA or respond to it
@@ -1572,6 +1575,8 @@ public boolean apply(final SpellAbility spellAbility) { //don't include removedA
return spellAbility instanceof LandAbility || (spellAbility.getHostCard() != null && ComputerUtilCard.isCardRemAIDeck(spellAbility.getHostCard()));
}
});
+ //update LivingEndPlayer
+ useLivingEnd = Iterables.any(player.getZone(ZoneType.Library), CardPredicates.nameEquals("Living End"));
SpellAbility chosenSa = chooseSpellAbilityToPlayFromList(saList, true);
@@ -1594,7 +1599,8 @@ private SpellAbility chooseSpellAbilityToPlayFromList(final List a
String assertex = ComparatorUtil.verifyTransitivity(ComputerUtilAbility.saEvaluator, all);
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
-
+ //avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
+ boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
// Don't add Counterspells to the "normal" playcard lookups
if (skipCounter && sa.getApi() == ApiType.Counter) {
@@ -1609,6 +1615,33 @@ private SpellAbility chooseSpellAbilityToPlayFromList(final List a
continue;
}
}
+ // living end AI decks
+ // TODO: generalize the implementation so that superfluous logic-specific checks for life, library size, etc. aren't needed
+ AiPlayDecision aiPlayDecision = AiPlayDecision.CantPlaySa;
+ if (useLivingEnd) {
+ if (sa.isCycling() && sa.canCastTiming(player) && player.getCardsIn(ZoneType.Library).size() >= 10) {
+ if (ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
+ if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostPayLife.class)
+ && !player.cantLoseForZeroOrLessLife()
+ && player.getLife() <= sa.getPayCosts().getCostPartByType(CostPayLife.class).getAbilityAmount(sa) * 2) {
+ aiPlayDecision = AiPlayDecision.CantAfford;
+ } else {
+ aiPlayDecision = AiPlayDecision.WillPlay;
+ }
+ }
+ } else if (sa.getHostCard().hasKeyword(Keyword.CASCADE)) {
+ if (isLifeInDanger) { //needs more tune up for certain conditions
+ aiPlayDecision = player.getCreaturesInPlay().size() >= 4 ? AiPlayDecision.CantPlaySa : AiPlayDecision.WillPlay;
+ } else if (CardLists.filter(player.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES).size() > 4) {
+ if (player.getCreaturesInPlay().size() >= 4) // it's good minimum
+ continue;
+ else if (!sa.getHostCard().isPermanent() && sa.canCastTiming(player) && ComputerUtilCost.canPayCost(sa, player, sa.isTrigger()))
+ aiPlayDecision = AiPlayDecision.WillPlay;// needs tuneup for bad matchups like reanimator and other things to check on opponent graveyard
+ } else {
+ continue;
+ }
+ }
+ }
sa.setActivatingPlayer(player, true);
SpellAbility root = sa.getRootAbility();
@@ -1617,8 +1650,8 @@ private SpellAbility chooseSpellAbilityToPlayFromList(final List a
sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard());
}
-
- AiPlayDecision opinion = canPlayAndPayFor(sa);
+ //override decision for living end player
+ AiPlayDecision opinion = useLivingEnd && AiPlayDecision.WillPlay.equals(aiPlayDecision) ? aiPlayDecision : canPlayAndPayFor(sa);
// reset LastStateBattlefield
sa.clearLastState();
@@ -1840,6 +1873,8 @@ public int chooseNumber(SpellAbility sa, String title, List options, Pl
} else {
return options.get(0);
}
+ case ChooseNumber:
+ return Aggregates.random(options);
default:
return options.get(0);
}
@@ -2068,9 +2103,7 @@ public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List orderPlaySa(List activePlayerSAs) {
List putCounter = filterListByApi(activePlayerSAs, ApiType.PutCounter);
List putCounterAll = filterListByApi(activePlayerSAs, ApiType.PutCounterAll);
- List evolve = filterList(putCounter, SpellAbilityPredicates.hasParam("Evolve"));
+ List evolve = filterList(putCounter, CardTraitPredicates.isKeyword(Keyword.EVOLVE));
List token = filterListByApi(activePlayerSAs, ApiType.Token);
List pump = filterListByApi(activePlayerSAs, ApiType.Pump);
diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java
index 07be4d52f7b..c6504bbae79 100644
--- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java
+++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java
@@ -56,6 +56,14 @@ public PaymentDecision visit(CostChooseCreatureType cost) {
return PaymentDecision.type(choice);
}
+ @Override
+ public PaymentDecision visit(CostCollectEvidence cost) {
+ int c = cost.getAbilityAmount(ability);
+ CardCollectionView chosen = ComputerUtil.chooseCollectEvidence(player, cost, source, c, ability);
+
+ return null == chosen ? null : PaymentDecision.card(chosen);
+ }
+
@Override
public PaymentDecision visit(CostDiscard cost) {
final String type = cost.getType();
@@ -141,20 +149,21 @@ public PaymentDecision visit(CostDraw cost) {
@Override
public PaymentDecision visit(CostExile cost) {
+ String type = cost.getType();
if (cost.payCostFromSource()) {
return PaymentDecision.card(source);
}
- if (cost.getType().equals("All")) {
+ if (type.equals("All")) {
return PaymentDecision.card(player.getCardsIn(cost.getFrom()));
}
- else if (cost.getType().contains("FromTopGrave")) {
+ else if (type.contains("FromTopGrave")) {
return null;
}
int c = cost.getAbilityAmount(ability);
- if (cost.getFrom().equals(ZoneType.Library)) {
+ if (cost.from.size() == 1 && cost.getFrom().get(0).equals(ZoneType.Library)) {
return PaymentDecision.card(player.getCardsIn(ZoneType.Library, c));
}
else if (cost.zoneRestriction == 0) {
@@ -301,7 +310,6 @@ public PaymentDecision visit(CostPayLife cost) {
if (!player.canPayLife(c, isEffect(), ability)) {
return null;
}
- // activator.payLife(c, null);
return PaymentDecision.number(c);
}
@@ -334,7 +342,7 @@ public PaymentDecision visit(CostPutCardToLib cost) {
list = CardLists.getValidCards(list, cost.getType().split(";"), player, source, ability);
if (cost.isSameZone()) {
- // Jotun Grunt
+ // Jötun Grunt
// TODO: improve AI
final FCollectionView players = game.getPlayers();
for (Player p : players) {
@@ -498,14 +506,14 @@ public PaymentDecision visit(CostReveal cost) {
}
@Override
- public PaymentDecision visit(CostRevealChosenPlayer cost) {
+ public PaymentDecision visit(CostRevealChosen cost) {
return PaymentDecision.number(1);
}
protected int removeCounter(GameEntityCounterTable table, List prefs, CounterEnumType cType, int stillToRemove) {
int removed = 0;
if (!prefs.isEmpty() && stillToRemove > 0) {
- Collections.sort(prefs, CardPredicates.compareByCounterType(cType));
+ prefs.sort(CardPredicates.compareByCounterType(cType));
for (Card prefCard : prefs) {
// already enough removed
@@ -667,7 +675,7 @@ public boolean apply(final Card crd) {
return crd.getCounters(CounterEnumType.QUEST) > e;
}
});
- Collections.sort(prefs, Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST)));
+ prefs.sort(Collections.reverseOrder(CardPredicates.compareByCounterType(CounterEnumType.QUEST)));
for (final Card crd : prefs) {
int e = 0;
diff --git a/forge-ai/src/main/java/forge/ai/AiProps.java b/forge-ai/src/main/java/forge/ai/AiProps.java
index ee162987ba9..2edeacba908 100644
--- a/forge-ai/src/main/java/forge/ai/AiProps.java
+++ b/forge-ai/src/main/java/forge/ai/AiProps.java
@@ -134,7 +134,12 @@ public enum AiProps { /** */
FLASH_BUFF_AURA_CHANCE_TO_RESPOND_TO_STACK("100"),
BLINK_RELOAD_PLANESWALKER_CHANCE("30"), /** */
BLINK_RELOAD_PLANESWALKER_MAX_LOYALTY("2"), /** */
- BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"); /** */
+ BLINK_RELOAD_PLANESWALKER_LOYALTY_DIFF("2"),
+ SACRIFICE_DEFAULT_PREF_ENABLE("true"),
+ SACRIFICE_DEFAULT_PREF_MIN_CMC("0"),
+ SACRIFICE_DEFAULT_PREF_MAX_CMC("2"),
+ SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS("true"),
+ SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL("135");
// Experimental features, must be promoted or removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
index a8f6e1e833b..13f83ee7cda 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java
@@ -17,14 +17,9 @@
*/
package forge.ai;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
+import forge.game.cost.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
@@ -35,7 +30,6 @@
import com.google.common.collect.Multimap;
import forge.ai.AiCardMemory.MemorySet;
-import forge.ai.ability.ChooseGenericEffectAi;
import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi;
import forge.card.CardStateName;
@@ -65,13 +59,6 @@
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
-import forge.game.cost.Cost;
-import forge.game.cost.CostDiscard;
-import forge.game.cost.CostExile;
-import forge.game.cost.CostPart;
-import forge.game.cost.CostPayment;
-import forge.game.cost.CostPutCounter;
-import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -302,7 +289,7 @@ public static final boolean playSpellAbilityWithoutPayingManaCost(final Player a
SpellAbility newSA = sa.copyWithNoManaCost();
newSA.setActivatingPlayer(ai, true);
- if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) {
+ if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA, false) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0, false)) {
return false;
}
@@ -433,12 +420,41 @@ public static Card getCardPreference(final Player ai, final Card activate, final
final CardCollection sacMeList = CardLists.filter(typeList, new Predicate() {
@Override
public boolean apply(final Card c) {
- return c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) == priority;
+ return (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) == priority)
+ || (priority == 1 && shouldSacrificeThreatenedCard(ai, c, sa));
}
});
if (!sacMeList.isEmpty()) {
CardLists.shuffle(sacMeList);
- return sacMeList.get(0);
+ return sacMeList.getFirst();
+ } else {
+ // empty sacMeList, so get some viable average preference if the option is enabled
+ if (ai.getController().isAI()) {
+ AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
+ boolean enableDefaultPref = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ENABLE);
+ if (enableDefaultPref) {
+ int minCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MIN_CMC);
+ int maxCMC = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CMC);
+ int maxCreatureEval = aic.getIntProperty(AiProps.SACRIFICE_DEFAULT_PREF_MAX_CREATURE_EVAL);
+ boolean allowTokens = aic.getBooleanProperty(AiProps.SACRIFICE_DEFAULT_PREF_ALLOW_TOKENS);
+ List dontSac = Arrays.asList("Black Lotus", "Mox Pearl", "Mox Jet", "Mox Emerald", "Mox Ruby", "Mox Sapphire", "Lotus Petal");
+ CardCollection allowList = CardLists.filter(typeList, new Predicate() {
+ @Override
+ public boolean apply(Card card) {
+ if (card.isCreature() && ComputerUtilCard.evaluateCreature(card) > maxCreatureEval) {
+ return false;
+ }
+
+ return (allowTokens && card.isToken())
+ || (card.getCMC() >= minCMC && card.getCMC() <= maxCMC && !dontSac.contains(card.getName()));
+ }
+ });
+ if (!allowList.isEmpty()) {
+ CardLists.sortByCmcDesc(allowList);
+ return allowList.getLast();
+ }
+ }
+ }
}
}
@@ -493,7 +509,7 @@ else if (pref.contains("DiscardCost")) { // search for permanents with DiscardMe
}
}
- if (ComputerUtilCost.isFreeCastAllowedByPermanent(ai, "Discard")) {
+ if (activate != null && ComputerUtilCost.isFreeCastAllowedByPermanent(ai, "Discard")) {
// Dream Halls allows to discard 1 worthless card to cast 1 expensive for free
// Do it even if nothing marked for discard in hand, if it's worth doing!
int mana = ComputerUtilMana.getAvailableManaEstimate(ai, false);
@@ -621,6 +637,12 @@ public static CardCollection chooseSacrificeType(final Player ai, final String t
// don't sacrifice the card we're pumping
typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, ability, ai);
+ // if the source has "Casualty", don't sacrifice cards that may have granted the effect
+ // TODO: is there a surefire way to determine which card added Casualty?
+ if (source.hasKeyword(Keyword.CASUALTY)) {
+ typeList = CardLists.filter(typeList, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
+ }
+
if (typeList.size() < amount) {
return null;
}
@@ -643,6 +665,33 @@ public static CardCollection chooseSacrificeType(final Player ai, final String t
return sacList;
}
+ public static CardCollection chooseCollectEvidence(final Player ai, CostCollectEvidence cost, final Card activate, int amount, SpellAbility sa) {
+ CardCollection typeList = new CardCollection(ai.getCardsIn(ZoneType.Graveyard));
+
+ if (CardLists.getTotalCMC(typeList) < amount) return null;
+
+ // FIXME: This is suboptimal, maybe implement a single comparator that'll take care of all of this?
+ CardLists.sortByCmcDesc(typeList);
+ Collections.reverse(typeList);
+
+
+ // TODO AI needs some improvements here
+ // Whats the best way to choose evidence to collect?
+ // Probably want to filter out cards that have graveyard abilities/castable from graveyard
+ // Ideally we remove as few cards as possible "Don't overspend"
+
+ final CardCollection exileList = new CardCollection();
+ while(amount > 0) {
+ Card c = typeList.remove(0);
+
+ amount -= c.getCMC();
+
+ exileList.add(c);
+ }
+
+ return exileList;
+ }
+
public static CardCollection chooseExileFrom(final Player ai, CostExile cost, final Card activate, final int amount, SpellAbility sa) {
CardCollection typeList;
if (cost.zoneRestriction != 1) {
@@ -660,6 +709,27 @@ public static CardCollection chooseExileFrom(final Player ai, CostExile cost, fi
}
CardLists.sortByPowerAsc(typeList);
+ if (sa.isCraft()) {
+ // remove anything above 3 CMC so that high tier stuff doesn't get exiled with this
+ CardCollection toRemove = new CardCollection();
+ for (Card exileTgt : typeList) {
+ if (exileTgt.isInPlay() && exileTgt.getCMC() >= 3) toRemove.add(exileTgt);
+ }
+ typeList.removeAll(toRemove);
+ if (typeList.size() < amount) return null;
+
+ // FIXME: This is suboptimal, maybe implement a single comparator that'll take care of all of this?
+ CardLists.sortByCmcDesc(typeList);
+ Collections.reverse(typeList);
+ typeList.sort(new Comparator() {
+ @Override
+ public int compare(final Card a, final Card b) {
+ if (!a.isInPlay() && b.isInPlay()) return -1;
+ else if (!b.isInPlay() && a.isInPlay()) return 1;
+ else return 0;
+ }
+ }); // something that's not on the battlefield should come first
+ }
final CardCollection exileList = new CardCollection();
for (int i = 0; i < amount; i++) {
@@ -697,7 +767,7 @@ public static CardCollection chooseTapType(final Player ai, final String type, f
all.removeAll(exclude);
CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
- typeList = CardLists.filter(typeList, Presets.UNTAPPED);
+ typeList = CardLists.filter(typeList, Presets.CAN_TAP);
if (tap) {
typeList.remove(activate);
@@ -725,14 +795,13 @@ public static CardCollection chooseTapTypeAccumulatePower(final Player ai, final
CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
all.removeAll(exclude);
- CardCollection typeList =
- CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
+ CardCollection typeList = CardLists.getValidCards(all, type.split(";"), activate.getController(), activate, sa);
if (sa.hasParam("Crew")) {
typeList = CardLists.getNotKeyword(typeList, "CARDNAME can't crew Vehicles.");
}
- typeList = CardLists.filter(typeList, Presets.UNTAPPED);
+ typeList = CardLists.filter(typeList, Presets.CAN_TAP);
if (tap) {
typeList.remove(activate);
@@ -862,8 +931,8 @@ public static CardCollection choosePermanentsToSacrifice(final Player ai, final
boolean exceptSelf = "ExceptSelf".equals(source.getParam("AILogic"));
boolean removedSelf = false;
- if (isOptional && (source.hasParam("Devour") || source.hasParam("Exploit"))) {
- if (source.hasParam("Exploit")) {
+ if (isOptional && (source.isKeyword(Keyword.DEVOUR) || source.isKeyword(Keyword.EXPLOIT))) {
+ if (source.isKeyword(Keyword.EXPLOIT)) {
for (Trigger t : host.getTriggers()) {
if (t.getMode() == TriggerType.Exploited) {
final SpellAbility exSA = t.ensureAbility().copy(ai);
@@ -923,7 +992,7 @@ public boolean apply(final Card c) {
}
for (int i = 0; i < max; i++) {
- Card c = chooseCardToSacrifice(remaining, ai, destroy);
+ Card c = chooseCardToSacrifice(source, remaining, ai, destroy);
remaining.remove(c);
if (c != null) {
sacrificed.add(c);
@@ -938,7 +1007,7 @@ public boolean apply(final Card c) {
}
// Precondition it wants: remaining are reverse-sorted by CMC
- private static Card chooseCardToSacrifice(final CardCollection remaining, final Player ai, final boolean destroy) {
+ private static Card chooseCardToSacrifice(final SpellAbility source, CardCollection remaining, final Player ai, final boolean destroy) {
// If somehow ("Drop of Honey") they suggest to destroy opponent's card - use the chance!
for (Card c : remaining) { // first compare is fast, second is precise
if (ai.isOpponentOf(c.getController()))
@@ -951,6 +1020,7 @@ private static Card chooseCardToSacrifice(final CardCollection remaining, final
return indestructibles.get(0);
}
}
+
for (int ip = 0; ip < 6; ip++) { // priority 0 is the lowest, priority 5 the highest
final int priority = 6 - ip;
for (Card card : remaining) {
@@ -960,6 +1030,11 @@ private static Card chooseCardToSacrifice(final CardCollection remaining, final
}
}
+ if (source.isEmerge() || source.isOffering()) {
+ // don't sac when cost wouldn't be reduced
+ remaining = CardLists.filter(remaining, CardPredicates.greaterCMC(1));
+ }
+
Card c = null;
if (CardLists.getNotType(remaining, "Creature").isEmpty()) {
c = ComputerUtilCard.getWorstCreatureAI(remaining);
@@ -1127,7 +1202,7 @@ public static boolean castPermanentInMain1(final Player ai, final SpellAbility s
return true;
}
- if (cardState.hasKeyword(Keyword.RIOT) && ChooseGenericEffectAi.preferHasteForRiot(sa, ai)) {
+ if (cardState.hasKeyword(Keyword.RIOT) && SpecialAiLogic.preferHasteForRiot(sa, ai)) {
// Planning to choose Haste for Riot, so do this in Main 1
return true;
}
@@ -1423,6 +1498,8 @@ public static boolean activateForCost(SpellAbility sa, final Player ai) {
if (type.equals("CARDNAME")) {
if (source.getSVar("SacMe").equals("6")) {
return true;
+ } else if (shouldSacrificeThreatenedCard(ai, source, sa)) {
+ return true;
}
continue;
}
@@ -1432,6 +1509,8 @@ public static boolean activateForCost(SpellAbility sa, final Player ai) {
for (Card c : typeList) {
if (c.getSVar("SacMe").equals("6")) {
return true;
+ } else if (shouldSacrificeThreatenedCard(ai, c, sa)) {
+ return true;
}
}
}
@@ -1798,6 +1877,13 @@ private static Iterable extends GameObject> predictThreatenedObjects(final Pla
}
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
+ if (saviour.usesTargeting() && !saviour.canTarget(c)) {
+ continue;
+ } else if (saviour.getPayCosts() != null && saviour.getPayCosts().hasSpecificCostType(CostSacrifice.class)
+ && (!ComputerUtilCost.isSacrificeSelfCost(saviour.getPayCosts())) || c == source) {
+ continue;
+ }
+
boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c, false);
if ((!topStack.usesTargeting() && !grantIndestructible && !canSave)
|| (!grantIndestructible && !grantShroud && !canSave)) {
@@ -1806,6 +1892,13 @@ private static Iterable extends GameObject> predictThreatenedObjects(final Pla
}
if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
+ if (saviour.usesTargeting() && !saviour.canTarget(c)) {
+ continue;
+ } else if (saviour.getPayCosts() != null && saviour.getPayCosts().hasSpecificCostType(CostSacrifice.class)
+ && (!ComputerUtilCost.isSacrificeSelfCost(saviour.getPayCosts())) || c == source) {
+ continue;
+ }
+
boolean canSave = ComputerUtilCombat.predictDamageTo(c, dmg - toughness, source, false) < ComputerUtilCombat.getDamageToKill(c, false);
if (!canSave) {
continue;
@@ -2026,25 +2119,35 @@ else if ((threatApi == ApiType.Attach && (topStack.isCurse() || "Curse".equals(t
* @return true if the creature dies according to current board position.
*/
public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa) {
+ return predictCreatureWillDieThisTurn(ai, creature, excludeSa, false);
+ }
+
+ public static boolean predictCreatureWillDieThisTurn(final Player ai, final Card creature, final SpellAbility excludeSa, final boolean nonCombatOnly) {
final Game game = ai.getGame();
// a creature will [hopefully] die from a spell on stack
boolean willDieFromSpell = false;
boolean noStackCheck = false;
- AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
- if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
- // See if permission is on stack and ignore this check if there is and the relevant AI flag is set
- // TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
- for (SpellAbilityStackInstance si : game.getStack()) {
- SpellAbility sa = si.getSpellAbility();
- if (sa.getApi() == ApiType.Counter) {
- noStackCheck = true;
- break;
+ if (ai.getController().isAI()) {
+ AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
+ if (aic.getBooleanProperty(AiProps.DONT_EVAL_KILLSPELLS_ON_STACK_WITH_PERMISSION)) {
+ // See if permission is on stack and ignore this check if there is and the relevant AI flag is set
+ // TODO: improve this so that this flag is not needed and the AI can properly evaluate spells in presence of counterspells.
+ for (SpellAbilityStackInstance si : game.getStack()) {
+ SpellAbility sa = si.getSpellAbility();
+ if (sa.getApi() == ApiType.Counter) {
+ noStackCheck = true;
+ break;
+ }
}
}
}
willDieFromSpell = !noStackCheck && predictThreatenedObjects(creature.getController(), excludeSa).contains(creature);
+ if (nonCombatOnly) {
+ return willDieFromSpell;
+ }
+
// a creature will die as a result of combat
boolean willDieInCombat = !willDieFromSpell && game.getPhaseHandler().inCombat()
&& ComputerUtilCombat.combatantWouldBeDestroyed(creature.getController(), creature, game.getCombat());
@@ -2140,6 +2243,10 @@ public boolean apply(final Card c) {
final int handSize = handList.size();
final int landSize = lands.size();
int score = handList.size();
+ //adjust score for Living End decks
+ final CardCollectionView livingEnd = CardLists.filter(handList, c -> "Living End".equalsIgnoreCase(c.getName()));
+ if (livingEnd.size() > 0)
+ score = -(livingEnd.size() * 10);
if (handSize/2 == landSize || handSize/2 == landSize +1) {
score += 10;
@@ -2369,7 +2476,7 @@ public boolean apply(final Card c) {
return goodChoices;
}
- Collections.sort(goodChoices, CardLists.TextLenComparator);
+ goodChoices.sort(CardLists.TextLenComparator);
CardLists.sortByCmcDesc(goodChoices);
@@ -2457,6 +2564,13 @@ public static String chooseSomeType(Player ai, String kindOfType, SpellAbility s
else if (logic.equals("MostProminentComputerControls")) {
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Battlefield), valid);
}
+ else if (logic.equals("MostProminentComputerControlsOrOwns")) {
+ CardCollectionView list = ai.getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
+ if (list.isEmpty()) {
+ list = ai.getCardsIn(Arrays.asList(ZoneType.Library));
+ }
+ chosen = ComputerUtilCard.getMostProminentType(list, valid);
+ }
else if (logic.equals("MostProminentOppControls")) {
CardCollection list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
chosen = ComputerUtilCard.getMostProminentType(list, valid);
@@ -2916,7 +3030,8 @@ public static boolean isNegativeCounter(CounterType type, Card c) {
|| type.is(CounterEnumType.GOLD) || type.is(CounterEnumType.MUSIC) || type.is(CounterEnumType.PUPA)
|| type.is(CounterEnumType.PARALYZATION) || type.is(CounterEnumType.SHELL) || type.is(CounterEnumType.SLEEP)
|| type.is(CounterEnumType.SLUMBER) || type.is(CounterEnumType.SLEIGHT) || type.is(CounterEnumType.WAGE)
- || type.is(CounterEnumType.INCARNATION) || type.is(CounterEnumType.RUST) || type.is(CounterEnumType.STUN);
+ || type.is(CounterEnumType.INCARNATION) || type.is(CounterEnumType.RUST) || type.is(CounterEnumType.STUN)
+ || type.is(CounterEnumType.FINALITY);
}
// this countertypes has no effect
@@ -3233,5 +3348,20 @@ public static boolean isETBprevented(Card c) {
List list = c.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.CantHappen);
return !list.isEmpty();
}
-
+
+ public static boolean shouldSacrificeThreatenedCard(Player ai, Card c, SpellAbility sa) {
+ if (!ai.getController().isAI()) {
+ return false; // only makes sense for actual AI decisions
+ } else if (sa != null && sa.getApi() == ApiType.Regenerate && sa.getHostCard().equals(c)) {
+ return false; // no use in sacrificing a card in an attempt to regenerate it
+ }
+ ComputerUtilCost.setSuppressRecursiveSacCostCheck(true);
+ Game game = ai.getGame();
+ Combat combat = game.getCombat();
+ boolean isThreatened = (c.isCreature() && ComputerUtil.predictCreatureWillDieThisTurn(ai, c, sa, false)
+ && (!ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, c, combat) && !ComputerUtilCombat.isDangerousToSacInCombat(ai, c, combat)))
+ || (!c.isCreature() && ComputerUtil.predictThreatenedObjects(ai, sa).contains(c));
+ ComputerUtilCost.setSuppressRecursiveSacCostCheck(false);
+ return isThreatened;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
index a76df0a12e6..371f98f4496 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilAbility.java
@@ -1,6 +1,5 @@
package forge.ai;
-import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
@@ -107,7 +106,7 @@ public static List getOriginalAndAltCostAbilities(final List saAltCosts = GameActionUtil.getAlternativeCosts(sa, player);
+ List saAltCosts = GameActionUtil.getAlternativeCosts(sa, player, false);
List priorityAltSa = Lists.newArrayList();
List otherAltSa = Lists.newArrayList();
for (SpellAbility altSa : saAltCosts) {
@@ -256,10 +255,27 @@ public int compareEvaluator(final SpellAbility a, final SpellAbility b, boolean
}
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
- if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
- return 1;
- } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
- return -1;
+ if (ApiType.RollPlanarDice == a.getApi() || ApiType.RollPlanarDice == b.getApi()) {
+ Card hostCardForGame = a.getHostCard();
+ if (hostCardForGame == null) {
+ if (b.getHostCard() != null) {
+ hostCardForGame = b.getHostCard();
+ } else {
+ return 0; // fallback if neither SA have a host card somehow
+ }
+ }
+ Game game = hostCardForGame.getGame();
+ if (game.getActivePlanes() != null) {
+ for (Card c : game.getActivePlanes()) {
+ if (c.hasSVar("AIRollPlanarDieParams") && c.getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
+ if (ApiType.RollPlanarDice == a.getApi()) {
+ return 1;
+ } else {
+ return -1;
+ }
+ }
+ }
+ }
}
// deprioritize pump spells with pure energy cost (can be activated last,
@@ -402,7 +418,7 @@ public static List sortCreatureSpells(final List all
return all;
}
// TODO this doesn't account for nearly identical creatures where one is a newer but more cost efficient variant
- Collections.sort(creatures, ComputerUtilCard.EvaluateCreatureSpellComparator);
+ creatures.sort(ComputerUtilCard.EvaluateCreatureSpellComparator);
int idx = 0;
for (int i = 0; i < all.size(); i++) {
if (all.get(i).getApi() == ApiType.PermanentCreature) {
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
index ae0e90961be..8ca2ab18064 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java
@@ -1,13 +1,7 @@
package forge.ai;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.Map.Entry;
-import java.util.Set;
import com.google.common.base.Function;
import forge.ai.simulation.GameStateEvaluator;
@@ -90,7 +84,7 @@ public boolean apply(final Card c) {
* @param list
*/
public static void sortByEvaluateCreature(final CardCollection list) {
- Collections.sort(list, ComputerUtilCard.EvaluateCreatureComparator);
+ list.sort(ComputerUtilCard.EvaluateCreatureComparator);
}
// The AI doesn't really pick the best artifact, just the most expensive.
@@ -237,7 +231,26 @@ public static Card getBestLandAI(final Iterable list) {
final List nbLand = CardLists.filter(land, Predicates.not(CardPredicates.Presets.BASIC_LANDS));
if (!nbLand.isEmpty()) {
- // TODO - Rank non basics?
+ // TODO - Improve ranking various non-basic lands depending on context
+
+ // Urza's Mine/Tower/Power Plant
+ final CardCollectionView aiAvailable = nbLand.get(0).getController().getCardsIn(Arrays.asList(ZoneType.Battlefield, ZoneType.Hand));
+ if (Iterables.any(list, CardPredicates.nameEquals("Urza's Mine"))) {
+ if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Mine")).isEmpty()) {
+ return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Mine")).getFirst();
+ }
+ }
+ if (Iterables.any(list, CardPredicates.nameEquals("Urza's Tower"))) {
+ if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Tower")).isEmpty()) {
+ return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Tower")).getFirst();
+ }
+ }
+ if (Iterables.any(list, CardPredicates.nameEquals("Urza's Power Plant"))) {
+ if (CardLists.filter(aiAvailable, CardPredicates.nameEquals("Urza's Power Plant")).isEmpty()) {
+ return CardLists.filter(nbLand, CardPredicates.nameEquals("Urza's Power Plant")).getFirst();
+ }
+ }
+
return Aggregates.random(nbLand);
}
@@ -539,12 +552,13 @@ public static final Card getCheapestSpellAI(final Iterable list) {
if (!Iterables.isEmpty(list)) {
CardCollection cc = CardLists.filter(list,
Predicates.or(CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
- Collections.sort(cc, CardLists.CmcComparatorInv);
if (cc.isEmpty()) {
return null;
}
+ cc.sort(CardLists.CmcComparatorInv);
+
Card cheapest = cc.getLast();
if (cheapest.hasSVar("DoNotDiscardIfAble")) {
for (int i = cc.size() - 1; i >= 0; i--) {
@@ -883,7 +897,7 @@ public static String getMostProminentType(final CardCollectionView list, final C
}
}
// special rule for Fabricate and Servo
- if (c.hasStartOfKeyword(Keyword.FABRICATE.toString())) {
+ if (c.hasKeyword(Keyword.FABRICATE)) {
Integer count = typesInDeck.getOrDefault("Servo", 0);
typesInDeck.put("Servo", count + weight);
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
index 6af016139c0..ac30b112a7b 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
@@ -1230,6 +1230,13 @@ public static int predictPowerBonusOfAttacker(final Card attacker, final Card bl
continue;
}
+ // Extra check for the Exalted trigger in case we're declaring more than one attacker
+ if (combat != null && trigger.isKeyword(Keyword.EXALTED)) {
+ if (!combat.getAttackers().isEmpty() && !combat.getAttackers().contains(attacker)) {
+ continue;
+ }
+ }
+
SpellAbility sa = trigger.ensureAbility();
if (sa == null) {
continue;
@@ -1250,7 +1257,7 @@ public static int predictPowerBonusOfAttacker(final Card attacker, final Card bl
sa.setActivatingPlayer(source.getController(), true);
if (sa.hasParam("Cost")) {
- if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
+ if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
continue;
}
}
@@ -1447,7 +1454,7 @@ public static int predictToughnessBonusOfAttacker(final Card attacker, final Car
continue;
}
if (sa.hasParam("Cost")) {
- if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
+ if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
continue;
}
}
@@ -1481,7 +1488,7 @@ public static int predictToughnessBonusOfAttacker(final Card attacker, final Car
continue;
}
if (sa.hasParam("Cost")) {
- if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
+ if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, true)) {
continue;
}
}
@@ -2557,4 +2564,62 @@ public static GameEntity addAttackerToCombat(SpellAbility sa, Card attacker, Ite
}
return Iterables.getFirst(defenders, null);
}
+
+ public static int checkAttackerLifelinkDamage(Combat combat) {
+ if (combat == null) {
+ return 0;
+ }
+
+ int totalLifeLinkDamage = 0;
+ for (Card attacker : combat.getAttackers()) {
+ int netDamage = attacker.getNetCombatDamage();
+ if ((attacker.hasKeyword(Keyword.LIFELINK) || attacker.hasSVar("LikeLifeLink")) && netDamage > 0) {
+ int damage = ComputerUtilCombat.predictDamageTo(combat.getDefenderByAttacker(attacker), netDamage, attacker, true);
+ boolean prevented = ComputerUtilCombat.isCombatDamagePrevented(attacker, combat.getDefenderByAttacker(attacker), damage);
+ if (!prevented) {
+ totalLifeLinkDamage += damage;
+ }
+ }
+ }
+ return totalLifeLinkDamage;
+ }
+
+ public static boolean willOpposingCreatureDieInCombat(final Player ai, final Card combatant, final Combat combat) {
+ if (combat != null) {
+ if (combat.isBlocking(combatant)) {
+ for (Card atk : combat.getAttackersBlockedBy(combatant)) {
+ if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, atk, combat)) {
+ return true;
+ }
+ }
+ } else if (combat.isBlocked(combatant)) {
+ for (Card blk : combat.getBlockers(combatant)) {
+ if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, blk, combat)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ public static boolean isDangerousToSacInCombat(final Player ai, final Card combatant, final Combat combat) {
+ if (combat != null) {
+ if (combat.isBlocking(combatant)) {
+ if (combatant.hasKeyword(Keyword.BANDING)) {
+ return true;
+ }
+ for (Card atk : combat.getAttackersBlockedBy(combatant)) {
+ if (atk.hasKeyword(Keyword.TRAMPLE)) {
+ return true;
+ }
+ }
+ } else if (combat.isBlocked(combatant)) {
+ if (combatant.hasKeyword(Keyword.BANDING)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
index ab5f6ee1406..d391af96602 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
@@ -35,6 +35,10 @@
public class ComputerUtilCost {
+ private static boolean suppressRecursiveSacCostCheck = false;
+ public static void setSuppressRecursiveSacCostCheck(boolean shouldSuppress) {
+ suppressRecursiveSacCostCheck = shouldSuppress;
+ }
/**
* Check add m1 m1 counter cost.
@@ -344,6 +348,10 @@ public static boolean checkSacrificeCost(final Player ai, final Cost cost, final
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) {
+ if (suppressRecursiveSacCostCheck) {
+ return false;
+ }
+
final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
@@ -356,6 +364,20 @@ public static boolean checkSacrificeCost(final Player ai, final Cost cost, final
if (!CardLists.filterControlledBy(source.getEnchantedBy(), source.getController()).isEmpty()) {
return false;
}
+ if (source.isCreature()) {
+ // e.g. Sakura-Tribe Elder
+ final Combat combat = ai.getGame().getCombat();
+ final boolean beforeNextTurn = ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn().equals(ai) && ComputerUtilCard.evaluateCreature(source) <= 150;
+ final boolean creatureInDanger = ComputerUtil.predictCreatureWillDieThisTurn(ai, source, sourceAbility, false)
+ && !ComputerUtilCombat.willOpposingCreatureDieInCombat(ai, source, combat);
+ final int lifeThreshold = ai.getController().isAI() ? (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD)) : 4;
+ final boolean aiInDanger = ai.getLife() <= lifeThreshold && ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife();
+ if (creatureInDanger && !ComputerUtilCombat.isDangerousToSacInCombat(ai, source, combat)) {
+ return true;
+ } else if (aiInDanger || !beforeNextTurn) {
+ return false;
+ }
+ }
continue;
}
@@ -398,10 +420,8 @@ public static boolean isSacrificeSelfCost(final Cost cost) {
return false;
}
for (final CostPart part : cost.getCostParts()) {
- if (part instanceof CostSacrifice) {
- if ("CARDNAME".equals(part.getType())) {
- return true;
- }
+ if (part instanceof CostSacrifice && part.payCostFromSource()) {
+ return true;
}
}
return false;
@@ -502,11 +522,12 @@ public static boolean canPayCost(final SpellAbility sa, final Player player, fin
sa.setActivatingPlayer(player, true); // complaints on NPE had came before this line was added.
}
- final boolean cannotBeCountered = !CardFactoryUtil.isCounterable(sa.getHostCard());
+ boolean cannotBeCountered = false;
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (sa instanceof Spell) {
+ cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
@@ -600,8 +621,22 @@ public boolean apply(Card card) {
}
}
+ // Bail early on Casualty in case there are no cards that would make sense to pay with
+ if (sa.getHostCard().hasKeyword(Keyword.CASUALTY)) {
+ for (final CostPart part : sa.getPayCosts().getCostParts()) {
+ if (part instanceof CostSacrifice) {
+ CardCollection valid = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), part.getType().split(";"),
+ sa.getActivatingPlayer(), sa.getHostCard(), sa);
+ valid = CardLists.filter(valid, Predicates.not(CardPredicates.hasSVar("AIDontSacToCasualty")));
+ if (valid.isEmpty()) {
+ return false;
+ }
+ }
+ }
+ }
+
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded, effect)
- && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
+ && CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa, effect);
}
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView payers) {
@@ -725,6 +760,8 @@ else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substri
int evalToken = ComputerUtilCard.evaluateCreatureList(tokenList);
return evalToken < evalCounter;
+ } else if ("Riot".equals(aiLogic)) {
+ return !SpecialAiLogic.preferHasteForRiot(sa, payer);
}
// Check for shocklands and similar ETB replacement effects
@@ -762,7 +799,7 @@ else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substri
if (ApiType.Counter.equals(sa.getApi())) {
List spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
for (SpellAbility toBeCountered : spells) {
- if (toBeCountered.isSpell() && !CardFactoryUtil.isCounterable(toBeCountered.getHostCard())) {
+ if (!toBeCountered.isCounterableBy(sa)) {
return false;
}
// no reason to pay if we don't plan to confirm
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
index 1252be9f17d..68ea74989a0 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
@@ -34,7 +34,7 @@
import forge.game.spellability.AbilityManaPart;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
-import forge.game.staticability.StaticAbility;
+import forge.game.staticability.StaticAbilityManaConvert;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.Zone;
@@ -126,7 +126,7 @@ private static void sortManaAbilities(final Multimap() {
+ orderedCards.sort(new Comparator() {
@Override
public int compare(final Card card1, final Card card2) {
return Integer.compare(manaCardMap.get(card1), manaCardMap.get(card2));
@@ -149,28 +149,28 @@ public int compare(final Card card1, final Card card2) {
System.out.println("Unsorted Abilities: " + newAbilities);
}
- Collections.sort(newAbilities, new Comparator() {
+ newAbilities.sort(new Comparator() {
@Override
public int compare(final SpellAbility ability1, final SpellAbility ability2) {
int preOrder = orderedCards.indexOf(ability1.getHostCard()) - orderedCards.indexOf(ability2.getHostCard());
- if (preOrder == 0) {
- // Mana abilities on the same card
- String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
+ if (preOrder != 0) {
+ return preOrder;
+ }
- boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
- boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
+ // Mana abilities on the same card
+ String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
- if (payWithAb1 && !payWithAb2) {
- return -1;
- } else if (payWithAb2 && !payWithAb1) {
- return 1;
- }
+ boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
+ boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
- return ability1.compareTo(ability2);
- } else {
- return preOrder;
+ if (payWithAb1 && !payWithAb2) {
+ return -1;
+ } else if (payWithAb2 && !payWithAb1) {
+ return 1;
}
+
+ return ability1.compareTo(ability2);
}
});
@@ -195,7 +195,7 @@ public int compare(final SpellAbility ability1, final SpellAbility ability2) {
final List prefSortedAbilities = new ArrayList<>(newAbilities);
final List otherSortedAbilities = new ArrayList<>(newAbilities);
- Collections.sort(prefSortedAbilities, new Comparator() {
+ prefSortedAbilities.sort(new Comparator() {
@Override
public int compare(final SpellAbility ability1, final SpellAbility ability2) {
if (ability1.getManaPart().mana(ability1).contains(preferredShard))
@@ -206,7 +206,7 @@ else if (ability2.getManaPart().mana(ability2).contains(preferredShard))
return 0;
}
});
- Collections.sort(otherSortedAbilities, new Comparator() {
+ otherSortedAbilities.sort(new Comparator() {
@Override
public int compare(final SpellAbility ability1, final SpellAbility ability2) {
if (ability1.getManaPart().mana(ability1).contains(preferredShard))
@@ -372,11 +372,6 @@ public boolean apply(final SpellAbility saPay) {
}
}
- final String typeRes = cost.getSourceRestriction();
- if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().isValid(typeRes, null, null, null)) {
- continue;
- }
-
if (!canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts, cost.getXManaCostPaidByColor())) {
continue;
}
@@ -394,7 +389,6 @@ public static String predictManaReplacement(SpellAbility saPayment, Player ai, M
Card hostCard = saPayment.getHostCard();
Game game = hostCard.getGame();
String manaProduced = toPay.isSnow() && hostCard.isSnow() ? "S" : GameActionUtil.generatedTotalMana(saPayment);
- //String originalProduced = manaProduced;
final Map repParams = AbilityKey.mapFromAffected(hostCard);
repParams.put(AbilityKey.Mana, manaProduced);
@@ -587,7 +581,7 @@ public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cos
}
// get a mana of this type from floating, bail if none available
- final Mana mana = CostPayment.getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1, cost.getXManaCostPaidByColor());
+ final Mana mana = CostPayment.getMana(ai, part, sa, (byte) -1, cost.getXManaCostPaidByColor());
if (mana != null) {
if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, false)) {
manaSpentToPay.add(mana);
@@ -598,7 +592,7 @@ public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cos
if (cost.isPaid()) {
// refund any mana taken from mana pool when test
- ManaPool.refundMana(manaSpentToPay, ai, sa);
+ ai.getManaPool().refundMana(manaSpentToPay);
CostPayment.handleOfferings(sa, true, cost.isPaid());
return manaSources;
}
@@ -606,7 +600,7 @@ public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cos
// arrange all mana abilities by color produced.
final ListMultimap manaAbilityMap = groupSourcesByManaColor(ai, true);
if (manaAbilityMap.isEmpty()) {
- ManaPool.refundMana(manaSpentToPay, ai, sa);
+ ai.getManaPool().refundMana(manaSpentToPay);
CostPayment.handleOfferings(sa, true, cost.isPaid());
return manaSources;
}
@@ -619,7 +613,7 @@ public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cos
ManaCostShard toPay;
// Loop over mana needed
while (!cost.isPaid()) {
- toPay = getNextShardToPay(cost);
+ toPay = getNextShardToPay(cost, sourcesForShards);
Collection saList = sourcesForShards.get(toPay);
if (saList == null) {
@@ -654,42 +648,47 @@ public static CardCollection getManaSourcesToPayCost(final ManaCostBeingPaid cos
}
CostPayment.handleOfferings(sa, true, cost.isPaid());
- ManaPool.refundMana(manaSpentToPay, ai, sa);
+ ai.getManaPool().refundMana(manaSpentToPay);
return manaSources;
}
private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, boolean checkPlayable, boolean effect) {
+ if (!CostPayment.handleOfferings(sa, test, cost.isPaid())) {
+ // nothing was chosen
+ return false;
+ }
+
AiCardMemory.clearMemorySet(ai, MemorySet.PAYS_TAP_COST);
AiCardMemory.clearMemorySet(ai, MemorySet.PAYS_SAC_COST);
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
List manaSpentToPay = test ? new ArrayList<>() : sa.getPayingMana();
List paymentList = Lists.newArrayList();
+ final ManaPool manapool = ai.getManaPool();
+
+ // Apply the color/type conversion matrix if necessary
+ manapool.restoreColorReplacements();
+ CardPlayOption mayPlay = sa.getMayPlayOption();
+ if (!effect) {
+ if (sa.isSpell() && mayPlay != null) {
+ mayPlay.applyManaConvert(manapool);
+ } else if (sa.isActivatedAbility() && sa.getGrantorStatic() != null && sa.getGrantorStatic().hasParam("ManaConversion")) {
+ AbilityUtils.applyManaColorConversion(manapool, sa.getGrantorStatic().getParam("ManaConversion"));
+ }
+ }
+ StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect ? null : sa);
if (ManaPool.payManaCostFromPool(cost, sa, ai, test, manaSpentToPay)) {
return true; // paid all from floating mana
}
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
-
- boolean ignoreColor = false, ignoreType = false;
- StaticAbility mayPlay = sa.getMayPlay();
- if (mayPlay != null) {
- if (mayPlay.hasParam("MayPlayIgnoreColor")) {
- ignoreColor = true;
- } else if (mayPlay.hasParam("MayPlayIgnoreType")) {
- ignoreType = true;
- }
- } else if (sa.hasParam("ActivateIgnoreColor")) {
- ignoreColor = true;
- }
boolean hasConverge = sa.getHostCard().hasConverge();
ListMultimap sourcesForShards = getSourcesForShards(cost, sa, ai, test,
- checkPlayable, hasConverge, ignoreColor, ignoreType);
+ checkPlayable, hasConverge);
int testEnergyPool = ai.getCounters(CounterEnumType.ENERGY);
- final ManaPool manapool = ai.getManaPool();
ManaCostShard toPay = null;
List saExcludeList = new ArrayList<>();
@@ -697,16 +696,6 @@ private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbil
while (!cost.isPaid()) {
while (!cost.isPaid() && !manapool.isEmpty()) {
boolean found = false;
-
- // Apply the color/type conversion matrix if necessary
- final CostPayment pay = new CostPayment(sa.getPayCosts(), sa);
- if (ignoreType) {
- AbilityUtils.applyManaColorConversion(pay, MagicColor.Constant.ANY_TYPE_CONVERSION);
- } else if (ignoreColor) {
- AbilityUtils.applyManaColorConversion(pay, MagicColor.Constant.ANY_COLOR_CONVERSION);
- }
- manapool.applyCardMatrix(pay);
-
for (byte color : ManaAtom.MANATYPES) {
if (manapool.tryPayCostWithColor(color, sa, cost, manaSpentToPay)) {
found = true;
@@ -725,7 +714,7 @@ private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbil
break; // no mana abilities to use for paying
}
- toPay = getNextShardToPay(cost);
+ toPay = getNextShardToPay(cost, sourcesForShards);
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
@@ -830,13 +819,7 @@ private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbil
}
}
- // FIXME: if we're ignoring color or type, assume that the color/type of the mana produced will fit the case
- // for the purpose of testing (since adding appropriate sources for shards in this particular case is handled
- // inside getSourcesForShards)
- // This is hacky and may be prone to bugs, so better implementation ideas are highly welcome.
- String manaProduced = ignoreColor || ignoreType ? MagicColor.toShortString(toPay.getColorMask())
- : predictManafromSpellAbility(saPayment, ai, toPay);
-
+ String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
payMultipleMana(cost, manaProduced, ai);
// remove from available lists
@@ -872,7 +855,7 @@ private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbil
// The cost is still unpaid, so refund the mana and report
if (!cost.isPaid()) {
- ManaPool.refundMana(manaSpentToPay, ai, sa);
+ manapool.refundMana(manaSpentToPay);
if (test) {
resetPayment(paymentList);
} else {
@@ -882,7 +865,7 @@ private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbil
}
if (test) {
- ManaPool.refundMana(manaSpentToPay, ai, sa);
+ manapool.refundMana(manaSpentToPay);
resetPayment(paymentList);
}
@@ -895,27 +878,12 @@ private static void resetPayment(List payments) {
}
}
- private static void addAllSourcesForMagicColorRange(final ListMultimap manaAbilityMap, final ListMultimap sourcesForShards, final byte[] range) {
- for (final byte b : range) {
- final ManaCostShard shard = ManaCostShard.valueOf(b);
- if (!sourcesForShards.containsKey(shard)) {
- for (final byte c : range) {
- for (SpellAbility saMana : manaAbilityMap.get((int) c)) {
- if (!sourcesForShards.get(shard).contains(saMana)) {
- sourcesForShards.get(shard).add(saMana);
- }
- }
- }
- }
- }
- }
-
/**
* Creates a mapping between the required mana shards and the available spell abilities to pay for them
*/
private static ListMultimap getSourcesForShards(final ManaCostBeingPaid cost,
final SpellAbility sa, final Player ai, final boolean test, final boolean checkPlayable,
- final boolean hasConverge, final boolean ignoreColor, final boolean ignoreType) {
+ final boolean hasConverge) {
// arrange all mana abilities by color produced.
final ListMultimap manaAbilityMap = groupSourcesByManaColor(ai, checkPlayable);
if (manaAbilityMap.isEmpty()) {
@@ -943,13 +911,6 @@ private static ListMultimap getSourcesForShards(fin
}
}
- // add all other types/colors if the specific type/color doesn't matter
- if (ignoreType) {
- addAllSourcesForMagicColorRange(manaAbilityMap, sourcesForShards, MagicColor.WUBRGC);
- } else if (ignoreColor) {
- addAllSourcesForMagicColorRange(manaAbilityMap, sourcesForShards, MagicColor.WUBRG);
- }
-
sortManaAbilities(sourcesForShards, sa);
if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: sourcesForShards = " + sourcesForShards);
@@ -1011,17 +972,11 @@ private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player a
return false;
}
- if (ma.hasParam("ActivationLimit")) {
- if (ma.getActivationsThisTurn() >= AbilityUtils.calculateAmount(sourceCard, ma.getParam("ActivationLimit"), ma)) {
- return false;
- }
- }
-
if (checkCosts) {
// Check if AI can still play this mana ability
ma.setActivatingPlayer(ai, true);
// if the AI can't pay the additional costs skip the mana ability
- if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) {
+ if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
return false;
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
return false;
@@ -1034,6 +989,10 @@ private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player a
continue;
}
+ if (!sa.allowsPayingWithShard(sourceCard, ManaAtom.fromName(s))) {
+ continue;
+ }
+
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s)))
return true;
}
@@ -1047,6 +1006,11 @@ private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player a
if (toPay == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(c), xManaCostPaidByColor)) {
continue;
}
+
+ if (!sa.allowsPayingWithShard(sourceCard, c)) {
+ continue;
+ }
+
if (ai.getManaPool().canPayForShardWithColor(toPay, c) && reflected.contains(MagicColor.toLongString(c))) {
m.setExpressChoice(MagicColor.toShortString(c));
return true;
@@ -1055,6 +1019,10 @@ private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player a
return false;
}
+ if (!sa.allowsPayingWithShard(sourceCard, MagicColor.fromName(m.getOrigProduced()))) {
+ return false;
+ }
+
if (toPay == ManaCostShard.COLORED_X) {
for (String s : m.mana(ma).split(" ")) {
if (ManaCostBeingPaid.canColoredXShardBePaidByColor(s, xManaCostPaidByColor)) {
@@ -1123,14 +1091,22 @@ private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbi
return false;
}
- private static ManaCostShard getNextShardToPay(ManaCostBeingPaid cost) {
+ private static ManaCostShard getNextShardToPay(ManaCostBeingPaid cost, Multimap sourcesForShards) {
+ List shardsToPay = Lists.newArrayList(cost.getDistinctShards());
+ // optimize order so that the shards with less available sources are considered first
+ shardsToPay.sort(new Comparator() {
+ @Override
+ public int compare(final ManaCostShard shard1, final ManaCostShard shard2) {
+ return sourcesForShards.get(shard1).size() - sourcesForShards.get(shard2).size();
+ }
+ });
// mind the priorities
- // * Pay mono-colored first,curPhase == PhaseType.CLEANUP
+ // * Pay mono-colored first
// * Pay 2/C with matching colors
// * pay hybrids
// * pay phyrexian, keep mana for colorless
// * pay generic
- return cost.getShardToPayByPriority(cost.getDistinctShards(), ColorSet.ALL_COLORS.getColor());
+ return cost.getShardToPayByPriority(shardsToPay, ColorSet.ALL_COLORS.getColor());
}
private static void adjustManaCostToAvoidNegEffects(ManaCostBeingPaid cost, final Card card, Player ai) {
@@ -1334,11 +1310,7 @@ public static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final b
CostPartMana manapart = payCosts != null ? payCosts.getCostMana() : null;
final ManaCost mana = payCosts != null ? ( manapart == null ? ManaCost.ZERO : manapart.getManaCostFor(sa) ) : ManaCost.NO_COST;
- String restriction = null;
- if (manapart != null) {
- restriction = manapart.getRestriction();
- }
- ManaCostBeingPaid cost = new ManaCostBeingPaid(mana, restriction);
+ ManaCostBeingPaid cost = new ManaCostBeingPaid(mana);
// Tack xMana Payments into mana here if X is a set value
if (cost.getXcounter() > 0 || extraMana > 0) {
@@ -1351,7 +1323,7 @@ public static ManaCostBeingPaid calculateManaCost(final SpellAbility sa, final b
manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * xCounter;
}
- if (manaToAdd < 1 && !payCosts.getCostMana().canXbe0()) {
+ if (manaToAdd < 1 && payCosts != null && payCosts.getCostMana().getXMin() > 0) {
// AI cannot really handle X costs properly but this keeps AI from violating rules
manaToAdd = 1;
}
@@ -1478,6 +1450,7 @@ public boolean apply(final Card c) {
final CardCollection sortedManaSources = new CardCollection();
final CardCollection otherManaSources = new CardCollection();
+ final CardCollection useLastManaSources = new CardCollection();
final CardCollection colorlessManaSources = new CardCollection();
final CardCollection oneManaSources = new CardCollection();
final CardCollection twoManaSources = new CardCollection();
@@ -1502,6 +1475,26 @@ public boolean apply(final Card c) {
}
}
}
+ // exclude cards that will deal lethal damage when tapped
+ if (ai.canLoseLife() && !ai.cantLoseForZeroOrLessLife()) {
+ boolean dealsLethalOnTap = false;
+ for (Trigger t : card.getTriggers()) {
+ if (t.getMode() == TriggerType.Taps || t.getMode() == TriggerType.TapsForMana) {
+ SpellAbility trigSa = t.getOverridingAbility();
+ if (trigSa.getApi() == ApiType.DealDamage && trigSa.getParamOrDefault("Defined", "").equals("You")) {
+ int numDamage = AbilityUtils.calculateAmount(card, trigSa.getParam("NumDmg"), null);
+ numDamage = ai.staticReplaceDamage(numDamage, card, false);
+ if (ai.getLife() <= numDamage) {
+ dealsLethalOnTap = true;
+ break;
+ }
+ }
+ }
+ }
+ if (dealsLethalOnTap) {
+ continue;
+ }
+ }
if (card.isCreature() || card.isEnchanted()) {
otherManaSources.add(card);
@@ -1510,6 +1503,7 @@ public boolean apply(final Card c) {
int usableManaAbilities = 0;
boolean needsLimitedResources = false;
+ boolean unpreferredCost = false;
boolean producesAnyColor = false;
final List manaAbilities = getAIPlayableMana(card);
@@ -1521,17 +1515,24 @@ public boolean apply(final Card c) {
final Cost cost = m.getPayCosts();
if (cost != null) {
- needsLimitedResources |= !cost.isReusuableResource();
-
// if the AI can't pay the additional costs skip the mana ability
m.setActivatingPlayer(ai, true);
- if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m)) {
+ if (!CostPayment.canPayAdditionalCosts(m.getPayCosts(), m, false)) {
continue;
}
+
+ if (!cost.isReusuableResource()) {
+ for(CostPart part : cost.getCostParts()) {
+ if (part instanceof CostSacrifice && !part.payCostFromSource()) {
+ unpreferredCost = true;
+ }
+ }
+ needsLimitedResources = !unpreferredCost;
+ }
}
- // don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
+ // We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
if (!SpellApiToAi.Converter.get(sub.getApi()).chkDrawbackWithSubs(ai, sub)) {
continue;
@@ -1541,7 +1542,9 @@ public boolean apply(final Card c) {
usableManaAbilities++;
}
- if (needsLimitedResources) {
+ if (unpreferredCost) {
+ useLastManaSources.add(card);
+ } else if (needsLimitedResources) {
otherManaSources.add(card);
} else if (producesAnyColor) {
anyColorManaSources.add(card);
@@ -1572,6 +1575,10 @@ public boolean apply(final Card c) {
ComputerUtilCard.sortByEvaluateCreature(otherManaSources);
Collections.reverse(otherManaSources);
sortedManaSources.addAll(sortedManaSources.size(), otherManaSources);
+ // This should be things like sacrifice other stuff.
+ ComputerUtilCard.sortByEvaluateCreature(useLastManaSources);
+ Collections.reverse(useLastManaSources);
+ sortedManaSources.addAll(sortedManaSources.size(), useLastManaSources);
if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources);
@@ -1709,12 +1716,16 @@ private static ListMultimap groupSourcesByManaColor(final
* @since 1.0.15
*/
public static int determineLeftoverMana(final SpellAbility sa, final Player player, final boolean effect) {
- for (int i = 1; i < 100; i++) {
+ int max = 99;
+ if (sa.hasParam("XMaxLimit")) {
+ max = Math.min(max, AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("XMaxLimit"), sa));
+ }
+ for (int i = 1; i <= max; i++) {
if (!canPayManaCost(sa.getRootAbility(), player, i, effect)) {
return i - 1;
}
}
- return 99;
+ return max;
}
/**
@@ -1759,7 +1770,7 @@ public static List getAIPlayableMana(Card c) {
// if a mana ability has a mana cost the AI will miscalculate
// if there is a parent ability the AI can't use it
final Cost cost = a.getPayCosts();
- if (!cost.hasNoManaCost() || (a.getApi() != ApiType.Mana && a.getApi() != ApiType.ManaReflected)) {
+ if (cost.hasManaCost() || (a.getApi() != ApiType.Mana && a.getApi() != ApiType.ManaReflected)) {
continue;
}
diff --git a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
index 02967040fa9..f35c873f368 100644
--- a/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/CreatureEvaluator.java
@@ -137,6 +137,8 @@ else if (c.hasKeyword(Keyword.WITHER)) {
// Protection
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
value += addValue(70, "darksteel");
+ } else {
+ value += addValue(20 * c.getCounters(CounterEnumType.SHIELD), "shielded");
}
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
value += addValue(60, "cho-manno");
@@ -183,6 +185,8 @@ else if (c.hasKeyword(Keyword.WITHER)) {
value = addValue(50 + (c.getCMC() * 5), "useless"); // reset everything - useless
} else if (c.hasKeyword("CARDNAME can't block.")) {
value -= subValue(10, "cant-block");
+ } else if (c.isGoaded()) {
+ value -= subValue(5, "goaded");
} else {
List mAEnt = StaticAbilityMustAttack.entitiesMustAttack(c);
if (mAEnt.contains(c)) {
@@ -215,10 +219,12 @@ else if (c.hasKeyword(Keyword.WITHER)) {
} else {
value -= subValue(50, "doesnt-untap");
}
+ } else {
+ value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
}
if (c.hasSVar("EndOfTurnLeavePlay")) {
value -= subValue(50, "eot-leaves");
- } else if (c.hasStartOfKeyword("Cumulative upkeep")) {
+ } else if (c.hasKeyword(Keyword.CUMULATIVE_UPKEEP)) {
value -= subValue(30, "cupkeep");
} else if (c.hasStartOfKeyword("UpkeepCost")) {
value -= subValue(20, "sac-unless");
@@ -226,10 +232,10 @@ else if (c.hasKeyword(Keyword.WITHER)) {
value -= subValue(10, "echo-unpaid");
}
if (c.hasKeyword(Keyword.FADING)) {
- value -= subValue(20, "fading");
+ value -= subValue(20 / (Math.max(1, c.getCounters(CounterEnumType.FADE))), "fading");
}
if (c.hasKeyword(Keyword.VANISHING)) {
- value -= subValue(20, "vanishing");
+ value -= subValue(20 / (Math.max(1, c.getCounters(CounterEnumType.TIME))), "vanishing");
}
// use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) {
@@ -243,8 +249,7 @@ else if (c.hasKeyword(Keyword.WITHER)) {
// card-specific evaluation modifier
if (c.hasSVar("AIEvaluationModifier")) {
- int mod = AbilityUtils.calculateAmount(c, c.getSVar("AIEvaluationModifier"), null);
- value += mod;
+ value += AbilityUtils.calculateAmount(c, c.getSVar("AIEvaluationModifier"), null);
}
return value;
diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java
index c48e7ffd618..8b48d675994 100644
--- a/forge-ai/src/main/java/forge/ai/GameState.java
+++ b/forge-ai/src/main/java/forge/ai/GameState.java
@@ -79,8 +79,7 @@ static class PlayerState {
private final Map> cardToRememberedId = new HashMap<>();
private final Map> cardToImprintedId = new HashMap<>();
private final Map> cardToMergedCards = new HashMap<>();
- private final Map cardToNamedCard = new HashMap<>();
- private final Map cardToNamedCard2 = new HashMap<>();
+ private final Map> cardToNamedCard = new HashMap<>();
private final Map cardToExiledWithId = new HashMap<>();
private final Map cardAttackMap = new HashMap<>();
@@ -293,6 +292,12 @@ private void addCard(ZoneType zoneType, Map cardTexts, Card c)
if (c.isRenowned()) {
newText.append("|Renowned");
}
+ if (c.isSolved()) {
+ newText.append("|Solved");
+ }
+ if (c.isSuspected()) {
+ newText.append("|Suspected");
+ }
if (c.isMonstrous()) {
newText.append("|Monstrous");
}
@@ -305,6 +310,9 @@ private void addCard(ZoneType zoneType, Map cardTexts, Card c)
if (c.isManifested()) {
newText.append(":Manifested");
}
+ if (c.isCloaked()) {
+ newText.append(":Cloaked");
+ }
}
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
@@ -343,9 +351,6 @@ private void addCard(ZoneType zoneType, Map cardTexts, Card c)
if (!c.getNamedCard().isEmpty()) {
newText.append("|NamedCard:").append(c.getNamedCard());
}
- if (!c.getNamedCard2().isEmpty()) {
- newText.append("|NamedCard2:").append(c.getNamedCard2());
- }
List chosenCardIds = Lists.newArrayList();
for (Card obj : c.getChosenCards()) {
@@ -405,7 +410,6 @@ private void addCard(ZoneType zoneType, Map cardTexts, Card c)
if (c.isForetoldThisTurn()) {
newText.append("|ForetoldThisTurn");
}
-
}
if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Exile) {
@@ -1012,15 +1016,11 @@ private void handleChosenEntities() {
}
// Named card
- for (Entry entry : cardToNamedCard.entrySet()) {
+ for (Entry> entry : cardToNamedCard.entrySet()) {
Card c = entry.getKey();
- c.setNamedCard(entry.getValue());
- }
-
- // Named card 2
- for (Entry entry : cardToNamedCard2.entrySet()) {
- Card c = entry.getKey();
- c.setNamedCard2(entry.getValue());
+ for (String s : entry.getValue()) {
+ c.addNamedCard(s);
+ }
}
// Chosen cards
@@ -1245,7 +1245,7 @@ private CardCollectionView processCardsForZone(final String[] data, final Player
System.err.println("ERROR: Tried to create a non-existent token named " + cardinfo[0] + " when loading game state!");
continue;
}
- c = Card.fromPaperCard(token, player, player.getGame());
+ c = CardFactory.getCard(token, player, player.getGame());
} else {
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode, artID);
if (pc == null) {
@@ -1262,9 +1262,13 @@ private CardCollectionView processCardsForZone(final String[] data, final Player
for (final String info : cardinfo) {
if (info.startsWith("Tapped")) {
- c.tap(false);
+ c.tap(false, null, null);
} else if (info.startsWith("Renowned")) {
c.setRenowned(true);
+ } else if (info.startsWith("Solved")) {
+ c.setSolved(true);
+ } else if (info.startsWith("Suspected")) {
+ c.setSuspected(true);
} else if (info.startsWith("Monstrous")) {
c.setMonstrous(true);
} else if (info.startsWith("PhasedOut")) {
@@ -1279,6 +1283,9 @@ private CardCollectionView processCardsForZone(final String[] data, final Player
if (info.endsWith("Manifested")) {
c.setManifested(true);
}
+ if (info.endsWith("Cloaked")) {
+ c.setCloaked(true);
+ }
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
c.setBackSide(true);
@@ -1358,9 +1365,8 @@ else if (info.startsWith("OnAdventure")) {
List cardNames = Arrays.asList(info.substring(info.indexOf(':') + 1).split(","));
cardToMergedCards.put(c, cardNames);
} else if (info.startsWith("NamedCard:")) {
- cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
- } else if (info.startsWith("NamedCard2:")) {
- cardToNamedCard2.put(c, info.substring(info.indexOf(':') + 1));
+ List cardNames = Arrays.asList(info.substring(info.indexOf(':') + 1).split(","));
+ cardToNamedCard.put(c, cardNames);
} else if (info.startsWith("ExecuteScript:")) {
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("RememberedCards:")) {
@@ -1383,7 +1389,7 @@ else if (info.startsWith("OnAdventure")) {
c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
- c.setForetoldThisTurn(true);
+ c.setTurnInZone(turn);
} else if (info.equals("IsToken")) {
c.setToken(true);
}
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index db0f9c0a9d8..6b84c143644 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -36,6 +36,7 @@
import forge.game.player.*;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*;
+import forge.game.staticability.StaticAbility;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
@@ -241,7 +242,7 @@ public SpellAbility chooseSingleSpellForEffect(List spells, SpellA
}
@Override
- public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
+ public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message, List options, Card cardToShow, Map params) {
return getAi().confirmAction(sa, mode, message, params);
}
@@ -252,8 +253,8 @@ public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, S
}
@Override
- public boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message) {
- return getAi().confirmStaticApplication(hostCard, affected, logic, message);
+ public boolean confirmStaticApplication(Card hostCard, PlayerActionConfirmMode mode, String message, String logic) {
+ return getAi().confirmStaticApplication(hostCard, logic);
}
@Override
@@ -337,14 +338,14 @@ public CardCollection orderAttackers(Card blocker, CardCollection attackers) {
}
@Override
- public void reveal(CardCollectionView cards, ZoneType zone, Player owner, String messagePrefix) {
+ public void reveal(CardCollectionView cards, ZoneType zone, Player owner, String messagePrefix, boolean addSuffix) {
for (Card c : cards) {
AiCardMemory.rememberCard(player, c, AiCardMemory.MemorySet.REVEALED_CARDS);
}
}
@Override
- public void reveal(List cards, ZoneType zone, PlayerView owner, String messagePrefix) {
+ public void reveal(List cards, ZoneType zone, PlayerView owner, String messagePrefix, boolean addSuffix) {
for (CardView cv : cards) {
AiCardMemory.rememberCard(player, player.getGame().findByView(cv), AiCardMemory.MemorySet.REVEALED_CARDS);
}
@@ -542,15 +543,8 @@ public CardCollectionView chooseCardsToDelve(int genericAmount, CardCollection g
@Override
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) {
- String [] splitUTypes = uType.split(",");
- CardCollection cardsOfType = new CardCollection();
- for (String part : splitUTypes) {
- CardCollection partCards = CardLists.getType(hand, part);
- if (!partCards.isEmpty()) {
- cardsOfType.addAll(partCards);
- }
- }
- if (!cardsOfType.isEmpty()) {
+ Iterable cardsOfType = Iterables.filter(hand, CardPredicates.restriction(uType.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa));
+ if (!Iterables.isEmpty(cardsOfType)) {
Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc);
return new CardCollection(toDiscard);
}
@@ -588,6 +582,12 @@ public PlanarDice choosePDRollToIgnore(List rolls) {
return Aggregates.random(rolls);
}
+ @Override
+ public Integer chooseRollToIgnore(List rolls) {
+ //TODO create AI logic for this
+ return Aggregates.random(rolls);
+ }
+
@Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -659,7 +659,6 @@ public boolean playChosenSpellAbility(SpellAbility sa) {
if (sa instanceof LandAbility) {
if (sa.canPlay()) {
sa.resolve();
- getGame().updateLastStateForCard(sa.getHostCard());
}
} else {
ComputerUtil.handlePlayingSpellAbility(player, sa, getGame());
@@ -980,6 +979,12 @@ public ReplacementEffect chooseSingleReplacementEffect(String prompt, List possibleStatics) {
+ // only matters in corner cases
+ return Iterables.getFirst(possibleStatics, null);
+ }
+
@Override
public String chooseProtectionType(String string, SpellAbility sa, List choices) {
String choice = choices.get(0);
@@ -1050,9 +1055,14 @@ public boolean payCostToPreventEffect(Cost cost, SpellAbility sa, boolean alread
final Ability emptyAbility = new AbilityStatic(source, cost, sa.getTargetRestrictions()) { @Override public void resolve() { } };
emptyAbility.setActivatingPlayer(player, true);
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
+ emptyAbility.setReplacingObjects(sa.getReplacingObjects());
+ emptyAbility.setTrigger(sa.getTrigger());
+ emptyAbility.setReplacementEffect(sa.getReplacementEffect());
emptyAbility.setSVars(sa.getSVars());
emptyAbility.setCardState(sa.getCardState());
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
+ emptyAbility.setTargets(sa.getTargets().clone());
+
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers)) {
boolean result = ComputerUtil.playNoStack(player, emptyAbility, getGame(), true); // AI needs something to resolve to pay that cost
if (!emptyAbility.getPaidHash().isEmpty()) {
@@ -1435,7 +1445,6 @@ public boolean confirmMulliganScry(Player p) {
@Override
public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterface keyword, String prompt, int max) {
// TODO: improve the logic depending on the keyword and the playability of the cost-modified SA (enough targets present etc.)
-
if (keyword.getKeyword() == Keyword.CASUALTY
&& "true".equalsIgnoreCase(sa.getHostCard().getSVar("AINoCasualtyPayment"))) {
// TODO: Grisly Sigil - currently will be misplayed if Casualty is paid (the cost is always paid, targeting is wrong).
@@ -1459,6 +1468,11 @@ public int chooseNumberForKeywordCost(SpellAbility sa, Cost cost, KeywordInterfa
return chosenAmount;
}
+ @Override
+ public int chooseNumberForCostReduction(final SpellAbility sa, final int min, final int max) {
+ return max;
+ }
+
@Override
public CardCollection chooseCardsForEffectMultiple(Map validMap, SpellAbility sa, String title, boolean isOptional) {
CardCollection choices = new CardCollection();
diff --git a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java
index 8082e20aaae..a4241778f79 100644
--- a/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java
+++ b/forge-ai/src/main/java/forge/ai/SpecialAiLogic.java
@@ -1,28 +1,25 @@
package forge.ai;
-import java.util.List;
-
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
import forge.ai.ability.TokenAi;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
-import forge.game.card.Card;
-import forge.game.card.CardCollection;
-import forge.game.card.CardLists;
-import forge.game.card.CardPredicates;
-import forge.game.card.CardUtil;
-import forge.game.card.CounterEnumType;
-import forge.game.card.CounterType;
+import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
+import forge.game.zone.ZoneType;
import forge.util.Aggregates;
+import forge.util.Expressions;
+
+import java.util.List;
/*
* This class contains logic which is shared by several cards with different ability types (e.g. AF ChangeZone / AF Destroy)
@@ -369,4 +366,83 @@ public boolean apply(Card card) {
return !sacFodder.isEmpty();
}
}
-}
+
+ // AF Branch Counterspell with UnlessCost logic (Bring the Ending, Anticognition)
+ public static boolean doBranchCounterspellLogic(final Player ai, final SpellAbility sa) {
+ // TODO: this is an ugly hack that needs a rewrite if more cards are added with different SA setups or
+ // if this is to be made more generic in the future.
+ SpellAbility top = ComputerUtilAbility.getTopSpellAbilityOnStack(ai.getGame(), sa);
+ if (top == null || !sa.canTarget(top)) {
+ return false;
+ }
+ Card host = sa.getHostCard();
+
+ // pre-target the object to calculate the branch condition SVar, then clean up before running the real check
+ sa.getTargets().add(top);
+ int value = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("BranchConditionSVar"), sa);
+ sa.resetTargets();
+
+ String branchCompare = sa.getParamOrDefault("BranchConditionSVarCompare", "GE1");
+ String operator = branchCompare.substring(0, 2);
+ String operand = branchCompare.substring(2);
+ final int operandValue = AbilityUtils.calculateAmount(host, operand, sa);
+ boolean conditionMet = Expressions.compare(value, operator, operandValue);
+
+ SpellAbility falseSub = sa.getAdditionalAbility("FalseSubAbility"); // this ability has the UnlessCost part
+ boolean willPlay = false;
+ if (!conditionMet && falseSub.hasParam("UnlessCost")) {
+ // FIXME: We're emulating the UnlessCost on the SA to run the proper checks.
+ // This is hacky, but it works. Perhaps a cleaner way exists?
+ sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost"));
+ willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
+ sa.getMapParams().remove("UnlessCost");
+ } else {
+ willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
+ }
+ return willPlay;
+ }
+
+ public static boolean preferHasteForRiot(SpellAbility sa, Player player) {
+ // returning true means preferring Haste, returning false means preferring a +1/+1 counter
+ final Card host = sa.getHostCard();
+ final Game game = host.getGame();
+ final Card copy = CardUtil.getLKICopy(host);
+ copy.setLastKnownZone(player.getZone(ZoneType.Battlefield));
+
+ // check state it would have on the battlefield
+ CardCollection preList = new CardCollection(copy);
+ game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
+ // reset again?
+ game.getAction().checkStaticAbilities(false);
+
+ // can't gain counters, use Haste
+ if (!copy.canReceiveCounters(CounterEnumType.P1P1)) {
+ return true;
+ }
+
+ // already has Haste, use counter
+ if (copy.hasKeyword(Keyword.HASTE)) {
+ return false;
+ }
+
+ // not AI turn
+ if (!game.getPhaseHandler().isPlayerTurn(player)) {
+ return false;
+ }
+
+ // not before Combat
+ if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
+ return false;
+ }
+
+ // TODO check other opponents too if able
+ final Player opp = player.getWeakestOpponent();
+ if (opp != null) {
+ // TODO add predict Combat Damage?
+ return opp.getLife() < copy.getNetPower();
+ }
+
+ // haste might not be good enough?
+ return false;
+ }
+}
\ No newline at end of file
diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
index c51a7a7d4af..7fabfae3974 100644
--- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
@@ -296,9 +296,7 @@ public static String chooseCard(final Player ai, final SpellAbility sa) {
public static class PithingNeedle {
public static String chooseCard(final Player ai, final SpellAbility sa) {
// TODO Remove names of cards already named by other Pithing Needles
- Card best = null;
-
- CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa);
+ CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa);
if (!oppPerms.isEmpty()) {
return chooseCardFromList(oppPerms).getName();
}
@@ -680,6 +678,7 @@ public static boolean consider(final Player ai, final SpellAbility sa) {
// Gideon Blackblade
public static class GideonBlackblade {
public static boolean consider(final Player ai, final SpellAbility sa) {
+ sa.resetTargets();
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
if (!otb.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
@@ -739,12 +738,11 @@ public static boolean consider(final Player ai, final SpellAbility sa) {
// Grothama, All-Devouring
public static class GrothamaAllDevouring {
public static boolean consider(final Player ai, final SpellAbility sa) {
- final Card source = sa.getHostCard();
- final Card devourer = AbilityUtils.getDefinedCards(source, sa.getParam("ExtraDefined"), sa).getFirst(); // maybe just getOriginalHost()?
+ final Card fighter = sa.getHostCard();
+ final Card devourer = sa.getOriginalHost();
if (ai.getTeamMates(true).contains(devourer.getController())) {
return false; // TODO: Currently, the AI doesn't ever fight its own (or allied) Grothama for card draw. This can be improved.
}
- final Card fighter = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa).getFirst();
boolean goodTradeOrNoTrade = devourer.canBeDestroyed() && (devourer.getNetPower() < fighter.getNetToughness() || !fighter.canBeDestroyed()
|| ComputerUtilCard.evaluateCreature(devourer) > ComputerUtilCard.evaluateCreature(fighter));
return goodTradeOrNoTrade && fighter.getNetPower() >= devourer.getNetToughness();
@@ -1228,7 +1226,7 @@ public static boolean consider(final Player ai, final SpellAbility sa) {
return false;
}
- final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
+ final CardCollectionView cards = ai.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command));
List all = ComputerUtilAbility.getSpellAbilities(cards, ai);
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableManaSources(ai, true), CardPredicates.Presets.UNTAPPED).size();
@@ -1611,6 +1609,22 @@ public boolean apply(Card card) {
}
}
+ // The One Ring
+ public static class TheOneRing {
+ public static boolean consider(final Player ai, final SpellAbility sa) {
+ if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
+ return true;
+ }
+
+ AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
+ int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
+ int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
+
+ return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
+ && ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1;
+ }
+ }
+
// The Scarab God
public static class TheScarabGod {
public static boolean consider(final Player ai, final SpellAbility sa) {
@@ -1668,6 +1682,41 @@ public static boolean consider(final Player ai, final SpellAbility sa) {
}
}
+ // Veil of Summer
+ public static class VeilOfSummer {
+ public static boolean consider(final Player ai, final SpellAbility sa) {
+ // check the top ability on stack if it's (a) an opponent's counterspell targeting the AI's spell;
+ // (b) a black or a blue spell targeting something that belongs to the AI
+ Game game = ai.getGame();
+ if (game.getStack().isEmpty()) {
+ return false;
+ }
+
+ SpellAbility topSA = game.getStack().peekAbility();
+ if (topSA.usesTargeting() && topSA.getActivatingPlayer().isOpponentOf(ai)) {
+ if (topSA.getApi() == ApiType.Counter) {
+ SpellAbility tgtSpell = topSA.getTargets().getFirstTargetedSpell();
+ if (tgtSpell != null && tgtSpell.getActivatingPlayer().equals(ai)) {
+ return true;
+ }
+ } else if (topSA.getHostCard().isBlack() || topSA.getHostCard().isBlue()) {
+ for (Player tgtP : topSA.getTargets().getTargetPlayers()) {
+ if (tgtP.equals(ai)) {
+ return true;
+ }
+ }
+ for (Card tgtC : topSA.getTargets().getTargetCards()) {
+ if (tgtC.getController().equals(ai)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
+
+
// Volrath's Shapeshifter
public static class VolrathsShapeshifter {
public static boolean consider(final Player ai, final SpellAbility sa) {
diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
index 729de19c216..d77c1083027 100644
--- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
@@ -22,6 +22,7 @@ public enum SpellApiToAi {
.put(ApiType.AddOrRemoveCounter, CountersPutOrRemoveAi.class)
.put(ApiType.AddPhase, AddPhaseAi.class)
.put(ApiType.AddTurn, AddTurnAi.class)
+ .put(ApiType.AlterAttribute, AlwaysPlayAi.class)
.put(ApiType.Amass, AmassAi.class)
.put(ApiType.Animate, AnimateAi.class)
.put(ApiType.AnimateAll, AnimateAllAi.class)
@@ -55,6 +56,7 @@ public enum SpellApiToAi {
.put(ApiType.Clash, ClashAi.class)
.put(ApiType.ClassLevelUp, AlwaysPlayAi.class)
.put(ApiType.Cleanup, AlwaysPlayAi.class)
+ .put(ApiType.Cloak, CloakAi.class)
.put(ApiType.Clone, CloneAi.class)
.put(ApiType.CompanionChoose, ChooseCompanionAi.class)
.put(ApiType.Connive, ConniveAi.class)
@@ -75,6 +77,7 @@ public enum SpellApiToAi {
.put(ApiType.DigMultiple, DigMultipleAi.class)
.put(ApiType.DigUntil, DigUntilAi.class)
.put(ApiType.Discard, DiscardAi.class)
+ .put(ApiType.Discover, DiscoverAi.class)
.put(ApiType.Draft, ChooseCardNameAi.class)
.put(ApiType.DrainMana, DrainManaAi.class)
.put(ApiType.Draw, DrawAi.class)
@@ -106,6 +109,7 @@ public enum SpellApiToAi {
.put(ApiType.Investigate, InvestigateAi.class)
.put(ApiType.Learn, LearnAi.class)
.put(ApiType.LoseLife, LifeLoseAi.class)
+ .put(ApiType.LosePerpetual, AlwaysPlayAi.class)
.put(ApiType.LosesGame, GameLossAi.class)
.put(ApiType.MakeCard, AlwaysPlayAi.class)
.put(ApiType.Mana, ManaEffectAi.class)
@@ -137,6 +141,7 @@ public enum SpellApiToAi {
.put(ApiType.PumpAll, PumpAllAi.class)
.put(ApiType.PutCounter, CountersPutAi.class)
.put(ApiType.PutCounterAll, CountersPutAllAi.class)
+ .put(ApiType.Radiation, AlwaysPlayAi.class)
.put(ApiType.RearrangeTopOfLibrary, RearrangeTopOfLibraryAi.class)
.put(ApiType.Regenerate, RegenerateAi.class)
.put(ApiType.RegenerateAll, RegenerateAllAi.class)
@@ -181,6 +186,7 @@ public enum SpellApiToAi {
.put(ApiType.TapAll, TapAllAi.class)
.put(ApiType.TapOrUntap, TapOrUntapAi.class)
.put(ApiType.TapOrUntapAll, TapOrUntapAllAi.class)
+ .put(ApiType.TimeTravel, TimeTravelAi.class)
.put(ApiType.Token, TokenAi.class)
.put(ApiType.TwoPiles, TwoPilesAi.class)
.put(ApiType.Unattach, CannotPlayAi.class)
@@ -188,6 +194,7 @@ public enum SpellApiToAi {
.put(ApiType.Untap, UntapAi.class)
.put(ApiType.UntapAll, UntapAllAi.class)
.put(ApiType.Venture, VentureAi.class)
+ .put(ApiType.VillainousChoice, AlwaysPlayAi.class)
.put(ApiType.Vote, VoteAi.class)
.put(ApiType.WinsGame, GameWinAi.class)
diff --git a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java
index 94340df7b36..60eefabbae4 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java
@@ -32,9 +32,11 @@ protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
if (!aiArmies.isEmpty()) {
return Iterables.any(aiArmies, CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
}
- final String tokenScript = "b_0_0_army";
- final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
final String type = sa.getParam("Type");
+ StringBuilder sb = new StringBuilder("b_0_0_");
+ sb.append(sa.getOriginalParam("Type").toLowerCase()).append("_army");
+ final String tokenScript = sb.toString();
+ final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
diff --git a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
index df93a804d00..1d8b8a706f0 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
@@ -12,6 +12,7 @@
import forge.game.ability.ApiType;
import forge.game.ability.effects.AnimateEffectBase;
import forge.game.card.*;
+import forge.game.combat.Combat;
import forge.game.cost.CostPutCounter;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -312,9 +313,21 @@ private boolean animateTgtAI(final SpellAbility sa) {
final Card animatedCopy = becomeAnimated(c, sa);
int aValue = ComputerUtilCard.evaluateCreature(animatedCopy);
- // animated creature has zero toughness, don't do that
+ // animated creature has zero toughness, don't do that unless the card will receive a counter to buff its toughness
if (animatedCopy.getNetToughness() <= 0) {
- continue;
+ boolean buffedToughness = false;
+ SpellAbility sub = sa.findSubAbilityByType(ApiType.PutCounter);
+ if (sub != null) {
+ if (animatedCopy.canReceiveCounters(CounterEnumType.P1P1)
+ && "Targeted".equals(sub.getParam("Defined"))
+ && "P1P1".equals(sub.getParam("CounterType"))) {
+ buffedToughness = true;
+ }
+ }
+
+ if (!buffedToughness) {
+ continue;
+ }
}
// if original is already a Creature,
@@ -386,6 +399,19 @@ private boolean animateTgtAI(final SpellAbility sa) {
}
}
+ if (logic.equals("ValuableAttackerOrBlocker")) {
+ if (ph.inCombat()) {
+ final Combat combat = ph.getCombat();
+ CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
+ for (Card c : list) {
+ Card animated = becomeAnimated(c, sa);
+ boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
+ boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
+ if (isValuableAttacker || isValuableBlocker)
+ return true;
+ }
+ }
+ }
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until
@@ -461,7 +487,7 @@ private static void becomeAnimated(final Card card, final boolean hasOriginalCar
}
// colors to be added or changed to
- ColorSet finalColors = ColorSet.getNullColor();
+ ColorSet finalColors = null;
if (sa.hasParam("Colors")) {
final String colors = sa.getParam("Colors");
if (colors.equals("ChosenColor")) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
index 62dd7c9df8c..729c94c84c4 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
@@ -635,8 +635,7 @@ public boolean apply(final Card c) {
}
// Cards that trigger on dealing damage
- private static Card attachAICuriosityPreference(final SpellAbility sa, final List list, final boolean mandatory,
- final Card attachSource) {
+ private static Card attachAICuriosityPreference(final SpellAbility sa, final List list, final boolean mandatory, final Card attachSource) {
Card chosen = null;
int priority = 0;
for (Card card : list) {
@@ -690,7 +689,6 @@ private static Card attachAICuriosityPreference(final SpellAbility sa, final Lis
}
}
-
return chosen;
}
/**
diff --git a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java
index 92f36e9b4a3..3828af7604c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/BidLifeAi.java
@@ -7,7 +7,6 @@
import forge.ai.SpellAbilityAi;
import forge.game.Game;
import forge.game.card.Card;
-import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -40,7 +39,7 @@ protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
}
final SpellAbility topSA = game.getStack().peekAbility();
- if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || aiPlayer.equals(topSA.getActivatingPlayer())) {
+ if (!topSA.isCounterableBy(sa) || aiPlayer.equals(topSA.getActivatingPlayer())) {
return false;
}
if (sa.canTargetSpellAbility(topSA)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/BranchAi.java b/forge-ai/src/main/java/forge/ai/ability/BranchAi.java
index ec33370223b..9f2206baf39 100644
--- a/forge-ai/src/main/java/forge/ai/ability/BranchAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/BranchAi.java
@@ -1,14 +1,22 @@
package forge.ai.ability;
-import java.util.Map;
-
+import com.google.common.base.Predicate;
+import forge.ai.ComputerUtilCard;
+import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
+import forge.game.GameEntity;
+import forge.game.card.Card;
+import forge.game.card.CardCollection;
+import forge.game.card.CardLists;
+import forge.game.combat.Combat;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
+import java.util.Map;
+
public class BranchAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
@@ -18,12 +26,41 @@ protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("GrislySigil".equals(aiLogic)) {
return SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
+ } else if ("BranchCounter".equals(aiLogic)) {
+ return SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); // Bring the Ending, Anticognition (hacky implementation)
+ } else if ("TgtAttacker".equals(aiLogic)) {
+ final Combat combat = aiPlayer.getGame().getCombat();
+ if (combat == null || combat.getAttackingPlayer() != aiPlayer) {
+ return false;
+ }
+
+ final CardCollection attackers = combat.getAttackers();
+ final CardCollection attackingBattle = CardLists.filter(attackers, new Predicate() {
+ @Override
+ public boolean apply(Card card) {
+ final GameEntity def = combat.getDefenderByAttacker(combat.getBandOfAttacker(card));
+ return def instanceof Card && ((Card)def).isBattle();
+ }
+ });
+
+ if (!attackingBattle.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackingBattle));
+ } else {
+ sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackers));
+ }
+
+ return sa.isTargetNumberValid();
}
// TODO: expand for other cases where the AI is needed to make a decision on a branch
return true;
}
+ @Override
+ protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
+ return canPlayAI(aiPlayer, sa) || mandatory;
+ }
+
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
return true;
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
index 8bebf86ee49..5dbe7c483de 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
@@ -19,6 +19,7 @@
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
+import forge.game.cost.CostExile;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.keyword.Keyword;
@@ -50,6 +51,31 @@ public class ChangeZoneAi extends SpellAbilityAi {
// cards where multiple cards are fetched at once and they need to be coordinated
private static CardCollection multipleCardsToChoose = new CardCollection();
+ protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
+ if (sa.isCraft()) {
+ CardCollection payingCards = new CardCollection();
+ int needed = 0;
+ for (final CostPart part : cost.getCostParts()) {
+ if (part instanceof CostExile) {
+ if (part.payCostFromSource()) {
+ continue;
+ }
+ int amt = part.getAbilityAmount(sa);
+ needed += amt;
+ CardCollection toAdd = ComputerUtil.chooseExileFrom(ai, (CostExile) part, source, amt, sa);
+ if (toAdd != null) {
+ payingCards.addAll(toAdd);
+ }
+ }
+ }
+ if (payingCards.size() < needed) {
+ return false;
+ }
+ }
+
+ return super.willPayCosts(ai, sa, cost, source);
+ }
+
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
@@ -235,13 +261,14 @@ private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
- ZoneType origin = null;
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
+ List origin = null;
final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai);
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) {
try {
- origin = ZoneType.smartValueOf(sa.getParam("Origin"));
+ origin = ZoneType.listValueOf(sa.getParam("Origin"));
} catch (IllegalArgumentException ex) {
// This happens when Origin is something like
// "Graveyard,Library" (Doomsday)
@@ -317,12 +344,11 @@ private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility
Iterable pDefined = Lists.newArrayList(source.getController());
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null && tgt.canTgtPlayer()) {
+ sa.resetTargets();
boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opponent)) {
- sa.resetTargets();
sa.getTargets().add(opponent);
} else if (!isCurse && sa.canTarget(ai)) {
- sa.resetTargets();
sa.getTargets().add(ai);
}
if (!sa.isTargetNumberValid()) {
@@ -366,8 +392,9 @@ public boolean apply(final Card c) {
}
});
}
- // TODO: prevent ai seaching its own library when Ob Nixilis, Unshackled is in play
- if (origin != null && origin.isKnown()) {
+ // TODO: prevent ai searching its own library when Ob Nixilis, Unshackled is in play
+ if (origin != null && origin.size() == 1 && origin.get(0).isKnown()) {
+ // FIXME: make this properly interact with several origin zones
list = CardLists.getValidCards(list, type, source.getController(), source, sa);
}
@@ -422,7 +449,8 @@ public boolean apply(final Card c) {
return false;
}
// Only tutor something in main1 if hand is almost empty
- if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")) {
+ if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")
+ && !aiLogic.equals("AnyMainPhase")) {
return false;
}
}
@@ -737,7 +765,8 @@ private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility
// predict Legendary cards already present
boolean nothingWillReturn = true;
for (final Card c : retrieval) {
- if (!(!c.ignoreLegendRule() && ai.isCardInPlay(c.getName()))) {
+ final boolean isCraftSa = sa.isCraft() && sa.getHostCard().equals(c);
+ if (isCraftSa || (!(!c.ignoreLegendRule() && ai.isCardInPlay(c.getName())))) {
nothingWillReturn = false;
break;
}
@@ -765,6 +794,8 @@ protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandle
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
+ } else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
+ return true;
}
if (sa.isHidden()) {
@@ -1366,7 +1397,7 @@ else if (!aiPlaneswalkers.isEmpty() && (sa.getHostCard().isSorcery() || !game.ge
chance = aic.getIntProperty(AiProps.BLINK_RELOAD_PLANESWALKER_CHANCE);
}
if (MyRandom.percentTrue(chance)) {
- Collections.sort(aiPlaneswalkers, CardPredicates.compareByCounterType(CounterEnumType.LOYALTY));
+ aiPlaneswalkers.sort(CardPredicates.compareByCounterType(CounterEnumType.LOYALTY));
for (Card pw : aiPlaneswalkers) {
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
int freshLoyalty = Integer.valueOf(pw.getCurrentState().getBaseLoyalty());
@@ -1662,9 +1693,14 @@ public boolean apply(final Card c) {
}
}
if (c == null) {
- fetchList = CardLists.getNotType(fetchList, "Land");
- // Prefer to pull a creature, generally more useful for AI.
- c = chooseCreature(decider, CardLists.filter(fetchList, CardPredicates.Presets.CREATURES));
+ if (Iterables.all(fetchList, Presets.LANDS)) {
+ // we're only choosing from lands, so get the best land
+ c = ComputerUtilCard.getBestLandAI(fetchList);
+ } else {
+ fetchList = CardLists.getNotType(fetchList, "Land");
+ // Prefer to pull a creature, generally more useful for AI.
+ c = chooseCreature(decider, CardLists.filter(fetchList, CardPredicates.Presets.CREATURES));
+ }
}
if (c == null) { // Could not find a creature.
if (decider.getLife() <= 5) { // Desperate?
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java
index 010b8f0fd9c..93bb914454b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAllAi.java
@@ -44,6 +44,7 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
+ final String aiLogic = sa.getParamOrDefault("AILogic" ,"");
if (abCost != null) {
// AI currently disabled for these costs
@@ -52,7 +53,7 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
- boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
+ boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll");
if (!aiLogicAllowsDiscard) {
return false;
@@ -86,16 +87,16 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
- if ("LivingDeath".equals(sa.getParam("AILogic"))) {
+ if ("LivingDeath".equals(aiLogic)) {
// Living Death AI
return SpecialCardAi.LivingDeath.consider(ai, sa);
- } else if ("Timetwister".equals(sa.getParam("AILogic"))) {
+ } else if ("Timetwister".equals(aiLogic)) {
// Timetwister AI
return SpecialCardAi.Timetwister.consider(ai, sa);
- } else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
+ } else if ("RetDiscardedThisTurn".equals(aiLogic)) {
// e.g. Shadow of the Grave
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
- } else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) {
+ } else if ("ExileGraveyards".equals(aiLogic)) {
for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
@@ -105,7 +106,7 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
}
}
return false;
- } else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
+ } else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) {
PlayerCollection players = ai.getOpponents();
players.add(ai);
int maxSize = 1;
@@ -217,7 +218,7 @@ else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEval
}
// Don't cast during main1?
- if (game.getPhaseHandler().is(PhaseType.MAIN1, ai)) {
+ if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) {
return false;
}
} else if (origin.equals(ZoneType.Graveyard)) {
@@ -245,15 +246,13 @@ else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEval
&& !ComputerUtil.isPlayingReanimator(ai);
}
} else if (origin.equals(ZoneType.Exile)) {
- String logic = sa.getParam("AILogic");
-
- if (logic != null && logic.startsWith("DiscardAllAndRetExiled")) {
+ if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
- int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
- boolean noDiscard = logic.contains(".noDiscard");
+ int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
+ boolean noDiscard = aiLogic.contains(".noDiscard");
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
index b1e981d9d6a..7a642b7a831 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
@@ -37,11 +37,6 @@ protected boolean checkApiLogic(Player ai, SpellAbility sa) {
min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParam("MinCharmNum"), sa) : num;
}
- // only randomize if not all possible together
- if (num < choices.size() || source.hasKeyword(Keyword.ESCALATE)) {
- Collections.shuffle(choices);
- }
-
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
// Reset the chosen list otherwise it will be locked in forever by earlier calls
@@ -51,11 +46,15 @@ protected boolean checkApiLogic(Player ai, SpellAbility sa) {
if (!ai.equals(sa.getActivatingPlayer())) {
// This branch is for "An Opponent chooses" Charm spells from Alliances
// Current just choose the first available spell, which seem generally less disastrous for the AI.
- //return choices.subList(0, 1);
chosenList = choices.subList(1, choices.size());
} else if ("Triskaidekaphobia".equals(ComputerUtilAbility.getAbilitySourceName(sa))) {
chosenList = chooseTriskaidekaphobia(choices, ai);
} else {
+ // only randomize if not all possible together
+ if (num < choices.size() || source.hasKeyword(Keyword.ESCALATE)) {
+ Collections.shuffle(choices);
+ }
+
/*
* The generic chooseOptionsAi uses canPlayAi() to determine good choices
* which means most "bonus" effects like life-gain and random pumps will
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
index 9edb682eabc..b63244ce892 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseGenericEffectAi.java
@@ -3,7 +3,6 @@
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
@@ -11,9 +10,7 @@
import forge.card.MagicColor;
import forge.game.Game;
import forge.game.card.*;
-import forge.game.combat.Combat;
import forge.game.cost.Cost;
-import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -33,8 +30,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Khans".equals(aiLogic) || "Dragons".equals(aiLogic)) {
return true;
- } else if ("Riot".equals(aiLogic)) {
- return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
if (SpellApiToAi.Converter.get(sb.getApi()).canPlayAIWithSubs(ai, sb)) {
@@ -86,9 +81,7 @@ protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells,
Map params) {
Card host = sa.getHostCard();
- final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = host.getGame();
- final Combat combat = game.getCombat();
final String logic = sa.getParam("AILogic");
if (logic == null) {
return spells.get(0);
@@ -287,54 +280,7 @@ public boolean apply(final SpellAbility sp) {
if (!filtered.isEmpty()) {
return filtered.get(0);
}
- } else if ("Riot".equals(logic)) {
- SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
- return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
}
return spells.get(0); // return first choice if no logic found
}
-
- public static boolean preferHasteForRiot(SpellAbility sa, Player player) {
- // returning true means preferring Haste, returning false means preferring a +1/+1 counter
- final Card host = sa.getHostCard();
- final Game game = host.getGame();
- final Card copy = CardUtil.getLKICopy(host);
- copy.setLastKnownZone(player.getZone(ZoneType.Battlefield));
-
- // check state it would have on the battlefield
- CardCollection preList = new CardCollection(copy);
- game.getAction().checkStaticAbilities(false, Sets.newHashSet(copy), preList);
- // reset again?
- game.getAction().checkStaticAbilities(false);
-
- // can't gain counters, use Haste
- if (!copy.canReceiveCounters(CounterEnumType.P1P1)) {
- return true;
- }
-
- // already has Haste, use counter
- if (copy.hasKeyword(Keyword.HASTE)) {
- return false;
- }
-
- // not AI turn
- if (!game.getPhaseHandler().isPlayerTurn(player)) {
- return false;
- }
-
- // not before Combat
- if (!game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
- return false;
- }
-
- // TODO check other opponents too if able
- final Player opp = player.getWeakestOpponent();
- if (opp != null) {
- // TODO add predict Combat Damage?
- return opp.getLife() < copy.getNetPower();
- }
-
- // haste might not be good enough?
- return false;
- }
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java
index 9180a411769..aebe9dfb3f3 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java
@@ -1,5 +1,6 @@
package forge.ai.ability;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -29,14 +30,18 @@
public class ChooseTypeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
- if (!sa.hasParam("AILogic")) {
+ String aiLogic = sa.getParamOrDefault("AILogic", "");
+
+ if (aiLogic.isEmpty()) {
return false;
- } else if ("MostProminentComputerControls".equals(sa.getParam("AILogic"))) {
+ } else if ("MostProminentComputerControls".equals(aiLogic)) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
return doMirrorEntityLogic(aiPlayer, sa);
}
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
- } else if ("MostProminentOppControls".equals(sa.getParam("AILogic"))) {
+ } else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) {
+ return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty();
+ } else if ("MostProminentOppControls".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
}
@@ -169,7 +174,7 @@ private String chooseType(SpellAbility sa, CardCollectionView cards) {
// Account for the situation when only changelings are on the battlefield
boolean allChangeling = false;
for (Card c : cards) {
- if (c.isCreature() && c.hasStartOfKeyword(Keyword.CHANGELING.toString())) {
+ if (c.isCreature() && c.hasKeyword(Keyword.CHANGELING)) {
chosenType = Aggregates.random(valid); // just choose a random type for changelings
allChangeling = true;
break;
diff --git a/forge-ai/src/main/java/forge/ai/ability/CloakAi.java b/forge-ai/src/main/java/forge/ai/ability/CloakAi.java
new file mode 100644
index 00000000000..62f6361ea6f
--- /dev/null
+++ b/forge-ai/src/main/java/forge/ai/ability/CloakAi.java
@@ -0,0 +1,43 @@
+package forge.ai.ability;
+
+import forge.ai.ComputerUtil;
+import forge.game.card.Card;
+import forge.game.card.CardUtil;
+import forge.game.player.Player;
+import forge.game.spellability.SpellAbility;
+
+public class CloakAi extends ManifestBaseAi {
+
+ @Override
+ protected boolean shouldApply(final Card card, final Player ai, final SpellAbility sa) {
+ // check to ensure that there are no replacement effects that prevent creatures ETBing from library
+ // (e.g. Grafdigger's Cage)
+ Card topCopy = CardUtil.getLKICopy(card);
+ topCopy.turnFaceDownNoUpdate();
+ topCopy.setCloaked(true);
+
+ if (ComputerUtil.isETBprevented(topCopy)) {
+ return false;
+ }
+
+ if (card.getView().canBeShownTo(ai.getView())) {
+ // try to avoid manifest a non Permanent
+ if (!card.isPermanent())
+ return false;
+
+ // do not manifest a card with X in its cost
+ if (card.getManaCost().countX() > 0)
+ return false;
+
+ // try to avoid manifesting a creature with zero or less toughness
+ if (card.isCreature() && card.getNetToughness() <= 0)
+ return false;
+
+ // card has ETBTrigger or ETBReplacement
+ if (card.hasETBTrigger(false) || card.hasETBReplacement()) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
index 5d274a154d8..0334decc633 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
@@ -125,6 +125,7 @@ protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, bool
final Player activator = sa.getActivatingPlayer();
final Game game = host.getGame();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
final boolean canCopyLegendary = sa.hasParam("NonLegendary");
if (sa.usesTargeting()) {
@@ -132,6 +133,13 @@ protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, bool
List list = CardUtil.getValidCardsToTarget(sa);
+ if (aiLogic.equals("Different")) {
+ // TODO: possibly improve the check, currently only checks if the name is the same
+ // Possibly also check if the card is threatened, and then allow to copy (this will, however, require a bit
+ // of a rewrite in canPlayAI to allow a response form of CopyPermanentAi)
+ list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(host.getName())));
+ }
+
//Nothing to target
if (list.isEmpty()) {
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
index 0012da0dc0e..2c8f8648701 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
@@ -19,7 +19,6 @@
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
-import forge.game.card.CardFactoryUtil;
import forge.game.cost.Cost;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostExile;
@@ -64,7 +63,7 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
- if ((topSA.isSpell() && !CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) {
+ if ((topSA.isSpell() && !topSA.isCounterableBy(sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness
return false;
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
@@ -325,7 +324,7 @@ public Pair chooseTargetSpellAbility(Game game, SpellAbil
leastBadOption = tgtSA;
}
- if ((tgtSA.isSpell() && !CardFactoryUtil.isCounterableBy(tgtSA.getHostCard(), sa)) ||
+ if ((tgtSA.isSpell() && !tgtSA.isCounterableBy(sa)) ||
tgtSA.getActivatingPlayer() == ai ||
!tgtSA.getActivatingPlayer().isOpponentOf(ai)) {
// Is this a "better" least bad option
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java
index 238cdb2bea6..94cd736a80b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersMultiplyAi.java
@@ -20,6 +20,7 @@
import forge.game.card.CardPredicates;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -212,7 +213,7 @@ private void addTargetsByCounterType(final Player ai, final SpellAbility sa, fin
sa.getTargets().add(c);
// check if Spell with Strive is still playable
- if (sa.isSpell() && sa.getHostCard().hasStartOfKeyword("Strive")) {
+ if (sa.isSpell() && sa.getHostCard().hasKeyword(Keyword.STRIVE)) {
// if not remove target again and break list
if (!ComputerUtilCost.canPayCost(sa, ai, false)) {
sa.getTargets().remove(c);
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
index dd576e9442a..1cff702ed3a 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
@@ -305,6 +305,12 @@ public boolean apply(Card input) {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
}
return willActivate;
+ } else if (logic.equals("ChargeToBestCMC")) {
+ return doChargeToCMCLogic(ai, sa);
+ } else if (logic.equals("ChargeToBestOppControlledCMC")) {
+ return doChargeToOppCtrlCMCLogic(ai, sa);
+ } else if (logic.equals("TheOneRing")) {
+ return SpecialCardAi.TheOneRing.consider(ai, sa);
}
if (!sa.metConditions() && sa.getSubAbility() == null) {
@@ -436,6 +442,9 @@ public boolean apply(Card input) {
}
}
+ final boolean hasSacCost = abCost.hasSpecificCostType(CostSacrifice.class);
+ final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost);
+
if (sa.usesTargeting()) {
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
@@ -449,15 +458,15 @@ public boolean apply(Card input) {
sa.addDividedAllocation(c, amount);
return true;
} else {
- return false;
+ if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
+ return false;
+ }
}
}
}
sa.resetTargets();
- final boolean sacSelf = ComputerUtilCost.isSacrificeSelfCost(abCost);
-
if (sa.isCurse()) {
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
} else {
@@ -470,6 +479,8 @@ public boolean apply(final Card c) {
// don't put the counter on the dead creature
if (sacSelf && c.equals(source)) {
return false;
+ } else if (hasSacCost && !ComputerUtil.shouldSacrificeThreatenedCard(ai, c, sa)) {
+ return false;
}
if ("NoCounterOfType".equals(sa.getParam("AILogic"))) {
for (String ctrType : types) {
@@ -624,7 +635,7 @@ public boolean apply(final Card c) {
}
// Instant +1/+1
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
- if (!(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
+ if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return false; // only if next turn and cost is reusable
}
}
@@ -740,6 +751,7 @@ public boolean chkAIDrawback(final SpellAbility sa, Player ai) {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
// boolean chance = true;
boolean preferred = true;
CardCollection list;
@@ -747,6 +759,7 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator
final boolean divided = sa.isDividedAsYouChoose();
final int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
int left = amount;
+
final String[] types;
String type = "";
if (sa.hasParam("CounterType")) {
@@ -759,6 +772,12 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator
type = types[0];
}
+ if ("ChargeToBestCMC".equals(aiLogic)) {
+ return doChargeToCMCLogic(ai, sa) || mandatory;
+ } else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
+ return doChargeToOppCtrlCMCLogic(ai, sa) || mandatory;
+ }
+
if (!sa.usesTargeting()) {
// No target. So must be defined. (Unused at the moment)
// list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
@@ -1206,4 +1225,38 @@ public int chooseNumber(Player player, SpellAbility sa, int min, int max, Map= curAmount) {
+ curAmount = numPerCMC;
+ optimalCMC = cmc;
+ }
+ }
+ return numCtrs < optimalCMC;
+ }
+
+ private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
+ Card source = sa.getHostCard();
+ CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.NONLAND_PERMANENTS);
+ int numCtrs = source.getCounters(CounterEnumType.CHARGE);
+ int maxCMC = Aggregates.max(oppInPlay, CardPredicates.Accessors.fnGetCmc);
+ int optimalCMC = 0;
+ int curAmount = 0;
+ for (int cmc = numCtrs; cmc <= maxCMC; cmc++) {
+ int numPerCMC = CardLists.filter(oppInPlay, CardPredicates.hasCMC(cmc)).size();
+ if (numPerCMC >= curAmount) {
+ curAmount = numPerCMC;
+ optimalCMC = cmc;
+ }
+ }
+ return numCtrs < optimalCMC;
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java
index a813ce8f182..7b16ec65525 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageAllAi.java
@@ -274,7 +274,8 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator
final String damage = sa.getParam("NumDmg");
int dmg;
- if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
+ if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")
+ && sa.getPayCosts() != null && sa.getPayCosts().hasXInAnyCostPart()) {
// Set PayX here to maximum value.
dmg = ComputerUtilCost.getMaxXValue(sa, ai, true);
sa.setXManaCostPaid(dmg);
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
index 86446e186e5..ddf543aab55 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
@@ -15,6 +15,7 @@
import forge.game.card.*;
import forge.game.cost.Cost;
import forge.game.cost.CostPartMana;
+import forge.game.cost.CostPutCounter;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -735,7 +736,8 @@ private boolean damageChoosingTargets(final Player ai, final SpellAbility sa, fi
|| (isSorcerySpeed(sa, ai) && phase.is(PhaseType.MAIN2))
|| immediately) {
boolean pingAfterAttack = "PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai);
- if ((pingAfterAttack && !avoidTargetP(ai, sa)) || shouldTgtP(ai, sa, dmg, noPrevention)) {
+ boolean isPWAbility = sa.isPwAbility() && sa.getPayCosts().hasSpecificCostType(CostPutCounter.class);
+ if (isPWAbility || (pingAfterAttack && !avoidTargetP(ai, sa)) || shouldTgtP(ai, sa, dmg, noPrevention)) {
tcs.add(enemy);
if (divided) {
sa.addDividedAllocation(enemy, dmg);
@@ -747,8 +749,22 @@ private boolean damageChoosingTargets(final Player ai, final SpellAbility sa, fi
}
// fell through all the choices, no targets left?
- if (tcs.size() < tgt.getMinTargets(source, sa) || tcs.size() == 0) {
+ int minTgts = tgt.getMinTargets(source, sa);
+ if (tcs.size() < minTgts || tcs.size() == 0) {
if (mandatory) {
+ // Sanity check: if there are any legal non-owned targets after the check (which may happen for complex cards like Rift Bolt),
+ // choose a random opponent's target before forcing targeting of own stuff
+ List allTgtEntities = sa.getTargetRestrictions().getAllCandidates(sa, true);
+ for (GameEntity ent : allTgtEntities) {
+ if ((ent instanceof Player && ((Player)ent).isOpponentOf(ai))
+ || (ent instanceof Card && ((Card)ent).getController().isOpponentOf(ai))) {
+ tcs.add(ent);
+ }
+ if (tcs.size() == minTgts) {
+ return true;
+ }
+ }
+
// If the trigger is mandatory, gotta choose my own stuff now
return damageChooseRequiredTargets(ai, sa, tgt, dmg);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java
index 1980ba91303..b8ecd9b6f96 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamagePreventAi.java
@@ -85,7 +85,7 @@ else if (!game.getStack().isEmpty()) {
// check stack for something on the stack will kill anything i control
final List objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa);
- if (objects.contains(ai)) {
+ if (objects.contains(ai) && sa.canTarget(ai)) {
tcs.add(ai);
chance = true;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java
new file mode 100644
index 00000000000..9541d8266d5
--- /dev/null
+++ b/forge-ai/src/main/java/forge/ai/ability/DiscoverAi.java
@@ -0,0 +1,66 @@
+package forge.ai.ability;
+
+import forge.ai.AiPlayDecision;
+import forge.ai.ComputerUtil;
+import forge.ai.PlayerControllerAi;
+import forge.ai.SpellAbilityAi;
+import forge.game.ability.AbilityUtils;
+import forge.game.card.Card;
+import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
+import forge.game.spellability.LandAbility;
+import forge.game.spellability.Spell;
+import forge.game.spellability.SpellAbility;
+
+import java.util.Map;
+
+public class DiscoverAi extends SpellAbilityAi {
+
+ @Override
+ protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
+ if (ComputerUtil.preventRunAwayActivations(sa)) {
+ return false; // prevent infinite loop
+ }
+
+ return true;
+ }
+
+ /**
+ *
+ * doTriggerAINoCost
+ *
+ * @param sa
+ * a {@link forge.game.spellability.SpellAbility} object.
+ * @param mandatory
+ * a boolean.
+ *
+ * @return a boolean.
+ */
+ @Override
+ protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
+ return mandatory || checkApiLogic(ai, sa);
+ }
+
+ @Override
+ public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
+ Card c = (Card)params.get("Card");
+ for (SpellAbility s : AbilityUtils.getBasicSpellsFromPlayEffect(c, ai)) {
+ if (s instanceof LandAbility) {
+ // return false or we get a ClassCastException later if the AI encounters MDFC with land backside
+ return false;
+ }
+ Spell spell = (Spell) s;
+ if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, false, true)) {
+ // Before accepting, see if the spell has a valid number of targets (it should at this point).
+ // Proceeding past this point if the spell is not correctly targeted will result
+ // in "Failed to add to stack" error and the card disappearing from the game completely.
+ if (!spell.isTargetNumberValid()) {
+ // if we won't be able to pay the cost, don't choose the card
+ return false;
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
index a1f0d31e9ec..072125b3340 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DrawAi.java
@@ -35,11 +35,7 @@
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
-import forge.game.cost.Cost;
-import forge.game.cost.CostDiscard;
-import forge.game.cost.CostPart;
-import forge.game.cost.CostPayLife;
-import forge.game.cost.PaymentDecision;
+import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -136,21 +132,15 @@ protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card sourc
*/
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
- String logic = sa.getParamOrDefault("AILogic", "");
-
- if (logic.startsWith("LifeLessThan.")) {
- // LifeLessThan logic presupposes activation as soon as possible in an
- // attempt to save the AI from dying
- return true;
- } else if (logic.equals("AtOppEOT")) {
- return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
- } else if (logic.equals("RespondToOwnActivation")) {
- return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
+ // Sacrificing a creature in response to something dangerous is generally good in any phase
+ boolean isSacCost = false;
+ if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
+ isSacCost = true;
}
// Don't use draw abilities before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
- && !ComputerUtil.castSpellInMain1(ai, sa)) {
+ && !ComputerUtil.castSpellInMain1(ai, sa) && !isSacCost) {
return false;
}
@@ -167,7 +157,17 @@ protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandle
*/
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
- if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
+ if (logic.equals("VeilOfSummer")) {
+ return SpecialCardAi.VeilOfSummer.consider(ai, sa); // this is more of a counterspell than a true draw card, so it's timed by the card-specific logic
+ } else if (logic.startsWith("LifeLessThan.")) {
+ // LifeLessThan logic presupposes activation as soon as possible in an
+ // attempt to save the AI from dying
+ return true;
+ } else if (logic.equals("AtOppEOT")) {
+ return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
+ } else if (logic.equals("RespondToOwnActivation")) {
+ return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
+ } else if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& ai.getCardsIn(ZoneType.Hand).size() > 1 && !ComputerUtil.activateForCost(sa, ai)
&& !"YawgmothsBargain".equals(logic)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
index b0fe07d4dcf..a1a93174e8d 100644
--- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
@@ -1,35 +1,19 @@
package forge.ai.ability;
-import java.util.List;
-import java.util.Map;
-
-import org.checkerframework.checker.nullness.qual.Nullable;
-
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
-
-import forge.ai.AiCardMemory;
-import forge.ai.AiController;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCard;
-import forge.ai.PlayerControllerAi;
-import forge.ai.SpecialCardAi;
-import forge.ai.SpellAbilityAi;
-import forge.ai.SpellApiToAi;
+import forge.ai.*;
import forge.game.CardTraitPredicates;
import forge.game.Game;
+import forge.game.GameEntity;
import forge.game.ability.AbilityKey;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
-import forge.game.card.Card;
-import forge.game.card.CardCollection;
-import forge.game.card.CardCollectionView;
-import forge.game.card.CardLists;
-import forge.game.card.CardPredicates;
-import forge.game.card.CardUtil;
+import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
+import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -44,6 +28,10 @@
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+import java.util.List;
+import java.util.Map;
public class EffectAi extends SpellAbilityAi {
@Override
@@ -207,15 +195,60 @@ public boolean apply(final Card c) {
}
return false;
} else if (logic.equals("NoGain")) {
- // basic logic to cancel GainLife on stack
- if (game.getStack().isEmpty()) {
- return false;
+ // basic logic to cancel GainLife on stack
+ if (!game.getStack().isEmpty()) {
+ SpellAbility topStack = game.getStack().peekAbility();
+ final Player activator = topStack.getActivatingPlayer();
+ if (activator.isOpponentOf(ai) && activator.canGainLife()) {
+ while (topStack != null) {
+ if (topStack.getApi() == ApiType.GainLife) {
+ if ("You".equals(topStack.getParam("Defined")) || topStack.isTargeting(activator) || (!topStack.usesTargeting() && !topStack.hasParam("Defined"))) {
+ return true;
+ }
+ } else if (topStack.getApi() == ApiType.DealDamage && topStack.getHostCard().hasKeyword(Keyword.LIFELINK)) {
+ Card host = topStack.getHostCard();
+ for (GameEntity target : topStack.getTargets().getTargetEntities()) {
+ if (ComputerUtilCombat.predictDamageTo(target,
+ AbilityUtils.calculateAmount(host, topStack.getParam("NumDmg"), topStack), host, false) > 0) {
+ return true;
+ }
+ }
+ }
+ topStack = topStack.getSubAbility();
+ }
+ }
}
- final SpellAbility topStack = game.getStack().peekAbility();
- return topStack.getActivatingPlayer().isOpponentOf(ai) && topStack.getApi() == ApiType.GainLife;
+ // also check for combat lifelink
+ if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
+ final Combat combat = ai.getGame().getCombat();
+ final Player attackingPlayer = combat.getAttackingPlayer();
+ if (attackingPlayer.isOpponentOf(ai) && attackingPlayer.canGainLife()) {
+ if (ComputerUtilCombat.checkAttackerLifelinkDamage(combat) > 0) {
+ return true;
+ }
+ }
+ }
+ return false;
+ } else if (logic.equals("NonCastCreature")) {
+ // TODO: add support for more cases with more convoluted API setups
+ if (!game.getStack().isEmpty()) {
+ SpellAbility topStack = game.getStack().peekAbility();
+ final Player activator = topStack.getActivatingPlayer();
+ if (activator.isOpponentOf(ai)) {
+ boolean changeZone = topStack.getApi() == ApiType.ChangeZone || topStack.getApi() == ApiType.ChangeZoneAll;
+ boolean toBattlefield = "Battlefield".equals(topStack.getParam("Destination"));
+ boolean reanimator = "true".equalsIgnoreCase(topStack.getSVar("IsReanimatorCard"));
+ if (changeZone && (toBattlefield || reanimator)) {
+ if ("Creature".equals(topStack.getParam("ChangeType")) || topStack.getParamOrDefault("Defined", "").contains("Creature"))
+ return true;
+ }
+ }
+ }
+ return false;
} else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0, 0);
} else if (logic.equals("Pump")) {
+ sa.resetTargets();
List options = CardUtil.getValidCardsToTarget(sa);
options = CardLists.filterControlledBy(options, ai);
if (sa.getPayCosts().hasTapCost()) {
@@ -363,10 +396,17 @@ public boolean apply(@Nullable Card input) {
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
+ if (sa.hasParam("AILogic")) {
+ if (canPlayAI(aiPlayer, sa)) {
+ return true; // if false, fall through further to do the mandatory stuff
+ }
+ }
+
// E.g. Nova Pentacle
if (sa.usesTargeting() && !sa.getTargetRestrictions().canTgtPlayer()) {
// try to target the opponent's best targetable permanent, if able
CardCollection oppPerms = CardLists.getValidCards(aiPlayer.getOpponents().getCardsIn(sa.getTargetRestrictions().getZone()), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
+ oppPerms = CardLists.filter(oppPerms, card -> sa.canTarget(card));
if (!oppPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
@@ -376,6 +416,7 @@ protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa
if (mandatory) {
// try to target the AI's worst targetable permanent, if able
CardCollection aiPerms = CardLists.getValidCards(aiPlayer.getCardsIn(sa.getTargetRestrictions().getZone()), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
+ aiPerms = CardLists.filter(aiPerms, card -> sa.canTarget(card));
if (!aiPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
diff --git a/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
index 7cc4abf0dce..b755e4fd41c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
@@ -13,9 +13,12 @@
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
+import java.util.Map;
+
public class ExploreAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
@@ -36,7 +39,7 @@ protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
}
- public static Card shouldPutInGraveyard(CardCollection top, Player ai) {
+ public static boolean shouldPutInGraveyard(Card topCard, Player ai) {
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
@@ -52,20 +55,20 @@ public static Card shouldPutInGraveyard(CardCollection top, Player ai) {
numLandsToStillNeedMore = aic.getIntProperty(AiProps.EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE);
}
- if (!top.isEmpty()) {
- Card topCard = top.getFirst();
- if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) {
- // We need more lands to improve our mana base, explore away the non-lands
- return topCard;
- }
- if (topCard.getCMC() - maxCMCDiff >= predictedMana && !topCard.hasSVar("DoNotDiscardIfAble")) {
- // We're not casting this in foreseeable future, put it in the graveyard
- return topCard;
- }
+ if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) {
+ // We need more lands to improve our mana base, explore away the non-lands
+ return true;
+ } else if (topCard.getCMC() - maxCMCDiff >= predictedMana && !topCard.hasSVar("DoNotDiscardIfAble")) {
+ // We're not casting this in foreseeable future, put it in the graveyard
+ return true;
}
// Put on top of the library (do not mark the card for placement in the graveyard)
- return null;
+ return false;
}
+ @Override
+ public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
+ return shouldPutInGraveyard((Card)params.get("RevealedCard"), player);
+ }
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/FightAi.java b/forge-ai/src/main/java/forge/ai/ability/FightAi.java
index d0022ccffe0..80504470a5b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java
@@ -167,7 +167,15 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator
public static boolean canFightAi(final Player ai, final SpellAbility sa, int power, int toughness) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
- final AbilitySub tgtFight = sa.getSubAbility();
+ AbilitySub tgtFight = sa.getSubAbility();
+ while (tgtFight != null && tgtFight.getApi() != ApiType.Fight && tgtFight.getApi() != ApiType.DealDamage) {
+ // Search for the Fight/DealDamage subability (matters e.g. for Ent's Fury where the Fight SA is not an immediate child of Pump)
+ tgtFight = tgtFight.getSubAbility();
+ }
+ if (tgtFight == null) {
+ System.out.println("Warning: couldn't find a Fight/DealDamage subability from FightAi.canFightAi for card " + source.toString());
+ tgtFight = sa.getSubAbility(); // at least avoid a NPE, although this will most likely fail
+ }
final boolean isChandrasIgnition = "Chandra's Ignition".equals(sourceName); // TODO: generalize this for other "fake Fight" cases that do not target
if ("Savage Punch".equals(sourceName) && !ai.hasFerocious()) {
power = 0;
diff --git a/forge-ai/src/main/java/forge/ai/ability/FogAi.java b/forge-ai/src/main/java/forge/ai/ability/FogAi.java
index 44c89517017..6c197ca8d4f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FogAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FogAi.java
@@ -23,8 +23,8 @@
public class FogAi extends SpellAbilityAi {
/* (non-Javadoc)
- * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
- */
+ * @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
+ */
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
@@ -121,7 +121,7 @@ private int countAvailableFogs(Player ai) {
int fogs = 0;
for (Card c : ai.getCardsActivatableInExternalZones(false)) {
for (SpellAbility ability : c.getSpellAbilities()) {
- if (ability.getApi().equals(ApiType.Fog)) {
+ if (ApiType.Fog.equals(ability.getApi())) {
fogs++;
break;
}
@@ -130,7 +130,7 @@ private int countAvailableFogs(Player ai) {
for (Card c : ai.getCardsIn(ZoneType.Hand)) {
for (SpellAbility ability : c.getSpellAbilities()) {
- if (ability.getApi().equals(ApiType.Fog)) {
+ if (ApiType.Fog.equals(ability.getApi())) {
fogs++;
break;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java
index cd0b4b67666..1b9214b9fa3 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ImmediateTriggerAi.java
@@ -1,6 +1,7 @@
package forge.ai.ability;
import forge.ai.*;
+import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
@@ -59,6 +60,25 @@ protected boolean canPlayAI(Player ai, SpellAbility sa) {
return false;
}
+ if (logic.equals("WeakerCreature")) {
+ Card ownCreature = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
+ if (ownCreature == null) {
+ return false;
+ }
+
+ int eval = ComputerUtilCard.evaluateCreature(ownCreature);
+ boolean foundWorse = false;
+ for (Card c : ai.getOpponents().getCreaturesInPlay()) {
+ if (eval + 100 < ComputerUtilCard.evaluateCreature(c) ) {
+ foundWorse = true;
+ break;
+ }
+ }
+ if (!foundWorse) {
+ return false;
+ }
+ }
+
trigsa.setActivatingPlayer(ai, true);
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
index 48f3585c596..1185df31edd 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeGainAi.java
@@ -81,6 +81,7 @@ protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card sourc
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
final Game game = ai.getGame();
final int life = ai.getLife();
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
boolean lifeCritical = life <= 5;
@@ -103,9 +104,15 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; }
}
+ // Sacrificing in response to something dangerous is generally good in any phase
+ boolean isSacCost = false;
+ if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
+ isSacCost = true;
+ }
+
// Don't use lifegain before main 2 if possible
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
- && !ComputerUtil.castSpellInMain1(ai, sa)) {
+ && !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) {
return false;
}
@@ -124,6 +131,7 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
final int life = ai.getLife();
final String amountStr = sa.getParam("LifeAmount");
@@ -185,7 +193,11 @@ protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|| sa.getSubAbility() != null || playReusable(ai, sa)) {
return true;
}
-
+
+ if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
+ return true; // sac costs should be performed at Instant speed when able
+ }
+
// Save instant-speed life-gain unless it is really worth it
final float value = 0.9f * lifeAmount / life;
if (value < 0.2f) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
index b7f75808bc5..b261a2fcf02 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeLoseAi.java
@@ -10,6 +10,7 @@
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.cost.Cost;
+import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
@@ -96,6 +97,7 @@ protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card sourc
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String amountStr = sa.getParam("LifeAmount");
+ final String aiLogic = sa.getParamOrDefault("AILogic", "");
int amount = 0;
if (sa.usesTargeting()) {
@@ -133,9 +135,15 @@ protected boolean checkApiLogic(Player ai, SpellAbility sa) {
return true;
}
+ // Sacrificing a creature in response to something dangerous is generally good in any phase
+ boolean isSacCost = false;
+ if (sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
+ isSacCost = true;
+ }
+
// Don't use loselife before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
- && !ComputerUtil.castSpellInMain1(ai, sa) && !"AnyPhase".equals(sa.getParam("AILogic"))) {
+ && !ComputerUtil.castSpellInMain1(ai, sa) && !aiLogic.contains("AnyPhase") && !isSacCost) {
return false;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java
index 3568466bbf7..4fa80b029d2 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeSetAi.java
@@ -109,6 +109,11 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
+ // TODO add AI logic for that
+ if (sa.hasParam("Redistribute")) {
+ return mandatory;
+ }
+
final String amountStr = sa.getParam("LifeAmount");
int amount;
diff --git a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
index 17cc2f8ed75..5525a2a539f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
@@ -6,14 +6,7 @@
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
-import forge.ai.AiPlayDecision;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilAbility;
-import forge.ai.ComputerUtilCard;
-import forge.ai.ComputerUtilCost;
-import forge.ai.ComputerUtilMana;
-import forge.ai.PlayerControllerAi;
-import forge.ai.SpellAbilityAi;
+import forge.ai.*;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaAtom;
@@ -50,7 +43,7 @@ public class ManaEffectAi extends SpellAbilityAi {
*/
@Override
protected boolean checkAiLogic(Player ai, SpellAbility sa, String aiLogic) {
- if (aiLogic.startsWith("ManaRitual")) {
+ if (aiLogic.startsWith("ManaRitual") || aiLogic.startsWith("BlackLotus")) {
return doManaRitualLogic(ai, sa, false);
} else if ("Always".equals(aiLogic)) {
return true;
@@ -89,7 +82,7 @@ protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandle
if (logic.startsWith("ManaRitual")) {
return ph.is(PhaseType.MAIN2, ai) || ph.is(PhaseType.MAIN1, ai);
} else if ("AtOppEOT".equals(logic)) {
- return !ai.getManaPool().hasBurn() && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
+ return (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) && ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
@@ -261,7 +254,8 @@ public static boolean doManaRitualLogic(Player ai, SpellAbility sa, boolean from
}
private boolean improvesPosition(Player ai, SpellAbility sa) {
- boolean activateForTrigger = Iterables.any(Iterables.filter(sa.getHostCard().getTriggers(), CardTraitPredicates.hasParam("AILogic", "ActivateOnce")),
+ boolean activateForTrigger = (!ai.getManaPool().hasBurn() || !ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) &&
+ Iterables.any(Iterables.filter(sa.getHostCard().getTriggers(), CardTraitPredicates.hasParam("AILogic", "ActivateOnce")),
t -> sa.getHostCard().getAbilityActivatedThisTurn(t.getOverridingAbility()) == 0);
PhaseHandler ph = ai.getGame().getPhaseHandler();
diff --git a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java
index 43b777b9789..d5528736df7 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ManifestAi.java
@@ -1,90 +1,15 @@
package forge.ai.ability;
-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.ComputerUtilCost;
-import forge.ai.SpellAbilityAi;
-import forge.game.Game;
import forge.game.card.Card;
-import forge.game.card.CardCollection;
-import forge.game.card.CardCollectionView;
-import forge.game.card.CardLists;
import forge.game.card.CardUtil;
-import forge.game.phase.PhaseHandler;
-import forge.game.phase.PhaseType;
import forge.game.player.Player;
-import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
-import forge.game.zone.ZoneType;
-import forge.util.MyRandom;
-/**
- * Created by friarsol on 1/23/15.
- */
-public class ManifestAi extends SpellAbilityAi {
+public class ManifestAi extends ManifestBaseAi {
@Override
- protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
- // Manifest doesn't have any "Pay X to manifest X triggers"
-
- return true;
- }
- /* (non-Javadoc)
- * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
- */
- @Override
- public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
- return true;
- }
-
- /**
- * Checks if the AI will play a SpellAbility based on its phase restrictions
- */
- @Override
- protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
- // Only manifest things on your turn if sorcery speed, or would pump one of my creatures
- if (ph.isPlayerTurn(ai)) {
- if (ph.getPhase().isBefore(PhaseType.MAIN2)
- && !sa.hasParam("ActivationPhases")
- && !ComputerUtil.castSpellInMain1(ai, sa)) {
- boolean buff = false;
- for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
- if ("Creature".equals(c.getSVar("BuffedBy"))) {
- buff = true;
- }
- }
- if (!buff) {
- return false;
- }
- } else if (!isSorcerySpeed(sa, ai)) {
- return false;
- }
- } else {
- // try to ambush attackers
- if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
- return false;
- }
- }
-
- if (sa.getSVar("X").equals("Count$xPaid")) {
- // Handle either Manifest X cards, or Manifest 1 card and give it X P1P1s
- // Set PayX here to maximum value.
- int x = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
- sa.setXManaCostPaid(x);
- if (x <= 0) {
- return false;
- }
- }
-
- return true;
- }
-
- static boolean shouldManyfest(final Card card, final Player ai, final SpellAbility sa) {
+ protected boolean shouldApply(final Card card, final Player ai, final SpellAbility sa) {
// check to ensure that there are no replacement effects that prevent creatures ETBing from library
// (e.g. Grafdigger's Cage)
Card topCopy = CardUtil.getLKICopy(card);
@@ -115,73 +40,4 @@ static boolean shouldManyfest(final Card card, final Player ai, final SpellAbili
}
return true;
}
-
- @Override
- protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
- final Game game = ai.getGame();
- final Card host = sa.getHostCard();
- if (ComputerUtil.preventRunAwayActivations(sa)) {
- return false;
- }
-
- if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) {
- ZoneType choiceZone = ZoneType.Hand;
- if (sa.hasParam("ChoiceZone")) {
- choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
- }
- CardCollection choices = new CardCollection(game.getCardsIn(choiceZone));
- if (sa.hasParam("Choices")) {
- choices = CardLists.getValidCards(choices, sa.getParam("Choices"), ai, host, sa);
- }
- if (choices.isEmpty()) {
- return false;
- }
- } else {
- // Library is empty, no Manifest
- final CardCollectionView library = ai.getCardsIn(ZoneType.Library);
- if (library.isEmpty())
- return false;
-
- // try not to mill himself with Manifest
- if (library.size() < 5 && !ai.isCardInPlay("Laboratory Maniac")) {
- return false;
- }
-
- if (!shouldManyfest(library.getFirst(), ai, sa)) {
- return false;
- }
- }
- // Probably should be a little more discerning on playing during OPPs turn
- if (playReusable(ai, sa)) {
- return true;
- }
- if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
- // Add blockers?
- return true;
- }
- if (sa.isAbility()) {
- return true;
- }
-
- return MyRandom.getRandom().nextFloat() < .8;
- }
-
- @Override
- protected Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) {
- if (Iterables.size(options) > 1 || isOptional) {
- CardCollection filtered = CardLists.filter(options, new Predicate() {
- @Override
- public boolean apply(Card input) {
- return shouldManyfest(input, ai, sa);
- }
- });
- if (!filtered.isEmpty()) {
- return ComputerUtilCard.getBestAI(filtered);
- }
- if (isOptional) {
- return null;
- }
- }
- return Iterables.getFirst(options, null);
- }
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java b/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java
new file mode 100644
index 00000000000..becde70e107
--- /dev/null
+++ b/forge-ai/src/main/java/forge/ai/ability/ManifestBaseAi.java
@@ -0,0 +1,156 @@
+package forge.ai.ability;
+
+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.ComputerUtilCost;
+import forge.ai.SpellAbilityAi;
+import forge.game.Game;
+import forge.game.card.Card;
+import forge.game.card.CardCollection;
+import forge.game.card.CardCollectionView;
+import forge.game.card.CardLists;
+import forge.game.phase.PhaseHandler;
+import forge.game.phase.PhaseType;
+import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
+import forge.game.spellability.SpellAbility;
+import forge.game.zone.ZoneType;
+import forge.util.MyRandom;
+
+/**
+ * Created by friarsol on 1/23/15.
+ */
+public abstract class ManifestBaseAi extends SpellAbilityAi {
+
+ @Override
+ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+ // Manifest doesn't have any "Pay X to manifest X triggers"
+
+ return true;
+ }
+ /* (non-Javadoc)
+ * @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
+ */
+ @Override
+ public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
+ return true;
+ }
+
+ /**
+ * Checks if the AI will play a SpellAbility based on its phase restrictions
+ */
+ @Override
+ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
+ // Only manifest things on your turn if sorcery speed, or would pump one of my creatures
+ if (ph.isPlayerTurn(ai)) {
+ if (ph.getPhase().isBefore(PhaseType.MAIN2)
+ && !sa.hasParam("ActivationPhases")
+ && !ComputerUtil.castSpellInMain1(ai, sa)) {
+ boolean buff = false;
+ for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
+ if ("Creature".equals(c.getSVar("BuffedBy"))) {
+ buff = true;
+ }
+ }
+ if (!buff) {
+ return false;
+ }
+ } else if (!isSorcerySpeed(sa, ai)) {
+ return false;
+ }
+ } else {
+ // try to ambush attackers
+ if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
+ return false;
+ }
+ }
+
+ if (sa.getSVar("X").equals("Count$xPaid")) {
+ // Handle either Manifest X cards, or Manifest 1 card and give it X P1P1s
+ // Set PayX here to maximum value.
+ int x = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
+ sa.setXManaCostPaid(x);
+ if (x <= 0) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ abstract protected boolean shouldApply(final Card card, final Player ai, final SpellAbility sa);
+
+ @Override
+ protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
+ final Game game = ai.getGame();
+ final Card host = sa.getHostCard();
+ if (ComputerUtil.preventRunAwayActivations(sa)) {
+ return false;
+ }
+
+ if (sa.hasParam("Choices") || sa.hasParam("ChoiceZone")) {
+ ZoneType choiceZone = ZoneType.Hand;
+ if (sa.hasParam("ChoiceZone")) {
+ choiceZone = ZoneType.smartValueOf(sa.getParam("ChoiceZone"));
+ }
+ CardCollection choices = new CardCollection(game.getCardsIn(choiceZone));
+ if (sa.hasParam("Choices")) {
+ choices = CardLists.getValidCards(choices, sa.getParam("Choices"), ai, host, sa);
+ }
+ if (choices.isEmpty()) {
+ return false;
+ }
+ } else if ("TopOfLibrary".equals(sa.getParamOrDefault("Defined", "TopOfLibrary"))) {
+ // Library is empty, no Manifest
+ final CardCollectionView library = ai.getCardsIn(ZoneType.Library);
+ if (library.isEmpty())
+ return false;
+
+ // try not to mill himself with Manifest
+ if (library.size() < 5 && !ai.isCardInPlay("Laboratory Maniac")) {
+ return false;
+ }
+
+ if (!shouldApply(library.getFirst(), ai, sa)) {
+ return false;
+ }
+ }
+ // Probably should be a little more discerning on playing during OPPs turn
+ if (playReusable(ai, sa)) {
+ return true;
+ }
+ if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
+ // Add blockers?
+ return true;
+ }
+ if (sa.isAbility()) {
+ return true;
+ }
+
+ return MyRandom.getRandom().nextFloat() < .8;
+ }
+
+ @Override
+ protected Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) {
+ if (Iterables.size(options) > 1 || isOptional) {
+ CardCollection filtered = CardLists.filter(options, new Predicate() {
+ @Override
+ public boolean apply(Card input) {
+ return shouldApply(input, ai, sa);
+ }
+ });
+ if (!filtered.isEmpty()) {
+ return ComputerUtilCard.getBestAI(filtered);
+ }
+ if (isOptional) {
+ return null;
+ }
+ }
+ return Iterables.getFirst(options, null);
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
index ccd93c27dbf..6f70d24d128 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
@@ -1,5 +1,7 @@
package forge.ai.ability;
+import org.apache.commons.lang3.StringUtils;
+
import com.google.common.base.Predicate;
import forge.ai.AiController;
@@ -36,19 +38,9 @@ public class PermanentCreatureAi extends PermanentAi {
*/
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
- final Game game = ai.getGame();
if ("Never".equals(aiLogic)) {
return false;
- } else if ("ZeroToughness".equals(aiLogic)) {
- // If Creature has Zero Toughness, make sure some static ability is in play
- // That will grant a toughness bonus
-
- final Card copy = CardUtil.getLKICopy(sa.getHostCard());
-
- ComputerUtilCard.applyStaticContPT(game, copy, null);
-
- return copy.getNetToughness() > 0;
}
return true;
}
@@ -175,8 +167,10 @@ public boolean apply(final Card card) {
boolean canCastAtOppTurn = true;
for (Card c : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) {
- if ("CantBeCast".equals(s.getParam("Mode")) && "True".equals(s.getParam("NonCasterTurn"))) {
+ if ("CantBeCast".equals(s.getParam("Mode")) && StringUtils.contains(s.getParam("Activator"), "NonActive")
+ && (!s.getParam("Activator").startsWith("You") || c.getController().equals(ai))) {
canCastAtOppTurn = false;
+ break;
}
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java
index dad49ca6304..434f47b836a 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentNoncreatureAi.java
@@ -20,17 +20,11 @@ public class PermanentNoncreatureAi extends PermanentAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
- if ("Never".equals(aiLogic) || "DontCast".equals(aiLogic)) {
- return false;
- }
-
- Game game = ai.getGame();
-
if ("PithingNeedle".equals(aiLogic)) {
// Make sure theres something in play worth Needlings.
// Planeswalker or equipment or something
- CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonmanaAbilities", ai, sa.getHostCard(), sa);
+ CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa);
if (oppPerms.isEmpty()) {
return false;
}
@@ -44,7 +38,7 @@ protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final Str
return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size();
}
- return true;
+ return super.checkAiLogic(ai, sa, aiLogic);
}
/**
diff --git a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
index f30551bb212..570e44e9d0b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
@@ -153,7 +153,7 @@ public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable getPlayableCards(SpellAbility sa, Player ai) {
final Iterator itr = cards.iterator();
while (itr.hasNext()) {
final Card c = itr.next();
- if (!Iterables.any(AbilityUtils.getBasicSpellsFromPlayEffect(c, ai), SpellAbilityPredicates.isValid(valid, ai , c, sa))) {
+ if (!Iterables.any(AbilityUtils.getBasicSpellsFromPlayEffect(c, ai), SpellAbilityPredicates.isValid(valid, ai , source, sa))) {
itr.remove();
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java
index 2af0c498087..346599e1cab 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ProtectAi.java
@@ -72,8 +72,8 @@ public static String toProtectFrom(final Card threat, SpellAbility sa) {
return null;
}
final List choices = ProtectEffect.getProtectionList(sa);
- if (threat.isArtifact() && choices.contains("artifacts")) {
- return "artifacts";
+ if (threat.isArtifact() && choices.contains("Artifact")) {
+ return "Artifact";
}
if (threat.isBlack() && choices.contains("black")) {
return "black";
@@ -157,7 +157,7 @@ public boolean apply(final Card c) {
}
});
return list;
- } // getProtectCreatures()
+ }
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
index 2b41e7b86ac..3d04ce7cb5f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
@@ -168,7 +168,6 @@ protected boolean checkApiLogic(Player ai, SpellAbility sa) {
CardCollection best = CardLists.filter(attr, new Predicate() {
@Override
public boolean apply(Card card) {
-
int amount = 0;
if (StringUtils.isNumeric(amountStr)) {
amount = AbilityUtils.calculateAmount(source, amountStr, moveSA);
@@ -322,6 +321,7 @@ public boolean apply(Card card) {
attack = root.getXManaCostPaid();
}
} else {
+ // TODO add Double
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
if (numAttack.contains("X") && sa.getSVar("X").equals("Count$CardsInYourHand") && source.isInZone(ZoneType.Hand)) {
attack--; // the card will be spent casting the spell, so actual power is 1 less
@@ -585,11 +585,10 @@ public boolean apply(Card input) {
list = getPumpCreatures(ai, sa, defense, attack, keywords, immediately);
} else {
ZoneType zone = tgt.getZone().get(0);
- list = new CardCollection(game.getCardsIn(zone));
+ list = CardLists.getTargetableCards(game.getCardsIn(zone), sa);
}
}
- list = CardLists.getTargetableCards(list, sa);
if (game.getStack().isEmpty()) {
// If the cost is tapping, don't activate before declare attack/block
if (sa.getPayCosts().hasTapCost()) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
index 995af654eff..a8bcaa38628 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAiBase.java
@@ -409,7 +409,7 @@ public boolean isUsefulPumpKeyword(final Player ai, final String keyword, final
*/
protected CardCollection getPumpCreatures(final Player ai, final SpellAbility sa, final int defense, final int attack,
final List keywords, final boolean immediately) {
- CardCollection list = ai.getCreaturesInPlay();
+ CardCollection list = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
list = CardLists.filter(list, new Predicate() {
@Override
public boolean apply(final Card c) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
index a95a44b904a..9c9a714cdc1 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
@@ -5,6 +5,7 @@
import java.util.List;
import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
@@ -141,7 +142,8 @@ else if (power < 0) { // -X/-0
return pumpAgainstRemoval(ai, sa, comp);
}
- return !CardLists.getValidCards(getPumpCreatures(ai, sa, defense, power, keywords, false), valid, source.getController(), source, sa).isEmpty();
+ return Iterables.any(ai.getCreaturesInPlay(), c -> c.isValid(valid, source.getController(), source, sa)
+ && ComputerUtilCard.shouldPumpCard(ai, sa, c, defense, power, keywords));
} // pumpAllCanPlayAI()
@Override
@@ -152,7 +154,7 @@ public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
// it might help so take it
- if (!sa.usesTargeting() && !sa.isCurse() && sa.getParam("ValidCards") != null && sa.getParam("ValidCards").contains("YouCtrl")) {
+ if (!sa.usesTargeting() && !sa.isCurse() && sa.hasParam("ValidCards") && sa.getParam("ValidCards").contains("YouCtrl")) {
return true;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java
index ca686734f15..32b21c964cb 100644
--- a/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/RollPlanarDiceAi.java
@@ -19,9 +19,16 @@ public class RollPlanarDiceAi extends SpellAbilityAi {
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
- AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
- Card plane = sa.getHostCard();
+ for (Card c : ai.getGame().getActivePlanes()) {
+ if (willRollOnPlane(ai, c)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ private boolean willRollOnPlane(Player ai, Card plane) {
+ AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
boolean decideToRoll = false;
boolean rollInMain1 = false;
String modeName = "never";
diff --git a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
index 470bbdc461f..ee98d496455 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
@@ -46,8 +46,10 @@ public boolean chkAIDrawback(SpellAbility sa, Player ai) {
*/
@Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
+ String logic = sa.getParamOrDefault("AILogic", "");
+
// For Brain in a Jar, avoid competing against the other ability in the opponent's EOT.
- if ("BrainJar".equals(sa.getParam("AILogic"))) {
+ if ("BrainJar".equals(logic)) {
return ph.getPhase().isAfter(PhaseType.MAIN2);
}
@@ -55,15 +57,15 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
// even if there's no mana cost.
- if (sa.getPayCosts().hasTapCost()
+ if (logic.equals("AtOppEOT") || (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
- && !isSorcerySpeed(sa, ai)) {
+ && !isSorcerySpeed(sa, ai))) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
// AI logic to scry in Main 1 if there is no better option, otherwise scry at opponent's EOT
// (e.g. Glimmer of Genius)
- if ("BestOpportunity".equals(sa.getParam("AILogic"))) {
+ if ("BestOpportunity".equals(logic)) {
return doBestOpportunityLogic(ai, sa, ph);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
index ea48f35905b..cd37f3ee3df 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
@@ -33,7 +33,7 @@ protected boolean checkApiLogic(final Player aiPlayer, final SpellAbility sa) {
// turning face is most likely okay
// TODO only do this at beneficial moment (e.g. surprise during combat or morph trigger), might want to reserve mana to protect them from easy removal
- if ("TurnFace".equals(mode)) {
+ if ("TurnFaceUp".equals(mode) || "TurnFaceDown".equals(mode)) {
return true;
}
@@ -103,7 +103,7 @@ public boolean apply(Card c) {
return sa.isMinTargetChosen();
}
- } else if ("TurnFace".equals(mode)) {
+ } else if ("TurnFaceUp".equals(mode) || "TurnFaceDown".equals(mode)) {
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -114,7 +114,7 @@ public boolean apply(Card c) {
}
for (final Card c : list) {
- if (shouldTurnFace(c, ai, ph) || "Always".equals(logic)) {
+ if (shouldTurnFace(c, ai, ph, mode) || "Always".equals(logic)) {
sa.getTargets().add(c);
if (!sa.canAddMoreTarget()) {
break;
@@ -128,7 +128,7 @@ public boolean apply(Card c) {
if (list.isEmpty()) {
return false;
}
- return shouldTurnFace(list.get(0), ai, ph) || "Always".equals(logic);
+ return shouldTurnFace(list.get(0), ai, ph, mode) || "Always".equals(logic);
}
}
return true;
@@ -150,8 +150,11 @@ private boolean shouldTransformCard(Card card, Player ai, PhaseHandler ph) {
return compareCards(card, transformed, ai, ph);
}
- private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
+ private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph, String mode) {
if (card.isFaceDown()) {
+ if ("TurnFaceDown".equals(mode)) {
+ return false;
+ }
// hidden agenda
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
&& card.isInZone(ZoneType.Command)) {
@@ -169,6 +172,9 @@ private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
return false;
}
} else {
+ if ("TurnFaceUp".equals(mode)) {
+ return false;
+ }
// doublefaced or meld cards can't be turned face down
if (card.isTransformable() || card.isMeldable()) {
return false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
index 955ca018906..54df44cafe1 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
@@ -57,7 +57,8 @@ protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa,
&& !isSorcerySpeed(sa, ai)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
-
+ if (sa.getHostCard() != null && !sa.getHostCard().isPermanent() && !isSorcerySpeed(sa, ai))
+ return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
// in the player's turn Surveil should only be done in Main1 or in Upkeep if able
if (ph.isPlayerTurn(ai)) {
if (isSorcerySpeed(sa, ai)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
index 8e7ac2bff0c..04aefba1c19 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
@@ -108,7 +108,7 @@ private boolean tapTargetList(final Player ai, final SpellAbility sa, final Card
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
final Game game = ai.getGame();
CardCollection tapList = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
- tapList = CardLists.filter(tapList, Presets.UNTAPPED);
+ tapList = CardLists.filter(tapList, Presets.CAN_TAP);
tapList = CardLists.filter(tapList, new Predicate() {
@Override
public boolean apply(final Card c) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java b/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java
new file mode 100644
index 00000000000..1fbda3e0273
--- /dev/null
+++ b/forge-ai/src/main/java/forge/ai/ability/TimeTravelAi.java
@@ -0,0 +1,46 @@
+package forge.ai.ability;
+
+
+import com.google.common.collect.Iterables;
+import forge.ai.ComputerUtil;
+import forge.ai.SpellAbilityAi;
+import forge.game.card.*;
+import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
+import forge.game.player.PlayerController;
+import forge.game.spellability.SpellAbility;
+import forge.game.zone.ZoneType;
+
+import java.util.Map;
+
+public class TimeTravelAi extends SpellAbilityAi {
+ @Override
+ protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
+ boolean hasSuspendedCards = Iterables.any(aiPlayer.getCardsIn(ZoneType.Exile), CardPredicates.hasSuspend());
+ boolean hasRelevantCardsOTB = Iterables.any(aiPlayer.getCardsIn(ZoneType.Battlefield), CardPredicates.hasCounter(CounterEnumType.TIME));
+
+ // TODO: add more logic for cards which may need it
+ return hasSuspendedCards || hasRelevantCardsOTB;
+ }
+
+ @Override
+ public boolean chooseBinary(PlayerController.BinaryChoiceType kindOfChoice, SpellAbility sa, Map params) {
+ // Returning true means "add counter", false means "remove counter"
+
+ // TODO: extend this (usually, stuff in exile such as Suspended cards with Time counters is played once no Time counters are left,
+ // so removing them is good; stuff on the battlefield is usually stuff like Vanishing or As Foretold, which favors adding Time
+ // counters for better effect, but exceptions should be added here).
+ Card target = (Card)params.get("Target");
+ return !ComputerUtil.isNegativeCounter(CounterType.get(CounterEnumType.TIME), target);
+ }
+
+ @Override
+ protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) {
+ return Iterables.getFirst(options, null);
+ }
+
+ @Override
+ public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map params) {
+ return true;
+ }
+}
diff --git a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
index f1d89d1ce2e..af159cb7dc1 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
@@ -1,5 +1,6 @@
package forge.ai.ability;
+import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
@@ -23,6 +24,7 @@
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
+import forge.game.card.CardUtil;
import forge.game.card.token.TokenInfo;
import forge.game.combat.Combat;
import forge.game.cost.CostPart;
@@ -162,6 +164,11 @@ protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
+
+ if (actualToken.getType().hasSubtype("Role")) {
+ return tgtRoleAura(ai, sa, actualToken, false);
+ }
+
if (tgt.canOnlyTgtOpponent() || "Opponent".equals(sa.getParam("AITgts"))) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
@@ -253,9 +260,16 @@ private boolean canInterruptSacrifice(final Player ai, final SpellAbility sa, fi
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
+ Card actualToken = spawnToken(ai, sa);
+
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
+
+ if (actualToken.getType().hasSubtype("Role")) {
+ return tgtRoleAura(ai, sa, actualToken, mandatory);
+ }
+
if (tgt.canOnlyTgtOpponent()) {
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (mandatory && targetableOpps.isEmpty()) {
@@ -268,7 +282,6 @@ protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandator
}
}
- Card actualToken = spawnToken(ai, sa);
String tokenPower = sa.getParamOrDefault("TokenPower", actualToken.getBasePowerString());
String tokenToughness = sa.getParamOrDefault("TokenToughness", actualToken.getBaseToughnessString());
String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
@@ -351,12 +364,49 @@ public static Card spawnToken(Player ai, SpellAbility sa) {
throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript"));
}
- result.setOwner(ai);
-
// Apply static abilities
final Game game = ai.getGame();
ComputerUtilCard.applyStaticContPT(game, result, null);
return result;
}
+ private boolean tgtRoleAura(final Player ai, final SpellAbility sa, final Card tok, final boolean mandatory) {
+ boolean isCurse = "Curse".equals(sa.getParam("AILogic")) ||
+ tok.getFirstAttachSpell().getParamOrDefault("AILogic", "").equals("Curse");
+ List tgts = CardUtil.getValidCardsToTarget(sa);
+
+ // look for card without role from ai
+ List prefListSBA = CardLists.filter(tgts, c ->
+ !Iterables.any(c.getAttachedCards(), att -> att.getController() == ai && att.getType().hasSubtype("Role")));
+
+ List prefList;
+ if (isCurse) {
+ prefList = CardLists.filterControlledBy(prefListSBA, ai.getOpponents());
+ } else {
+ prefList = CardLists.filterControlledBy(prefListSBA, ai.getYourTeam());
+ }
+
+ if (prefList.isEmpty()) {
+ if (mandatory) {
+ if (sa.isTargetNumberValid()) {
+ // TODO try replace Curse <-> Pump depending on target controller
+ return true;
+ }
+ if (!prefListSBA.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(prefListSBA));
+ return true;
+ }
+ if (!tgts.isEmpty()) {
+ sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(tgts));
+ return true;
+ }
+ }
+ } else {
+ sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(prefList));
+ return true;
+ }
+
+ return false;
+ }
+
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
index 82d5d4fdbcf..0fb6560ca92 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
@@ -64,14 +64,12 @@ protected boolean checkApiLogic(Player ai, SpellAbility sa) {
return false;
}
- if (!sa.usesTargeting()) {
- final List pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
- return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
- } else {
- // If we already selected a target just use that
-
+ if (sa.usesTargeting()) {
return untapPrefTargeting(ai, sa, false);
}
+
+ final List pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
+ return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
}
@Override
@@ -134,11 +132,11 @@ private static boolean untapPrefTargeting(final Player ai, final SpellAbility sa
}
sa.resetTargets();
- final PlayerCollection targetController = new PlayerCollection();
- if (sa.isCurse()) {
- targetController.addAll(ai.getOpponents());
+ final PlayerCollection targetController;
+ if (sa.isCurse() || (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.GainControl)) {
+ targetController = ai.getOpponents();
} else {
- targetController.add(ai);
+ targetController = ai.getYourTeam();
}
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
@@ -431,7 +429,7 @@ public boolean apply(Card card) {
if (!ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) {
// TODO: Currently limited to predicting something that can be paid with any color,
// can ideally be improved to work by color.
- ManaCostBeingPaid reduced = new ManaCostBeingPaid(ab.getPayCosts().getCostMana().getManaCostFor(ab), ab.getPayCosts().getCostMana().getRestriction());
+ ManaCostBeingPaid reduced = new ManaCostBeingPaid(ab.getPayCosts().getCostMana().getManaCostFor(ab));
reduced.decreaseShard(ManaCostShard.GENERIC, untappingCards.size());
if (ComputerUtilMana.canPayManaCost(reduced, ab, ai, false)) {
CardCollection manaLandsTapped = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
@@ -445,7 +443,7 @@ public boolean apply(Card card) {
// pool one additional mana by tapping a land to try to ramp to something
CardCollection manaLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
- Presets.LANDS_PRODUCING_MANA, Presets.UNTAPPED);
+ Presets.LANDS_PRODUCING_MANA, Presets.CAN_TAP);
manaLands = CardLists.getValidCards(manaLands, sa.getParam("ValidTgts"), ai, source, null);
if (manaLands.isEmpty()) {
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
index b3e316f5022..2b173d08b29 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
@@ -14,6 +14,8 @@
import forge.LobbyPlayer;
import forge.ai.AIOption;
import forge.ai.LobbyPlayerAi;
+import forge.card.CardRarity;
+import forge.card.CardRules;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GameObject;
@@ -38,6 +40,7 @@
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZoneBattlefield;
import forge.game.zone.ZoneType;
+import forge.item.PaperCard;
public class GameCopier {
private static final ZoneType[] ZONES = new ZoneType[] {
@@ -95,6 +98,7 @@ public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) {
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
newPlayer.setBlessing(origPlayer.hasBlessing());
newPlayer.setRevolt(origPlayer.hasRevolt());
+ newPlayer.setDescended(origPlayer.getDescended());
newPlayer.setLibrarySearched(origPlayer.getLibrarySearched());
newPlayer.setSpellsCastLastTurn(origPlayer.getSpellsCastLastTurn());
for (int j = 0; j < origPlayer.getSpellsCastThisTurn(); j++) {
@@ -118,7 +122,7 @@ public Game makeCopy(PhaseType advanceToPhase, Player aiPlayer) {
((PlayerZoneBattlefield) p.getZone(ZoneType.Battlefield)).setTriggers(false);
}
- copyGameState(newGame);
+ copyGameState(newGame, aiPlayer);
for (Player p : newGame.getPlayers()) {
List commanders = Lists.newArrayList();
@@ -231,7 +235,7 @@ private RegisteredPlayer clonePlayer(RegisteredPlayer p) {
return clone;
}
- private void copyGameState(Game newGame) {
+ private void copyGameState(Game newGame, Player aiPlayer) {
newGame.setAge(origGame.getAge());
// TODO countersAddedThisTurn
@@ -254,7 +258,7 @@ private void copyGameState(Game newGame) {
for (ZoneType zone : ZONES) {
for (Card card : origGame.getCardsIn(zone)) {
- addCard(newGame, zone, card);
+ addCard(newGame, zone, card, aiPlayer);
}
// TODO CardsAddedThisTurn is now messed up
}
@@ -290,15 +294,24 @@ private void copyGameState(Game newGame) {
}
}
+ private static PaperCard hidden_info_card = new PaperCard(CardRules.fromScript(Lists.newArrayList("Name:hidden", "Types:Artifact", "Oracle:")), "", CardRarity.Common);
+ private static final boolean PRUNE_HIDDEN_INFO = false;
private static final boolean USE_FROM_PAPER_CARD = true;
- private Card createCardCopy(Game newGame, Player newOwner, Card c) {
+ private Card createCardCopy(Game newGame, Player newOwner, Card c, Player aiPlayer) {
if (c.isToken() && !c.isImmutable()) {
Card result = new TokenInfo(c).makeOneToken(newOwner);
CardFactory.copyCopiableCharacteristics(c, result, null, null);
return result;
}
if (USE_FROM_PAPER_CARD && !c.isImmutable() && c.getPaperCard() != null) {
- Card newCard = Card.fromPaperCard(c.getPaperCard(), newOwner);
+ Card newCard;
+ if (PRUNE_HIDDEN_INFO && !c.getView().canBeShownTo(aiPlayer.getView())) {
+ // TODO also check REVEALED_CARDS memory
+ newCard = new Card(newGame.nextCardId(), hidden_info_card, newGame);
+ newCard.setOwner(newOwner);
+ } else {
+ newCard = Card.fromPaperCard(c.getPaperCard(), newOwner);
+ }
newCard.setCommander(c.isCommander());
return newCard;
}
@@ -327,9 +340,9 @@ private Card createCardCopy(Game newGame, Player newOwner, Card c) {
return newCard;
}
- private void addCard(Game newGame, ZoneType zone, Card c) {
+ private void addCard(Game newGame, ZoneType zone, Card c, Player aiPlayer) {
final Player owner = playerMap.get(c.getOwner());
- final Card newCard = createCardCopy(newGame, owner, c);
+ final Card newCard = createCardCopy(newGame, owner, c, aiPlayer);
cardMap.put(c, newCard);
// TODO ExiledWith
@@ -377,6 +390,9 @@ private void addCard(Game newGame, ZoneType zone, Card c) {
if (c.isManifested()) {
newCard.setManifested(true);
}
+ if (c.isCloaked()) {
+ newCard.setCloaked(true);
+ }
}
if (c.isMonstrous()) {
newCard.setMonstrous(true);
@@ -384,6 +400,12 @@ private void addCard(Game newGame, ZoneType zone, Card c) {
if (c.isRenowned()) {
newCard.setRenowned(true);
}
+ if (c.isSolved()) {
+ newCard.setSolved(true);
+ }
+ if (c.isSuspected()) {
+ newCard.setSuspected(true);
+ }
if (c.isPlaneswalker()) {
for (SpellAbility sa : c.getAllSpellAbilities()) {
int active = sa.getActivationsThisTurn();
@@ -419,11 +441,8 @@ private void addCard(Game newGame, ZoneType zone, Card c) {
if (c.hasChosenColor()) {
newCard.setChosenColors(Lists.newArrayList(c.getChosenColors()));
}
- if (!c.getNamedCard().isEmpty()) {
- newCard.setNamedCard(c.getNamedCard());
- }
- if (!c.getNamedCard2().isEmpty()) {
- newCard.setNamedCard2(c.getNamedCard());
+ if (c.hasNamedCard()) {
+ newCard.setNamedCards(Lists.newArrayList(c.getNamedCards()));
}
newCard.setSVars(c.getSVars());
newCard.copyChangedSVarsFrom(c);
diff --git a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java
index ce4965686f5..e7c62d72f9e 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameStateEvaluator.java
@@ -50,7 +50,7 @@ private CombatSimResult simulateUpcomingCombatThisTurn(final Game evalGame, fina
return null;
}
GameCopier copier = new GameCopier(evalGame);
- Game gameCopy = copier.makeCopy();
+ Game gameCopy = copier.makeCopy(null, aiPlayer);
gameCopy.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DAMAGE, new Runnable() {
@Override
public void run() {
@@ -210,7 +210,6 @@ public int evalManaBase(Game game, Player player, AIDeckStatistics statistics) {
// excess mana is valued less than getting enough to use everything
value += max(0, max_total - statistics.maxCost) * 5;
-
return value;
}
diff --git a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
index c48232afec4..16717055564 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
@@ -12,7 +12,6 @@
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ability.ChangeZoneAi;
-import forge.ai.ability.ExploreAi;
import forge.ai.ability.LearnAi;
import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game;
@@ -324,6 +323,13 @@ private boolean atLeastOneConditionMet(SpellAbility saOrSubSa) {
}
private AiPlayDecision canPlayAndPayForSim(final SpellAbility sa) {
+ if (!sa.isLegalAfterStack()) {
+ return AiPlayDecision.CantPlaySa;
+ }
+ if (!sa.checkRestrictions(sa.getHostCard(), player)) {
+ return AiPlayDecision.CantPlaySa;
+ }
+
if (sa instanceof LandAbility) {
return AiPlayDecision.WillPlay;
}
@@ -428,9 +434,7 @@ public Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List= 2) {
if (interceptor != null) {
diff --git a/forge-core/pom.xml b/forge-core/pom.xml
index f0588905e72..6c15ba6626e 100644
--- a/forge-core/pom.xml
+++ b/forge-core/pom.xml
@@ -6,7 +6,7 @@
forge
forge
- 1.6.58-SNAPSHOT
+ 1.6.60-SNAPSHOT
forge-core
diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java
index c0285c51b15..587880425e1 100644
--- a/forge-core/src/main/java/forge/ImageKeys.java
+++ b/forge-core/src/main/java/forge/ImageKeys.java
@@ -23,7 +23,9 @@ public final class ImageKeys {
public static final String HIDDEN_CARD = "hidden";
public static final String MORPH_IMAGE = "morph";
+ public static final String DISGUISED_IMAGE = "disguised";
public static final String MANIFEST_IMAGE = "manifest";
+ public static final String CLOAKED_IMAGE = "cloaked";
public static final String FORETELL_IMAGE = "foretell";
public static final String BACKFACE_POSTFIX = "$alt";
@@ -406,7 +408,7 @@ public static boolean hasImage(PaperCard pc, boolean update) {
CardEdition ed = StaticData.instance().getEditions().get(setFolder);
if (ed != null && !editionAlias.containsKey(setFolder)) {
String alias = ed.getAlias();
- Set aliasSet = new HashSet<>();
+ Set aliasSet = new HashSet<>();
if (alias != null) {
if (!alias.equalsIgnoreCase(setFolder))
aliasSet.add(alias);
diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java
index f111aee7271..7c327565010 100644
--- a/forge-core/src/main/java/forge/StaticData.java
+++ b/forge-core/src/main/java/forge/StaticData.java
@@ -49,6 +49,8 @@ public class StaticData {
private boolean enableSmartCardArtSelection;
private boolean loadNonLegalCards;
+ private boolean sourceImageForClone;
+
// Loaded lazily:
private IStorage boosters;
private IStorage specialBoosters;
@@ -900,6 +902,13 @@ public void setEnableSmartCardArtSelection(boolean isEnabled) {
this.enableSmartCardArtSelection = isEnabled;
}
+ public boolean useSourceImageForClone() {
+ return sourceImageForClone;
+ }
+ public void setSourceImageForClone(final boolean b) {
+ this.sourceImageForClone = b;
+ }
+
public boolean isRebalanced(String name)
{
if (!name.startsWith("A-")) {
diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java
index e0a8bbc3639..896592d415a 100644
--- a/forge-core/src/main/java/forge/card/CardDb.java
+++ b/forge-core/src/main/java/forge/card/CardDb.java
@@ -47,6 +47,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
private final Map uniqueCardsByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final Map rulesByName;
private final Map facesByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
+ private final Map normalizedNames = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private static Map artPrefs = Maps.newHashMap();
private final Map alternateName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
@@ -233,22 +234,34 @@ public CardDb(Map rules, CardEdition.Collection editions0, Li
for (final CardRules rule : rules.values()) {
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
continue;
- final ICardFace main = rule.getMainPart();
- facesByName.put(main.getName(), main);
- if (main.getAltName() != null) {
- alternateName.put(main.getAltName(), main.getName());
- }
- final ICardFace other = rule.getOtherPart();
- if (other != null) {
- facesByName.put(other.getName(), other);
- if (other.getAltName() != null) {
- alternateName.put(other.getAltName(), other.getName());
- }
+ for (ICardFace face : rule.getAllFaces()) {
+ addFaceToDbNames(face);
}
}
setCardArtPreference(cardArtPreference);
}
+ private void addFaceToDbNames(ICardFace face) {
+ if (face == null) {
+ return;
+ }
+ final String name = face.getName();
+ facesByName.put(name, face);
+ final String normalName = StringUtils.stripAccents(name);
+ if (!normalName.equals(name)) {
+ normalizedNames.put(normalName, name);
+ }
+
+ final String altName = face.getAltName();
+ if (altName != null) {
+ alternateName.put(altName, face.getName());
+ final String normalAltName = StringUtils.stripAccents(altName);
+ if (!normalAltName.equals(altName)) {
+ normalizedNames.put(normalAltName, altName);
+ }
+ }
+ }
+
private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) {
int artIdx = IPaperCard.DEFAULT_ART_INDEX;
String key = e.getCode() + "/" + cis.name;
@@ -262,7 +275,8 @@ private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) {
public void loadCard(String cardName, String setCode, CardRules cr) {
// @leriomaggio: This method is called when lazy-loading is set
- System.out.println("[LOG]: (Lazy) Loading Card: " + cardName);
+ // OR if a card is trying to load from an edition its not from
+ //System.out.println("[LOG]: (Lazy) Loading Card: " + cardName);
rulesByName.put(cardName, cr);
boolean reIndexNecessary = false;
CardEdition ed = editions.get(setCode);
@@ -293,6 +307,9 @@ public void initialize(boolean logMissingPerEdition, boolean logMissingSummary,
CardEdition upcomingSet = null;
Date today = new Date();
+ // do this first so they're not considered missing
+ buildRenamedCards();
+
for (CardEdition e : editions.getOrderedEditions()) {
boolean coreOrExpSet = e.getType() == CardEdition.Type.CORE || e.getType() == CardEdition.Type.EXPANSION;
boolean isCoreExpSet = coreOrExpSet || e.getType() == CardEdition.Type.REPRINT;
@@ -353,6 +370,38 @@ public void initialize(boolean logMissingPerEdition, boolean logMissingSummary,
reIndex();
}
+ private void buildRenamedCards() {
+ // for now just check Universes Within
+ for (CardInSet cis : editions.get("SLX").getCards()) {
+ String orgName = alternateName.get(cis.name);
+ if (orgName != null) {
+ // found original (beyond) print
+ CardRules org = getRules(orgName);
+
+ CardFace renamedMain = (CardFace) ((CardFace) org.getMainPart()).clone();
+ renamedMain.setName(renamedMain.getAltName());
+ renamedMain.setAltName(null);
+ // TODO this could mess up some "named ..." cardname literals but there's no printing like that currently
+ renamedMain.setOracleText(renamedMain.getOracleText().replace(orgName, renamedMain.getName()));
+ facesByName.put(renamedMain.getName(), renamedMain);
+ CardFace renamedOther = null;
+ if (org.getOtherPart() != null) {
+ renamedOther = (CardFace) ((CardFace) org.getOtherPart()).clone();
+ orgName = renamedOther.getName();
+ renamedOther.setName(renamedOther.getAltName());
+ renamedOther.setAltName(null);
+ renamedOther.setOracleText(renamedOther.getOracleText().replace(orgName, renamedOther.getName()));
+ facesByName.put(renamedOther.getName(), renamedOther);
+ }
+
+ CardRules within = new CardRules(new ICardFace[] { renamedMain, renamedOther, null, null, null, null, null }, org.getSplitType(), org.getAiHints());
+ // so workshop can edit same script
+ within.setNormalizedName(org.getNormalizedName());
+ rulesByName.put(cis.name, within);
+ }
+ }
+ }
+
public void addCard(PaperCard paperCard) {
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
return;
@@ -919,7 +968,7 @@ public boolean apply(final PaperCard paperCard) {
CardEdition edition = null;
try {
edition = editions.getEditionByCodeOrThrow(paperCard.getEdition());
- if (edition.getType() == Type.PROMO||edition.getType() == Type.REPRINT)
+ if (edition.getType() == Type.PROMO || edition.getType() == Type.REPRINT || edition.getType()==Type.COLLECTOR_EDITION)
return false;
} catch (Exception ex) {
return false;
@@ -930,7 +979,13 @@ public boolean apply(final PaperCard paperCard) {
}
public String getName(final String cardName) {
- if (alternateName.containsKey(cardName)) {
+ return getName(cardName, false);
+ }
+ public String getName(String cardName, boolean engine) {
+ // normalize Names first
+ cardName = normalizedNames.getOrDefault(cardName, cardName);
+ if (alternateName.containsKey(cardName) && engine) {
+ // TODO might want to implement GUI option so it always fetches the Within version
return alternateName.get(cardName);
}
return cardName;
diff --git a/forge-core/src/main/java/forge/card/CardFace.java b/forge-core/src/main/java/forge/card/CardFace.java
index 0d5c5bfc622..d35ceb0670e 100644
--- a/forge-core/src/main/java/forge/card/CardFace.java
+++ b/forge-core/src/main/java/forge/card/CardFace.java
@@ -20,7 +20,7 @@
*
* Do not use reference to class except for card parsing.
Always use reference to interface type outside of package.
*/
-final class CardFace implements ICardFace {
+final class CardFace implements ICardFace, Cloneable {
public enum FaceSelectionMethod { //
USE_ACTIVE_FACE,
@@ -32,7 +32,7 @@ public enum FaceSelectionMethod { //
private final static List emptyList = Collections.unmodifiableList(new ArrayList<>());
private final static Map emptyMap = Collections.unmodifiableMap(new TreeMap<>());
- private final String name;
+ private String name;
private String altName = null;
private CardType type = null;
private ManaCost manaCost = ManaCost.NO_COST;
@@ -86,6 +86,7 @@ public CardFace(String name0) {
throw new RuntimeException("Card name is empty");
}
// Here come setters to allow parser supply values
+ void setName(String name) { this.name = name; }
void setAltName(String name) { this.altName = name; }
void setType(CardType type0) { this.type = type0; }
void setManaCost(ManaCost manaCost0) { this.manaCost = manaCost0; }
@@ -153,4 +154,14 @@ public String toString() {
public int compareTo(ICardFace o) {
return getName().compareTo(o.getName());
}
+
+ /** {@inheritDoc} */
+ @Override
+ public final Object clone() {
+ try {
+ return super.clone();
+ } catch (final Exception ex) {
+ throw new RuntimeException("CardFace : clone() error, " + ex);
+ }
+ }
}
diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java
index a199b31349e..72a88f95450 100644
--- a/forge-core/src/main/java/forge/card/CardRules.java
+++ b/forge-core/src/main/java/forge/card/CardRules.java
@@ -22,6 +22,7 @@
import org.apache.commons.lang3.StringUtils;
import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
import forge.card.mana.IParserManaCost;
import forge.card.mana.ManaCost;
@@ -43,11 +44,8 @@ public final class CardRules implements ICardCharacteristics {
private CardSplitType splitType;
private ICardFace mainPart;
private ICardFace otherPart;
- private ICardFace wSpecialize;
- private ICardFace uSpecialize;
- private ICardFace bSpecialize;
- private ICardFace rSpecialize;
- private ICardFace gSpecialize;
+
+ private Map specializedParts = Maps.newHashMap();
private CardAiHints aiHints;
private ColorSet colorIdentity;
private ColorSet deckbuildingColors;
@@ -55,15 +53,18 @@ public final class CardRules implements ICardCharacteristics {
private String partnerWith;
private boolean custom;
- private CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
+ public CardRules(ICardFace[] faces, CardSplitType altMode, CardAiHints cah) {
splitType = altMode;
mainPart = faces[0];
otherPart = faces[1];
- wSpecialize = faces[2];
- uSpecialize = faces[3];
- bSpecialize = faces[4];
- rSpecialize = faces[5];
- gSpecialize = faces[6];
+
+ if (CardSplitType.Specialize.equals(splitType)) {
+ specializedParts.put(CardStateName.SpecializeW, faces[2]);
+ specializedParts.put(CardStateName.SpecializeU, faces[3]);
+ specializedParts.put(CardStateName.SpecializeB, faces[4]);
+ specializedParts.put(CardStateName.SpecializeR, faces[5]);
+ specializedParts.put(CardStateName.SpecializeG, faces[6]);
+ }
aiHints = cah;
meldWith = "";
@@ -85,11 +86,7 @@ void reinitializeFromRules(CardRules newRules) {
splitType = newRules.splitType;
mainPart = newRules.mainPart;
otherPart = newRules.otherPart;
- wSpecialize = newRules.wSpecialize;
- uSpecialize = newRules.uSpecialize;
- bSpecialize = newRules.bSpecialize;
- rSpecialize = newRules.rSpecialize;
- gSpecialize = newRules.gSpecialize;
+ specializedParts = Maps.newHashMap(newRules.specializedParts);
aiHints = newRules.aiHints;
colorIdentity = newRules.colorIdentity;
meldWith = newRules.meldWith;
@@ -148,20 +145,28 @@ public ICardFace getOtherPart() {
return otherPart;
}
+ public Map getSpecializeParts() {
+ return specializedParts;
+ }
+
+ public Iterable getAllFaces() {
+ return Iterables.concat(Arrays.asList(mainPart, otherPart), specializedParts.values());
+ }
+
public ICardFace getWSpecialize() {
- return wSpecialize;
+ return specializedParts.get(CardStateName.SpecializeW);
}
public ICardFace getUSpecialize() {
- return uSpecialize;
+ return specializedParts.get(CardStateName.SpecializeU);
}
public ICardFace getBSpecialize() {
- return bSpecialize;
+ return specializedParts.get(CardStateName.SpecializeB);
}
public ICardFace getRSpecialize() {
- return rSpecialize;
+ return specializedParts.get(CardStateName.SpecializeR);
}
public ICardFace getGSpecialize() {
- return gSpecialize;
+ return specializedParts.get(CardStateName.SpecializeG);
}
public String getName() {
@@ -275,18 +280,53 @@ public boolean canBeCommander() {
return false;
}
+ public boolean canBePartnerCommanders(CardRules b) {
+ if (!(canBePartnerCommander() && b.canBePartnerCommander())) {
+ return false;
+ }
+ boolean legal = false;
+ if (hasKeyword("Partner") && b.hasKeyword("Partner")) {
+ legal = true; // normal partner commander
+ }
+ if (getName().equals(b.getPartnerWith()) && b.getName().equals(getPartnerWith())) {
+ legal = true; // paired partner commander
+ }
+ if (hasKeyword("Friends forever") && b.hasKeyword("Friends forever")) {
+ legal = true; // Stranger Things Secret Lair gimmick partner commander
+ }
+ if (hasKeyword("Choose a Background") && b.canBeBackground()
+ || b.hasKeyword("Choose a Background") && canBeBackground()) {
+ legal = true; // commander with background
+ }
+ if (isDoctor() && b.hasKeyword("Doctor's companion")
+ || hasKeyword("Doctor's companion") && b.isDoctor()) {
+ legal = true; // Doctor Who partner commander
+ }
+ return legal;
+ }
+
public boolean canBePartnerCommander() {
if (canBeBackground()) {
return true;
}
return canBeCommander() && (hasKeyword("Partner") || !this.partnerWith.isEmpty() ||
- hasKeyword("Friends forever") || hasKeyword("Choose a Background"));
+ hasKeyword("Friends forever") || hasKeyword("Choose a Background") ||
+ hasKeyword("Doctor's companion") || isDoctor());
}
public boolean canBeBackground() {
return mainPart.getType().hasSubtype("Background");
}
+ public boolean isDoctor() {
+ for (String type : mainPart.getType().getSubtypes()) {
+ if (!type.equals("Time Lord") && !type.equals("Doctor")) {
+ return false;
+ }
+ }
+ return true;
+ }
+
public boolean canBeOathbreaker() {
CardType type = mainPart.getType();
return type.isPlaneswalker();
@@ -395,6 +435,11 @@ public final void reset() {
this.curFace = 0;
this.faces[0] = null;
this.faces[1] = null;
+ this.faces[2] = null;
+ this.faces[3] = null;
+ this.faces[4] = null;
+ this.faces[5] = null;
+ this.faces[6] = null;
this.handLife = null;
this.altMode = CardSplitType.None;
diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java
index d41735da5fa..572d9dd6664 100644
--- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java
+++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java
@@ -4,6 +4,8 @@
import java.util.List;
import java.util.Set;
+import org.apache.commons.lang3.StringUtils;
+
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
@@ -361,23 +363,59 @@ public enum CardField {
private final String operand;
private final LeafString.CardField field;
+ protected boolean checkName(String name) {
+ return op(name, this.operand)
+ || op(CardTranslation.getTranslatedName(name), this.operand)
+ || op(StringUtils.stripAccents(name), this.operand);
+ }
+ protected boolean checkOracle(ICardFace face) {
+ if (face == null) {
+ return false;
+ }
+ if (op(face.getOracleText(), operand) || op(CardTranslation.getTranslatedOracle(face.getName()), operand)) {
+ return true;
+ }
+ return false;
+ }
+ protected boolean checkType(ICardFace face) {
+ if (face == null) {
+ return false;
+ }
+ return (op(CardTranslation.getTranslatedType(face.getName(), face.getType().toString()), operand) || op(face.getType().toString(), operand));
+ }
+
@Override
public boolean apply(final CardRules card) {
boolean shouldContain;
switch (this.field) {
case NAME:
- boolean otherName = false;
- if (card.getOtherPart() != null) {
- otherName = (op(CardTranslation.getTranslatedName(card.getOtherPart().getName()), this.operand) || op(card.getOtherPart().getName(), this.operand));
+ for (ICardFace face : card.getAllFaces()) {
+ if (face != null && checkName(face.getName())) {
+ return true;
+ }
}
- return otherName || (op(CardTranslation.getTranslatedName(card.getName()), this.operand) || op(card.getName(), this.operand));
+ return false;
case SUBTYPE:
shouldContain = (this.getOperator() == StringOp.CONTAINS) || (this.getOperator() == StringOp.EQUALS);
return shouldContain == card.getType().hasSubtype(this.operand);
case ORACLE_TEXT:
- return (op(CardTranslation.getTranslatedOracle(card.getName()), operand) || op(card.getOracleText(), this.operand));
+ for (ICardFace face : card.getAllFaces()) {
+ if (checkOracle(face)) {
+ return true;
+ }
+ }
+ return false;
case JOINED_TYPE:
- return (op(CardTranslation.getTranslatedType(card.getName(), card.getType().toString()), operand) || op(card.getType().toString(), operand));
+ if ((op(CardTranslation.getTranslatedType(card.getName(), card.getType().toString()), operand) || op(card.getType().toString(), operand))) {
+ return true;
+ }
+ for (ICardFace face : card.getAllFaces()) {
+ if (checkType(face)) {
+ return true;
+ }
+ }
+
+ return false;
case COST:
final String cost = card.getManaCost().toString();
return op(cost, operand);
diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java
index a5aa5f7e059..0cb5edbbb58 100644
--- a/forge-core/src/main/java/forge/card/CardType.java
+++ b/forge-core/src/main/java/forge/card/CardType.java
@@ -250,7 +250,7 @@ public boolean setCreatureTypes(Collection ctypes) {
@Override
public boolean isEmpty() {
- return coreTypes.isEmpty() && supertypes.isEmpty() && subtypes.isEmpty() && !excludedCreatureSubtypes.isEmpty();
+ return coreTypes.isEmpty() && supertypes.isEmpty() && subtypes.isEmpty() && excludedCreatureSubtypes.isEmpty();
}
@Override
@@ -753,20 +753,18 @@ public static CardType parse(final String typeText, boolean incomplete) {
final CardType result = new CardType(incomplete);
int iTypeStart = 0;
- int iSpace = typeText.indexOf(space);
- boolean hasMoreTypes = typeText.length() > 0;
+ int max = typeText.length();
+ boolean hasMoreTypes = max > 0;
while (hasMoreTypes) {
- final String type = typeText.substring(iTypeStart, iSpace == -1 ? typeText.length() : iSpace);
- hasMoreTypes = iSpace != -1;
final String rest = typeText.substring(iTypeStart);
- if (isMultiwordType(rest)) {
- result.add(rest);
- break;
+ String type = getMultiwordType(rest);
+ if (type == null) {
+ int iSpace = typeText.indexOf(space, iTypeStart);
+ type = typeText.substring(iTypeStart, iSpace == -1 ? max : iSpace);
}
-
- iTypeStart = iSpace + 1;
result.add(type);
- iSpace = typeText.indexOf(space, iSpace + 1);
+ iTypeStart += type.length() + 1;
+ hasMoreTypes = iTypeStart < max;
}
return result;
}
@@ -782,8 +780,13 @@ public static CardType combine(final CardType a, final CardType b) {
return result;
}
- private static boolean isMultiwordType(final String type) {
- return Constant.MultiwordTypes.contains(type);
+ private static String getMultiwordType(final String type) {
+ for (String multi : Constant.MultiwordTypes) {
+ if (type.startsWith(multi)) {
+ return multi;
+ }
+ }
+ return null;
}
public static class Constant {
@@ -985,6 +988,8 @@ public static boolean isAPlanarType(final String cardType) {
* Otherwise, simply return the input.
* @param type a String.
* @return the corresponding type.
+ *
+ * @deprecated
*/
public static final String getSingularType(final String type) {
if (Constant.singularTypes.containsKey(type)) {
diff --git a/forge-core/src/main/java/forge/card/MagicColor.java b/forge-core/src/main/java/forge/card/MagicColor.java
index 71ff66741e6..40f91ea74e1 100644
--- a/forge-core/src/main/java/forge/card/MagicColor.java
+++ b/forge-core/src/main/java/forge/card/MagicColor.java
@@ -2,7 +2,6 @@
import com.google.common.base.Function;
import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
/**
* Holds byte values for each color magic has.
@@ -140,13 +139,9 @@ public static final class Constant {
/** The Basic lands. */
public static final ImmutableList BASIC_LANDS = ImmutableList.of("Plains", "Island", "Swamp", "Mountain", "Forest");
public static final ImmutableList SNOW_LANDS = ImmutableList.of("Snow-Covered Plains", "Snow-Covered Island", "Snow-Covered Swamp", "Snow-Covered Mountain", "Snow-Covered Forest");
- public static final ImmutableMap ANY_COLOR_CONVERSION = new ImmutableMap.Builder()
- .put("ManaConversion", "AnyType->AnyColor")
- .build();
+ public static final String ANY_COLOR_CONVERSION = "AnyType->AnyColor";
- public static final ImmutableMap ANY_TYPE_CONVERSION = new ImmutableMap.Builder()
- .put("ManaConversion", "AnyType->AnyType")
- .build();
+ public static final String ANY_TYPE_CONVERSION = "AnyType->AnyType";
/**
* Private constructor to prevent instantiation.
*/
diff --git a/forge-core/src/main/java/forge/card/mana/ManaAtom.java b/forge-core/src/main/java/forge/card/mana/ManaAtom.java
index 83b298116a9..f4ba3c00e25 100644
--- a/forge-core/src/main/java/forge/card/mana/ManaAtom.java
+++ b/forge-core/src/main/java/forge/card/mana/ManaAtom.java
@@ -64,6 +64,9 @@ public static byte fromConversion(String s) {
case "AnyColor": return ALL_MANA_COLORS;
case "AnyType": return ALL_MANA_TYPES;
}
+ if (s.startsWith("non")) {
+ return (byte) (fromName(s.substring(3)) ^ ALL_MANA_TYPES);
+ }
byte b = 0;
if (s.length() > 2) {
// check for color word
diff --git a/forge-core/src/main/java/forge/deck/CardPool.java b/forge-core/src/main/java/forge/deck/CardPool.java
index 05d7d6398d0..e1029521e9f 100644
--- a/forge-core/src/main/java/forge/deck/CardPool.java
+++ b/forge-core/src/main/java/forge/deck/CardPool.java
@@ -440,7 +440,7 @@ public static List> processCardList(final Iterable
public String toCardList(String separator) {
List> main2sort = Lists.newArrayList(this);
- Collections.sort(main2sort, ItemPoolSorter.BY_NAME_THEN_SET);
+ main2sort.sort(ItemPoolSorter.BY_NAME_THEN_SET);
final CardDb commonDb = StaticData.instance().getCommonCards();
StringBuilder sb = new StringBuilder();
diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java
index d2b39e98c99..514af661290 100644
--- a/forge-core/src/main/java/forge/deck/DeckFormat.java
+++ b/forge-core/src/main/java/forge/deck/DeckFormat.java
@@ -255,18 +255,7 @@ public String getDeckConformanceProblem(Deck deck) {
PaperCard a = commanders.get(0);
PaperCard b = commanders.get(1);
- if (a.getRules().hasKeyword("Partner") && b.getRules().hasKeyword("Partner")) {
- // normal partner commander
- } else if (a.getName().equals(b.getRules().getPartnerWith())
- && b.getName().equals(a.getRules().getPartnerWith())) {
- // paired partner commander
- } else if (a.getRules().hasKeyword("Friends forever") &&
- b.getRules().hasKeyword("Friends forever")) {
- // Stranger Things Secret Lair gimmick partner commander
- } else if (a.getRules().hasKeyword("Choose a Background") && b.getRules().canBeBackground()
- || b.getRules().hasKeyword("Choose a Background") && a.getRules().canBeBackground()) {
- // commander with background
- } else {
+ if (!a.getRules().canBePartnerCommanders(b.getRules())) {
return "has an illegal commander partnership";
}
}
@@ -341,7 +330,7 @@ public String getDeckConformanceProblem(Deck deck) {
final CardPool allCards = deck.getAllCardsInASinglePool(hasCommander());
// should group all cards by name, so that different editions of same card are really counted as the same card
- for (final Entry cp : Aggregates.groupSumBy(allCards, PaperCard.FN_GET_NAME)) {
+ for (final Entry cp : Aggregates.groupSumBy(allCards, pc -> StaticData.instance().getCommonCards().getName(pc.getName(), true))) {
IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey());
if (simpleCard != null && simpleCard.getRules().isCustom() && !StaticData.instance().allowCustomCardsInDecksConformance())
return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck.");
diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java
index 3b80a4ec6ef..9b3a706ff80 100644
--- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java
+++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java
@@ -414,7 +414,7 @@ public static TokenKey fromString(String keyString){
public static final String REGRP_CARD = "cardname";
public static final String REGRP_CARDNO = "count";
- public static final String REX_CARD_NAME = String.format("(\\[)?(?<%s>[a-zA-Z0-9&',\\.:!\\+\\\"\\/\\-\\s]+)(\\])?", REGRP_CARD);
+ public static final String REX_CARD_NAME = String.format("(\\[)?(?<%s>[a-zA-Z0-9à -ÿÀ-Ÿ&',\\.:!\\+\\\"\\/\\-\\s]+)(\\])?", REGRP_CARD);
public static final String REX_SET_CODE = String.format("(?<%s>[a-zA-Z0-9_]{2,7})", REGRP_SET);
public static final String REX_COLL_NUMBER = String.format("(?<%s>\\*?[0-9A-Z]+\\S?[A-Z]*)", REGRP_COLLNR);
public static final String REX_CARD_COUNT = String.format("(?<%s>[\\d]{1,2})(?x)?", REGRP_CARDNO);
diff --git a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java
index 87bb8b48e0a..7499ff9b879 100644
--- a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java
+++ b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java
@@ -130,6 +130,7 @@ protected int addSome(int cnt, List source) {
int res = 0;
while (res < cnt) {
PaperCard cp = source.get(MyRandom.getRandom().nextInt(srcLen));
+ // TODO AltName conversion needed?
int newCount = cardCounts.get(cp.getName()) + 1;
//add card to deck if not already maxed out on card
@@ -414,7 +415,6 @@ protected List getDualLandList(Predicate canPlay) {
}
public List regexLandSearch(String pattern, Iterable landCards) {
- //final List dLands = new ArrayList<>();
Pattern p = Pattern.compile(pattern);
for (PaperCard card:landCards) {
Matcher matcher = p.matcher(card.getRules().getOracleText());
@@ -436,7 +436,6 @@ public List regexLandSearch(String pattern, Iterable landCard
public List regexFetchLandSearch(Iterable landCards) {
final String fetchPattern="Search your library for an* ([^\\s]*) or ([^\\s]*) card";
- //final List dLands = new ArrayList();
Map colorLookup= new HashMap<>();
colorLookup.put("Plains","W");
colorLookup.put("Forest","G");
diff --git a/forge-core/src/main/java/forge/item/PaperCard.java b/forge-core/src/main/java/forge/item/PaperCard.java
index 95f61461b80..082e3efc9a8 100644
--- a/forge-core/src/main/java/forge/item/PaperCard.java
+++ b/forge-core/src/main/java/forge/item/PaperCard.java
@@ -25,6 +25,7 @@
import forge.util.ImageUtil;
import forge.util.Localizer;
import forge.util.TextUtil;
+import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.ObjectInputStream;
@@ -311,7 +312,8 @@ private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IO
@Override
public String getImageKey(boolean altState) {
- String imageKey = ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator
+ String noramlizedName = StringUtils.stripAccents(name);
+ String imageKey = ImageKeys.CARD_PREFIX + noramlizedName + CardDb.NameSetSeparator
+ edition + CardDb.NameSetSeparator + artIndex;
if (altState) {
imageKey += ImageKeys.BACKFACE_POSTFIX;
diff --git a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java
index a0bebbacc47..39091036793 100644
--- a/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java
+++ b/forge-core/src/main/java/forge/item/generation/BoosterGenerator.java
@@ -280,6 +280,7 @@ public static List getBoosterPack(SealedProduct.Template template) {
PrintSheet replaceSheet = getPrintSheet(replaceKey);
result.addAll(replaceSheet.random(1, true));
sheetsUsed.add(replaceSheet);
+ System.out.println("Common was replaced with something from the replace sheet...");
replaceCommon = false;
}
diff --git a/forge-core/src/main/java/forge/util/Aggregates.java b/forge-core/src/main/java/forge/util/Aggregates.java
index 1d98afb8c5f..1d336285c3b 100644
--- a/forge-core/src/main/java/forge/util/Aggregates.java
+++ b/forge-core/src/main/java/forge/util/Aggregates.java
@@ -113,7 +113,7 @@ public static final T random(final Iterable source) {
default: return src.get(MyRandom.getRandom().nextInt(len));
}
}
-
+
T candidate = null;
int lowest = Integer.MAX_VALUE;
for (final T item : source) {
diff --git a/forge-core/src/main/java/forge/util/ImageUtil.java b/forge-core/src/main/java/forge/util/ImageUtil.java
index 24998c875cd..e3bfaf4dacf 100644
--- a/forge-core/src/main/java/forge/util/ImageUtil.java
+++ b/forge-core/src/main/java/forge/util/ImageUtil.java
@@ -6,6 +6,7 @@
import forge.card.CardRules;
import forge.card.CardSplitType;
import forge.item.PaperCard;
+import org.apache.commons.lang3.StringUtils;
public class ImageUtil {
public static float getNearestHQSize(float baseSize, float actualSize) {
@@ -161,6 +162,10 @@ public static String getDownloadUrl(PaperCard cp, String face) {
}
public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop){
+ return getScryfallDownloadUrl(cp, face, setCode, langCode, useArtCrop, false);
+ }
+
+ public static String getScryfallDownloadUrl(PaperCard cp, String face, String setCode, String langCode, boolean useArtCrop, boolean hyphenateAlchemy){
String editionCode;
if ((setCode != null) && (setCode.length() > 0))
editionCode = setCode;
@@ -179,6 +184,12 @@ public static String getScryfallDownloadUrl(PaperCard cp, String face, String se
} else if (cardCollectorNumber.startsWith("OPC2")) {
editionCode = "opc2";
cardCollectorNumber = cardCollectorNumber.substring("OPC2".length());
+ } else if (hyphenateAlchemy) {
+ if (!cardCollectorNumber.startsWith("A")) {
+ return null;
+ }
+
+ cardCollectorNumber = cardCollectorNumber.replace("A", "A-");
}
String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = "";
@@ -190,6 +201,7 @@ public static String getScryfallDownloadUrl(PaperCard cp, String face, String se
}
public static String toMWSFilename(String in) {
+ in = StringUtils.stripAccents(in);
final StringBuilder out = new StringBuilder();
char c;
for (int i = 0; i < in.length(); i++) {
diff --git a/forge-game/pom.xml b/forge-game/pom.xml
index 797184d751a..0efc92a11d5 100644
--- a/forge-game/pom.xml
+++ b/forge-game/pom.xml
@@ -6,7 +6,7 @@
forge
forge
- 1.6.58-SNAPSHOT
+ 1.6.60-SNAPSHOT
forge-game
diff --git a/forge-game/src/main/java/forge/game/CardTraitBase.java b/forge-game/src/main/java/forge/game/CardTraitBase.java
index defab246258..c5070d29e54 100644
--- a/forge-game/src/main/java/forge/game/CardTraitBase.java
+++ b/forge-game/src/main/java/forge/game/CardTraitBase.java
@@ -20,6 +20,7 @@
import forge.game.card.CardState;
import forge.game.card.CardView;
import forge.game.card.IHasCardView;
+import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -63,7 +64,7 @@ public abstract class CardTraitBase extends GameObject implements IHasCardView,
* Keys that should not changed
*/
private static final ImmutableList noChangeKeys = ImmutableList.builder()
- .add("TokenScript", "LegacyImage", "TokenImage", "NewName", "ChooseFromList")
+ .add("TokenScript", "TokenImage", "NewName", "ChooseFromList")
.add("AddAbility").build();
/**
@@ -86,6 +87,10 @@ public String getParam(String key) {
return mapParams.get(key);
}
+ public String getOriginalParam(String key) {
+ return originalMapParams.get(key);
+ }
+
public boolean hasParam(String key) {
return mapParams.containsKey(key);
}
@@ -141,6 +146,9 @@ public void setHostCard(final Card c) {
this.hostCard = c;
}
+ public boolean isKeyword(Keyword kw) {
+ return this.keyword != null && this.keyword.getKeyword() == kw;
+ }
public KeywordInterface getKeyword() {
return this.keyword;
}
@@ -229,10 +237,11 @@ public boolean matchesValid(final Object o, final String[] valids) {
}
public boolean matchesValidParam(String param, final Object o, final Card srcCard) {
+ boolean result = hasParam("Invert" + param);
if (hasParam(param) && !matchesValid(o, getParam(param).split(","), srcCard)) {
- return false;
+ return result;
}
- return true;
+ return !result;
}
public boolean matchesValidParam(String param, final Object o) {
@@ -263,7 +272,7 @@ protected boolean meetsCommonRequirements(Map params) {
final Game game = hostController.getGame();
// intervening if check, make sure to use right controller
- if (game.getStack().isResolving(getHostCard())) {
+ if (!game.getStack().isEmpty() && game.getStack().isResolving(getHostCard())) {
SpellAbility sa = game.getStack().peek().getSpellAbility();
if (sa.isTrigger()) {
hostController = sa.getActivatingPlayer();
@@ -531,31 +540,6 @@ else if ("None".equalsIgnoreCase(params.get("Revolt"))) {
return true;
}
- public void changeText() {
- // copy changed text words into card trait there
- this.changedTextColors = getHostCard().getChangedTextColorWords();
- this.changedTextTypes = getHostCard().getChangedTextTypeWords();
-
- for (final String key : this.mapParams.keySet()) {
- final String value = this.originalMapParams.get(key), newValue;
- if (noChangeKeys.contains(key)) {
- continue;
- } else if (descriptiveKeys.contains(key)) {
- // change descriptions differently
- newValue = AbilityUtils.applyDescriptionTextChangeEffects(value, this);
- } else if (this.getHostCard().hasSVar(value)) {
- // don't change literal SVar names!
- newValue = null;
- } else {
- newValue = AbilityUtils.applyAbilityTextChangeEffects(value, this);
- }
-
- if (newValue != null) {
- this.mapParams.put(key, newValue);
- }
- }
- }
-
@Override
public CardView getCardView() {
return CardView.get(hostCard);
@@ -694,9 +678,37 @@ public void changeTextIntrinsic(Map colorMap, Map
this.originalMapParams = Maps.newHashMap(this.mapParams);
}
+ public void changeText() {
+ // copy changed text words into card trait there
+ this.changedTextColors = getHostCard().getChangedTextColorWords();
+ this.changedTextTypes = getHostCard().getChangedTextTypeWords();
+
+ for (final String key : this.mapParams.keySet()) {
+ final String value = this.originalMapParams.get(key), newValue;
+ if (noChangeKeys.contains(key)) {
+ continue;
+ } else if (descriptiveKeys.contains(key)) {
+ // change descriptions differently
+ newValue = AbilityUtils.applyDescriptionTextChangeEffects(value, this);
+ } else if (this.getHostCard().hasSVar(value)) {
+ // don't change literal SVar names!
+ newValue = null;
+ } else {
+ newValue = AbilityUtils.applyAbilityTextChangeEffects(value, this);
+ }
+
+ if (newValue != null) {
+ this.mapParams.put(key, newValue);
+ }
+ }
+ }
+
protected void copyHelper(CardTraitBase copy, Card host) {
+ copyHelper(copy, host, false);
+ }
+ protected void copyHelper(CardTraitBase copy, Card host, boolean keepTextChanges) {
copy.originalMapParams = Maps.newHashMap(originalMapParams);
- copy.mapParams = Maps.newHashMap(originalMapParams);
+ copy.mapParams = Maps.newHashMap(keepTextChanges ? mapParams : originalMapParams);
copy.setSVars(sVars);
copy.setCardState(cardState);
// dont use setHostCard to not trigger the not copied parts yet
diff --git a/forge-game/src/main/java/forge/game/CardTraitPredicates.java b/forge-game/src/main/java/forge/game/CardTraitPredicates.java
index c89e0556648..d8d1c9256b7 100644
--- a/forge-game/src/main/java/forge/game/CardTraitPredicates.java
+++ b/forge-game/src/main/java/forge/game/CardTraitPredicates.java
@@ -3,6 +3,7 @@
import com.google.common.base.Predicate;
import forge.game.card.Card;
+import forge.game.keyword.Keyword;
public class CardTraitPredicates {
@@ -15,6 +16,15 @@ public boolean apply(final CardTraitBase sa) {
};
}
+ public static final Predicate isKeyword(final Keyword kw) {
+ return new Predicate() {
+ @Override
+ public boolean apply(final CardTraitBase sa) {
+ return sa.isKeyword(kw);
+ }
+ };
+ }
+
public static final Predicate hasParam(final String name) {
return new Predicate() {
@Override
diff --git a/forge-game/src/main/java/forge/game/ForgeScript.java b/forge-game/src/main/java/forge/game/ForgeScript.java
index ee625766e9a..820dff98e3c 100644
--- a/forge-game/src/main/java/forge/game/ForgeScript.java
+++ b/forge-game/src/main/java/forge/game/ForgeScript.java
@@ -23,6 +23,7 @@
import forge.util.Expressions;
import org.apache.commons.lang3.StringUtils;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
@@ -57,6 +58,19 @@ public static boolean cardStateHasProperty(CardState cardState, String property,
if (property.endsWith("Source") && isColorlessSource)
return false;
return property.startsWith("non") != colors.isMulticolor();
+ } else if (property.contains("EnemyColor")) {
+ if (property.endsWith("Source") && isColorlessSource)
+ return false;
+ if (colors.countColors() != 2) {
+ return false;
+ }
+ // i want only enemy colors
+ for (final byte pair : Arrays.copyOfRange(MagicColor.COLORPAIR, 5, 10)) {
+ if (colors.hasExactlyColor(pair)) {
+ return true;
+ }
+ }
+ return false;
} else if (property.contains("AllColors")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
@@ -193,16 +207,22 @@ public static boolean spellAbilityHasProperty(SpellAbility sa, String property,
} else if (property.equals("hasTapCost")) {
Cost cost = sa.getPayCosts();
return cost != null && cost.hasTapCost();
+ } else if (property.equals("Bargain")) {
+ return sa.isBargain();
} else if (property.equals("Backup")) {
return sa.isBackup();
} else if (property.equals("Blitz")) {
return sa.isBlitz();
} else if (property.equals("Buyback")) {
return sa.isBuyBackAbility();
+ } else if (property.equals("Craft")) {
+ return sa.isCraft();
} else if (property.equals("Cycling")) {
return sa.isCycling();
} else if (property.equals("Dash")) {
return sa.isDash();
+ } else if (property.equals("Disturb")) {
+ return sa.isDisturb();
} else if (property.equals("Flashback")) {
return sa.isFlashBackAbility();
} else if (property.equals("Jumpstart")) {
@@ -217,6 +237,10 @@ public static boolean spellAbilityHasProperty(SpellAbility sa, String property,
return sa.isAftermath();
} else if (property.equals("MorphUp")) {
return sa.isMorphUp();
+ } else if (property.equals("ManifestUp")) {
+ return sa.isManifestUp();
+ } else if (property.equals("isCastFaceDown")) {
+ return sa.isCastFaceDown();
} else if (property.equals("Modular")) {
return sa.hasParam("Modular");
} else if (property.equals("Equip")) {
@@ -246,6 +270,8 @@ public static boolean spellAbilityHasProperty(SpellAbility sa, String property,
if (sa.getChapter() == sa.getHostCard().getCounters(CounterEnumType.LORE)) {
return false;
}
+ } else if (property.equals("CumulativeUpkeep")) {
+ return sa.isCumulativeUpkeep();
} else if (property.equals("LastChapter")) {
return sa.isLastChapter();
} else if (property.startsWith("ManaSpent")) {
@@ -323,7 +349,7 @@ public static boolean spellAbilityHasProperty(SpellAbility sa, String property,
} else {
y = sa.getPayCosts().getTotalMana().getCMC();
}
- int x = AbilityUtils.calculateAmount(spellAbility.getHostCard(), property.substring(5), spellAbility);
+ int x = AbilityUtils.calculateAmount(source, property.substring(5), spellAbility);
if (!Expressions.compare(y, property, x)) {
return false;
}
diff --git a/forge-game/src/main/java/forge/game/Game.java b/forge-game/src/main/java/forge/game/Game.java
index 179d19a3e3d..7574e7716e8 100644
--- a/forge-game/src/main/java/forge/game/Game.java
+++ b/forge-game/src/main/java/forge/game/Game.java
@@ -97,6 +97,9 @@ public class Game {
private Table>> countersAddedThisTurn = HashBasedTable.create();
private Multimap> countersRemovedThisTurn = ArrayListMultimap.create();
+ private List leftBattlefieldThisTurn = Lists.newArrayList();
+ private List leftGraveyardThisTurn = Lists.newArrayList();
+
private FCollection globalDamageHistory = new FCollection<>();
private IdentityHashMap, Pair> damageThisTurnLKI = new IdentityHashMap<>();
@@ -208,7 +211,8 @@ public void updateLastStateForCard(Card c) {
: null;
if (lookup != null) {
- lookup.remove(c);
+ lastStateBattlefield.remove(c);
+ lastStateGraveyard.remove(c);
lookup.add(CardUtil.getLKICopy(c));
}
}
@@ -239,6 +243,27 @@ public final void clearChangeZoneLKIInfo() {
changeZoneLKIInfo.clear();
}
+ public void addLeftBattlefieldThisTurn(Card lki) {
+ leftBattlefieldThisTurn.add(lki);
+ }
+ public void addLeftGraveyardThisTurn(Card lki) {
+ leftGraveyardThisTurn.add(lki);
+ }
+
+ public List getLeftBattlefieldThisTurn() {
+ return leftBattlefieldThisTurn;
+ }
+ public List getLeftGraveyardThisTurn() {
+ return leftGraveyardThisTurn;
+ }
+
+ public void clearLeftBattlefieldThisTurn() {
+ leftBattlefieldThisTurn.clear();
+ }
+ public void clearLeftGraveyardThisTurn() {
+ leftGraveyardThisTurn.clear();
+ }
+
public Game(Iterable players0, GameRules rules0, Match match0) {
this(players0, rules0, match0, null, -1);
}
@@ -646,8 +671,11 @@ public Card findById(int id) {
return visit.getFound();
}
- // Allows visiting cards in game without allocating a temporary list.
public void forEachCardInGame(Visitor visitor) {
+ forEachCardInGame(visitor, false);
+ }
+ // Allows visiting cards in game without allocating a temporary list.
+ public void forEachCardInGame(Visitor visitor, boolean withSideboard) {
for (final Player player : getPlayers()) {
if (!visitor.visitAll(player.getZone(ZoneType.Graveyard).getCards())) {
return;
@@ -667,6 +695,9 @@ public void forEachCardInGame(Visitor visitor) {
if (!visitor.visitAll(player.getZone(ZoneType.Command).getCards())) {
return;
}
+ if (withSideboard && !visitor.visitAll(player.getZone(ZoneType.Sideboard).getCards())) {
+ return;
+ }
if (!visitor.visitAll(player.getInboundTokens())) {
return;
}
@@ -760,8 +791,9 @@ public void onPlayerLost(Player p) {
// Rule 800.4 Losing a Multiplayer game
CardCollectionView cards = this.getCardsInGame();
boolean planarControllerLost = false;
+ boolean planarOwnerLost = false;
boolean isMultiplayer = getPlayers().size() > 2;
- CardZoneTable triggerList = new CardZoneTable();
+ CardZoneTable triggerList = new CardZoneTable(getLastStateBattlefield(), getLastStateGraveyard());
// 702.142f & 707.9
// If a player leaves the game, all face-down cards that player owns must be revealed to all players.
@@ -782,8 +814,13 @@ public void onPlayerLost(Player p) {
}
for (Card c : cards) {
- if (c.getController().equals(p) && (c.isPlane() || c.isPhenomenon())) {
- planarControllerLost = true;
+ if (c.isPlane() || c.isPhenomenon()) {
+ if (c.getController().equals(p)) {
+ planarControllerLost = true;
+ }
+ if (c.getOwner().equals(p)) {
+ planarOwnerLost = true;
+ }
}
if (isMultiplayer) {
@@ -813,8 +850,8 @@ public void onPlayerLost(Player p) {
SpellAbilityStackInstance si = getStack().getInstanceMatchingSpellAbilityID(c.getCastSA());
si.setActivatingPlayer(c.getController());
}
- if (c.getController().equals(p)) {
- getAction().exile(c, null);
+ if (c.getController().equals(p) && !(c.isPlane() || c.isPhenomenon())) {
+ getAction().exile(c, null, null);
triggerList.put(ZoneType.Battlefield, c.getZone().getZoneType(), c);
}
}
@@ -826,13 +863,35 @@ public void onPlayerLost(Player p) {
triggerList.triggerChangesZoneAll(this, null);
// 901.6: If the current planar controller would leave the game, instead the next player
- // in turn order that wouldn’t leave the game becomes the planar controller, then the old
+ // in turn order that wouldn't leave the game becomes the planar controller, then the old
// planar controller leaves
+ if (planarControllerLost) {
+ for (Card c : getActivePlanes()) {
+ if (c != null && !c.getOwner().equals(p)) {
+ c.setController(getNextPlayerAfter(p), 0);
+ getAction().controllerChangeZoneCorrection(c);
+ }
+ }
+ }
// 901.10: When a player leaves the game, all objects owned by that player except abilities
// from phenomena leave the game. (See rule 800.4a.) If that includes a face-up plane card
// or phenomenon card, the planar controller turns the top card of his or her planar deck face up.the game.
- if (planarControllerLost) {
- getNextPlayerAfter(p).initPlane();
+ if (planarOwnerLost) {
+ Player planarController = getPhaseHandler().getPlayerTurn();
+ if (planarController.equals(p)) {
+ planarController = getNextPlayerAfter(p);
+ }
+ final Map runParams = AbilityKey.newMap();
+ CardCollection planesLeavingGame = new CardCollection();
+ for (Card c : getActivePlanes()) {
+ if (c != null && c.getOwner().equals(p)) {
+ planesLeavingGame.add(c);
+ planarController.removeCurrentPlane(c);
+ }
+ }
+ runParams.put(AbilityKey.Cards, planesLeavingGame);
+ getTriggerHandler().runTrigger(TriggerType.PlaneswalkedFrom, runParams, false);
+ planarController.planeswalk(null);
}
if (p.isMonarch()) {
@@ -1076,6 +1135,8 @@ public Player getControlVote() {
}
public void onCleanupPhase() {
+ clearLeftBattlefieldThisTurn();
+ clearLeftGraveyardThisTurn();
clearCounterAddedThisTurn();
clearCounterRemovedThisTurn();
clearGlobalDamageHistory();
diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java
index f507e7810f2..2fed780b748 100644
--- a/forge-game/src/main/java/forge/game/GameAction.java
+++ b/forge-game/src/main/java/forge/game/GameAction.java
@@ -19,15 +19,13 @@
import com.google.common.base.Predicate;
import com.google.common.collect.*;
+
import forge.GameCommand;
import forge.StaticData;
import forge.card.CardStateName;
import forge.card.MagicColor;
import forge.deck.DeckSection;
-import forge.game.ability.AbilityFactory;
-import forge.game.ability.AbilityKey;
-import forge.game.ability.AbilityUtils;
-import forge.game.ability.ApiType;
+import forge.game.ability.*;
import forge.game.card.*;
import forge.game.event.*;
import forge.game.keyword.Keyword;
@@ -57,6 +55,7 @@
import forge.util.*;
import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView;
+
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*;
@@ -77,11 +76,8 @@ public GameAction(Game game0) {
}
public final void resetActivationsPerTurn() {
- // Reset Activations per Turn
for (final Card card : game.getCardsInGame()) {
card.resetActivationsPerTurn();
- // need to reset this in exile
- card.resetForetoldThisTurn();
}
}
@@ -115,11 +111,12 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
boolean toBattlefield = zoneTo.is(ZoneType.Battlefield) || zoneTo.is(ZoneType.Merged);
boolean fromBattlefield = zoneFrom != null && zoneFrom.is(ZoneType.Battlefield);
+ boolean fromGraveyard = zoneFrom != null && zoneFrom.is(ZoneType.Graveyard);
boolean wasFacedown = c.isFaceDown();
// Rule 111.8: A token that has left the battlefield can't move to another zone
if (!c.isSpell() && c.isToken() && !fromBattlefield && zoneFrom != null && !zoneFrom.is(ZoneType.Stack)
- && (cause == null || !(cause instanceof SpellPermanent) || !cause.hasSVar("IsCastFromPlayEffect"))) {
+ && (cause == null || !(cause instanceof SpellPermanent) || !cause.isCastFromPlayEffect())) {
return c;
}
@@ -186,30 +183,20 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
lastKnownInfo = (Card) cause.getReplacingObject(AbilityKey.CardLKI);
}
}
- CardCollectionView lastBattlefield = null;
- if (params != null) {
- lastBattlefield = (CardCollectionView) params.get(AbilityKey.LastStateBattlefield);
- }
- if (lastBattlefield == null && cause != null) {
- lastBattlefield = cause.getLastStateBattlefield();
- }
- if (lastBattlefield == null) {
- lastBattlefield = game.getLastStateBattlefield();
- }
+ CardCollectionView lastBattlefield = getLastState(AbilityKey.LastStateBattlefield, cause, params);
+ CardCollectionView lastGraveyard = getLastState(AbilityKey.LastStateGraveyard, cause, params);
if (c.isSplitCard()) {
boolean resetToOriginal = false;
- if (c.isManifested()) {
+ if (c.isManifested() || c.isCloaked()) {
if (fromBattlefield) {
// Make sure the card returns from the battlefield as the original card with two halves
resetToOriginal = true;
}
- } else {
- if (!zoneTo.is(ZoneType.Stack)) {
- // For regular splits, recreate the original state unless the card is going to stack as one half
- resetToOriginal = true;
- }
+ } else if (!zoneTo.is(ZoneType.Stack)) {
+ // For regular splits, recreate the original state unless the card is going to stack as one half
+ resetToOriginal = true;
}
if (resetToOriginal) {
@@ -242,6 +229,12 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
lastKnownInfo = lastBattlefield.get(idx);
}
}
+ if (fromGraveyard) {
+ int idx = lastGraveyard.indexOf(c);
+ if (idx != -1) {
+ lastKnownInfo = lastGraveyard.get(idx);
+ }
+ }
if (lastKnownInfo == null) {
lastKnownInfo = CardUtil.getLKICopy(c);
@@ -303,6 +296,16 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
}
}
+ // perpetual stuff
+ if (c.hasIntensity()) {
+ copied.setIntensity(c.getIntensity(false));
+ }
+ if (c.isSpecialized()) {
+ copied.setState(c.getCurrentStateName(), false);
+ }
+ if (c.hasPerpetual()) {
+ copied.setPerpetual(c);
+ }
// ensure that any leftover keyword/type changes are cleared in the state view
copied.updateStateForView();
@@ -348,7 +351,7 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
ReplacementResult repres = game.getReplacementHandler().run(ReplacementType.Moved, repParams);
if (repres != ReplacementResult.NotReplaced && repres != ReplacementResult.Updated) {
// reset failed manifested Cards back to original
- if (c.isManifested() && !c.isInPlay()) {
+ if ((c.isManifested() || c.isCloaked()) && !c.isInPlay()) {
c.forceTurnFaceUp();
}
@@ -372,7 +375,6 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
copied.clearDevoured();
copied.clearDelved();
- copied.clearConvoked();
copied.clearExploited();
} else if (toBattlefield && !c.isInPlay()) {
// was replaced with another Zone Change
@@ -419,7 +421,7 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
CardCollection cards = new CardCollection(c.getMergedCards());
// replace top card with copied card for correct name for human to choose.
cards.set(cards.indexOf(c), copied);
- // 723.3b
+ // 725.3b
if (cause != null && zoneTo.getZoneType() == ZoneType.Exile) {
cards = (CardCollection) cause.getHostCard().getController().getController().orderMoveToZoneList(cards, zoneTo.getZoneType(), cause);
} else {
@@ -493,11 +495,10 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
// 607.2q linked ability can find cards exiled as cost while it was a spell
copied.addExiledCards(c.getExiledCards());
}
- }
- // if an adventureCard is put from Stack somewhere else, need to reset to Original State
- if (copied.isAdventureCard() && ((zoneFrom != null && zoneFrom.is(ZoneType.Stack)) || !zoneTo.is(ZoneType.Stack))) {
- copied.setState(CardStateName.Original, false);
+ if (cause != null && cause.isCraft() && toBattlefield) { // retain cards crafted while ETB transformed
+ copied.retainPaidList(cause, "ExiledCards");
+ }
}
GameEntityCounterTable table = new GameEntityCounterTable();
@@ -534,13 +535,19 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
if (repres != ReplacementResult.NotReplaced) continue;
}
if (card == c) {
+ storeChangesZoneAll(copied, zoneFrom, zoneTo, params);
zoneTo.add(copied, position, toBattlefield ? null : lastKnownInfo); // the modified state of the card is also reported here (e.g. for Morbid + Awaken)
} else {
+ storeChangesZoneAll(card, zoneFrom, zoneTo, params);
zoneTo.add(card, position, CardUtil.getLKICopy(card));
+ card.setState(CardStateName.Original, false);
+ card.setBackSide(false);
+ card.updateStateForView();
}
card.setZone(zoneTo);
}
} else {
+ storeChangesZoneAll(copied, zoneFrom, zoneTo, params);
// "enter the battlefield as a copy" - apply code here
// but how to query for input here and continue later while the callers assume synchronous result?
zoneTo.add(copied, position, toBattlefield ? null : lastKnownInfo); // the modified state of the card is also reported here (e.g. for Morbid + Awaken)
@@ -550,8 +557,12 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
if (fromBattlefield) {
// order here is important so it doesn't unattach cards that might have returned from UntilHostLeavesPlay
unattachCardLeavingBattlefield(copied);
+ game.addLeftBattlefieldThisTurn(lastKnownInfo);
c.runLeavesPlayCommands();
}
+ if (fromGraveyard) {
+ game.addLeftGraveyardThisTurn(lastKnownInfo);
+ }
// do ETB counters after zone add
if (!suppress && toBattlefield && !copied.getEtbCounters().isEmpty()) {
@@ -560,15 +571,6 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
copied.clearEtbCounters();
}
- // intensity is perpetual
- if (c.hasIntensity()) {
- copied.setIntensity(c.getIntensity(false));
- }
- // specialize is perpetual
- if (c.isSpecialized()) {
- copied.setState(c.getCurrentStateName(), false);
- }
-
// update state for view
copied.updateStateForView();
@@ -581,9 +583,17 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
}
}
+ if (!table.isEmpty()) {
+ // we don't want always trigger before counters are placed
+ game.getTriggerHandler().suppressMode(TriggerType.Always);
+ // Need to apply any static effects to produce correct triggers
+ checkStaticAbilities();
+ }
+
table.replaceCounterEffect(game, null, true, true, params);
- // Need to apply any static effects to produce correct triggers
+ // update static abilities after etb counters have been placed
+ game.getTriggerHandler().clearSuppression(TriggerType.Always);
checkStaticAbilities();
// 400.7g try adding keyword back into card if it doesn't already have it
@@ -647,7 +657,6 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
if (!c.isRealToken() && !toBattlefield) {
copied.clearDevoured();
copied.clearDelved();
- copied.clearConvoked();
copied.clearExploited();
}
@@ -714,6 +723,12 @@ private Card changeZone(final Zone zoneFrom, Zone zoneTo, final Card c, Integer
return copied;
}
+ private void storeChangesZoneAll(Card c, Zone zoneFrom, Zone zoneTo, Map params) {
+ if (params != null && params.containsKey(AbilityKey.InternalTriggerTable)) {
+ ((CardZoneTable) params.get(AbilityKey.InternalTriggerTable)).put(zoneFrom != null ? zoneFrom.getZoneType() : null, zoneTo.getZoneType(), c);
+ }
+ }
+
private static void unattachCardLeavingBattlefield(final Card copied) {
// remove attachments from creatures
copied.unAttachAllCards();
@@ -899,24 +914,14 @@ public final Card moveToVariantDeck(Card c, ZoneType zone, int deckPosition, Spe
return changeZone(game.getZoneOf(c), deck, c, deckPosition, cause, params);
}
- public final Card exile(final Card c, SpellAbility cause) {
- if (c == null) {
- return null;
- }
- return exile(new CardCollection(c), cause).get(0);
- }
- public final CardCollection exile(final CardCollection cards, SpellAbility cause) {
- CardZoneTable table = new CardZoneTable();
+ public final CardCollection exile(final CardCollection cards, SpellAbility cause, Map params) {
+ CardZoneTable table = new CardZoneTable(getLastState(AbilityKey.LastStateBattlefield, cause, params), getLastState(AbilityKey.LastStateGraveyard, cause, params));
CardCollection result = new CardCollection();
for (Card card : cards) {
- if (cause != null) {
- table.put(card.getZone().getZoneType(), ZoneType.Exile, card);
- }
- result.add(exile(card, cause, null));
- }
- if (cause != null) {
- table.triggerChangesZoneAll(game, cause);
+ table.put(card.getZone().getZoneType(), ZoneType.Exile, card);
+ result.add(exile(card, cause, params));
}
+ table.triggerChangesZoneAll(game, cause);
return result;
}
public final Card exile(final Card c, SpellAbility cause, Map params) {
@@ -933,6 +938,8 @@ public final Card exile(final Card c, SpellAbility cause, Map runParams = AbilityKey.mapFromCard(c);
+ runParams.put(AbilityKey.CardLKI, lki);
+ runParams.put(AbilityKey.Origin, c.getZone().getZoneType().name());
+ game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, false);
}
- final Map runParams = AbilityKey.mapFromCard(c);
- runParams.put(AbilityKey.CardLKI, lki);
- runParams.put(AbilityKey.Origin, c.getZone().getZoneType().name());
- game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, false);
}
}
@@ -1029,7 +1035,6 @@ public final void controllerChangeZoneCorrection(final Card c) {
game.getTriggerHandler().runTrigger(TriggerType.ChangesController, runParams, false);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
-
}
// Temporarily disable (if mode = true) actively checking static abilities.
@@ -1078,9 +1083,6 @@ public final void checkStaticAbilities(final boolean runEvents, final Set
game.getStaticEffects().clearStaticEffects(affectedCards);
for (final Player p : game.getPlayers()) {
- if (!game.getStack().isFrozen()) {
- p.getManaPool().restoreColorReplacements();
- }
p.clearStaticAbilities();
}
@@ -1103,7 +1105,7 @@ public boolean visit(final Card c) {
}
return true;
}
- });
+ }, true);
final Comparator comp = new Comparator() {
@Override
@@ -1170,18 +1172,6 @@ public int compare(final StaticAbility a, final StaticAbility b) {
}
c.getStaticCommandList().removeAll(toRemove);
}
- // Exclude cards in hidden zones from update
- /*
- * Refactoring this code to affectedCards.removeIf((Card c) -> c.isInZone(ZoneType.Library));
- * causes Android build not to compile
- * */
- Iterator it = affectedCards.iterator();
- while (it.hasNext()) {
- Card c = it.next();
- if (c.isInZone(ZoneType.Library)) {
- it.remove();
- }
- }
// preList means that this is run by a pre Check with LKI objects
// in that case Always trigger should not Run
@@ -1266,7 +1256,8 @@ public boolean checkStateEffects(final boolean runEvents, final Set affect
checkStaticAbilities(false, affectedCards, CardCollection.EMPTY);
boolean checkAgain = false;
- CardZoneTable table = new CardZoneTable();
+ CardZoneTable table = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
+ mapParams.put(AbilityKey.InternalTriggerTable, table);
for (final Player p : game.getPlayers()) {
for (final ZoneType zt : ZoneType.values()) {
@@ -1298,6 +1289,10 @@ public boolean checkStateEffects(final boolean runEvents, final Set affect
if (c.getNetToughness() <= 0) {
noRegCreats.add(c);
checkAgainCard = true;
+ } else if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
+ //702.12b. A permanent with indestructible can't be destroyed.
+ // Such permanents aren't destroyed by lethal damage, and they
+ // ignore the state-based action that checks for lethal damage
} else if (c.hasKeyword("CARDNAME can't be destroyed by lethal damage unless lethal damage dealt by a single source is marked on it.")) {
if (c.getLethal() <= c.getMaxDamageFromSource() || c.hasBeenDealtDeathtouchDamage()) {
if (desCreats == null) {
@@ -1322,6 +1317,7 @@ else if (c.hasBeenDealtDeathtouchDamage() || (c.getDamage() > 0 && c.getLethal()
checkAgainCard |= stateBasedAction_Saga(c, sacrificeList);
checkAgainCard |= stateBasedAction_Battle(c, noRegCreats);
+ checkAgainCard |= stateBasedAction_Role(c, unAttachList);
checkAgainCard |= stateBasedAction704_attach(c, unAttachList); // Attachment
checkAgainCard |= stateBasedAction704_5r(c); // annihilate +1/+1 counters with -1/-1 ones
@@ -1407,7 +1403,7 @@ else if (c.hasBeenDealtDeathtouchDamage() || (c.getDamage() > 0 && c.getLethal()
}
for (Card c : noRegCreats) {
c.updateWasDestroyed(true);
- sacrificeDestroy(c, null, table, mapParams);
+ sacrificeDestroy(c, null, mapParams);
}
if (desCreats != null) {
@@ -1419,7 +1415,7 @@ else if (c.hasBeenDealtDeathtouchDamage() || (c.getDamage() > 0 && c.getLethal()
orderedDesCreats = true;
}
for (Card c : desCreats) {
- destroy(c, null, true, table, mapParams);
+ destroy(c, null, true, mapParams);
}
}
@@ -1429,7 +1425,7 @@ else if (c.hasBeenDealtDeathtouchDamage() || (c.getDamage() > 0 && c.getLethal()
}
for (Card c : sacrificeList) {
c.updateWasDestroyed(true);
- sacrifice(c, null, true, table, mapParams);
+ sacrifice(c, null, true, mapParams);
}
setHoldCheckingStaticAbilities(false);
@@ -1489,7 +1485,7 @@ else if (c.hasBeenDealtDeathtouchDamage() || (c.getDamage() > 0 && c.getLethal()
private boolean stateBasedAction_Saga(Card c, CardCollection sacrificeList) {
boolean checkAgain = false;
- if (!c.getType().hasSubtype("Saga")) {
+ if (!c.isSaga()) {
return false;
}
if (!c.canBeSacrificedBy(null, true)) {
@@ -1514,7 +1510,7 @@ private boolean stateBasedAction_Battle(Card c, CardCollection removeList) {
if (c.getCounters(CounterEnumType.DEFENSE) > 0) {
return false;
}
- // 704.5v If a battle has defense 0 and it isn’t the source of an ability that has triggered but not yet left the stack,
+ // 704.5v If a battle has defense 0 and it isn't the source of an ability that has triggered but not yet left the stack,
// it’s put into its owner’s graveyard.
if (!game.getStack().hasSourceOnStack(c, SpellAbilityPredicates.isTrigger())) {
removeList.add(c);
@@ -1522,6 +1518,28 @@ private boolean stateBasedAction_Battle(Card c, CardCollection removeList) {
}
return checkAgain;
}
+ private boolean stateBasedAction_Role(Card c, CardCollection removeList) {
+ if (!c.hasCardAttachments()) {
+ return false;
+ }
+ boolean checkAgain = false;
+ CardCollection roles = CardLists.filter(c.getAttachedCards(), CardPredicates.isType("Role"));
+ if (roles.isEmpty()) {
+ return false;
+ }
+
+ for (Player p : game.getPlayers()) {
+ CardCollection rolesByPlayer = CardLists.filterControlledBy(roles, p);
+ if (rolesByPlayer.size() <= 1) {
+ continue;
+ }
+ // sort by game timestamp
+ rolesByPlayer.sort(CardPredicates.compareByTimestamp());
+ removeList.addAll(rolesByPlayer.subList(0, rolesByPlayer.size() - 1));
+ checkAgain = true;
+ }
+ return checkAgain;
+ }
private void stateBasedAction_Dungeon(Card c) {
if (!c.getType().isDungeon() || !c.isInLastRoom()) {
@@ -1537,7 +1555,8 @@ private boolean stateBasedAction704_attach(Card c, CardCollection unAttachList)
if (c.isAttachedToEntity()) {
final GameEntity ge = c.getEntityAttachedTo();
- if (!ge.canBeAttached(c, null, true)) {
+ // Rule 704.5q - Creature attached to an object or player, becomes unattached
+ if (c.isCreature() || c.isBattle() || !ge.canBeAttached(c, null, true)) {
unAttachList.add(c);
checkAgain = true;
}
@@ -1551,10 +1570,7 @@ private boolean stateBasedAction704_attach(Card c, CardCollection unAttachList)
}
}
}
- if ((c.isCreature() || c.isBattle()) && c.isAttachedToEntity()) { // Rule 704.5q - Creature attached to an object or player, becomes unattached
- unAttachList.add(c);
- checkAgain = true;
- }
+
return checkAgain;
}
@@ -1847,7 +1863,7 @@ private boolean handleWorldRule(CardCollection noRegCreats) {
return true;
}
- public final Card sacrifice(final Card c, final SpellAbility source, final boolean effect, CardZoneTable table, Map params) {
+ public final Card sacrifice(final Card c, final SpellAbility source, final boolean effect, Map params) {
if (!c.canBeSacrificedBy(source, effect)) {
return null;
}
@@ -1855,17 +1871,17 @@ public final Card sacrifice(final Card c, final SpellAbility source, final boole
c.getController().addSacrificedThisTurn(c, source);
c.updateWasDestroyed(true);
- return sacrificeDestroy(c, source, table, params);
+ return sacrificeDestroy(c, source, params);
}
- public final boolean destroy(final Card c, final SpellAbility sa, final boolean regenerate, CardZoneTable table, Map params) {
+ public final boolean destroy(final Card c, final SpellAbility sa, final boolean regenerate, Map params) {
if (!c.canBeDestroyed()) {
return false;
}
// Replacement effects
final Map repRunParams = AbilityKey.mapFromAffected(c);
- repRunParams.put(AbilityKey.Source, sa);
+ repRunParams.put(AbilityKey.Cause, sa);
repRunParams.put(AbilityKey.Regeneration, regenerate);
if (params != null) {
repRunParams.putAll(params);
@@ -1895,7 +1911,7 @@ public final boolean destroy(final Card c, final SpellAbility sa, final boolean
// in case the destroyed card has such a trigger
game.getTriggerHandler().registerActiveLTBTrigger(c);
- final Card sacrificed = sacrificeDestroy(c, sa, table, params);
+ final Card sacrificed = sacrificeDestroy(c, sa, params);
return sacrificed != null;
}
@@ -1903,15 +1919,12 @@ public final boolean destroy(final Card c, final SpellAbility sa, final boolean
* @return the sacrificed Card in its new location, or {@code null} if the
* sacrifice wasn't successful.
*/
- protected final Card sacrificeDestroy(final Card c, SpellAbility cause, CardZoneTable table, Map params) {
+ protected final Card sacrificeDestroy(final Card c, SpellAbility cause, Map params) {
if (!c.isInPlay()) {
return null;
}
final Card newCard = moveToGraveyard(c, cause, params);
- if (table != null && newCard != null && newCard.getZone() != null) {
- table.put(ZoneType.Battlefield, newCard.getZone().getZoneType(), newCard);
- }
return newCard;
}
@@ -1923,15 +1936,15 @@ public void revealTo(final CardCollectionView cards, final Player to) {
revealTo(cards, to, null);
}
public void revealTo(final CardCollectionView cards, final Player to, String messagePrefix) {
- revealTo(cards, Collections.singleton(to), messagePrefix);
+ revealTo(cards, Collections.singleton(to), messagePrefix, true);
}
public void revealTo(final Card card, final Iterable to) {
revealTo(new CardCollection(card), to);
}
public void revealTo(final CardCollectionView cards, final Iterable to) {
- revealTo(cards, to, null);
+ revealTo(cards, to, null, true);
}
- public void revealTo(final CardCollectionView cards, final Iterable to, String messagePrefix) {
+ public void revealTo(final CardCollectionView cards, final Iterable to, String messagePrefix, boolean addSuffix) {
if (cards.isEmpty()) {
return;
}
@@ -1939,7 +1952,7 @@ public void revealTo(final CardCollectionView cards, final Iterable to,
final ZoneType zone = cards.getFirst().getZone().getZoneType();
final Player owner = cards.getFirst().getOwner();
for (final Player p : to) {
- p.getController().reveal(cards, zone, owner, messagePrefix);
+ p.getController().reveal(cards, zone, owner, messagePrefix, addSuffix);
}
}
@@ -1950,18 +1963,25 @@ public void reveal(CardCollectionView cards, Player cardOwner, boolean dontRevea
reveal(cards, cardOwner, dontRevealToOwner, null);
}
public void reveal(CardCollectionView cards, Player cardOwner, boolean dontRevealToOwner, String messagePrefix) {
+ reveal(cards, cardOwner, dontRevealToOwner, messagePrefix, true);
+ }
+ public void reveal(CardCollectionView cards, Player cardOwner, boolean dontRevealToOwner, String messagePrefix, boolean msgAddSuffix) {
Card firstCard = Iterables.getFirst(cards, null);
if (firstCard == null) {
return;
}
- reveal(cards, game.getZoneOf(firstCard).getZoneType(), cardOwner, dontRevealToOwner, messagePrefix);
+ reveal(cards, game.getZoneOf(firstCard).getZoneType(), cardOwner, dontRevealToOwner, messagePrefix, msgAddSuffix);
}
+
public void reveal(CardCollectionView cards, ZoneType zt, Player cardOwner, boolean dontRevealToOwner, String messagePrefix) {
+ reveal(cards, zt, cardOwner, dontRevealToOwner, messagePrefix, true);
+ }
+ public void reveal(CardCollectionView cards, ZoneType zt, Player cardOwner, boolean dontRevealToOwner, String messagePrefix, boolean msgAddSuffix) {
for (Player p : game.getPlayers()) {
if (dontRevealToOwner && cardOwner == p) {
continue;
}
- p.getController().reveal(cards, zt, cardOwner, messagePrefix);
+ p.getController().reveal(cards, zt, cardOwner, messagePrefix, msgAddSuffix);
}
}
@@ -2074,6 +2094,9 @@ public void startGame(GameOutcome lastGameOutcome, Runnable startGameHook) {
//
if (game.getRules().hasAppliedVariant(GameType.Planechase)) {
first.initPlane();
+ for (final Player p1 : game.getPlayers()) {
+ p1.createPlanechaseEffects(game);
+ }
}
first = runOpeningHandActions(first);
@@ -2308,7 +2331,7 @@ public void takeInitiative(final Player p, final String set) {
// 701.17a To "scry N" means to look at the top N cards of your library, then put any number of them
// on the bottom of your library in any order and the rest on top of your library in any order.
// 701.17b If a player is instructed to scry 0, no scry event occurs. Abilities that trigger whenever a
- // player scries won’t trigger.
+ // player scries won't trigger.
// 701.17c If multiple players scry at once, each of those players looks at the top cards of their library
// at the same time. Those players decide in APNAP order (see rule 101.4) where to put those
// cards, then those cards move at the same time.
@@ -2426,7 +2449,7 @@ public void dealDamage(final boolean isCombat, final CardDamageMap damageMap, fi
lethalDamage.put(c, c.getExcessDamageValue(false));
}
- e.setValue(Integer.valueOf(e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, isCombat, counterTable)));
+ e.setValue(Integer.valueOf(e.getKey().addDamageAfterPrevention(e.getValue(), sourceLKI, cause, isCombat, counterTable)));
sum += e.getValue();
sourceLKI.getDamageHistory().registerDamage(e.getValue(), isCombat, sourceLKI, e.getKey(), lkiCache);
@@ -2570,4 +2593,28 @@ private static void unanimateOnAbortedChange(final SpellAbility cause, final Car
}
}
}
+
+ private CardCollectionView getLastState(final AbilityKey key, final SpellAbility cause, final Map params) {
+ CardCollectionView lastState = null;
+ if (params != null) {
+ lastState = (CardCollectionView) params.get(key);
+ }
+ if (lastState == null && cause != null) {
+ if (key == AbilityKey.LastStateBattlefield) {
+ lastState = cause.getLastStateBattlefield();
+ }
+ if (key == AbilityKey.LastStateGraveyard) {
+ lastState = cause.getLastStateGraveyard();
+ }
+ }
+ if (lastState == null) {
+ if (key == AbilityKey.LastStateBattlefield) {
+ lastState = game.getLastStateBattlefield();
+ }
+ if (key == AbilityKey.LastStateGraveyard) {
+ lastState = game.getLastStateGraveyard();
+ }
+ }
+ return lastState;
+ }
}
diff --git a/forge-game/src/main/java/forge/game/GameActionUtil.java b/forge-game/src/main/java/forge/game/GameActionUtil.java
index d3ccbeec2d3..1f3e35345d8 100644
--- a/forge-game/src/main/java/forge/game/GameActionUtil.java
+++ b/forge-game/src/main/java/forge/game/GameActionUtil.java
@@ -18,6 +18,7 @@
package forge.game;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
@@ -81,7 +82,7 @@ private GameActionUtil() {
* a possible alternative cost the provided activator can use to pay
* the provided {@link SpellAbility}.
*/
- public static final List getAlternativeCosts(final SpellAbility sa, final Player activator) {
+ public static final List getAlternativeCosts(final SpellAbility sa, final Player activator, boolean altCostOnly) {
final List alternatives = Lists.newArrayList();
Card source = sa.getHostCard();
@@ -115,7 +116,8 @@ public static final List getAlternativeCosts(final SpellAbility sa
continue;
}
// non basic are only allowed if PayManaCost is yes
- if ((!sa.isBasicSpell() || (sa.costHasManaX() && !sa.getPayCosts().getCostMana().canXbe0())) && o.getPayManaCost() == PayManaCost.NO) {
+ if ((!sa.isBasicSpell() || (sa.costHasManaX() && sa.getPayCosts().getCostMana() != null
+ && sa.getPayCosts().getCostMana().getXMin() > 0)) && o.getPayManaCost() == PayManaCost.NO) {
continue;
}
final Card host = o.getHost();
@@ -132,6 +134,9 @@ public static final List getAlternativeCosts(final SpellAbility sa
newSA.setBasicSpell(false);
changedManaCost = true;
} else {
+ if (altCostOnly) {
+ continue;
+ }
newSA = sa.copy(activator);
}
@@ -144,7 +149,7 @@ public static final List getAlternativeCosts(final SpellAbility sa
sar.setInstantSpeed(true);
}
sar.setZone(null);
- newSA.setMayPlay(o.getAbility());
+ newSA.setMayPlay(o);
if (changedManaCost) {
if ("0".equals(sa.getParam("ActivationLimit")) && sa.getHostCard().getManaCost().isNoCost()) {
@@ -440,7 +445,10 @@ public static List getOptionalCostValues(final SpellAbility s
for (KeywordInterface inst : source.getKeywords()) {
final String keyword = inst.getOriginal();
- if (keyword.startsWith("Buyback")) {
+ if (keyword.equals("Bargain")) {
+ final Cost cost = new Cost("Sac<1/Artifact;Enchantment;Card.token/artifact, enchantment or token>", false);
+ costs.add(new OptionalCostValue(OptionalCost.Bargain, cost));
+ } else if (keyword.startsWith("Buyback")) {
final Cost cost = new Cost(keyword.substring(8), false);
costs.add(new OptionalCostValue(OptionalCost.Buyback, cost));
} else if (keyword.startsWith("Entwine")) {
@@ -741,6 +749,7 @@ public static Card createETBCountersEffect(Card sourceCard, Card c, Player contr
ReplacementEffect re = ReplacementHandler.parseReplacement(repeffstr, eff, true);
re.setLayer(ReplacementLayer.Other);
re.setOverridingAbility(sa);
+ re.setActiveZone(EnumSet.of(ZoneType.Command));
eff.addReplacementEffect(re);
@@ -907,6 +916,12 @@ public static void rollbackAbility(SpellAbility ability, final Zone fromZone, fi
}
}
+ if (ability.getApi() == ApiType.Charm) {
+ // reset chain
+ ability.setSubAbility(null);
+ ability.setChosenList(null);
+ }
+
ability.clearTargets();
ability.resetOnceResolved();
diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java
index 36dffae2242..6da7f65ccd7 100644
--- a/forge-game/src/main/java/forge/game/GameEntity.java
+++ b/forge-game/src/main/java/forge/game/GameEntity.java
@@ -71,7 +71,7 @@ public void setName(final String s) {
}
// This function handles damage after replacement and prevention effects are applied
- public abstract int addDamageAfterPrevention(final int damage, final Card source, final boolean isCombat, GameEntityCounterTable counterTable);
+ public abstract int addDamageAfterPrevention(final int damage, final Card source, final SpellAbility cause, final boolean isCombat, GameEntityCounterTable counterTable);
// This should be also usable by the AI to forecast an effect (so it must
// not change the game state)
diff --git a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java
index ff31ef1a3e3..fde1634e6e8 100644
--- a/forge-game/src/main/java/forge/game/GameEntityCounterTable.java
+++ b/forge-game/src/main/java/forge/game/GameEntityCounterTable.java
@@ -127,9 +127,9 @@ public void replaceCounterEffect(final Game game, final SpellAbility cause, fina
}
@SuppressWarnings("unchecked")
- public void replaceCounterEffect(final Game game, final SpellAbility cause, final boolean effect, final boolean etb, Map params) {
+ public boolean replaceCounterEffect(final Game game, final SpellAbility cause, final boolean effect, final boolean etb, Map params) {
if (isEmpty()) {
- return;
+ return false;
}
GameEntityCounterTable result = new GameEntityCounterTable();
for (Map.Entry, Map>> gm : columnMap().entrySet()) {
@@ -158,6 +158,7 @@ public void replaceCounterEffect(final Game game, final SpellAbility cause, fina
// Add ETB flag
final Map runParams = AbilityKey.newMap();
runParams.put(AbilityKey.ETB, etb);
+ repParams.put(AbilityKey.Cause, cause);
if (params != null) {
runParams.putAll(params);
}
@@ -181,5 +182,6 @@ public void replaceCounterEffect(final Game game, final SpellAbility cause, fina
}
}
result.triggerCountersPutAll(game);
+ return !result.isEmpty();
}
}
diff --git a/forge-game/src/main/java/forge/game/GameEntityView.java b/forge-game/src/main/java/forge/game/GameEntityView.java
index 282d28c8a1b..81b02a64c33 100644
--- a/forge-game/src/main/java/forge/game/GameEntityView.java
+++ b/forge-game/src/main/java/forge/game/GameEntityView.java
@@ -1,6 +1,7 @@
package forge.game;
-import forge.game.card.CardCollectionView;
+import com.google.common.collect.Iterables;
+
import forge.game.card.CardView;
import forge.trackable.TrackableCollection;
import forge.trackable.TrackableObject;
@@ -45,35 +46,34 @@ protected void updateName(GameEntity e) {
public int getPreventNextDamage() {
return get(TrackableProperty.PreventNextDamage);
}
- protected void updatePreventNextDamage(GameEntity e) {
+ public void updatePreventNextDamage(GameEntity e) {
set(TrackableProperty.PreventNextDamage, e.getPreventNextDamageTotalShields());
}
public Iterable getAttachedCards() {
- return get(TrackableProperty.AttachedCards);
+ if (hasAnyCardAttachments()) {
+ Iterable