types = c.getCounters().keySet();
for(CounterType ct : types) {
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, sa, true, null);
@@ -1709,7 +1710,7 @@ public class ComputerUtilCard {
}
}
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
- pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, true);
+ pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, 0, true);
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
return pumped;
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
index 148952c0840..b025e10deca 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCombat.java
@@ -89,7 +89,7 @@ public class ComputerUtilCombat {
return ComputerUtilCombat.canAttackNextTurn(attacker, input);
}
});
- } // canAttackNextTurn(Card)
+ }
/**
*
@@ -176,7 +176,6 @@ public class ComputerUtilCombat {
return n;
}
-
// Returns the damage an unblocked attacker would deal
/**
*
@@ -291,14 +290,12 @@ public class ComputerUtilCombat {
* @return a int.
*/
public static int lifeThatWouldRemain(final Player ai, final Combat combat) {
-
int damage = 0;
final List attackers = combat.getAttackersOf(ai);
final List unblocked = Lists.newArrayList();
for (final Card attacker : attackers) {
-
final List blockers = combat.getBlockers(attacker);
if ((blockers.size() == 0)
@@ -333,7 +330,6 @@ public class ComputerUtilCombat {
* @return a int.
*/
public static int resultingPoison(final Player ai, final Combat combat) {
-
// ai can't get poison counters, so the value can't change
if (!ai.canReceiveCounters(CounterEnumType.POISON)) {
return ai.getPoisonCounters();
@@ -345,7 +341,6 @@ public class ComputerUtilCombat {
final List unblocked = Lists.newArrayList();
for (final Card attacker : attackers) {
-
final List blockers = combat.getBlockers(attacker);
if ((blockers.size() == 0)
@@ -394,7 +389,6 @@ public class ComputerUtilCombat {
public static boolean lifeInDanger(final Player ai, final Combat combat) {
return lifeInDanger(ai, combat, 0);
}
-
public static boolean lifeInDanger(final Player ai, final Combat combat, final int payment) {
// life in danger only cares about the player's life. Not Planeswalkers' life
if (ai.cantLose() || combat == null || combat.getAttackingPlayer() == ai) {
@@ -418,7 +412,6 @@ public class ComputerUtilCombat {
final List threateningCommanders = getLifeThreateningCommanders(ai,combat);
for (final Card attacker : attackers) {
-
final List blockers = combat.getBlockers(attacker);
if (blockers.isEmpty()) {
@@ -472,7 +465,6 @@ public class ComputerUtilCombat {
* @return a boolean.
*/
public static boolean wouldLoseLife(final Player ai, final Combat combat) {
-
return (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) < ai.getLife());
}
@@ -489,7 +481,6 @@ public class ComputerUtilCombat {
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) {
return lifeInSeriousDanger(ai, combat, 0);
}
-
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) {
// life in danger only cares about the player's life. Not about a Planeswalkers life
if (ai.cantLose() || combat == null) {
@@ -502,7 +493,6 @@ public class ComputerUtilCombat {
final List attackers = combat.getAttackersOf(ai);
for (final Card attacker : attackers) {
-
final List blockers = combat.getBlockers(attacker);
if (blockers.isEmpty()) {
@@ -510,7 +500,7 @@ public class ComputerUtilCombat {
return true;
}
}
- if(threateningCommanders.contains(attacker)) {
+ if (threateningCommanders.contains(attacker)) {
return true;
}
}
@@ -704,7 +694,6 @@ public class ComputerUtilCombat {
* @return a boolean.
*/
public static boolean combatantWouldBeDestroyed(Player ai, final Card combatant, Combat combat) {
-
if (combat.isAttacking(combatant)) {
return ComputerUtilCombat.attackerWouldBeDestroyed(ai, combatant, combat);
}
@@ -1268,8 +1257,9 @@ public class ComputerUtilCombat {
continue;
}
+ sa.setActivatingPlayer(source.getController());
+
if (sa.hasParam("Cost")) {
- sa.setActivatingPlayer(source.getController());
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
}
@@ -1475,7 +1465,6 @@ public class ComputerUtilCombat {
toughness -= predictDamageTo(attacker, damage, 0, source, false);
continue;
} else if (ApiType.Pump.equals(sa.getApi())) {
-
if (sa.hasParam("Cost")) {
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1508,7 +1497,6 @@ public class ComputerUtilCombat {
toughness += AbilityUtils.calculateAmount(source, bonus, sa);
}
} else if (ApiType.PumpAll.equals(sa.getApi())) {
-
if (sa.hasParam("Cost")) {
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
continue;
@@ -1843,7 +1831,6 @@ public class ComputerUtilCombat {
}
public static boolean canDestroyBlockerBeforeFirstStrike(final Card blocker, final Card attacker, final boolean withoutAbilities) {
-
if (attacker.isEquippedBy("Godsend")) {
return true;
}
@@ -1854,7 +1841,6 @@ public class ComputerUtilCombat {
int flankingMagnitude = 0;
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
-
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
if (flankingMagnitude >= blocker.getNetToughness()) {
@@ -2232,7 +2218,6 @@ public class ComputerUtilCombat {
return killDamage;
}
-
/**
*
* predictDamage.
@@ -2267,7 +2252,7 @@ public class ComputerUtilCombat {
if (!re.matchesValidParam("ValidSource", source)) {
continue;
}
- if (!re.matchesValidParam("ValidTarget", source)) {
+ if (!re.matchesValidParam("ValidTarget", target)) {
continue;
}
if (re.hasParam("IsCombat")) {
@@ -2305,8 +2290,6 @@ public class ComputerUtilCombat {
* a boolean.
* @return a int.
*/
- // This function helps the AI calculate the actual amount of damage an
- // effect would deal
public final static int predictDamageTo(final Card target, final int damage, final Card source, final boolean isCombat) {
return predictDamageTo(target, damage, 0, source, isCombat);
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
index 468da12f98d..7fd2d6b050a 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCost.java
@@ -1,46 +1,22 @@
package forge.ai;
-import java.util.List;
-import java.util.Set;
-
-import forge.game.ability.ApiType;
-import org.apache.commons.lang3.ObjectUtils;
-import org.apache.commons.lang3.StringUtils;
-
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
-
+import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
-import forge.game.card.Card;
-import forge.game.card.CardCollection;
-import forge.game.card.CardCollectionView;
-import forge.game.card.CardFactoryUtil;
-import forge.game.card.CardLists;
-import forge.game.card.CardPredicates;
+import forge.game.ability.ApiType;
+import forge.game.card.*;
import forge.game.card.CardPredicates.Presets;
-import forge.game.card.CardUtil;
-import forge.game.card.CounterEnumType;
-import forge.game.card.CounterType;
import forge.game.combat.Combat;
-import forge.game.cost.Cost;
-import forge.game.cost.CostDamage;
-import forge.game.cost.CostDiscard;
-import forge.game.cost.CostPart;
-import forge.game.cost.CostPayLife;
-import forge.game.cost.CostPayment;
-import forge.game.cost.CostPutCounter;
-import forge.game.cost.CostRemoveAnyCounter;
-import forge.game.cost.CostRemoveCounter;
-import forge.game.cost.CostSacrifice;
-import forge.game.cost.CostTapType;
-import forge.game.cost.PaymentDecision;
+import forge.game.cost.*;
import forge.game.keyword.Keyword;
+import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
@@ -48,6 +24,11 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
+import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+import java.util.Set;
public class ComputerUtilCost {
@@ -153,8 +134,14 @@ public class ComputerUtilCost {
final CostDiscard disc = (CostDiscard) part;
final String type = disc.getType();
- if (type.equals("CARDNAME") && source.getAbilityText().contains("Bloodrush")) {
- continue;
+ if (type.equals("CARDNAME")) {
+ if (source.getAbilityText().contains("Bloodrush")) {
+ continue;
+ } else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
+ && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
+ // Better do something than just discard stuff
+ return true;
+ }
}
final CardCollection typeList = CardLists.getValidCards(hand, type.split(","), source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
@@ -245,6 +232,51 @@ public class ComputerUtilCost {
return true;
}
+ public static boolean checkForManaSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
+ // TODO cheating via autopay can still happen, need to get the real ai player from controlledBy
+ if (cost == null || !ai.isAI()) {
+ return true;
+ }
+ for (final CostPart part : cost.getCostParts()) {
+ if (part instanceof CostSacrifice) {
+ CardCollection list = new CardCollection();
+ final CardCollection exclude = new CardCollection();
+ if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST) != null) {
+ exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_SAC_COST));
+ }
+ if (part.payCostFromSource()) {
+ list.add(source);
+ } else if (part.getType().equals("OriginalHost")) {
+ list.add(sourceAbility.getOriginalHost());
+ } else if (part.getAmount().equals("All")) {
+ // Does the AI want to use Sacrifice All?
+ return false;
+ } else {
+ final String amount = part.getAmount();
+ Integer c = part.convertAmount();
+
+ if (c == null) {
+ c = AbilityUtils.calculateAmount(source, amount, sourceAbility);
+ }
+ final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
+ CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, c, exclude);
+ if (choices != null) {
+ list.addAll(choices);
+ }
+ }
+ list.removeAll(exclude);
+ if (list.isEmpty()) {
+ return false;
+ }
+ for (Card choice : list) {
+ AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_SAC_COST);
+ }
+ return true;
+ }
+ }
+ return true;
+ }
+
/**
* Check creature sacrifice cost.
*
@@ -346,6 +378,19 @@ public class ComputerUtilCost {
return true;
}
+ /**
+ * Check sacrifice cost.
+ *
+ * @param cost
+ * the cost
+ * @param source
+ * the source
+ * @return true, if successful
+ */
+ public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
+ return checkSacrificeCost(ai, cost, source, sourceAbility, true);
+ }
+
public static boolean isSacrificeSelfCost(final Cost cost) {
if (cost == null) {
return false;
@@ -375,6 +420,8 @@ public class ComputerUtilCost {
}
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostTapType) {
+ String type = part.getType();
+
/*
* Only crew with creatures weaker than vehicle
*
@@ -385,7 +432,6 @@ public class ComputerUtilCost {
if (sa.hasParam("Crew")) {
Card vehicle = AnimateAi.becomeAnimated(source, sa);
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
- String type = part.getType();
String totalP = type.split("withTotalPowerGE")[1];
type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
CardCollection exclude = CardLists.getValidCards(
@@ -400,25 +446,41 @@ public class ComputerUtilCost {
return ComputerUtil.chooseTapTypeAccumulatePower(ai, type, sa, true,
Integer.parseInt(totalP), exclude) != null;
}
+
+ // check if we have a valid card to tap (e.g. Jaspera Sentinel)
+ Integer c = part.convertAmount();
+ if (c == null) {
+ c = AbilityUtils.calculateAmount(source, part.getAmount(), sa);
+ }
+ CardCollection exclude = new CardCollection();
+ if (AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST) != null) {
+ exclude.addAll(AiCardMemory.getMemorySet(ai, MemorySet.PAYS_TAP_COST));
+ }
+ // trying to produce mana that includes tapping source that will already be tapped
+ if (exclude.contains(source) && cost.hasTapCost()) {
+ return false;
+ }
+ // if we want to pay for an ability with tapping the source can't be chosen
+ if (sa.getPayCosts().hasTapCost()) {
+ exclude.add(sa.getHostCard());
+ }
+ CardCollection tapChoices = ComputerUtil.chooseTapType(ai, type, source, cost.hasTapCost(), c, exclude, sa);
+ if (tapChoices != null) {
+ for (Card choice : tapChoices) {
+ AiCardMemory.rememberCard(ai, choice, MemorySet.PAYS_TAP_COST);
+ }
+ // if manasource gets tapped to produce it also can't help paying another
+ if (cost.hasTapCost()) {
+ AiCardMemory.rememberCard(ai, source, MemorySet.PAYS_TAP_COST);
+ }
+ return true;
+ }
return false;
}
}
return true;
}
- /**
- * Check sacrifice cost.
- *
- * @param cost
- * the cost
- * @param source
- * the source
- * @return true, if successful
- */
- public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
- return checkSacrificeCost(ai, cost, source, sourceAbility, true);
- }
-
/**
*
* shouldPayCost.
@@ -633,7 +695,7 @@ public class ComputerUtilCost {
}
// Check if the AI intends to play the card and if it can pay for it with the mana it has
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
- boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
+ boolean canPay = c.getManaCost().canBePaidWithAvailable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
return canPay && willPlay;
}
}
@@ -662,7 +724,6 @@ public class ComputerUtilCost {
public static Set getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
}
-
public static Set getAvailableManaColors(Player ai, List additionalLands) {
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
Set colorsAvailable = Sets.newHashSet();
@@ -695,8 +756,9 @@ public class ComputerUtilCost {
public static int getMaxXValue(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
- final SpellAbility root = sa.getRootAbility();
+ SpellAbility root = sa.getRootAbility();
final Cost abCost = root.getPayCosts();
+
if (abCost == null || !abCost.hasXInAnyCostPart()) {
return 0;
}
diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
index a1a3168b5ed..4f7bdbbac3d 100644
--- a/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
+++ b/forge-ai/src/main/java/forge/ai/ComputerUtilMana.java
@@ -1,26 +1,10 @@
package forge.ai;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
-
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimap;
+import com.google.common.collect.*;
+import forge.ai.AiCardMemory.MemorySet;
import forge.ai.ability.AnimateAi;
import forge.card.ColorSet;
import forge.card.MagicColor;
@@ -34,20 +18,10 @@ import forge.game.GameActionUtil;
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.CounterEnumType;
+import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
-import forge.game.cost.Cost;
-import forge.game.cost.CostAdjustment;
-import forge.game.cost.CostPartMana;
-import forge.game.cost.CostPayEnergy;
-import forge.game.cost.CostPayment;
+import forge.game.cost.*;
import forge.game.keyword.Keyword;
import forge.game.mana.Mana;
import forge.game.mana.ManaCostBeingPaid;
@@ -67,6 +41,10 @@ import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.TextUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.util.*;
public class ComputerUtilMana {
private final static boolean DEBUG_MANA_PAYMENT = false;
@@ -75,38 +53,20 @@ public class ComputerUtilMana {
cost = new ManaCostBeingPaid(cost); //check copy of cost so it doesn't modify the exist cost being paid
return payManaCost(cost, sa, ai, true, true);
}
-
- public static boolean payManaCost(ManaCostBeingPaid cost, final SpellAbility sa, final Player ai) {
- return payManaCost(cost, sa, ai, false, true);
- }
-
public static boolean canPayManaCost(final SpellAbility sa, final Player ai, final int extraMana) {
return payManaCost(sa, ai, true, extraMana, true);
}
- /**
- * Return the number of colors used for payment for Converge
- */
- public static int getConvergeCount(final SpellAbility sa, final Player ai) {
- ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
- if (payManaCost(cost, sa, ai, true, true)) {
- return cost.getSunburst();
- } else {
- return 0;
- }
+ public static boolean payManaCost(ManaCostBeingPaid cost, final SpellAbility sa, final Player ai) {
+ return payManaCost(cost, sa, ai, false, true);
}
-
- // Does not check if mana sources can be used right now, just checks for potential chance.
- public static boolean hasEnoughManaSourcesToCast(final SpellAbility sa, final Player ai) {
- if(ai == null || sa == null)
- return false;
- sa.setActivatingPlayer(ai);
- return payManaCost(sa, ai, true, 0, false);
- }
-
public static boolean payManaCost(final Player ai, final SpellAbility sa) {
return payManaCost(sa, ai, false, 0, true);
}
+ private static boolean payManaCost(final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable) {
+ ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, test, extraMana);
+ return payManaCost(cost, sa, ai, test, checkPlayable);
+ }
private static void refundMana(List manaSpent, Player ai, SpellAbility sa) {
if (sa.getHostCard() != null) {
@@ -118,9 +78,23 @@ public class ComputerUtilMana {
manaSpent.clear();
}
- private static boolean payManaCost(final SpellAbility sa, final Player ai, final boolean test, final int extraMana, boolean checkPlayable) {
- ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, test, extraMana);
- return payManaCost(cost, sa, ai, test, checkPlayable);
+ /**
+ * Return the number of colors used for payment for Converge
+ */
+ public static int getConvergeCount(final SpellAbility sa, final Player ai) {
+ ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
+ if (payManaCost(cost, sa, ai, true, true)) {
+ return cost.getSunburst();
+ }
+ return 0;
+ }
+
+ // Does not check if mana sources can be used right now, just checks for potential chance.
+ public static boolean hasEnoughManaSourcesToCast(final SpellAbility sa, final Player ai) {
+ if (ai == null || sa == null)
+ return false;
+ sa.setActivatingPlayer(ai);
+ return payManaCost(sa, ai, true, 0, false);
}
private static Integer scoreManaProducingCard(final Card card) {
@@ -272,28 +246,99 @@ public class ComputerUtilMana {
public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay,
Collection saList, boolean checkCosts) {
+ Card saHost = sa.getHostCard();
+
+ // CastTotalManaSpent (AIPreference:ManaFrom$Type or AIManaPref$ Type)
+ String manaSourceType = "";
+ if (saHost.hasSVar("AIPreference")) {
+ String condition = saHost.getSVar("AIPreference");
+ if (condition.startsWith("ManaFrom")) {
+ manaSourceType = TextUtil.split(condition, '$')[1];
+ }
+ } else if (sa.hasParam("AIManaPref")) {
+ manaSourceType = sa.getParam("AIManaPref");
+ }
+ if (manaSourceType != "") {
+ List filteredList = Lists.newArrayList(saList);
+ switch (manaSourceType) {
+ case "Snow":
+ filteredList.sort(new Comparator() {
+ @Override
+ public int compare(SpellAbility ab1, SpellAbility ab2) {
+ return ab1.getHostCard() != null && ab1.getHostCard().isSnow()
+ && ab2.getHostCard() != null && !ab2.getHostCard().isSnow() ? -1 : 1;
+ }
+ });
+ saList = filteredList;
+ break;
+ case "Treasure":
+ // Try to spend only one Treasure if possible
+ filteredList.sort(new Comparator() {
+ @Override
+ public int compare(SpellAbility ab1, SpellAbility ab2) {
+ return ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure")
+ && ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1;
+ }
+ });
+ SpellAbility first = filteredList.get(0);
+ if (first.getHostCard() != null && first.getHostCard().getType().hasSubtype("Treasure")) {
+ saList.remove(first);
+ List updatedList = Lists.newArrayList();
+ updatedList.add(first);
+ updatedList.addAll(saList);
+ saList = updatedList;
+ }
+ break;
+ case "TreasureMax":
+ // Ok to spend as many Treasures as possible
+ filteredList.sort(new Comparator() {
+ @Override
+ public int compare(SpellAbility ab1, SpellAbility ab2) {
+ return ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure")
+ && ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1;
+ }
+ });
+ saList = filteredList;
+ break;
+ default:
+ break;
+ }
+ }
+
for (final SpellAbility ma : saList) {
- if (ma.getHostCard() == sa.getHostCard()) {
+ if (ma.getHostCard() == saHost) {
continue;
}
- if (sa.getHostCard() != null) {
+ if (saHost != null) {
+ if (ma.getPayCosts().hasTapCost() && AiCardMemory.isRememberedCard(ai, ma.getHostCard(), MemorySet.PAYS_TAP_COST)) {
+ continue;
+ }
+
+ if (!ComputerUtilCost.checkTapTypeCost(ai, ma.getPayCosts(), ma.getHostCard(), sa)) {
+ continue;
+ }
+
+ if (!ComputerUtilCost.checkForManaSacrificeCost(ai, ma.getPayCosts(), ma.getHostCard(), ma)) {
+ continue;
+ }
+
if (sa.getApi() == ApiType.Animate) {
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
- if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
- && ma.getHostCard() == sa.getHostCard().getEnchantingCard()
+ if (saHost.isAura() && "Enchanted".equals(sa.getParam("Defined"))
+ && ma.getHostCard() == saHost.getEnchantingCard()
&& ma.getPayCosts().hasTapCost()) {
continue;
}
// If a manland was previously animated this turn, do not tap it to animate another manland
- if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
+ if (saHost.isLand() && ma.getHostCard().isLand()
&& ai.getController().isAI()
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
continue;
}
} else if (sa.getApi() == ApiType.Pump) {
- if ((sa.getHostCard().isInstant() || sa.getHostCard().isSorcery())
+ if ((saHost.isInstant() || saHost.isSorcery())
&& ma.getHostCard().isCreature()
&& ai.getController().isAI()
&& ma.getPayCosts().hasTapCost()
@@ -303,7 +348,7 @@ public class ComputerUtilMana {
continue;
}
} else if (sa.getApi() == ApiType.Attach
- && "AvoidPayingWithAttachTarget".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
+ && "AvoidPayingWithAttachTarget".equals(saHost.getSVar("AIPaymentPreference"))) {
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
// be tapped to pay its own cost if there's another untapped land like that available
if (ma.getHostCard().equals(sa.getTargetCard())) {
@@ -312,7 +357,6 @@ public class ComputerUtilMana {
}
}
}
-
}
SpellAbility paymentChoice = ma;
@@ -320,7 +364,7 @@ public class ComputerUtilMana {
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
// to attempt to make the spell uncounterable when possible.
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
- && sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
+ && saHost.getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
continue;
@@ -486,7 +530,7 @@ public class ComputerUtilMana {
continue;
}
if (ApiType.Mana.equals(trSA.getApi())) {
- int pAmount = AbilityUtils.calculateAmount(trSA.getHostCard(), trSA.getParamOrDefault("Amount", "1"), trSA);
+ int pAmount = AbilityUtils.calculateAmount(trSA.getHostCard(), trSA.getParamOrDefault("Amount", "1"), trSA);
String produced = trSA.getParam("Produced");
if (produced.equals("Chosen")) {
produced = MagicColor.toShortString(trSA.getHostCard().getChosenColor());
@@ -623,7 +667,10 @@ public class ComputerUtilMana {
} // getManaSourcesToPayCost()
private static boolean payManaCost(final ManaCostBeingPaid cost, final SpellAbility sa, final Player ai, final boolean test, boolean checkPlayable) {
+ 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();
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
int testEnergyPool = ai.getCounters(CounterEnumType.ENERGY);
@@ -770,6 +817,10 @@ public class ComputerUtilMana {
setExpressColorChoice(sa, ai, cost, toPay, saPayment);
+ if (saPayment.getPayCosts().hasTapCost()) {
+ AiCardMemory.rememberCard(ai, saPayment.getHostCard(), MemorySet.PAYS_TAP_COST);
+ }
+
if (test) {
// Check energy when testing
CostPayEnergy energyCost = saPayment.getPayCosts().getCostEnergy();
@@ -792,8 +843,7 @@ public class ComputerUtilMana {
// remove from available lists
Iterables.removeIf(sourcesForShards.values(), CardTraitPredicates.isHostCard(saPayment.getHostCard()));
- }
- else {
+ } else {
final CostPayment pay = new CostPayment(saPayment.getPayCosts(), saPayment);
if (!pay.payComputerCosts(new AiCostDecision(ai, saPayment))) {
saList.remove(saPayment);
@@ -828,9 +878,8 @@ public class ComputerUtilMana {
if (test) {
resetPayment(paymentList);
return false;
- }
- else {
- System.out.println("ComputerUtil : payManaCost() cost was not paid for " + sa.getHostCard().getName() + ". Didn't find what to pay for " + toPay);
+ } else {
+ System.out.println("ComputerUtilMana: payManaCost() cost was not paid for " + sa.toString() + " (" + sa.getHostCard().getName() + "). Didn't find what to pay for " + toPay);
return false;
}
}
@@ -1124,8 +1173,7 @@ public class ComputerUtilMana {
// if the AI can't pay the additional costs skip the mana ability
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma)) {
return false;
- }
- else if (sourceCard.isTapped()) {
+ } else if (sourceCard.isTapped()) {
return false;
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
return false;
@@ -1173,8 +1221,7 @@ public class ComputerUtilMana {
// isManaSourceReserved returns true if sourceCard is reserved as a mana source for payment
// for the future spell to be cast in another phase. However, if "sa" (the spell ability that is
- // being considered for casting) is high priority, then mana source reservation will be
- // ignored.
+ // being considered for casting) is high priority, then mana source reservation will be ignored.
private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) {
if (sa == null) {
return false;
@@ -1198,8 +1245,7 @@ public class ComputerUtilMana {
if (!(ai.getGame().getPhaseHandler().isPlayerTurn(ai))) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
- }
- else
+ } else
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
} else {
if ((AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) ||
@@ -1220,8 +1266,7 @@ public class ComputerUtilMana {
if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
- }
- else {
+ } else {
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
// This mana source is held elsewhere for a Main Phase 2 spell.
return true;
@@ -1231,7 +1276,6 @@ public class ComputerUtilMana {
return false;
}
-
private static ManaCostShard getNextShardToPay(ManaCostBeingPaid cost) {
// mind the priorities
// * Pay mono-colored first,curPhase == PhaseType.CLEANUP
@@ -1317,8 +1361,7 @@ public class ComputerUtilMana {
String commonColor = ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Hand));
if (!commonColor.isEmpty() && satisfiesColorChoice(abMana, choiceString, MagicColor.toShortString(commonColor)) && abMana.getComboColors().contains(MagicColor.toShortString(commonColor))) {
choice = MagicColor.toShortString(commonColor);
- }
- else {
+ } else {
// default to first available color
for (String c : comboColors) {
if (satisfiesColorChoice(abMana, choiceString, c)) {
@@ -1363,8 +1406,7 @@ public class ComputerUtilMana {
break;
}
}
- }
- else {
+ } else {
String color = MagicColor.toShortString(manaPart);
boolean wasNeeded = testCost.ai_payMana(color, p.getManaPool());
if (!wasNeeded) {
@@ -1447,7 +1489,7 @@ public class ComputerUtilMana {
String restriction = null;
if (payCosts != null && payCosts.getCostMana() != null) {
- restriction = payCosts.getCostMana().getRestiction();
+ restriction = payCosts.getCostMana().getRestriction();
}
ManaCostBeingPaid cost = new ManaCostBeingPaid(mana, restriction);
@@ -1462,6 +1504,11 @@ public class ComputerUtilMana {
manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * xCounter;
}
+ if (manaToAdd < 1 && !payCosts.getCostMana().canXbe0()) {
+ // AI cannot really handle X costs properly but this keeps AI from violating rules
+ manaToAdd = 1;
+ }
+
String xColor = sa.getParamOrDefault("XColor", "1");
if (card.hasKeyword("Spend only colored mana on X. No more than one mana of each color may be spent this way.")) {
xColor = "WUBRGX";
@@ -1500,7 +1547,6 @@ public class ComputerUtilMana {
public static int getAvailableManaEstimate(final Player p) {
return getAvailableManaEstimate(p, true);
}
-
public static int getAvailableManaEstimate(final Player p, final boolean checkPlayable) {
int availableMana = 0;
@@ -1864,8 +1910,7 @@ public class ComputerUtilMana {
if (!res.contains(a)) {
if (cost.isReusuableResource()) {
res.add(0, a);
- }
- else {
+ } else {
res.add(res.size(), a);
}
}
diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java
index 2fc61fab01b..93fc295c084 100644
--- a/forge-ai/src/main/java/forge/ai/GameState.java
+++ b/forge-ai/src/main/java/forge/ai/GameState.java
@@ -314,14 +314,14 @@ public abstract class GameState {
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
newText.append("|Modal");
}
- if (c.isAttachedToEntity()) {
- newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
- }
+
if (c.getPlayerAttachedTo() != null) {
// TODO: improve this for game states with more than two players
newText.append("|EnchantingPlayer:");
Player p = c.getPlayerAttachedTo();
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
+ } else if (c.isAttachedToEntity()) {
+ newText.append("|AttachedTo:").append(c.getEntityAttachedTo().getId());
}
if (c.getDamage() > 0) {
@@ -434,7 +434,7 @@ public abstract class GameState {
boolean first = true;
StringBuilder counterString = new StringBuilder();
- for(Entry kv : counters.entrySet()) {
+ for (Entry kv : counters.entrySet()) {
if (!first) {
counterString.append(",");
}
@@ -470,7 +470,7 @@ public abstract class GameState {
}
public void parse(List lines) {
- for(String line : lines) {
+ for (String line : lines) {
parseLine(line);
}
}
@@ -1110,13 +1110,13 @@ public abstract class GameState {
private void handleCardAttachments() {
// Unattach all permanents first
- for(Entry entry : cardToAttachId.entrySet()) {
+ for (Entry entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
attachedTo.unAttachAllCards();
}
// Attach permanents by ID
- for(Entry entry : cardToAttachId.entrySet()) {
+ for (Entry entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
if (attacher.isAttachment()) {
@@ -1125,7 +1125,7 @@ public abstract class GameState {
}
// Enchant players by ID
- for(Entry entry : cardToEnchantPlayerId.entrySet()) {
+ for (Entry entry : cardToEnchantPlayerId.entrySet()) {
// TODO: improve this for game states with more than two players
Card attacher = entry.getKey();
Game game = attacher.getGame();
@@ -1136,9 +1136,9 @@ public abstract class GameState {
}
private void handleMergedCards() {
- for(Entry> entry : cardToMergedCards.entrySet()) {
+ for (Entry> entry : cardToMergedCards.entrySet()) {
Card mergedTo = entry.getKey();
- for(String mergedCardName : entry.getValue()) {
+ for (String mergedCardName : entry.getValue()) {
Card c;
PaperCard pc = StaticData.instance().getCommonCards().getCard(mergedCardName. replace("^", ","));
if (pc == null) {
@@ -1202,6 +1202,8 @@ public abstract class GameState {
p.getZone(zt).removeAllCards(true);
}
+ p.setCommanders(Lists.newArrayList());
+
Map playerCards = new EnumMap<>(ZoneType.class);
for (Entry kv : cardTexts.entrySet()) {
String value = kv.getValue();
@@ -1212,6 +1214,8 @@ public abstract class GameState {
p.setLandsPlayedThisTurn(landsPlayed);
p.setLandsPlayedLastTurn(landsPlayedLastTurn);
+ p.clearPaidForSA();
+
for (Entry kv : playerCards.entrySet()) {
PlayerZone zone = p.getZone(kv.getKey());
if (kv.getKey() == ZoneType.Battlefield) {
@@ -1236,7 +1240,7 @@ public abstract class GameState {
// (will be overridden later, so the actual value shouldn't matter)
//FIXME it shouldn't be able to attach itself
- c.setEntityAttachedTo(c);
+ c.setEntityAttachedTo(CardFactory.copyCard(c, true));
}
if (cardsWithoutETBTrigs.contains(c)) {
@@ -1343,7 +1347,9 @@ public abstract class GameState {
c.setExiledBy(c.getController());
} else if (info.startsWith("IsCommander")) {
c.setCommander(true);
- player.setCommanders(Lists.newArrayList(c));
+ List cmd = Lists.newArrayList(player.getCommanders());
+ cmd.add(c);
+ player.setCommanders(cmd);
} else if (info.startsWith("Id:")) {
int id = Integer.parseInt(info.substring(3));
idToCard.put(id, c);
diff --git a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
index 97dc8521d78..c3f032c24f6 100644
--- a/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
+++ b/forge-ai/src/main/java/forge/ai/PlayerControllerAi.java
@@ -1064,7 +1064,7 @@ public class PlayerControllerAi extends PlayerController {
}
}
- private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory){
+ private boolean prepareSingleSa(final Card host, final SpellAbility sa, boolean isMandatory) {
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(host, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
@@ -1291,6 +1291,27 @@ public class PlayerControllerAi extends PlayerController {
return SpellApiToAi.Converter.get(api).chooseCardName(player, sa, faces);
}
+ @Override
+ public Card chooseDungeon(Player ai, List dungeonCards, String message) {
+ // TODO: improve the conditions that define which dungeon is a viable option to choose
+ List dungeonNames = Lists.newArrayList();
+ for (PaperCard pc : dungeonCards) {
+ dungeonNames.add(pc.getName());
+ }
+
+ // Don't choose Tomb of Annihilation when life in danger unless we can win right away or can't lose for 0 life
+ if (ai.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
+ int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
+ if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
+ && !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
+ dungeonNames.remove("Tomb of Annihilation");
+ }
+ }
+
+ int i = MyRandom.getRandom().nextInt(dungeonNames.size());
+ return Card.fromPaperCard(dungeonCards.get(i), ai);
+ }
+
@Override
public List chooseCardsForSplice(SpellAbility sa, List cards) {
// sort from best to worst
diff --git a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
index 501a3ab4a61..0da2f61ca26 100644
--- a/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpecialCardAi.java
@@ -1099,7 +1099,7 @@ public class SpecialCardAi {
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
- boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames(
+ boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, sa.getHostCard())).getColor());
byte colorProfile = cost.getColorProfile();
diff --git a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
index adcfe9728cd..07ff3f7179b 100644
--- a/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellAbilityAi.java
@@ -1,12 +1,7 @@
package forge.ai;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
-
import forge.card.CardStateName;
import forge.card.ICardFace;
import forge.card.mana.ManaCost;
@@ -24,8 +19,13 @@ import forge.game.player.PlayerController.BinaryChoiceType;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
+import forge.game.zone.ZoneType;
import forge.util.MyRandom;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
/**
* Base class for API-specific AI logic
*
@@ -72,10 +72,12 @@ public abstract class SpellAbilityAi {
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
+ final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
+ && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
if (!checkAiLogic(ai, sa, logic)) {
return false;
}
- if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
+ if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false;
}
} else {
diff --git a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
index b7f5d88d664..45cd74ab065 100644
--- a/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
+++ b/forge-ai/src/main/java/forge/ai/SpellApiToAi.java
@@ -50,8 +50,10 @@ public enum SpellApiToAi {
.put(ApiType.ChooseSource, ChooseSourceAi.class)
.put(ApiType.ChooseType, ChooseTypeAi.class)
.put(ApiType.Clash, ClashAi.class)
+ .put(ApiType.ClassLevelUp, AlwaysPlayAi.class)
.put(ApiType.Cleanup, AlwaysPlayAi.class)
.put(ApiType.Clone, CloneAi.class)
+ .put(ApiType.CompanionChoose, ChooseCompanionAi.class)
.put(ApiType.CopyPermanent, CopyPermanentAi.class)
.put(ApiType.CopySpellAbility, CopySpellAbilityAi.class)
.put(ApiType.ControlPlayer, CannotPlayAi.class)
@@ -142,6 +144,7 @@ public enum SpellApiToAi {
.put(ApiType.ReplaceEffect, AlwaysPlayAi.class)
.put(ApiType.ReplaceDamage, AlwaysPlayAi.class)
.put(ApiType.ReplaceSplitDamage, AlwaysPlayAi.class)
+ .put(ApiType.ReplaceToken, AlwaysPlayAi.class)
.put(ApiType.RestartGame, RestartGameAi.class)
.put(ApiType.Reveal, RevealAi.class)
.put(ApiType.RevealHand, RevealHandAi.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 2e8066cb5bd..e5e7c4d08d6 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AmassAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AmassAi.java
@@ -34,7 +34,7 @@ public class AmassAi extends SpellAbilityAi {
final String tokenScript = "b_0_0_zombie_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
- Card token = TokenInfo.getProtoType(tokenScript, sa, false);
+ Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
if (token == null) {
return false;
@@ -98,4 +98,3 @@ public class AmassAi extends SpellAbilityAi {
return ComputerUtilCard.getBestAI(better);
}
}
-
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 eb1e863f80b..f7ed5467432 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AnimateAi.java
@@ -146,6 +146,14 @@ public class AnimateAi extends SpellAbilityAi {
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa)) {
return false; // prevent crewing with equal or better creatures
}
+
+ if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
+ // Set PayX here to maximum value.
+ final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer);
+
+ sa.setXManaCostPaid(xPay);
+ }
+
if (!sa.usesTargeting()) {
final List defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
@@ -252,7 +260,7 @@ public class AnimateAi extends SpellAbilityAi {
private boolean animateTgtAI(final SpellAbility sa) {
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
- final boolean alwaysActivatePWAbility = sa.hasParam("Planeswalker")
+ final boolean alwaysActivatePWAbility = sa.isPwAbility()
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
&& sa.getTargetRestrictions() != null
&& sa.getTargetRestrictions().getMinTargets(sa.getHostCard(), sa) == 0;
@@ -480,7 +488,7 @@ public class AnimateAi extends SpellAbilityAi {
timestamp);
// check if animate added static Abilities
- CardTraitChanges traits = card.getChangedCardTraits().get(timestamp);
+ CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
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 05a76410082..6248b29c1e4 100644
--- a/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/AttachAi.java
@@ -115,8 +115,7 @@ public class AttachAi extends SpellAbilityAi {
}
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
- // Set PayX here to maximum value. (Endless Scream and Venarian
- // Gold)
+ // Set PayX here to maximum value. (Endless Scream and Venarian Gold)
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
if (xPay == 0) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/BondAi.java b/forge-ai/src/main/java/forge/ai/ability/BondAi.java
index 8619983df69..bd7ea559f1c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/BondAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/BondAi.java
@@ -49,7 +49,6 @@ public final class BondAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
} // end bondCanPlayAI()
-
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java b/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java
index a0afe996eac..2e303b29776 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CanPlayAsDrawbackAi.java
@@ -1,9 +1,5 @@
package forge.ai.ability;
-
-import java.util.List;
-import java.util.Map;
-
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -36,11 +32,4 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
return false;
}
-
- @Override
- public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List spells,
- Map params) {
- // This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
- return spells.get(0);
- }
}
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 6e2fc09207e..c8cd1122060 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChangeZoneAi.java
@@ -407,6 +407,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (num.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
+ if (xPay == 0) return false;
xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay);
}
@@ -1129,7 +1130,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
- boolean doWithoutTarget = sa.hasParam("Planeswalker") && sa.usesTargeting()
+ boolean doWithoutTarget = sa.isPwAbility() && sa.usesTargeting()
&& sa.getMinTargets() == 0
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class);
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 777776f5aa7..3a73a3876d0 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CharmAi.java
@@ -23,21 +23,22 @@ import forge.util.collect.FCollection;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
- // sa is Entwined, no need for extra logic
- if (sa.isEntwine()) {
- return true;
- }
-
final Card source = sa.getHostCard();
+ List choices = CharmEffect.makePossibleOptions(sa);
- final int num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
- final int min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
+ final int num;
+ final int min;
+ if (sa.isEntwine()) {
+ num = min = choices.size();
+ } else {
+ num = AbilityUtils.calculateAmount(source, sa.getParamOrDefault("CharmNum", "1"), sa);
+ min = sa.hasParam("MinCharmNum") ? AbilityUtils.calculateAmount(source, sa.getParamOrDefault("MinCharmNum", "1"), sa) : num;
+ }
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
sa.setChosenList(null);
- List choices = CharmEffect.makePossibleOptions(sa);
List chosenList;
if (!ai.equals(sa.getActivatingPlayer())) {
@@ -159,7 +160,7 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(allyTainted ? gain : lose);
} else if (oppTainted || ai.getGame().isCardInPlay("Rain of Gore")) {
// Rain of Gore does negate lifegain, so don't benefit the others
- // same for if a oppoent does control Tainted Remedy
+ // same for if a opponent does control Tainted Remedy
// but if ai cant gain life, the effects are negated
chosenList.add(ai.canGainLife() ? lose : gain);
} else if (ai.getGame().isCardInPlay("Sulfuric Vortex")) {
@@ -177,13 +178,13 @@ public class CharmAi extends SpellAbilityAi {
chosenList.add(gain);
} else if(!ai.canGainLife() && aiLife == 14 ) {
// ai cant gain life, but try to avoid falling to 13
- // but if a oppoent does control Tainted Remedy its irrelevant
+ // but if a opponent does control Tainted Remedy its irrelevant
chosenList.add(oppTainted ? lose : gain);
} else if (allyTainted) {
// Tainted Remedy negation logic, try gain instead of lose
// because negation does turn it into lose for opponents
boolean oppCritical = false;
- // an oppoent is Critical = 14, and can't gain life, try to lose life instead
+ // an opponent is Critical = 14, and can't gain life, try to lose life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 14) {
for (Player p : opponents) {
@@ -197,7 +198,7 @@ public class CharmAi extends SpellAbilityAi {
} else {
// normal logic, try to gain life if its critical
boolean oppCritical = false;
- // an oppoent is Critical = 12, and can gain life, try to gain life instead
+ // an opponent is Critical = 12, and can gain life, try to gain life instead
// but only if ai doesn't kill itself with that.
if (aiLife != 12) {
for (Player p : opponents) {
@@ -224,6 +225,8 @@ public class CharmAi extends SpellAbilityAi {
goodChoice = sub;
} else {
// Standard canPlayAi()
+ sub.setActivatingPlayer(ai);
+ sub.getRestrictions().setZone(sub.getParent().getRestrictions().getZone());
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
chosenList.add(sub);
if (chosenList.size() == min) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java
index 4a0ded95dc2..9775cd32395 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseCardNameAi.java
@@ -63,7 +63,6 @@ public class ChooseCardNameAi extends SpellAbilityAi {
*/
@Override
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) {
-
return ComputerUtilCard.getBestAI(options);
}
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 91690a61212..c6eba2b0c10 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ChooseTypeAi.java
@@ -34,6 +34,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
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"))) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java
index 0c6c65ff151..b02be7b92ab 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ClashAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ClashAi.java
@@ -33,7 +33,6 @@ public class ClashAi extends SpellAbilityAi {
return legalAction;
}
-
/*
* (non-Javadoc)
*
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 e0f03379bd2..0799f5a1ba8 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CopyPermanentAi.java
@@ -12,6 +12,7 @@ import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
+import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -35,7 +36,6 @@ import forge.game.zone.ZoneType;
public class CopyPermanentAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
- // TODO - I'm sure someone can do this AI better
Card source = sa.getHostCard();
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -45,7 +45,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
if ("MomirAvatar".equals(aiLogic)) {
- return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
+ return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
} else if ("MimicVat".equals(aiLogic)) {
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
} else if ("AtEOT".equals(aiLogic)) {
@@ -59,7 +59,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
}
- if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
+ if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) {
return false;
}
@@ -77,6 +77,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
}
+ if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
+ // Set PayX here to maximum value. (Osgir)
+ final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer);
+
+ sa.setXManaCostPaid(xPay);
+ }
+
if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) {
sa.resetTargets();
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
@@ -106,7 +113,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
return false;
}
} else {
- return this.doTriggerAINoCost(aiPlayer, sa, false);
+ return doTriggerAINoCost(aiPlayer, sa, false);
}
}
@@ -118,7 +125,6 @@ public class CopyPermanentAi extends SpellAbilityAi {
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final boolean canCopyLegendary = sa.hasParam("NonLegendary");
-
// ////
// Targeting
if (sa.usesTargeting()) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java
index 2ee98ae00b4..c24df2a5c39 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CopySpellAbilityAi.java
@@ -60,7 +60,6 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
-
// Filter AI-specific targets if provided
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
if (!top.getActivatingPlayer().equals(aiPlayer)) {
@@ -148,4 +147,3 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
}
-
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 37366e9c008..f9bce402881 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CounterAi.java
@@ -64,7 +64,6 @@ public class CounterAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
-
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
if (!CardFactoryUtil.isCounterableBy(topSA.getHostCard(), sa) || topSA.getActivatingPlayer() == ai
|| ai.getAllies().contains(topSA.getActivatingPlayer())) {
@@ -317,7 +316,7 @@ public class CounterAi extends SpellAbilityAi {
Iterator it = game.getStack().iterator();
SpellAbilityStackInstance si = null;
- while(it.hasNext()) {
+ while (it.hasNext()) {
si = it.next();
tgtSA = si.getSpellAbility(true);
if (!sa.canTargetSpellAbility(tgtSA)) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java
index 32e995d7f70..bb33b00f10e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersMoveAi.java
@@ -370,8 +370,7 @@ public class CountersMoveAi extends SpellAbilityAi {
Card lki = CardUtil.getLKICopy(src);
if (cType == null) {
lki.clearCounters();
- }
- else {
+ } else {
lki.setCounters(cType, 0);
}
// go for opponent when higher value implies debuff
diff --git a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java
index e95b3282b4d..1015832cf06 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersProliferateAi.java
@@ -27,7 +27,6 @@ public class CountersProliferateAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
-
final List cperms = Lists.newArrayList();
final List allies = ai.getAllies();
allies.add(ai);
@@ -87,7 +86,6 @@ public class CountersProliferateAi extends SpellAbilityAi {
}));
}
-
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
}
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 a7639d5b81e..aab3783cac0 100644
--- a/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/CountersPutAi.java
@@ -324,7 +324,7 @@ public class CountersPutAi extends SpellAbilityAi {
return false;
}
- if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
+ if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility());
if (!prot.isEmpty()) {
sa.getTargets().add(prot.get(0));
@@ -501,7 +501,7 @@ public class CountersPutAi extends SpellAbilityAi {
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
// but try to do it in Main 2 then so that the AI has a chance to play creatures first.
if (list.isEmpty()
- && sa.hasParam("Planeswalker")
+ && sa.isPwAbility()
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
&& sa.isTargetNumberValid()
&& sa.getTargets().size() == 0
@@ -707,8 +707,7 @@ public class CountersPutAi extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
- if ((!sa.isTargetNumberValid())
- || (sa.getTargets().size() == 0)) {
+ if ((!sa.isTargetNumberValid()) || (sa.getTargets().size() == 0)) {
sa.resetTargets();
return false;
} else {
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 fc08cab9ceb..5fe42d70cc4 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageDealAi.java
@@ -162,6 +162,12 @@ public class DamageDealAi extends DamageAiBase {
String logic = sa.getParamOrDefault("AILogic", "");
if ("DiscardLands".equals(logic)) {
dmg = 2;
+ } else if ("OpponentHasCreatures".equals(logic)) {
+ for (Player opp : ai.getOpponents()) {
+ if (!opp.getCreaturesInPlay().isEmpty()){
+ return true;
+ }
+ }
} else if (logic.startsWith("ProcRaid.")) {
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
@@ -806,14 +812,16 @@ public class DamageDealAi extends DamageAiBase {
} else if (o instanceof Player) {
final Player p = (Player) o;
final int restDamage = ComputerUtilCombat.predictDamageTo(p, dmg, saMe.getHostCard(), false);
- if (!p.isOpponentOf(ai) && p.canLoseLife() && restDamage + 3 >= p.getLife() && restDamage > 0) {
- // from this spell will kill me
- return false;
- }
- if (p.isOpponentOf(ai) && p.canLoseLife()) {
- positive = true;
- if (p.getLife() + 3 <= restDamage) {
- urgent = true;
+ if (restDamage > 0 && p.canLoseLife()) {
+ if (!p.isOpponentOf(ai) && restDamage + 3 >= p.getLife()) {
+ // from this spell will kill me
+ return false;
+ }
+ if (p.isOpponentOf(ai)) {
+ positive = true;
+ if (p.getLife() - 3 <= restDamage) {
+ urgent = true;
+ }
}
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java
index 443acaadc69..4d2e0dd4467 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DamageEachAi.java
@@ -47,7 +47,6 @@ public class DamageEachAi extends DamageAiBase {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
-
return mandatory || canPlayAI(ai, sa);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
index 14e97275514..7d7a9c47823 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java
@@ -219,7 +219,6 @@ public class DestroyAi extends SpellAbilityAi {
return false;
}
-
// target loop
// TODO use can add more Targets
while (sa.getTargets().size() < maxTargets) {
@@ -411,7 +410,6 @@ public class DestroyAi extends SpellAbilityAi {
} else {
return mandatory;
}
-
}
public boolean doLandForLandRemovalLogic(SpellAbility sa, Player ai, Card tgtLand, String logic) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java
index 17d69dcdbc0..9bdbeb8f6c1 100644
--- a/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/DiscardAi.java
@@ -163,8 +163,6 @@ public class DiscardAi extends SpellAbilityAi {
return false;
} // discardTargetAI()
-
-
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -211,9 +209,8 @@ public class DiscardAi extends SpellAbilityAi {
return true;
} // discardCheckDrawbackAI()
-
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
- if ( mode == PlayerActionConfirmMode.Random ) { //
+ if ( mode == PlayerActionConfirmMode.Random ) {
// TODO For now AI will always discard Random used currently with: Balduvian Horde and similar cards
return true;
}
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 e643da47300..7b7ee50f573 100644
--- a/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/EffectAi.java
@@ -233,10 +233,10 @@ public class EffectAi extends SpellAbilityAi {
return ai.getCreaturesInPlay().size() >= i;
}
return true;
- } else if (logic.equals("CastFromGraveThisTurn")) {
+ } else if (logic.equals("ReplaySpell")) {
CardCollection list = new CardCollection(game.getCardsIn(ZoneType.Graveyard));
list = CardLists.getValidCards(list, sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
- if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
+ if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return false;
}
} else if (logic.equals("Bribe")) {
@@ -313,6 +313,5 @@ public class EffectAi extends SpellAbilityAi {
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
-
}
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java
index 53587f59cd6..3f68bd73ed8 100644
--- a/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/EncodeAi.java
@@ -56,7 +56,6 @@ public final class EncodeAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
}
-
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
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 261c4e9d9d6..7cc4abf0dce 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
@@ -69,4 +69,3 @@ public class ExploreAi extends SpellAbilityAi {
}
}
-
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 f93c625b14b..56bea9a0ac2 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FightAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FightAi.java
@@ -284,7 +284,7 @@ public class FightAi extends SpellAbilityAi {
}
return 0;
}
-
+
private static boolean shouldFight(Card fighter, Card opponent, int pumpAttack, int pumpDefense) {
if (canKill(fighter, opponent, pumpAttack)) {
if (!canKill(opponent, fighter, -pumpDefense)) { // can survive
diff --git a/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java b/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java
index 984f3c75fab..d41ac5a2aea 100644
--- a/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/FlipACoinAi.java
@@ -14,7 +14,6 @@ public class FlipACoinAi extends SpellAbilityAi {
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
-
if (sa.hasParam("AILogic")) {
String ailogic = sa.getParam("AILogic");
if (ailogic.equals("Never")) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java
index 1ae9da74c7b..e58cf83d66c 100644
--- a/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/InvestigateAi.java
@@ -24,4 +24,3 @@ public class InvestigateAi extends SpellAbilityAi {
return true;
}
}
-
diff --git a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java
index f91ef1ba505..7ac6840c602 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LegendaryRuleAi.java
@@ -4,8 +4,10 @@ import java.util.Map;
import com.google.common.collect.Iterables;
+import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
+import forge.game.card.CardCollection;
import forge.game.card.CounterEnumType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -23,15 +25,17 @@ public class LegendaryRuleAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false; // should not get here
}
-
@Override
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable options, boolean isOptional, Player targetedPlayer, Map params) {
// Choose a single legendary/planeswalker card to keep
- Card firstOption = Iterables.getFirst(options, null);
+ CardCollection legends = new CardCollection(options);
+ CardCollection badOptions = ComputerUtil.choosePermanentsToSacrifice(ai, legends, legends.size() -1, sa, false, false);
+ legends.removeAll(badOptions);
+ Card firstOption = Iterables.getFirst(legends, null);
boolean choosingFromPlanewalkers = firstOption.isPlaneswalker();
- if ( choosingFromPlanewalkers ) {
+ if (choosingFromPlanewalkers) {
// AI decision making - should AI compare counters?
} else {
// AI decision making - should AI compare damage and debuffs?
diff --git a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java
index 2aba049fa22..2f906d3e53f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/LifeExchangeAi.java
@@ -74,7 +74,6 @@ public class LifeExchangeAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa,
final boolean mandatory) {
-
final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (tgt != null) {
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 0d26dcd0667..35bc3ad2afe 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ManaEffectAi.java
@@ -167,7 +167,7 @@ public class ManaEffectAi extends SpellAbilityAi {
List all = ComputerUtilAbility.getSpellAbilities(ai.getCardsIn(ZoneType.Hand), ai);
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana();
- boolean canPayWithAvailableColors = cost.canBePaidWithAvaliable(ColorSet.fromNames(
+ boolean canPayWithAvailableColors = cost.canBePaidWithAvailable(ColorSet.fromNames(
ComputerUtilCost.getAvailableManaColors(ai, (List)null)).getColor());
if (cost.getCMC() == 0 && cost.countX() == 0) {
diff --git a/forge-ai/src/main/java/forge/ai/ability/MillAi.java b/forge-ai/src/main/java/forge/ai/ability/MillAi.java
index 520a54b63df..f10a3ed8104 100644
--- a/forge-ai/src/main/java/forge/ai/ability/MillAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/MillAi.java
@@ -53,7 +53,7 @@ public class MillAi extends SpellAbilityAi {
} else if ("ExileAndPlayOrDealDamage".equals(sa.getParam("AILogic"))) {
return (ph.is(PhaseType.MAIN1) || ph.is(PhaseType.MAIN2)) && ph.isPlayerTurn(ai); // Chandra, Torch of Defiance and similar
}
- if (!sa.hasParam("Planeswalker")) { // Planeswalker abilities are only activated at sorcery speed
+ if (!sa.isPwAbility()) { // Planeswalker abilities are only activated at sorcery speed
if ("You".equals(sa.getParam("Defined")) && !(!SpellAbilityAi.isSorcerySpeed(sa) && ph.is(PhaseType.END_OF_TURN)
&& ph.getNextTurn().equals(ai))) {
return false; // only self-mill at opponent EOT
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 479f964478b..bf81698f1b7 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PermanentCreatureAi.java
@@ -175,7 +175,6 @@ public class PermanentCreatureAi extends PermanentAi {
}
}
-
if (hasFloatMana || willDiscardNow || willDieNow) {
// Will lose mana in pool or about to discard a card in cleanup or about to die in combat, so use this opportunity
return true;
@@ -206,7 +205,6 @@ public class PermanentCreatureAi extends PermanentAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
-
if (!super.checkApiLogic(ai, sa)) {
return false;
}
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 cfd1c82ce58..8b57bfd3f92 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PlayAi.java
@@ -1,47 +1,33 @@
package forge.ai.ability;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import forge.ai.*;
+import forge.card.CardStateName;
+import forge.card.CardTypeView;
+import forge.card.mana.ManaCost;
+import forge.game.Game;
+import forge.game.GameType;
+import forge.game.ability.AbilityUtils;
+import forge.game.card.*;
+import forge.game.cost.Cost;
+import forge.game.keyword.Keyword;
+import forge.game.player.Player;
+import forge.game.player.PlayerActionConfirmMode;
+import forge.game.spellability.*;
+import forge.game.zone.ZoneType;
+import forge.util.MyRandom;
+
import java.util.Iterator;
import java.util.List;
import java.util.Map;
-import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Lists;
-
-import forge.ai.AiController;
-import forge.ai.AiPlayDecision;
-import forge.ai.AiProps;
-import forge.ai.ComputerUtil;
-import forge.ai.ComputerUtilCard;
-import forge.ai.PlayerControllerAi;
-import forge.ai.SpellAbilityAi;
-import forge.card.CardStateName;
-import forge.card.CardTypeView;
-import forge.game.Game;
-import forge.game.GameType;
-import forge.game.ability.AbilityUtils;
-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.cost.Cost;
-import forge.game.keyword.Keyword;
-import forge.game.player.Player;
-import forge.game.spellability.Spell;
-import forge.game.spellability.SpellAbility;
-import forge.game.spellability.SpellAbilityPredicates;
-import forge.game.spellability.SpellPermanent;
-import forge.game.spellability.TargetRestrictions;
-import forge.game.zone.ZoneType;
-import forge.util.MyRandom;
-
public class PlayAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
final String logic = sa.hasParam("AILogic") ? sa.getParam("AILogic") : "";
-
+
final Game game = ai.getGame();
final Card source = sa.getHostCard();
// don't use this as a response (ReplaySpell logic is an exception, might be called from a subability
@@ -54,34 +40,9 @@ public class PlayAi extends SpellAbilityAi {
return false; // prevent infinite loop
}
- CardCollection cards = null;
- final TargetRestrictions tgt = sa.getTargetRestrictions();
- if (tgt != null) {
- ZoneType zone = tgt.getZone().get(0);
- cards = CardLists.getValidCards(game.getCardsIn(zone), tgt.getValidTgts(), ai, source, sa);
- if (cards.isEmpty()) {
- return false;
- }
- } else if (!sa.hasParam("Valid")) {
- cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
- if (cards.isEmpty()) {
- return false;
- }
- }
-
- if (sa.hasParam("ValidSA")) {
- final String valid[] = {sa.getParam("ValidSA")};
- final Iterator itr = cards.iterator();
- while (itr.hasNext()) {
- final Card c = itr.next();
- final List validSA = Lists.newArrayList(Iterables.filter(AbilityUtils.getBasicSpellsFromPlayEffect(c, ai), SpellAbilityPredicates.isValid(valid, ai , c, sa)));
- if (validSA.size() == 0) {
- itr.remove();
- }
- }
- if (cards.isEmpty()) {
- return false;
- }
+ CardCollection cards = getPlayableCards(sa, ai);
+ if (cards.isEmpty()) {
+ return false;
}
if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) {
@@ -100,25 +61,32 @@ public class PlayAi extends SpellAbilityAi {
}
}
- // Ensure that if a ValidZone is specified, there's at least something to choose from in that zone.
- CardCollectionView validOpts = new CardCollection();
- if (sa.hasParam("ValidZone")) {
- validOpts = AbilityUtils.filterListByType(game.getCardsIn(ZoneType.valueOf(sa.getParam("ValidZone"))),
- sa.getParam("Valid"), sa);
- if (validOpts.isEmpty()) {
- return false;
- }
- }
-
if ("ReplaySpell".equals(logic)) {
- return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
+ return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"), false);
} else if (logic.startsWith("NeedsChosenCard")) {
int minCMC = 0;
if (sa.getPayCosts().getCostMana() != null) {
minCMC = sa.getPayCosts().getTotalMana().getCMC();
}
- validOpts = CardLists.filter(validOpts, CardPredicates.greaterCMC(minCMC));
- return chooseSingleCard(ai, sa, validOpts, sa.hasParam("Optional"), null, null) != null;
+ cards = CardLists.filter(cards, CardPredicates.greaterCMC(minCMC));
+ return chooseSingleCard(ai, sa, cards, sa.hasParam("Optional"), null, null) != null;
+ } else if ("WithTotalCMC".equals(logic)) {
+ // Try to play only when there are more than three playable cards.
+ if (cards.size() < 3)
+ return false;
+ ManaCost mana = sa.getPayCosts().getTotalMana();
+ if (mana.countX() > 0) {
+ int amount = ComputerUtilCost.getMaxXValue(sa, ai);
+ if (amount < ComputerUtilCard.getBestAI(cards).getCMC())
+ return false;
+ int totalCMC = 0;
+ for (Card c : cards) {
+ totalCMC += c.getCMC();
+ }
+ if (amount > totalCMC)
+ amount = totalCMC;
+ sa.setXManaCostPaid(amount);
+ }
}
if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) {
@@ -133,7 +101,7 @@ public class PlayAi extends SpellAbilityAi {
return true;
}
-
+
/**
*
* doTriggerAINoCost
@@ -151,13 +119,22 @@ public class PlayAi extends SpellAbilityAi {
if (!sa.hasParam("AILogic")) {
return false;
}
-
+
+ if ("ReplaySpell".equals(sa.getParam("AILogic"))) {
+ return ComputerUtil.targetPlayableSpellCard(ai, getPlayableCards(sa, ai), sa, sa.hasParam("WithoutManaCost"), mandatory);
+ }
+
return checkApiLogic(ai, sa);
}
return true;
}
+ @Override
+ public boolean confirmAction(Player ai, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
+ return true;
+ }
+
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
*/
@@ -167,12 +144,19 @@ public class PlayAi extends SpellAbilityAi {
List tgtCards = CardLists.filter(options, new Predicate() {
@Override
public boolean apply(final Card c) {
+ // TODO needs to be aligned for MDFC along with getAbilityToPlay so the knowledge
+ // of which spell was the reason for the choice can be used there
for (SpellAbility s : c.getBasicSpells(c.getState(CardStateName.Original))) {
Spell spell = (Spell) s;
s.setActivatingPlayer(ai);
// timing restrictions still apply
if (!s.getRestrictions().checkTimingRestrictions(c, s))
continue;
+ if (params != null && params.containsKey("CMCLimit")) {
+ Integer cmcLimit = (Integer) params.get("CMCLimit");
+ if (spell.getPayCosts().getTotalMana().getCMC() > cmcLimit)
+ continue;
+ }
if (sa.hasParam("WithoutManaCost")) {
// Try to avoid casting instants and sorceries with X in their cost, since X will be assumed to be 0.
if (!(spell instanceof SpellPermanent)) {
@@ -192,7 +176,7 @@ public class PlayAi extends SpellAbilityAi {
spell = (Spell) spell.copyWithDefinedCost(abCost);
}
- if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
+ if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !(isOptional || sa.hasParam("Optional")), 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.
@@ -204,4 +188,36 @@ public class PlayAi extends SpellAbilityAi {
});
return ComputerUtilCard.getBestAI(tgtCards);
}
+
+ private static CardCollection getPlayableCards(SpellAbility sa, Player ai) {
+ CardCollection cards = new CardCollection();
+ final TargetRestrictions tgt = sa.getTargetRestrictions();
+ final Card source = sa.getHostCard();
+
+ if (tgt != null) {
+ ZoneType zone = tgt.getZone().get(0);
+ cards = CardLists.getValidCards(ai.getGame().getCardsIn(zone), tgt.getValidTgts(), ai, source, sa);
+ } else if (!sa.hasParam("Valid")) {
+ cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
+ }
+
+ if (cards != null & sa.hasParam("ValidSA")) {
+ final String valid[] = sa.getParam("ValidSA").split(",");
+ 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))) {
+ itr.remove();
+ }
+ }
+ }
+
+ // Ensure that if a ValidZone is specified, there's at least something to choose from in that zone.
+ if (sa.hasParam("ValidZone")) {
+ cards = new CardCollection(AbilityUtils.filterListByType(ai.getGame().getCardsIn(ZoneType.valueOf(sa.getParam("ValidZone"))),
+ sa.getParam("Valid"), sa));
+ }
+ return cards;
+ }
+
}
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 5582a1d16dc..8bd2b53a857 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAi.java
@@ -596,7 +596,7 @@ public class PumpAi extends PumpAiBase {
}
if ("Snapcaster".equals(sa.getParam("AILogic"))) {
- if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
+ if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return false;
}
}
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 da96861d9a5..82620607240 100644
--- a/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/PumpAllAi.java
@@ -85,9 +85,6 @@ public class PumpAllAi extends PumpAiBase {
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
- if (!game.getStack().isEmpty() && !sa.isCurse()) {
- return pumpAgainstRemoval(ai, sa, comp);
- }
if (sa.isCurse()) {
if (defense < 0) { // try to destroy creatures
comp = CardLists.filter(comp, new Predicate() {
@@ -143,6 +140,10 @@ public class PumpAllAi extends PumpAiBase {
return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human);
} // end Curse
+ if (!game.getStack().isEmpty()) {
+ return pumpAgainstRemoval(ai, sa, comp);
+ }
+
return !CardLists.getValidCards(getPumpCreatures(ai, sa, defense, power, keywords, false), valid, source.getController(), source, sa).isEmpty();
} // pumpAllCanPlayAI()
@@ -153,6 +154,11 @@ public class PumpAllAi extends PumpAiBase {
@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")) {
+ return true;
+ }
+
// important to call canPlay first so targets are added if needed
return canPlayAI(ai, sa) || mandatory;
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java
index 004591af7a8..b3bd6f61e57 100644
--- a/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/RearrangeTopOfLibraryAi.java
@@ -107,7 +107,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
}
- Player p = pc.getFirst(); // FIXME: is this always a single target spell?
+ Player p = pc.getFirst(); // currently always a single target spell
Card top = p.getCardsIn(ZoneType.Library).getFirst();
int landsOTB = CardLists.filter(p.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.LANDS_PRODUCING_MANA).size();
int cmc = top.isSplitCard() ? Math.min(top.getCMC(Card.SplitCMCMode.LeftSplitCMC), top.getCMC(Card.SplitCMCMode.RightSplitCMC))
diff --git a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java
index afcd0879fbd..1cb7766e11f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/RepeatEachAi.java
@@ -50,7 +50,7 @@ public class RepeatEachAi extends SpellAbilityAi {
return false;
}
}
- } else if ("OpponentHasCreatures".equals(logic)) {
+ } else if ("OpponentHasCreatures".equals(logic)) { //TODO convert this to NeedsToPlayVar
for (Player opp : aiPlayer.getOpponents()) {
if (!opp.getCreaturesInPlay().isEmpty()){
return true;
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 9fd83435a03..f3b160a1f86 100644
--- a/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/ScryAi.java
@@ -24,7 +24,6 @@ public class ScryAi extends SpellAbilityAi {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
-
if (sa.usesTargeting()) { // It doesn't appear that Scry ever targets
// ability is targeted
sa.resetTargets();
@@ -69,7 +68,7 @@ public class ScryAi extends SpellAbilityAi {
// in the playerturn Scry should only be done in Main1 or in upkeep if able
if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa)) {
- return ph.is(PhaseType.MAIN1) || sa.hasParam("Planeswalker");
+ return ph.is(PhaseType.MAIN1) || sa.isPwAbility();
} else {
return ph.is(PhaseType.UPKEEP);
}
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 29ded831c7f..ff78b3cc8fd 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SetStateAi.java
@@ -199,7 +199,6 @@ public class SetStateAi extends SpellAbilityAi {
return false;
}
-
// check which state would be better for attacking
if (ph.isPlayerTurn(ai) && ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
boolean transformAttack = false;
diff --git a/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java b/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java
index bec7848d51e..7e2cdf2423b 100644
--- a/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/StoreSVarAi.java
@@ -66,7 +66,6 @@ public class StoreSVarAi extends SpellAbilityAi {
return false;
}
-
return true;
}
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 94c40655dec..573ab9e993e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/SurveilAi.java
@@ -24,7 +24,6 @@ public class SurveilAi extends SpellAbilityAi {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
-
if (sa.usesTargeting()) { // TODO: It doesn't appear that Surveil ever targets, is this necessary?
sa.resetTargets();
sa.getTargets().add(ai);
@@ -60,7 +59,7 @@ public class SurveilAi extends SpellAbilityAi {
// in the player's turn Surveil should only be done in Main1 or in Upkeep if able
if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa)) {
- return ph.is(PhaseType.MAIN1) || sa.hasParam("Planeswalker");
+ return ph.is(PhaseType.MAIN1) || sa.isPwAbility();
} else {
return ph.is(PhaseType.UPKEEP);
}
diff --git a/forge-ai/src/main/java/forge/ai/ability/TapAi.java b/forge-ai/src/main/java/forge/ai/ability/TapAi.java
index 7216345b1f8..52abe9535f0 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapAi.java
@@ -17,7 +17,6 @@ import forge.game.spellability.SpellAbility;
public class TapAi extends TapAiBase {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
-
final PhaseHandler phase = ai.getGame().getPhaseHandler();
final Player turn = phase.getPlayerTurn();
@@ -71,7 +70,6 @@ public class TapAi extends TapAiBase {
sa.resetTargets();
return tapPrefTargeting(ai, source, sa, false);
}
-
}
}
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 95efebb90c8..035bb2d4cd9 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TapAiBase.java
@@ -311,7 +311,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
}
final List pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
- return pDefined.isEmpty() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai);
+ // might be from ETBreplacement
+ return pDefined.isEmpty() || !pDefined.get(0).isInPlay() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai);
} else {
sa.resetTargets();
if (tapPrefTargeting(ai, source, sa, mandatory)) {
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 411291947a8..3b3a31a953e 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TokenAi.java
@@ -95,7 +95,7 @@ public class TokenAi extends SpellAbilityAi {
if (sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
x = ComputerUtilCost.getMaxXValue(sa, ai);
- sa.setXManaCostPaid(x);
+ sa.getRootAbility().setXManaCostPaid(x);
}
if (x <= 0) {
return false; // 0 tokens or 0 toughness token(s)
@@ -250,9 +250,6 @@ public class TokenAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
- String tokenAmount = sa.getParamOrDefault("TokenAmount", "1");
-
- final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
@@ -262,16 +259,21 @@ public class TokenAi extends SpellAbilityAi {
sa.getTargets().add(ai);
}
}
+
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");
+ final Card source = sa.getHostCard();
if ("X".equals(tokenAmount) || "X".equals(tokenPower) || "X".equals(tokenToughness)) {
int x = AbilityUtils.calculateAmount(source, tokenAmount, sa);
if (sa.getSVar("X").equals("Count$xPaid")) {
- // Set PayX here to maximum value.
- x = ComputerUtilCost.getMaxXValue(sa, ai);
- sa.setXManaCostPaid(x);
+ if (x == 0) { // already paid outside trigger
+ // Set PayX here to maximum value.
+ x = ComputerUtilCost.getMaxXValue(sa, ai);
+ sa.setXManaCostPaid(x);
+ }
}
if (x <= 0) {
return false;
@@ -358,7 +360,8 @@ public class TokenAi extends SpellAbilityAi {
if (!sa.hasParam("TokenScript")) {
throw new RuntimeException("Spell Ability has no TokenScript: " + sa);
}
- Card result = TokenInfo.getProtoType(sa.getParam("TokenScript"), sa);
+ // TODO for now, only checking the first token is good enough
+ Card result = TokenInfo.getProtoType(sa.getParam("TokenScript").split(",")[0], sa, ai);
if (result == null) {
throw new RuntimeException("don't find Token for TokenScript: " + sa.getParam("TokenScript"));
diff --git a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java
index d5b687f0774..fd55af81ab8 100644
--- a/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/TwoPilesAi.java
@@ -47,8 +47,7 @@ public class TwoPilesAi extends SpellAbilityAi {
CardCollectionView pool;
if (sa.hasParam("DefinedCards")) {
pool = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("DefinedCards"), sa);
- }
- else {
+ } else {
pool = p.getCardsIn(zone);
}
pool = CardLists.getValidCards(pool, valid, card.getController(), card, sa);
diff --git a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java
index 1e017a982d8..2bd45aa83ca 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UnattachAllAi.java
@@ -22,7 +22,6 @@ public class UnattachAllAi extends SpellAbilityAi {
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
-
// prevent run-away activations - first time will always return true
boolean chance = MyRandom.getRandom().nextFloat() <= .9;
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 8816b0a5506..4b75a426036 100644
--- a/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/UntapAi.java
@@ -374,7 +374,7 @@ public class UntapAi extends SpellAbilityAi {
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().getRestiction());
+ ManaCostBeingPaid reduced = new ManaCostBeingPaid(ab.getPayCosts().getCostMana().getManaCostFor(ab), ab.getPayCosts().getCostMana().getRestriction());
reduced.decreaseShard(ManaCostShard.GENERIC, untappingCards.size());
if (ComputerUtilMana.canPayManaCost(reduced, ab, ai)) {
CardCollection manaLandsTapped = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
diff --git a/forge-ai/src/main/java/forge/ai/ability/VentureAi.java b/forge-ai/src/main/java/forge/ai/ability/VentureAi.java
index e836ad3fde2..f3e7c88642f 100644
--- a/forge-ai/src/main/java/forge/ai/ability/VentureAi.java
+++ b/forge-ai/src/main/java/forge/ai/ability/VentureAi.java
@@ -2,10 +2,8 @@ package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.AiPlayDecision;
-import forge.ai.AiProps;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
-import forge.card.ICardFace;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
@@ -52,24 +50,4 @@ public class VentureAi extends SpellAbilityAi {
return Aggregates.random(spells); // If we're here, we should choose at least something, so choose a random thing then
}
- // AI that chooses which dungeon to venture into
- @Override
- public String chooseCardName(Player ai, SpellAbility sa, List faces) {
- // TODO: improve the conditions that define which dungeon is a viable option to choose
- List dungeonNames = Lists.newArrayList();
- for (ICardFace face : faces) {
- dungeonNames.add(face.getName());
- }
-
- // Don't choose Tomb of Annihilation when life in danger unless we can win right away or can't lose for 0 life
- if (ai.getController().isAI()) { // FIXME: is this needed? Can simulation ever run this for a non-AI player?
- int lifeInDanger = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
- if ((ai.getLife() <= lifeInDanger && !ai.cantLoseForZeroOrLessLife())
- && !(ai.getLife() > 1 && ai.getWeakestOpponent().getLife() == 1)) {
- dungeonNames.remove("Tomb of Annihilation");
- }
- }
-
- return Aggregates.random(dungeonNames);
- }
}
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 3e9d300fe7b..5950f124f0f 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/GameCopier.java
@@ -229,12 +229,12 @@ public class GameCopier {
private static final boolean USE_FROM_PAPER_CARD = true;
private Card createCardCopy(Game newGame, Player newOwner, Card c) {
- if (c.isToken() && !c.isEmblem()) {
+ if (c.isToken() && !c.isImmutable()) {
Card result = new TokenInfo(c).makeOneToken(newOwner);
CardFactory.copyCopiableCharacteristics(c, result);
return result;
}
- if (USE_FROM_PAPER_CARD && !c.isEmblem() && c.getPaperCard() != null) {
+ if (USE_FROM_PAPER_CARD && !c.isImmutable() && c.getPaperCard() != null) {
Card newCard = Card.fromPaperCard(c.getPaperCard(), newOwner);
newCard.setCommander(c.isCommander());
return newCard;
@@ -285,8 +285,12 @@ public class GameCopier {
}
newCard.setPTBoost(c.getPTBoostTable());
newCard.setDamage(c.getDamage());
-
+
+ newCard.setChangedCardColors(c.getChangedCardColorsMap());
+ newCard.setChangedCardColorsCharacterDefining(c.getChangedCardColorsCharacterDefiningMap());
+
newCard.setChangedCardTypes(c.getChangedCardTypesMap());
+ newCard.setChangedCardTypesCharacterDefining(c.getChangedCardTypesCharacterDefiningMap());
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
newCard.setChangedCardNames(c.getChangedCardNames());
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 06340311dda..412f2bd8495 100644
--- a/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
+++ b/forge-ai/src/main/java/forge/ai/simulation/SpellAbilityPicker.java
@@ -431,7 +431,7 @@ public class SpellAbilityPicker {
}
}
- public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount) {
+ public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount, final CardCollectionView exclude) {
if (amount == 1) {
Card source = ability.getHostCard();
CardCollection cardList = CardLists.getValidCards(player.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
@@ -447,7 +447,7 @@ public class SpellAbilityPicker {
}
}
}
- return ComputerUtil.chooseSacrificeType(player, type, ability, ability.getTargetCard(), amount);
+ return ComputerUtil.chooseSacrificeType(player, type, ability, ability.getTargetCard(), amount, exclude);
}
public static class PlayLandAbility extends LandAbility {
diff --git a/forge-core/pom.xml b/forge-core/pom.xml
index 1508c2e0070..3833b85bb38 100644
--- a/forge-core/pom.xml
+++ b/forge-core/pom.xml
@@ -6,7 +6,7 @@
forge
forge
- 1.6.43-SNAPSHOT
+ 1.6.45-SNAPSHOT
forge-core
diff --git a/forge-core/src/main/java/forge/CardStorageReader.java b/forge-core/src/main/java/forge/CardStorageReader.java
index d83f642b715..3911b078599 100644
--- a/forge-core/src/main/java/forge/CardStorageReader.java
+++ b/forge-core/src/main/java/forge/CardStorageReader.java
@@ -17,39 +17,20 @@
*/
package forge;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.Charset;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Comparator;
-import java.util.Enumeration;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.TreeSet;
-import java.util.concurrent.Callable;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Future;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
-
-import org.apache.commons.lang3.time.StopWatch;
-
import com.google.common.io.Files;
-
import forge.card.CardRules;
import forge.util.BuildInfo;
import forge.util.FileUtil;
import forge.util.Localizer;
import forge.util.ThreadUtil;
+import org.apache.commons.lang3.time.StopWatch;
+
+import java.io.*;
+import java.nio.charset.Charset;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
/**
*
@@ -138,7 +119,7 @@ public class CardStorageReader {
final CardRules.Reader rulesReader = new CardRules.Reader();
final List result = new ArrayList<>();
- for(int i = from; i < to; i++) {
+ for (int i = from; i < to; i++) {
final ZipEntry ze = files.get(i);
// if (ze.getName().endsWith(CardStorageReader.CARD_FILE_DOT_EXTENSION)) // already filtered!
result.add(this.loadCard(rulesReader, ze));
@@ -157,10 +138,13 @@ public class CardStorageReader {
if (c == '\'') {
continue;
}
- if (c < 'a' || c > 'z') {
+ if ((c < 'a' || c > 'z') && (c < '0' || c > '9')) {
if (charIndex > 0 && chars[charIndex - 1] == '_') {
continue;
}
+ // Comma separator in numbers: "Borrowing 100,000 Arrows"
+ if ((c == ',') && (charIndex > 0) && (chars[charIndex-1] >= '0' || chars[charIndex-1] <= '9'))
+ continue;
c = '_';
}
chars[charIndex++] = c;
@@ -218,7 +202,7 @@ public class CardStorageReader {
return file;
}
- public final CardRules attemptToLoadCard(String cardName, String setCode) {
+ public final CardRules attemptToLoadCard(String cardName) {
String transformedName = transformName(cardName);
CardRules rules = null;
@@ -310,16 +294,16 @@ public class CardStorageReader {
private void executeLoadTask(final Collection result, final List>> tasks, final CountDownLatch cdl) {
try {
- if ( useThreadPool ) {
+ if (useThreadPool) {
final ExecutorService executor = ThreadUtil.getComputingPool(0.5f);
final List>> parts = executor.invokeAll(tasks);
executor.shutdown();
cdl.await();
- for(final Future> pp : parts) {
+ for (final Future> pp : parts) {
result.addAll(pp.get());
}
} else {
- for(final Callable> c : tasks) {
+ for (final Callable> c : tasks) {
result.addAll(c.call());
}
}
diff --git a/forge-core/src/main/java/forge/ImageKeys.java b/forge-core/src/main/java/forge/ImageKeys.java
index 61baf6c0089..e825c09750a 100644
--- a/forge-core/src/main/java/forge/ImageKeys.java
+++ b/forge-core/src/main/java/forge/ImageKeys.java
@@ -1,15 +1,12 @@
package forge;
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-import org.apache.commons.lang3.StringUtils;
-
import forge.item.PaperCard;
import forge.util.FileUtil;
-import forge.util.ImageUtil;
import forge.util.TextUtil;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.util.*;
public final class ImageKeys {
public static final String CARD_PREFIX = "c:";
@@ -69,9 +66,8 @@ public final class ImageKeys {
}
public static File getImageFile(String key) {
- if (StringUtils.isEmpty(key)) {
+ if (StringUtils.isEmpty(key))
return null;
- }
final String dir;
final String filename;
@@ -130,6 +126,25 @@ public final class ImageKeys {
// if there's a 1st art variant try with it for .full images
file = findFile(dir, filename.replaceAll("[0-9]*.full", "1.full"));
if (file != null) { return file; }
+ //setlookup
+ if (!StaticData.instance().getSetLookup().isEmpty()) {
+ for (String setKey : StaticData.instance().getSetLookup().keySet()) {
+ if (filename.startsWith(setKey)) {
+ for (String setLookup : StaticData.instance().getSetLookup().get(setKey)) {
+ //.fullborder lookup
+ file = findFile(dir, TextUtil.fastReplace(fullborderFile, setKey, getSetFolder(setLookup)));
+ if (file != null) { return file; }
+ file = findFile(dir, TextUtil.fastReplace(fullborderFile, setKey, getSetFolder(setLookup)).replaceAll("[0-9]*.fullborder", "1.fullborder"));
+ if (file != null) { return file; }
+ //.full lookup
+ file = findFile(dir, TextUtil.fastReplace(filename, setKey, getSetFolder(setLookup)));
+ if (file != null) { return file; }
+ file = findFile(dir, TextUtil.fastReplace(filename, setKey, getSetFolder(setLookup)).replaceAll("[0-9]*.full", "1.full"));
+ if (file != null) { return file; }
+ }
+ }
+ }
+ }
}
//if an image, like phenomenon or planes is missing .full in their filenames but you have an existing images that have .full/.fullborder
if (!filename.contains(".full")) {
@@ -138,12 +153,6 @@ public final class ImageKeys {
file = findFile(dir, TextUtil.addSuffix(filename,".fullborder"));
if (file != null) { return file; }
}
- // some S00 cards are really part of 6ED
- String s2kAlias = getSetFolder("S00");
- if (filename.startsWith(s2kAlias)) {
- file = findFile(dir, TextUtil.fastReplace(filename, s2kAlias, getSetFolder("6ED")));
- if (file != null) { return file; }
- }
if (dir.equals(CACHE_TOKEN_PICS_DIR)) {
int index = filename.lastIndexOf('_');
@@ -208,14 +217,37 @@ public final class ImageKeys {
//shortcut for determining if a card image exists for a given card
//should only be called from PaperCard.hasImage()
+ static HashMap> cachedContent=new HashMap<>();
public static boolean hasImage(PaperCard pc) {
Boolean editionHasImage = editionImageLookup.get(pc.getEdition());
if (editionHasImage == null) {
String setFolder = getSetFolder(pc.getEdition());
editionHasImage = FileUtil.isDirectoryWithFiles(CACHE_CARD_PICS_DIR + setFolder);
editionImageLookup.put(pc.getEdition(), editionHasImage);
+ if (editionHasImage){
+ File f = new File(CACHE_CARD_PICS_DIR + setFolder); // no need to check this, otherwise editionHasImage would be false!
+ HashSet setFolderContent = new HashSet<>();
+ for (String filename : Arrays.asList(f.list())) {
+ // TODO: should this use FILE_EXTENSIONS ?
+ if (!filename.endsWith(".jpg") && !filename.endsWith(".png"))
+ continue; // not image - not interested
+ setFolderContent.add(filename.split("\\.")[0]); // get rid of any full or fullborder
+ }
+ cachedContent.put(setFolder, setFolderContent);
+ }
}
+ String[] keyParts = StringUtils.split(pc.getCardImageKey(), "//");
+ if (keyParts.length != 2)
+ return false;
+ HashSet content = cachedContent.getOrDefault(keyParts[0], null);
//avoid checking for file if edition doesn't have any images
- return editionHasImage && findFile(CACHE_CARD_PICS_DIR, ImageUtil.getImageKey(pc, false, true)) != null;
+ return editionHasImage && hitCache(content, keyParts[1]);
+ }
+
+ private static boolean hitCache(HashSet cache, String filename){
+ if (cache == null || cache.isEmpty())
+ return false;
+ final String keyPrefix = filename.split("\\.")[0];
+ return cache.contains(keyPrefix);
}
}
diff --git a/forge-core/src/main/java/forge/StaticData.java b/forge-core/src/main/java/forge/StaticData.java
index ac7c162207b..6b23eb176de 100644
--- a/forge-core/src/main/java/forge/StaticData.java
+++ b/forge-core/src/main/java/forge/StaticData.java
@@ -1,17 +1,7 @@
package forge;
-import java.io.File;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
import com.google.common.base.Predicate;
-
import forge.card.CardDb;
-import forge.card.CardDb.CardRequest;
import forge.card.CardEdition;
import forge.card.CardRules;
import forge.card.PrintSheet;
@@ -20,9 +10,14 @@ import forge.item.FatPack;
import forge.item.PaperCard;
import forge.item.SealedProduct;
import forge.token.TokenDb;
+import forge.util.FileUtil;
+import forge.util.TextUtil;
import forge.util.storage.IStorage;
import forge.util.storage.StorageBase;
+import java.io.File;
+import java.util.*;
+
/**
* The class holding game invariants, such as cards, editions, game formats. All that data, which is not supposed to be changed by player
@@ -53,7 +48,8 @@ public class StaticData {
private MulliganDefs.MulliganRule mulliganRule = MulliganDefs.getDefaultRule();
- private String prefferedArt;
+ private boolean allowCustomCardsInDecksConformance;
+ private boolean enableSmartCardArtSelection;
// Loaded lazily:
private IStorage boosters;
@@ -62,21 +58,28 @@ public class StaticData {
private IStorage fatPacks;
private IStorage boosterBoxes;
private IStorage printSheets;
+ private final Map> setLookup = new HashMap<>();
+ private List blocksLandCodes = new ArrayList<>();
private static StaticData lastInstance = null;
- public StaticData(CardStorageReader cardReader, CardStorageReader customCardReader, String editionFolder, String customEditionsFolder, String blockDataFolder, String prefferedArt, boolean enableUnknownCards, boolean loadNonLegalCards) {
- this(cardReader, null, customCardReader, editionFolder, customEditionsFolder, blockDataFolder, prefferedArt, enableUnknownCards, loadNonLegalCards);
+ public StaticData(CardStorageReader cardReader, CardStorageReader customCardReader, String editionFolder, String customEditionsFolder, String blockDataFolder, String cardArtPreference, boolean enableUnknownCards, boolean loadNonLegalCards) {
+ this(cardReader, null, customCardReader, editionFolder, customEditionsFolder, blockDataFolder, "", cardArtPreference, enableUnknownCards, loadNonLegalCards, false, false);
}
- public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, CardStorageReader customCardReader, String editionFolder, String customEditionsFolder, String blockDataFolder, String prefferedArt, boolean enableUnknownCards, boolean loadNonLegalCards) {
+ public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, CardStorageReader customCardReader, String editionFolder, String customEditionsFolder, String blockDataFolder, String setLookupFolder, String cardArtPreference, boolean enableUnknownCards, boolean loadNonLegalCards, boolean allowCustomCardsInDecksConformance){
+ this(cardReader, tokenReader, customCardReader, editionFolder, customEditionsFolder, blockDataFolder, setLookupFolder, cardArtPreference, enableUnknownCards, loadNonLegalCards, allowCustomCardsInDecksConformance, false);
+ }
+
+ public StaticData(CardStorageReader cardReader, CardStorageReader tokenReader, CardStorageReader customCardReader, String editionFolder, String customEditionsFolder, String blockDataFolder, String setLookupFolder, String cardArtPreference, boolean enableUnknownCards, boolean loadNonLegalCards, boolean allowCustomCardsInDecksConformance, boolean enableSmartCardArtSelection) {
this.cardReader = cardReader;
this.tokenReader = tokenReader;
this.editions = new CardEdition.Collection(new CardEdition.Reader(new File(editionFolder)));
this.blockDataFolder = blockDataFolder;
this.customCardReader = customCardReader;
- this.customEditions = new CardEdition.Collection(new CardEdition.Reader(new File(customEditionsFolder)));
- this.prefferedArt = prefferedArt;
+ this.customEditions = new CardEdition.Collection(new CardEdition.Reader(new File(customEditionsFolder), true));
+ this.allowCustomCardsInDecksConformance = allowCustomCardsInDecksConformance;
+ this.enableSmartCardArtSelection = enableSmartCardArtSelection;
lastInstance = this;
List funnyCards = new ArrayList<>();
List filtered = new ArrayList<>();
@@ -121,9 +124,9 @@ public class StaticData {
Collections.sort(filtered);
}
- commonCards = new CardDb(regularCards, editions, filtered);
- variantCards = new CardDb(variantsCards, editions, filtered);
- customCards = new CardDb(customizedCards, customEditions, filtered);
+ commonCards = new CardDb(regularCards, editions, filtered, cardArtPreference);
+ variantCards = new CardDb(variantsCards, editions, filtered, cardArtPreference);
+ customCards = new CardDb(customizedCards, customEditions, filtered, cardArtPreference);
//must initialize after establish field values for the sake of card image logic
commonCards.initialize(false, false, enableUnknownCards);
@@ -131,15 +134,25 @@ public class StaticData {
customCards.initialize(false, false, enableUnknownCards);
}
- {
+ if (this.tokenReader != null){
final Map tokens = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- for (CardRules card : tokenReader.loadCards()) {
+ for (CardRules card : this.tokenReader.loadCards()) {
if (null == card) continue;
tokens.put(card.getNormalizedName(), card);
}
allTokens = new TokenDb(tokens, editions);
+ } else {
+ allTokens = null;
+ }
+ //initialize setLookup
+ if (FileUtil.isDirectoryWithFiles(setLookupFolder)){
+ for (File f : Objects.requireNonNull(new File(setLookupFolder).listFiles())){
+ if (f.isFile()) {
+ setLookup.put(f.getName().replace(".txt",""), FileUtil.readFile(f));
+ }
+ }
}
}
@@ -147,6 +160,10 @@ public class StaticData {
return lastInstance;
}
+ public Map> getSetLookup() {
+ return setLookup;
+ }
+
public final CardEdition.Collection getEditions() {
return this.editions;
}
@@ -174,6 +191,22 @@ public class StaticData {
return sortedEditions;
}
+ private TreeMap> editionsTypeMap;
+ public final Map> getEditionsTypeMap(){
+ if (editionsTypeMap == null){
+ editionsTypeMap = new TreeMap<>();
+ for (CardEdition.Type editionType : CardEdition.Type.values()){
+ editionsTypeMap.put(editionType, new ArrayList<>());
+ }
+ for (CardEdition edition : this.getSortedEditions()){
+ CardEdition.Type key = edition.getType();
+ List editionsOfType = editionsTypeMap.get(key);
+ editionsOfType.add(edition);
+ }
+ }
+ return editionsTypeMap;
+ }
+
public CardEdition getCardEdition(String setCode){
CardEdition edition = this.editions.get(setCode);
if (edition == null) // try custom editions
@@ -183,60 +216,43 @@ public class StaticData {
public PaperCard getOrLoadCommonCard(String cardName, String setCode, int artIndex, boolean foil) {
PaperCard card = commonCards.getCard(cardName, setCode, artIndex);
- boolean isCustom = false;
if (card == null) {
attemptToLoadCard(cardName, setCode);
card = commonCards.getCard(cardName, setCode, artIndex);
}
- if (card == null) {
- card = commonCards.getCard(cardName, setCode, -1);
- }
- if (card == null) {
+ if (card == null)
+ card = commonCards.getCard(cardName, setCode);
+ if (card == null)
card = customCards.getCard(cardName, setCode, artIndex);
- if (card != null)
- isCustom = true;
- }
- if (card == null) {
- card = customCards.getCard(cardName, setCode, -1);
- if (card != null)
- isCustom = true;
- }
- if (card == null) {
+ if (card == null)
+ card = customCards.getCard(cardName, setCode);
+ if (card == null)
return null;
- }
- if (isCustom)
- return foil ? customCards.getFoiled(card) : card;
- return foil ? commonCards.getFoiled(card) : card;
+ return foil ? card.getFoiled() : card;
}
- public void attemptToLoadCard(String encodedCardName, String setCode) {
- CardDb.CardRequest r = CardRequest.fromString(encodedCardName);
- String cardName = r.cardName;
- CardRules rules = cardReader.attemptToLoadCard(cardName, setCode);
+ public void attemptToLoadCard(String cardName){
+ this.attemptToLoadCard(cardName, null);
+ }
+
+ public void attemptToLoadCard(String cardName, String setCode){
+ CardRules rules = cardReader.attemptToLoadCard(cardName);
CardRules customRules = null;
if (customCardReader != null) {
- customRules = customCardReader.attemptToLoadCard(cardName, setCode);
+ customRules = customCardReader.attemptToLoadCard(cardName);
}
if (rules != null) {
if (rules.isVariant()) {
- variantCards.loadCard(cardName, rules);
+ variantCards.loadCard(cardName, setCode, rules);
} else {
- commonCards.loadCard(cardName, rules);
+ commonCards.loadCard(cardName, setCode, rules);
}
}
if (customRules != null) {
- customCards.loadCard(cardName, customRules);
+ customCards.loadCard(cardName, setCode, customRules);
}
}
- // TODO Remove these in favor of them being associated to the Edition
- /** @return {@link forge.util.storage.IStorage}<{@link forge.item.SealedProduct.Template}> */
- public IStorage getFatPacks() {
- if (fatPacks == null)
- fatPacks = new StorageBase<>("Fat packs", new FatPack.Template.Reader(blockDataFolder + "fatpacks.txt"));
- return fatPacks;
- }
-
/** @return {@link forge.util.storage.IStorage}<{@link forge.item.SealedProduct.Template}> */
public final IStorage getTournamentPacks() {
if (tournaments == null)
@@ -275,9 +291,24 @@ public class StaticData {
return variantCards;
}
+ public Map getAvailableDatabases(){
+ Map databases = new HashMap<>();
+ databases.put("Common", commonCards);
+ databases.put("Custom", customCards);
+ databases.put("Variant", variantCards);
+ return databases;
+ }
+
+ public List getBlockLands() {
+ return blocksLandCodes;
+ }
+
public TokenDb getAllTokens() { return allTokens; }
-
+ public boolean allowCustomCardsInDecksConformance() {
+ return this.allowCustomCardsInDecksConformance;
+ }
+
public void setStandardPredicate(Predicate standardPredicate) { this.standardPredicate = standardPredicate; }
@@ -303,88 +334,241 @@ public class StaticData {
public Predicate getBrawlPredicate() { return brawlPredicate; }
- public String getPrefferedArtOption() { return prefferedArt; }
-
public void setFilteredHandsEnabled(boolean filteredHandsEnabled){
this.filteredHandsEnabled = filteredHandsEnabled;
}
- public PaperCard getCardByEditionDate(PaperCard card, Date editionDate) {
-
- PaperCard c = this.getCommonCards().getCardFromEdition(card.getName(), editionDate, CardDb.SetPreference.LatestCoreExp, card.getArtIndex());
-
- if (null != c) {
- return c;
- }
-
- c = this.getCommonCards().getCardFromEdition(card.getName(), editionDate, CardDb.SetPreference.LatestCoreExp, -1);
-
- if (null != c) {
- return c;
- }
-
- c = this.getCommonCards().getCardFromEdition(card.getName(), editionDate, CardDb.SetPreference.Latest, -1);
-
- if (null != c) {
- return c;
- }
-
- // I give up!
- return card;
+ /**
+ * Get an alternative card print for the given card wrt. the input setReleaseDate.
+ * The reference release date will be used to retrieve the alternative art, according
+ * to the Card Art Preference settings.
+ *
+ * Note: if input card is Foil, and an alternative card art is found, it will be returned foil too!
+ *
+ * @see StaticData#getAlternativeCardPrint(forge.item.PaperCard, java.util.Date)
+ * @param card Input Reference Card
+ * @param setReleaseDate reference set release date
+ * @return Alternative Card Art (from a different edition) of input card, or null if not found.
+ */
+ public PaperCard getAlternativeCardPrint(PaperCard card, final Date setReleaseDate) {
+ boolean isCardArtPreferenceLatestArt = this.cardArtPreferenceIsLatest();
+ boolean cardArtPreferenceHasFilter = this.isCoreExpansionOnlyFilterSet();
+ return this.getAlternativeCardPrint(card, setReleaseDate, isCardArtPreferenceLatestArt,
+ cardArtPreferenceHasFilter);
}
- public PaperCard getCardFromLatestorEarliest(PaperCard card) {
-
- PaperCard c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.Latest, card.getArtIndex());
-
- if (null != c && c.hasImage()) {
- return c;
- }
-
- c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.Latest, -1);
-
- if (null != c && c.hasImage()) {
- return c;
- }
-
- c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.LatestCoreExp, -1);
-
- if (null != c) {
- return c;
- }
-
- c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.EarliestCoreExp, -1);
-
- if (null != c) {
- return c;
- }
-
- // I give up!
- return card;
+ /**
+ * Retrieve an alternative card print for a given card, and the input reference set release date.
+ * The setReleaseDate will be used depending on the desired Card Art Preference policy to apply
+ * when looking for alternative card, namely Latest Art and with or without filters
+ * on editions.
+ *
+ * In more details:
+ * - If card art preference is Latest Art first, the alternative card print will be chosen from
+ * the first edition that has been released **after** the reference date.
+ * - Conversely, if card art preference is Original Art first, the alternative card print will be
+ * chosen from the first edition that has been released **before** the reference date.
+ *
+ * The rationale behind this strategy is to select an alternative card print from the lower-bound extreme
+ * (upper-bound extreme) among the latest (original) editions where the card can be found.
+ *
+ * @param card The instance of PaperCard to look for an alternative print
+ * @param setReleaseDate The reference release date used to control the search for alternative card print.
+ * The chose candidate will be gathered from an edition printed before (upper bound) or
+ * after (lower bound) the reference set release date.
+ * @param isCardArtPreferenceLatestArt Determines whether or not "Latest Art" Card Art preference should be used
+ * when looking for an alternative candidate print.
+ * @param cardArtPreferenceHasFilter Determines whether or not the search should only consider
+ * Core, Expansions, or Reprints sets when looking for alternative candidates.
+ * @return an instance of PaperCard that is the selected alternative candidate, or null
+ * if None could be found.
+ */
+ public PaperCard getAlternativeCardPrint(PaperCard card, Date setReleaseDate,
+ boolean isCardArtPreferenceLatestArt,
+ boolean cardArtPreferenceHasFilter){
+ Date searchReferenceDate = getReferenceDate(setReleaseDate, isCardArtPreferenceLatestArt);
+ CardDb.CardArtPreference searchCardArtStrategy = getSearchStrategyForAlternativeCardArt(isCardArtPreferenceLatestArt,
+ cardArtPreferenceHasFilter);
+ return searchAlternativeCardCandidate(card, isCardArtPreferenceLatestArt, searchReferenceDate,
+ searchCardArtStrategy);
}
- public PaperCard getCardFromEarliestCoreExp(PaperCard card) {
+ /**
+ * This method extends the defatult getAlternativeCardPrint with extra settings to be used for
+ * alternative card print.
+ *
+ *
+ * These options for Alternative Card Print make sense as part of the harmonisation/theme-matching process for
+ * cards in Deck Sections (i.e. CardPool). In fact, the values of the provided flags for alternative print
+ * for a single card will be determined according to whole card pool (Deck section) the card appears in.
+ *
+ * @param card The instance of PaperCard to look for an alternative print
+ * @param setReleaseDate The reference release date used to control the search for alternative card print.
+ * The chose candidate will be gathered from an edition printed before (upper bound) or
+ * after (lower bound) the reference set release date.
+ * @param isCardArtPreferenceLatestArt Determines whether or not "Latest Art" Card Art preference should be used
+ * when looking for an alternative candidate print.
+ * @param cardArtPreferenceHasFilter Determines whether or not the search should only consider
+ * Core, Expansions, or Reprints sets when looking for alternative candidates.
+ * @param preferCandidatesFromExpansionSets Whenever the selected Card Art Preference has filter, try to get
+ * prefer candidates from Expansion Sets over those in Core or Reprint
+ * Editions (whenever possible)
+ * e.g. Necropotence from Ice Age rather than 5th Edition (w/ Latest=false)
+ * @param preferModernFrame If True, Modern Card Frame will be preferred over Old Frames.
+ * @return an instance of PaperCard that is the selected alternative candidate, or null
+ * if None could be found.
+ */
+ public PaperCard getAlternativeCardPrint(PaperCard card, Date setReleaseDate, boolean isCardArtPreferenceLatestArt,
+ boolean cardArtPreferenceHasFilter,
+ boolean preferCandidatesFromExpansionSets, boolean preferModernFrame) {
- PaperCard c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.EarliestCoreExp, card.getArtIndex());
+ PaperCard altCard = this.getAlternativeCardPrint(card, setReleaseDate, isCardArtPreferenceLatestArt,
+ cardArtPreferenceHasFilter);
+ if (altCard == null)
+ return altCard;
+ // from here on, we're sure we do have a candidate already!
- if (null != c && c.hasImage()) {
- return c;
+ /* Try to refine selection by getting one candidate with frame matching current
+ Card Art Preference (that is NOT the lookup strategy!)*/
+ PaperCard refinedAltCandidate = this.tryToGetCardPrintWithMatchingFrame(altCard,
+ isCardArtPreferenceLatestArt,
+ cardArtPreferenceHasFilter,
+ preferModernFrame);
+ if (refinedAltCandidate != null)
+ altCard = refinedAltCandidate;
+
+ if (cardArtPreferenceHasFilter && preferCandidatesFromExpansionSets){
+ /* Now try to refine selection by looking for an alternative choice extracted from an Expansion Set.
+ NOTE: At this stage, any future selection should be already compliant with previous filter on
+ Card Frame (if applied) given that we'll be moving either UP or DOWN the timeline of Card Edition */
+ refinedAltCandidate = this.tryToGetCardPrintFromExpansionSet(altCard, isCardArtPreferenceLatestArt,
+ preferModernFrame);
+ if (refinedAltCandidate != null)
+ altCard = refinedAltCandidate;
}
+ return altCard;
+ }
- c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.EarliestCoreExp, -1);
+ private PaperCard searchAlternativeCardCandidate(PaperCard card, boolean isCardArtPreferenceLatestArt,
+ Date searchReferenceDate,
+ CardDb.CardArtPreference searchCardArtStrategy) {
+ // Note: this won't apply to Custom Nor Variant Cards, so won't bother including it!
+ CardDb cardDb = this.commonCards;
+ String cardName = card.getName();
+ int artIndex = card.getArtIndex();
+ PaperCard altCard = null;
- if (null != c && c.hasImage()) {
- return c;
+ if (isCardArtPreferenceLatestArt) { // RELEASED AFTER REFERENCE DATE
+ altCard = cardDb.getCardFromEditionsReleasedAfter(cardName, searchCardArtStrategy, artIndex, searchReferenceDate);
+ if (altCard == null) // relax artIndex condition
+ altCard = cardDb.getCardFromEditionsReleasedAfter(cardName, searchCardArtStrategy, searchReferenceDate);
+ } else { // RELEASED BEFORE REFERENCE DATE
+ altCard = cardDb.getCardFromEditionsReleasedBefore(cardName, searchCardArtStrategy, artIndex, searchReferenceDate);
+ if (altCard == null) // relax artIndex constraint
+ altCard = cardDb.getCardFromEditionsReleasedBefore(cardName, searchCardArtStrategy, searchReferenceDate);
}
+ if (altCard == null)
+ return null;
+ return card.isFoil() ? altCard.getFoiled() : altCard;
+ }
- c = this.getCommonCards().getCardFromEdition(card.getName(), null, CardDb.SetPreference.Earliest, -1);
+ private Date getReferenceDate(Date setReleaseDate, boolean isCardArtPreferenceLatestArt) {
+ Calendar cal = Calendar.getInstance();
+ cal.setTime(setReleaseDate);
+ if (isCardArtPreferenceLatestArt)
+ cal.add(Calendar.DATE, -2); // go two days behind to also include the original reference set
+ else
+ cal.add(Calendar.DATE, 2); // go two days ahead to also include the original reference set
+ return cal.getTime();
+ }
- if (null != c) {
- return c;
+ private CardDb.CardArtPreference getSearchStrategyForAlternativeCardArt(boolean isCardArtPreferenceLatestArt, boolean cardArtPreferenceHasFilter) {
+ CardDb.CardArtPreference lookupStrategy;
+ if (isCardArtPreferenceLatestArt) {
+ // Get Lower bound (w/ Original Art and Edition Released AFTER Pivot Date)
+ if (cardArtPreferenceHasFilter)
+ lookupStrategy = CardDb.CardArtPreference.ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY; // keep the filter
+ else
+ lookupStrategy = CardDb.CardArtPreference.ORIGINAL_ART_ALL_EDITIONS;
+ } else {
+ // Get Upper bound (w/ Latest Art and Edition released BEFORE Pivot Date)
+ if (cardArtPreferenceHasFilter)
+ lookupStrategy = CardDb.CardArtPreference.LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY; // keep the filter
+ else
+ lookupStrategy = CardDb.CardArtPreference.LATEST_ART_ALL_EDITIONS;
}
+ return lookupStrategy;
+ }
- // I give up!
- return card;
+ private PaperCard tryToGetCardPrintFromExpansionSet(PaperCard altCard,
+ boolean isCardArtPreferenceLatestArt,
+ boolean preferModernFrame){
+ CardEdition altCardEdition = editions.get(altCard.getEdition());
+ if (altCardEdition.getType() == CardEdition.Type.EXPANSION)
+ return null; // Nothing to do here!
+ boolean searchStrategyFlag = (isCardArtPreferenceLatestArt == preferModernFrame) == isCardArtPreferenceLatestArt;
+ // We'll force the filter on to strictly reduce the alternative candidates retrieved to those
+ // from Expansions, Core, and Reprint sets.
+ CardDb.CardArtPreference searchStrategy = getSearchStrategyForAlternativeCardArt(searchStrategyFlag,
+ true);
+ PaperCard altCandidate = altCard;
+ while (altCandidate != null){
+ Date referenceDate = editions.get(altCandidate.getEdition()).getDate();
+ altCandidate = this.searchAlternativeCardCandidate(altCandidate, preferModernFrame,
+ referenceDate, searchStrategy);
+ if (altCandidate != null) {
+ CardEdition altCandidateEdition = editions.get(altCandidate.getEdition());
+ if (altCandidateEdition.getType() == CardEdition.Type.EXPANSION)
+ break;
+ }
+ }
+ // this will be either a true candidate or null if the cycle broke because of no other suitable candidates
+ return altCandidate;
+ }
+
+ private PaperCard tryToGetCardPrintWithMatchingFrame(PaperCard altCard,
+ boolean isCardArtPreferenceLatestArt,
+ boolean cardArtHasFilter,
+ boolean preferModernFrame){
+ CardEdition altCardEdition = editions.get(altCard.getEdition());
+ boolean frameIsCompliantAlready = (altCardEdition.isModern() == preferModernFrame);
+ if (frameIsCompliantAlready)
+ return null; // Nothing to do here!
+ boolean searchStrategyFlag = (isCardArtPreferenceLatestArt == preferModernFrame) == isCardArtPreferenceLatestArt;
+ CardDb.CardArtPreference searchStrategy = getSearchStrategyForAlternativeCardArt(searchStrategyFlag,
+ cardArtHasFilter);
+ PaperCard altCandidate = altCard;
+ while (altCandidate != null){
+ Date referenceDate = editions.get(altCandidate.getEdition()).getDate();
+ altCandidate = this.searchAlternativeCardCandidate(altCandidate, preferModernFrame,
+ referenceDate, searchStrategy);
+ if (altCandidate != null) {
+ CardEdition altCandidateEdition = editions.get(altCandidate.getEdition());
+ if (altCandidateEdition.isModern() == preferModernFrame)
+ break;
+ }
+ }
+ // this will be either a true candidate or null if the cycle broke because of no other suitable candidates
+ return altCandidate;
+ }
+
+
+
+ /**
+ * Get the Art Count for a given PaperCard looking for a candidate in all
+ * available databases.
+ *
+ * @param card Instance of target PaperCard
+ * @return The number of available arts for the given card in the corresponding set, or 0 if not found.
+ */
+ public int getCardArtCount(PaperCard card){
+ Collection databases = this.getAvailableDatabases().values();
+ for (CardDb db: databases){
+ int artCount = db.getArtCount(card.getName(), card.getEdition());
+ if (artCount > 0)
+ return artCount;
+ }
+ return 0;
}
public boolean getFilteredHandsEnabled(){
@@ -399,4 +583,53 @@ public class StaticData {
return mulliganRule;
}
+ public void setCardArtPreference(boolean latestArt, boolean coreExpansionOnly){
+ this.commonCards.setCardArtPreference(latestArt, coreExpansionOnly);
+ this.variantCards.setCardArtPreference(latestArt, coreExpansionOnly);
+ this.customCards.setCardArtPreference(latestArt, coreExpansionOnly);
+ }
+
+ public String getCardArtPreferenceName(){
+ return this.commonCards.getCardArtPreference().toString();
+ }
+
+ public CardDb.CardArtPreference getCardArtPreference(){
+ return this.commonCards.getCardArtPreference();
+ }
+
+
+ public boolean isCoreExpansionOnlyFilterSet(){ return this.commonCards.getCardArtPreference().filterSets; }
+
+ public boolean cardArtPreferenceIsLatest(){
+ return this.commonCards.getCardArtPreference().latestFirst;
+ }
+
+ // === MOBILE APP Alternative Methods (using String Labels, not yet localised!!) ===
+ // Note: only used in mobile
+ public String[] getCardArtAvailablePreferences(){
+ CardDb.CardArtPreference[] preferences = CardDb.CardArtPreference.values();
+ String[] preferences_avails = new String[preferences.length];
+ for (int i = 0; i < preferences.length; i++) {
+ StringBuilder label = new StringBuilder();
+ String[] fullNames = preferences[i].toString().split("_");
+ for (String name : fullNames)
+ label.append(TextUtil.capitalize(name.toLowerCase())).append(" ");
+ preferences_avails[i] = label.toString().trim();
+ }
+ return preferences_avails;
+ }
+ public void setCardArtPreference(String artPreference){
+ this.commonCards.setCardArtPreference(artPreference);
+ this.variantCards.setCardArtPreference(artPreference);
+ this.customCards.setCardArtPreference(artPreference);
+ }
+
+ //
+ public boolean isEnabledCardArtSmartSelection(){
+ return this.enableSmartCardArtSelection;
+ }
+ public void setEnableSmartCardArtSelection(boolean isEnabled){
+ this.enableSmartCardArtSelection = isEnabled;
+ }
+
}
diff --git a/forge-core/src/main/java/forge/card/CardDb.java b/forge-core/src/main/java/forge/card/CardDb.java
index 366be7e81f8..998593fa468 100644
--- a/forge-core/src/main/java/forge/card/CardDb.java
+++ b/forge-core/src/main/java/forge/card/CardDb.java
@@ -17,41 +17,21 @@
*/
package forge.card;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeMap;
-
-import forge.StaticData;
-
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.tuple.Pair;
-
import com.google.common.base.Predicate;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.common.collect.Multimaps;
-
+import com.google.common.collect.*;
import forge.card.CardEdition.CardInSet;
import forge.card.CardEdition.Type;
import forge.deck.generation.IDeckGenPool;
+import forge.item.IPaperCard;
import forge.item.PaperCard;
-import forge.util.Aggregates;
import forge.util.CollectionSuppliers;
import forge.util.Lang;
-import forge.util.MyRandom;
import forge.util.TextUtil;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.tuple.Pair;
+
+import java.util.*;
+import java.util.Map.Entry;
public final class CardDb implements ICardDatabase, IDeckGenPool {
public final static String foilSuffix = "+";
@@ -60,7 +40,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
private final String exlcudedCardSet = "DS0";
// need this to obtain cardReference by name+set+artindex
- private final ListMultimap allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), CollectionSuppliers.arrayLists());
+ private final ListMultimap allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), CollectionSuppliers.arrayLists());
private final Map uniqueCardsByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
private final Map rulesByName;
private final Map facesByName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
@@ -72,78 +52,158 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
private final CardEdition.Collection editions;
private List filtered;
- public enum SetPreference {
- Latest(false),
- LatestCoreExp(true),
- Earliest(false),
- EarliestCoreExp(true),
- Random(false);
+ public enum CardArtPreference {
+ LATEST_ART_ALL_EDITIONS(false, true),
+ LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY(true, true),
+ ORIGINAL_ART_ALL_EDITIONS(false, false),
+ ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY(true, false);
- final boolean filterSets;
- SetPreference(boolean filterIrregularSets) {
+ public final boolean filterSets;
+ public final boolean latestFirst;
+
+ CardArtPreference(boolean filterIrregularSets, boolean latestSetFirst) {
filterSets = filterIrregularSets;
+ latestFirst = latestSetFirst;
}
public boolean accept(CardEdition ed) {
- if (ed == null) return false;
+ if (ed == null) return false;
return !filterSets || ed.getType() == Type.CORE || ed.getType() == Type.EXPANSION || ed.getType() == Type.REPRINT;
}
}
- // NO GETTERS/SETTERS HERE!
+ // Placeholder to setup default art Preference - to be moved from Static Data!
+ private CardArtPreference defaultCardArtPreference;
+
public static class CardRequest {
- // TODO Move Request to its own class
public String cardName;
public String edition;
public int artIndex;
public boolean isFoil;
+ public String collectorNumber;
- private CardRequest(String name, String edition, int artIndex, boolean isFoil) {
+ private CardRequest(String name, String edition, int artIndex, boolean isFoil, String collectorNumber) {
cardName = name;
this.edition = edition;
this.artIndex = artIndex;
this.isFoil = isFoil;
+ this.collectorNumber = collectorNumber;
}
- public static CardRequest fromString(String name) {
- boolean isFoil = name.endsWith(foilSuffix);
- if (isFoil) {
- name = name.substring(0, name.length() - foilSuffix.length());
+ public static String compose(String cardName, boolean isFoil){
+ if (isFoil)
+ return cardName+foilSuffix;
+ return cardName;
+ }
+
+ public static String compose(String cardName, String setCode) {
+ setCode = setCode != null ? setCode : "";
+ cardName = cardName != null ? cardName : "";
+ if (cardName.indexOf(NameSetSeparator) != -1)
+ // If cardName is another RequestString, just get card name and forget about the rest.
+ cardName = CardRequest.fromString(cardName).cardName;
+ return cardName + NameSetSeparator + setCode;
+ }
+
+ public static String compose(String cardName, String setCode, int artIndex) {
+ String requestInfo = compose(cardName, setCode);
+ artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX);
+ return requestInfo + NameSetSeparator + artIndex;
+ }
+
+ public static String compose(String cardName, String setCode, String collectorNumber) {
+ String requestInfo = compose(cardName, setCode);
+ // CollectorNumber will be wrapped in square brackets
+ collectorNumber = preprocessCollectorNumber(collectorNumber);
+ return requestInfo + NameSetSeparator + collectorNumber;
+ }
+
+ private static String preprocessCollectorNumber(String collectorNumber) {
+ if (collectorNumber == null)
+ return "";
+ collectorNumber = collectorNumber.trim();
+ if (!collectorNumber.startsWith("["))
+ collectorNumber = "[" + collectorNumber;
+ if (!collectorNumber.endsWith("]"))
+ collectorNumber += "]";
+ return collectorNumber;
+ }
+
+ public static String compose(String cardName, String setCode, int artIndex, String collectorNumber) {
+ String requestInfo = compose(cardName, setCode, artIndex);
+ // CollectorNumber will be wrapped in square brackets
+ collectorNumber = preprocessCollectorNumber(collectorNumber);
+ return requestInfo + NameSetSeparator + collectorNumber;
+ }
+
+ private static boolean isCollectorNumber(String s) {
+ return s.startsWith("[") && s.endsWith("]");
+ }
+
+ private static boolean isArtIndex(String s) {
+ return StringUtils.isNumeric(s) && s.length() <= 2 ; // only artIndex between 1-99
+ }
+
+ private static boolean isSetCode(String s) {
+ return !StringUtils.isNumeric(s);
+ }
+
+ public static CardRequest fromString(String reqInfo) {
+ if (reqInfo == null)
+ return null;
+
+ String[] info = TextUtil.split(reqInfo, NameSetSeparator);
+ int setPos;
+ int artPos;
+ int cNrPos;
+ if (info.length >= 4) { // name|set|artIndex|[collNr]
+ setPos = isSetCode(info[1]) ? 1 : -1;
+ artPos = isArtIndex(info[2]) ? 2 : -1;
+ cNrPos = isCollectorNumber(info[3]) ? 3 : -1;
+ } else if (info.length == 3) { // name|set|artIndex (or CollNr)
+ setPos = isSetCode(info[1]) ? 1 : -1;
+ artPos = isArtIndex(info[2]) ? 2 : -1;
+ cNrPos = isCollectorNumber(info[2]) ? 2 : -1;
+ } else if (info.length == 2) { // name|set (or artIndex, even if not possible via compose)
+ setPos = isSetCode(info[1]) ? 1 : -1;
+ artPos = isArtIndex(info[1]) ? 1 : -1;
+ cNrPos = -1;
+ } else {
+ setPos = -1;
+ artPos = -1;
+ cNrPos = -1;
}
-
- String preferredArt = artPrefs.get(name);
- if (preferredArt != null) { //account for preferred art if needed
- name += NameSetSeparator + preferredArt;
- }
-
- String[] nameParts = TextUtil.split(name, NameSetSeparator);
-
- int setPos = nameParts.length >= 2 && !StringUtils.isNumeric(nameParts[1]) ? 1 : -1;
- int artPos = nameParts.length >= 2 && StringUtils.isNumeric(nameParts[1]) ? 1 : nameParts.length >= 3 && StringUtils.isNumeric(nameParts[2]) ? 2 : -1;
-
- String cardName = nameParts[0];
+ String cardName = info[0];
+ boolean isFoil = false;
if (cardName.endsWith(foilSuffix)) {
cardName = cardName.substring(0, cardName.length() - foilSuffix.length());
isFoil = true;
}
- int artIndex = artPos > 0 ? Integer.parseInt(nameParts[artPos]) : 0;
- String setName = setPos > 0 ? nameParts[setPos] : null;
- if ("???".equals(setName)) {
+ String preferredArt = artPrefs.get(cardName);
+ int artIndex = artPos > 0 ? Integer.parseInt(info[artPos]) : IPaperCard.NO_ART_INDEX; // default: no art index
+ if (preferredArt != null) { //account for preferred art if needed
+ System.err.println("I AM HERE - DECIDE WHAT TO DO");
+ }
+ String collectorNumber = cNrPos > 0 ? info[cNrPos].substring(1, info[cNrPos].length() - 1) : IPaperCard.NO_COLLECTOR_NUMBER;
+ String setName = setPos > 0 ? info[setPos] : null;
+ if (setName != null && setName.equals(CardEdition.UNKNOWN.getCode())) { // ???
setName = null;
}
-
- return new CardRequest(cardName, setName, artIndex, isFoil);
+ // finally, check whether any between artIndex and CollectorNumber has been set
+ if (collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER) && artIndex == IPaperCard.NO_ART_INDEX)
+ artIndex = IPaperCard.DEFAULT_ART_INDEX;
+ return new CardRequest(cardName, setName, artIndex, isFoil, collectorNumber);
}
}
- public CardDb(Map rules, CardEdition.Collection editions0, List filteredCards) {
+ public CardDb(Map rules, CardEdition.Collection editions0, List filteredCards, String cardArtPreference) {
this.filtered = filteredCards;
this.rulesByName = rules;
this.editions = editions0;
// create faces list from rules
- for (final CardRules rule : rules.values() ) {
+ for (final CardRules rule : rules.values()) {
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
continue;
final ICardFace main = rule.getMainPart();
@@ -159,37 +219,45 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
}
}
- }
-
- private ListMultimap getAllCardsByName() {
- return allCardsByName;
+ setCardArtPreference(cardArtPreference);
}
private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) {
- int artIdx = 1;
+ int artIdx = IPaperCard.DEFAULT_ART_INDEX;
String key = e.getCode() + "/" + cis.name;
if (artIds.containsKey(key)) {
artIdx = artIds.get(key) + 1;
}
artIds.put(key, artIdx);
- addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx));
+ addCard(new PaperCard(cr, e.getCode(), cis.rarity, artIdx, false, cis.collectorNumber, cis.artistName));
}
- public void loadCard(String cardName, 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);
rulesByName.put(cardName, cr);
- // This seems very unperformant. Does this get called often?
- System.out.println("Inside loading card");
-
- for (CardEdition e : editions) {
- for (CardInSet cis : e.getAllCardsInSet()) {
- if (cis.name.equalsIgnoreCase(cardName)) {
+ boolean reIndexNecessary = false;
+ CardEdition ed = editions.get(setCode);
+ if (ed == null || ed.equals(CardEdition.UNKNOWN)) {
+ // look for all possible editions
+ for (CardEdition e : editions) {
+ List cardsInSet = e.getCardInSet(cardName); // empty collection if not present
+ for (CardInSet cis : cardsInSet) {
addSetCard(e, cis, cr);
+ reIndexNecessary = true;
}
}
+ } else {
+ List cardsInSet = ed.getCardInSet(cardName); // empty collection if not present
+ for (CardInSet cis : cardsInSet) {
+ addSetCard(ed, cis, cr);
+ reIndexNecessary = true;
+ }
}
- reIndex();
+ if (reIndexNecessary)
+ reIndex();
}
public void initialize(boolean logMissingPerEdition, boolean logMissingSummary, boolean enableUnknownCards) {
@@ -212,16 +280,14 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
CardRules cr = rulesByName.get(cis.name);
if (cr != null) {
addSetCard(e, cis, cr);
- }
- else {
+ } else {
missingCards.add(cis.name);
}
}
if (isCoreExpSet && logMissingPerEdition) {
if (missingCards.isEmpty()) {
System.out.println(" ... 100% ");
- }
- else {
+ } else {
int missing = (e.getAllCardsInSet().size() - missingCards.size()) * 10000 / e.getAllCardsInSet().size();
System.out.printf(" ... %.2f%% (%s missing: %s)%n", missing * 0.01f, Lang.nounWithAmount(missingCards.size(), "card"), StringUtils.join(missingCards, " | "));
}
@@ -244,10 +310,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
for (CardRules cr : rulesByName.values()) {
if (!contains(cr.getName())) {
if (upcomingSet != null) {
- addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown, 1));
- } else if(enableUnknownCards) {
+ addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown));
+ } else if (enableUnknownCards && !this.filtered.contains(cr.getName())) {
System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. ");
- addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special, 1));
+ addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
}
}
}
@@ -261,7 +327,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
allCardsByName.put(paperCard.getName(), paperCard);
- if (paperCard.getRules().getSplitType() == CardSplitType.None) { return; }
+ if (paperCard.getRules().getSplitType() == CardSplitType.None) {
+ return;
+ }
if (paperCard.getRules().getOtherPart() != null) {
//allow looking up card by the name of other faces
@@ -272,20 +340,21 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
allCardsByName.put(paperCard.getRules().getMainPart().getName(), paperCard);
}
}
+
private boolean excludeCard(String cardName, String cardEdition) {
if (filtered.isEmpty())
return false;
if (filtered.contains(cardName)) {
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
return true;
- else if (!exlcudedCardName.equalsIgnoreCase(cardName))
- return true;
+ else return !exlcudedCardName.equalsIgnoreCase(cardName);
}
return false;
}
+
private void reIndex() {
uniqueCardsByName.clear();
- for (Entry> kv : getAllCardsByName().asMap().entrySet()) {
+ for (Entry> kv : allCardsByName.asMap().entrySet()) {
PaperCard pc = getFirstWithImage(kv.getValue());
uniqueCardsByName.put(kv.getKey(), pc);
}
@@ -315,15 +384,40 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return false;
}
- public CardRules getRules(String cardname) {
- CardRules result = rulesByName.get(cardname);
+ public CardRules getRules(String cardName) {
+ CardRules result = rulesByName.get(cardName);
if (result != null) {
return result;
} else {
- return CardRules.getUnsupportedCardNamed(cardname);
+ return CardRules.getUnsupportedCardNamed(cardName);
}
}
+ public CardArtPreference getCardArtPreference(){ return this.defaultCardArtPreference; }
+ public void setCardArtPreference(boolean latestArt, boolean coreExpansionOnly){
+ if (coreExpansionOnly){
+ this.defaultCardArtPreference = latestArt ? CardArtPreference.LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY : CardArtPreference.ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY;
+ } else {
+ this.defaultCardArtPreference = latestArt ? CardArtPreference.LATEST_ART_ALL_EDITIONS : CardArtPreference.ORIGINAL_ART_ALL_EDITIONS;
+ }
+ }
+
+ public void setCardArtPreference(String artPreference){
+ artPreference = artPreference.toLowerCase().trim();
+ boolean isLatest = artPreference.contains("latest");
+ // additional check in case of unrecognised values wrt. to legacy opts
+ if (!artPreference.contains("original") && !artPreference.contains("earliest"))
+ isLatest = true; // this must be default
+ boolean hasFilter = artPreference.contains("core");
+ this.setCardArtPreference(isLatest, hasFilter);
+ }
+
+
+ /*
+ * ======================
+ * 1. CARD LOOKUP METHODS
+ * ======================
+ */
@Override
public PaperCard getCard(String cardName) {
CardRequest request = CardRequest.fromString(cardName);
@@ -332,248 +426,318 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public PaperCard getCard(final String cardName, String setCode) {
- CardRequest request = CardRequest.fromString(cardName);
- if (setCode != null) {
- request.edition = setCode;
- }
+ CardRequest request = CardRequest.fromString(CardRequest.compose(cardName, setCode));
return tryGetCard(request);
}
@Override
public PaperCard getCard(final String cardName, String setCode, int artIndex) {
- CardRequest request = CardRequest.fromString(cardName);
- if (setCode != null) {
- request.edition = setCode;
- }
- if (artIndex > 0) {
- request.artIndex = artIndex;
- }
+ String reqInfo = CardRequest.compose(cardName, setCode, artIndex);
+ CardRequest request = CardRequest.fromString(reqInfo);
return tryGetCard(request);
}
- public String getCardCollectorNumber(String cardName, String reqEdition, int artIndex) {
- cardName = getName(cardName);
- CardEdition edition = editions.get(reqEdition);
- if (edition == null)
- return null;
- int numMatches = 0;
- for (CardInSet card : edition.getAllCardsInSet()) {
- if (card.name.equalsIgnoreCase(cardName)) {
- numMatches += 1;
- if (numMatches == artIndex) {
- return card.collectorNumber;
- }
- }
- }
- return null;
+ @Override
+ public PaperCard getCard(final String cardName, String setCode, String collectorNumber) {
+ String reqInfo = CardRequest.compose(cardName, setCode, collectorNumber);
+ CardRequest request = CardRequest.fromString(reqInfo);
+ return tryGetCard(request);
+ }
+
+ @Override
+ public PaperCard getCard(final String cardName, String setCode, int artIndex, String collectorNumber) {
+ String reqInfo = CardRequest.compose(cardName, setCode, artIndex, collectorNumber);
+ CardRequest request = CardRequest.fromString(reqInfo);
+ return tryGetCard(request);
}
private PaperCard tryGetCard(CardRequest request) {
- Collection cards = getAllCards(request.cardName);
- if (cards == null) { return null; }
-
- PaperCard result = null;
-
- String reqEdition = request.edition;
- if (reqEdition != null && !editions.contains(reqEdition)) {
- CardEdition edition = editions.get(reqEdition);
- if (edition != null) {
- reqEdition = edition.getCode();
- }
- }
-
- if (request.artIndex <= 0) { // this stands for 'random art'
- Collection candidates;
- if (reqEdition == null) {
- candidates = new ArrayList<>(cards);
- }
- else {
- candidates = new ArrayList<>();
- for (PaperCard pc : cards) {
- if (pc.getEdition().equalsIgnoreCase(reqEdition)) {
- candidates.add(pc);
- }
- }
- }
- if (candidates.isEmpty()) {
- return null;
- }
- result = Aggregates.random(candidates);
-
- //if card image doesn't exist for chosen candidate, try another one if possible
- while (candidates.size() > 1 && !result.hasImage()) {
- candidates.remove(result);
- result = Aggregates.random(candidates);
- }
- }
- else {
- for (PaperCard pc : cards) {
- if (pc.getEdition().equalsIgnoreCase(reqEdition) && request.artIndex == pc.getArtIndex()) {
- result = pc;
- break;
- }
- }
- }
- if (result == null) { return null; }
-
- return request.isFoil ? getFoiled(result) : result;
- }
-
- @Override
- public PaperCard getCardFromEdition(final String cardName, SetPreference fromSet) {
- return getCardFromEdition(cardName, null, fromSet);
- }
-
- @Override
- public PaperCard getCardFromEdition(final String cardName, final Date printedBefore, final SetPreference fromSet) {
- return getCardFromEdition(cardName, printedBefore, fromSet, -1);
- }
-
- @Override
- public PaperCard getCardFromEdition(final String cardName, final Date printedBefore, final SetPreference fromSets, int artIndex) {
- final CardRequest cr = CardRequest.fromString(cardName);
- SetPreference fromSet = fromSets;
- List cards = getAllCards(cr.cardName);
- if (printedBefore != null){
- cards = Lists.newArrayList(Iterables.filter(cards, new Predicate() {
- @Override public boolean apply(PaperCard c) {
- CardEdition ed = editions.get(c.getEdition());
- return ed.getDate().before(printedBefore); }
- }));
- }
-
- if (cards.size() == 0) // Don't bother continuing! No cards has been found!
+ // Before doing anything, check that a non-null request has been provided
+ if (request == null)
return null;
- boolean cardsListReadOnly = true;
-
- //overrides
- if (StaticData.instance().getPrefferedArtOption().equals("Earliest"))
- fromSet = SetPreference.EarliestCoreExp;
-
- if (StringUtils.isNotBlank(cr.edition)) {
- cards = Lists.newArrayList(Iterables.filter(cards, new Predicate() {
- @Override public boolean apply(PaperCard input) { return input.getEdition().equalsIgnoreCase(cr.edition); }
- }));
- }
- if (artIndex == -1 && cr.artIndex > 0) {
- artIndex = cr.artIndex;
+ // 1. First off, try using all possible search parameters, to narrow down the actual cards looked for.
+ String reqEditionCode = request.edition;
+ PaperCard result = null;
+ if ((reqEditionCode != null) && (reqEditionCode.length() > 0)) {
+ // This get is robust even against expansion aliases (e.g. TE and TMP both valid for Tempest) -
+ // MOST of the extensions have two short codes, 141 out of 221 (so far)
+ // ALSO: Set Code are always UpperCase
+ CardEdition edition = editions.get(reqEditionCode.toUpperCase());
+ return this.getCardFromSet(request.cardName, edition, request.artIndex,
+ request.collectorNumber, request.isFoil);
}
- int sz = cards.size();
- if (fromSet == SetPreference.Earliest || fromSet == SetPreference.EarliestCoreExp) {
- PaperCard firstWithoutImage = null;
- for (int i = sz - 1 ; i >= 0 ; i--) {
- PaperCard pc = cards.get(i);
- CardEdition ed = editions.get(pc.getEdition());
- if (!fromSet.accept(ed)) {
- continue;
- }
-
- if ((artIndex <= 0 || pc.getArtIndex() == artIndex) && (printedBefore == null || ed.getDate().before(printedBefore))) {
- if (pc.hasImage()) {
- return pc;
- }
- else if (firstWithoutImage == null) {
- firstWithoutImage = pc; //ensure first without image returns if none have image
- }
- }
- }
- return firstWithoutImage;
- }
- else if (fromSet == SetPreference.LatestCoreExp || fromSet == SetPreference.Latest || fromSet == null || fromSet == SetPreference.Random) {
- PaperCard firstWithoutImage = null;
- for (int i = 0; i < sz; i++) {
- PaperCard pc = cards.get(i);
- CardEdition ed = editions.get(pc.getEdition());
- if (fromSet != null && !fromSet.accept(ed)) {
- continue;
- }
-
- if ((artIndex < 0 || pc.getArtIndex() == artIndex) && (printedBefore == null || ed.getDate().before(printedBefore))) {
- if (fromSet == SetPreference.LatestCoreExp || fromSet == SetPreference.Latest) {
- if (pc.hasImage()) {
- return pc;
- }
- else if (firstWithoutImage == null) {
- firstWithoutImage = pc; //ensure first without image returns if none have image
- }
- }
- else {
- while (sz > i) {
- int randomIndex = i + MyRandom.getRandom().nextInt(sz - i);
- pc = cards.get(randomIndex);
- if (pc.hasImage()) {
- return pc;
- }
- else {
- if (firstWithoutImage == null) {
- firstWithoutImage = pc; //ensure first without image returns if none have image
- }
- if (cardsListReadOnly) { //ensure we don't modify a cached collection
- cards = new ArrayList<>(cards);
- cardsListReadOnly = false;
- }
- cards.remove(randomIndex); //remove card from collection and try another random card
- sz--;
- }
- }
- }
- }
- }
- return firstWithoutImage;
- }
- return null;
+ // 2. Card lookup in edition with specified filter didn't work.
+ // So now check whether the cards exist in the DB first,
+ // and select pick the card based on current SetPreference policy as a fallback
+ Collection cards = getAllCards(request.cardName);
+ if (cards.isEmpty()) // Never null being this a view in MultiMap
+ return null;
+ // Either No Edition has been specified OR as a fallback in case of any error!
+ // get card using the default card art preference
+ String cardRequest = CardRequest.compose(request.cardName, request.isFoil);
+ return getCardFromEditions(cardRequest, this.defaultCardArtPreference, request.artIndex);
}
- public PaperCard getFoiled(PaperCard card0) {
- // Here - I am still unsure if there should be a cache Card->Card from unfoiled to foiled, to avoid creation of N instances of single plains
- return new PaperCard(card0.getRules(), card0.getEdition(), card0.getRarity(), card0.getArtIndex(), true);
+ /*
+ * ==========================================
+ * 2. CARD LOOKUP FROM A SINGLE EXPANSION SET
+ * ==========================================
+ *
+ * NOTE: All these methods always try to return a PaperCard instance
+ * that has an Image (if any).
+ * Therefore, the single Edition request can be overruled if no image is found
+ * for the corresponding requested edition.
+ */
+ @Override
+ public PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil) {
+ return getCardFromSet(cardName, edition, IPaperCard.NO_ART_INDEX,
+ IPaperCard.NO_COLLECTOR_NUMBER, isFoil);
}
@Override
- public int getPrintCount(String cardName, String edition) {
- int cnt = 0;
- if (edition == null || cardName == null)
- return cnt;
- for (PaperCard pc : getAllCards(cardName)) {
- if (pc.getEdition().equals(edition)) {
- cnt++;
- }
- }
- return cnt;
+ public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil) {
+ return getCardFromSet(cardName, edition, artIndex, IPaperCard.NO_COLLECTOR_NUMBER, isFoil);
}
@Override
- public int getMaxPrintCount(String cardName) {
- int max = -1;
+ public PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil) {
+ return getCardFromSet(cardName, edition, IPaperCard.NO_ART_INDEX, collectorNumber, isFoil);
+ }
+
+ @Override
+ public PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex,
+ String collectorNumber, boolean isFoil) {
+ if (edition == null || cardName == null) // preview cards
+ return null; // No cards will be returned
+
+ // Allow to pass in cardNames with foil markers, and adapt accordingly
+ CardRequest cardNameRequest = CardRequest.fromString(cardName);
+ cardName = cardNameRequest.cardName;
+ isFoil = isFoil || cardNameRequest.isFoil;
+
+ List candidates = getAllCards(cardName, new Predicate() {
+ @Override
+ public boolean apply(PaperCard c) {
+ boolean artIndexFilter = true;
+ boolean collectorNumberFilter = true;
+ boolean setFilter = ((c.getEdition().equalsIgnoreCase(edition.getCode())) ||
+ (c.getEdition().equalsIgnoreCase(edition.getCode2())));
+ if (artIndex > 0)
+ artIndexFilter = (c.getArtIndex() == artIndex);
+ if ((collectorNumber != null) && (collectorNumber.length() > 0)
+ && !(collectorNumber.equals(IPaperCard.NO_COLLECTOR_NUMBER)))
+ collectorNumberFilter = (c.getCollectorNumber().equals(collectorNumber));
+ return setFilter && artIndexFilter && collectorNumberFilter;
+ }
+ });
+ if (candidates.isEmpty())
+ return null;
+
+ Iterator candidatesIterator = candidates.iterator();
+ PaperCard candidate = candidatesIterator.next();
+ // Before returning make sure that actual candidate has Image.
+ // If not, try to replace current candidate with one having image,
+ // so to align this implementation with old one.
+ while (!candidate.hasImage() && candidatesIterator.hasNext()) {
+ candidate = candidatesIterator.next();
+ }
+ return isFoil ? candidate.getFoiled() : candidate;
+ }
+
+ /*
+ * ====================================================
+ * 3. CARD LOOKUP BASED ON CARD ART PREFERENCE OPTION
+ * ====================================================
+ */
+
+ /* Get Card from Edition using the default `CardArtPreference`
+ NOTE: this method has NOT been included in the Interface API refactoring as it
+ relies on a specific (new) attribute included in the `CardDB` that sets the
+ default `ArtPreference`. This attribute does not necessarily belongs to any
+ class implementing ICardInterface, and so the not inclusion in the API
+ */
+ public PaperCard getCardFromEditions(final String cardName) {
+ return this.getCardFromEditions(cardName, this.defaultCardArtPreference);
+ }
+
+ @Override
+ public PaperCard getCardFromEditions(final String cardName, CardArtPreference artPreference) {
+ return getCardFromEditions(cardName, artPreference, IPaperCard.NO_ART_INDEX);
+ }
+
+ @Override
+ public PaperCard getCardFromEditions(final String cardInfo, final CardArtPreference artPreference, int artIndex) {
+ return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex);
+ }
+
+ /*
+ * ===============================================
+ * 4. SPECIALISED CARD LOOKUP BASED ON
+ * CARD ART PREFERENCE AND EDITION RELEASE DATE
+ * ===============================================
+ */
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedBefore(String cardName, Date releaseDate){
+ return this.getCardFromEditionsReleasedBefore(cardName, this.defaultCardArtPreference, PaperCard.DEFAULT_ART_INDEX, releaseDate);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedBefore(String cardName, int artIndex, Date releaseDate){
+ return this.getCardFromEditionsReleasedBefore(cardName, this.defaultCardArtPreference, artIndex, releaseDate);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate){
+ return this.getCardFromEditionsReleasedBefore(cardName, artPreference, PaperCard.DEFAULT_ART_INDEX, releaseDate);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, int artIndex, Date releaseDate){
+ return this.tryToGetCardFromEditions(cardName, artPreference, artIndex, releaseDate, true);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedAfter(String cardName, Date releaseDate){
+ return this.getCardFromEditionsReleasedAfter(cardName, this.defaultCardArtPreference, PaperCard.DEFAULT_ART_INDEX, releaseDate);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedAfter(String cardName, int artIndex, Date releaseDate){
+ return this.getCardFromEditionsReleasedAfter(cardName, this.defaultCardArtPreference, artIndex, releaseDate);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedAfter(String cardName, CardArtPreference artPreference, Date releaseDate){
+ return this.getCardFromEditionsReleasedAfter(cardName, artPreference, PaperCard.DEFAULT_ART_INDEX, releaseDate);
+ }
+
+ @Override
+ public PaperCard getCardFromEditionsReleasedAfter(String cardName, CardArtPreference artPreference, int artIndex, Date releaseDate){
+ return this.tryToGetCardFromEditions(cardName, artPreference, artIndex, releaseDate, false);
+ }
+
+ // Override when there is no date
+ private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex){
+ return this.tryToGetCardFromEditions(cardInfo, artPreference, artIndex, null, false);
+ }
+
+ private PaperCard tryToGetCardFromEditions(String cardInfo, CardArtPreference artPreference, int artIndex,
+ Date releaseDate, boolean releasedBeforeFlag){
+ if (cardInfo == null)
+ return null;
+ final CardRequest cr = CardRequest.fromString(cardInfo);
+ // Check whether input `frame` is null. In that case, fallback to default SetPreference !-)
+ final CardArtPreference artPref = artPreference != null ? artPreference : this.defaultCardArtPreference;
+ cr.artIndex = Math.max(cr.artIndex, IPaperCard.DEFAULT_ART_INDEX);
+ if (cr.artIndex != artIndex && artIndex > IPaperCard.DEFAULT_ART_INDEX )
+ cr.artIndex = artIndex; // 2nd cond. is to verify that some actual value has been passed in.
+
+ List cards;
+ if (releaseDate != null) {
+ cards = getAllCards(cr.cardName, new Predicate() {
+ @Override
+ public boolean apply(PaperCard c) {
+ if (c.getArtIndex() != cr.artIndex)
+ return false; // not interested anyway!
+ CardEdition ed = editions.get(c.getEdition());
+ if (ed == null) return false;
+ if (releasedBeforeFlag)
+ return ed.getDate().before(releaseDate);
+ else
+ return ed.getDate().after(releaseDate);
+ }
+ });
+ } else // filter candidates based on requested artIndex
+ cards = getAllCards(cr.cardName, new Predicate() {
+ @Override
+ public boolean apply(PaperCard card) {
+ return card.getArtIndex() == cr.artIndex;
+ }
+ });
+
+ if (cards.size() == 1) // if only one candidate, there much else we should do
+ return cr.isFoil ? cards.get(0).getFoiled() : cards.get(0);
+
+ /* 2. Retrieve cards based of [Frame]Set Preference
+ ================================================ */
+ // Collect the list of all editions found for target card
+ List cardEditions = new ArrayList<>();
+ Map candidatesCard = new HashMap<>();
+ for (PaperCard card : cards) {
+ String setCode = card.getEdition();
+ CardEdition ed;
+ if (setCode.equals(CardEdition.UNKNOWN.getCode()))
+ ed = CardEdition.UNKNOWN;
+ else
+ ed = editions.get(card.getEdition());
+ if (ed != null) {
+ cardEditions.add(ed);
+ candidatesCard.put(setCode, card);
+ }
+ }
+ if (cardEditions.isEmpty())
+ return null; // nothing to do
+
+ // Filter Cards Editions based on set preferences
+ List acceptedEditions = Lists.newArrayList(Iterables.filter(cardEditions, new Predicate() {
+ @Override
+ public boolean apply(CardEdition ed) {
+ return artPref.accept(ed);
+ }
+ }));
+
+ /* At this point, it may be possible that Art Preference is too-strict for the requested card!
+ i.e. acceptedEditions.size() == 0!
+ This may be the case of Cards Only available in NON-CORE/EXPANSIONS/REPRINT sets.
+ (NOTE: We've already checked that any print of the request card exists in the DB)
+ If this happens, we won't try to iterate over an empty list. Instead, we will fall back
+ to original lists of editions (unfiltered, of course) AND STILL sorted according to chosen art preference.
+ */
+ if (acceptedEditions.isEmpty())
+ acceptedEditions.addAll(cardEditions);
+
+ if (acceptedEditions.size() > 1) {
+ Collections.sort(acceptedEditions); // CardEdition correctly sort by (release) date
+ if (artPref.latestFirst)
+ Collections.reverse(acceptedEditions); // newest editions first
+ }
+
+ final Iterator editionIterator = acceptedEditions.iterator();
+ CardEdition ed = editionIterator.next();
+ PaperCard candidate = candidatesCard.get(ed.getCode());
+ while (!candidate.hasImage() && editionIterator.hasNext()) {
+ ed = editionIterator.next();
+ candidate = candidatesCard.get(ed.getCode());
+ }
+ //If any, we're sure that at least one candidate is always returned despite it having any image
+ return cr.isFoil ? candidate.getFoiled() : candidate;
+ }
+
+ @Override
+ public int getMaxArtIndex(String cardName) {
if (cardName == null)
- return max;
+ return IPaperCard.NO_ART_INDEX;
+ int max = IPaperCard.NO_ART_INDEX;
for (PaperCard pc : getAllCards(cardName)) {
- if (max < pc.getArtIndex()) {
+ if (max < pc.getArtIndex())
max = pc.getArtIndex();
- }
}
return max;
}
@Override
- public int getArtCount(String cardName, String setName) {
- int cnt = 0;
- if (cardName == null || setName == null)
- return cnt;
-
- Collection cards = getAllCards(cardName);
- if (null == cards) {
+ public int getArtCount(String cardName, String setCode) {
+ if (cardName == null || setCode == null)
return 0;
- }
-
- for (PaperCard pc : cards) {
- if (pc.getEdition().equalsIgnoreCase(setName)) {
- cnt++;
+ Collection cardsInSet = getAllCards(cardName, new Predicate() {
+ @Override
+ public boolean apply(PaperCard card) {
+ return card.getEdition().equalsIgnoreCase(setCode);
}
- }
-
- return cnt;
+ });
+ return cardsInSet.size();
}
// returns a list of all cards from their respective latest (or preferred) editions
@@ -607,11 +771,11 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public Collection getAllCards() {
- return Collections.unmodifiableCollection(getAllCardsByName().values());
+ return Collections.unmodifiableCollection(allCardsByName.values());
}
public Collection getAllCardsNoAlt() {
- return Multimaps.filterEntries(getAllCardsByName(), new Predicate>() {
+ return Multimaps.filterEntries(allCardsByName, new Predicate>() {
@Override
public boolean apply(Entry entry) {
return entry.getKey().equals(entry.getValue().getName());
@@ -629,7 +793,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} catch (Exception ex) {
return false;
}
- return edition != null && edition.getType() != Type.PROMOS;
+ return edition != null && edition.getType() != Type.PROMO;
}
}));
}
@@ -641,7 +805,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
CardEdition edition = null;
try {
edition = editions.getEditionByCodeOrThrow(paperCard.getEdition());
- if (edition.getType() == Type.PROMOS||edition.getType() == Type.REPRINT)
+ if (edition.getType() == Type.PROMO||edition.getType() == Type.REPRINT)
return false;
} catch (Exception ex) {
return false;
@@ -660,11 +824,11 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public List getAllCards(String cardName) {
- return getAllCardsByName().get(getName(cardName));
+ return allCardsByName.get(getName(cardName));
}
public List getAllCardsNoAlt(String cardName) {
- return Lists.newArrayList(Multimaps.filterEntries(getAllCardsByName(), new Predicate>() {
+ return Lists.newArrayList(Multimaps.filterEntries(allCardsByName, new Predicate>() {
@Override
public boolean apply(Entry entry) {
return entry.getKey().equals(entry.getValue().getName());
@@ -672,23 +836,32 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}).get(getName(cardName)));
}
- /** Returns a modifiable list of cards matching the given predicate */
+ /**
+ * Returns a modifiable list of cards matching the given predicate
+ */
@Override
public List getAllCards(Predicate predicate) {
return Lists.newArrayList(Iterables.filter(getAllCards(), predicate));
}
- /** Returns a modifiable list of cards matching the given predicate */
+ @Override
+ public List getAllCards(final String cardName, Predicate predicate){
+ return Lists.newArrayList(Iterables.filter(getAllCards(cardName), predicate));
+ }
+
+ /**
+ * Returns a modifiable list of cards matching the given predicate
+ */
public List getAllCardsNoAlt(Predicate predicate) {
return Lists.newArrayList(Iterables.filter(getAllCardsNoAlt(), predicate));
}
// Do I want a foiled version of these cards?
@Override
- public List getAllCardsFromEdition(CardEdition edition) {
+ public Collection getAllCards(CardEdition edition) {
List cards = Lists.newArrayList();
- for(CardInSet cis : edition.getAllCardsInSet()) {
+ for (CardInSet cis : edition.getAllCardsInSet()) {
PaperCard card = this.getCard(cis.name, edition.getCode());
if (card == null) {
// Just in case the card is listed in the edition file but Forge doesn't support it
@@ -702,7 +875,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
@Override
public boolean contains(String name) {
- return getAllCardsByName().containsKey(getName(name));
+ return allCardsByName.containsKey(getName(name));
}
@Override
@@ -710,6 +883,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return getAllCards().iterator();
}
+ @Override
public Predicate super PaperCard> wasPrintedInSets(List setCodes) {
return new PredicateExistsInSets(setCodes);
}
@@ -731,7 +905,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return false;
}
}
+
// This Predicate validates if a card was printed at [rarity], on any of its printings
+ @Override
public Predicate super PaperCard> wasPrintedAtRarity(CardRarity rarity) {
return new PredicatePrintedAtRarity(rarity);
}
@@ -747,6 +923,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
}
}
+
@Override
public boolean apply(final PaperCard subject) {
return matchingCards.contains(subject.getName());
@@ -754,7 +931,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
}
public StringBuilder appendCardToStringBuilder(PaperCard card, StringBuilder sb) {
- final boolean hasBadSetInfo = "???".equals(card.getEdition()) || StringUtils.isBlank(card.getEdition());
+ final boolean hasBadSetInfo = (card.getEdition()).equals(CardEdition.UNKNOWN.getCode()) || StringUtils.isBlank(card.getEdition());
sb.append(card.getName());
if (card.isFoil()) {
sb.append(CardDb.foilSuffix);
@@ -763,7 +940,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
if (!hasBadSetInfo) {
int artCount = getArtCount(card.getName(), card.getEdition());
sb.append(CardDb.NameSetSeparator).append(card.getEdition());
- if (artCount > 1) {
+ if (artCount >= IPaperCard.DEFAULT_ART_INDEX) {
sb.append(CardDb.NameSetSeparator).append(card.getArtIndex()); // indexes start at 1 to match image file name conventions
}
}
@@ -771,12 +948,13 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
return sb;
}
- public PaperCard createUnsupportedCard(String cardName) {
- CardRequest request = CardRequest.fromString(cardName);
+ public PaperCard createUnsupportedCard(String cardRequest) {
+
+ CardRequest request = CardRequest.fromString(cardRequest);
CardEdition cardEdition = CardEdition.UNKNOWN;
CardRarity cardRarity = CardRarity.Unknown;
- // May iterate over editions and find out if there is any card named 'cardName' but not implemented with Forge script.
+ // May iterate over editions and find out if there is any card named 'cardRequest' but not implemented with Forge script.
if (StringUtils.isBlank(request.edition)) {
for (CardEdition edition : editions) {
for (CardInSet cardInSet : edition.getAllCardsInSet()) {
@@ -799,31 +977,41 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
break;
}
}
- }
- else {
+ } else {
cardEdition = CardEdition.UNKNOWN;
}
}
+ // Note for myself: no localisation needed here as this goes in logs
if (cardRarity == CardRarity.Unknown) {
- System.err.println("Forge does not know of such a card's existence. Have you mistyped the card name?");
+ System.err.println("Forge could not find this card in the Database. Any chance you might have mistyped the card name?");
} else {
- System.err.println("We're sorry, but you cannot use this card yet.");
+ System.err.println("We're sorry, but this card is not supported yet.");
}
- return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);
+ return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity);
+
}
private final Editor editor = new Editor();
- public Editor getEditor() { return editor; }
+
+ public Editor getEditor() {
+ return editor;
+ }
+
public class Editor {
private boolean immediateReindex = true;
- public CardRules putCard(CardRules rules) { return putCard(rules, null); /* will use data from editions folder */ }
- public CardRules putCard(CardRules rules, List> whenItWasPrinted){ // works similarly to Map, returning prev. value
+
+ public CardRules putCard(CardRules rules) {
+ return putCard(rules, null); /* will use data from editions folder */
+ }
+
+ public CardRules putCard(CardRules rules, List> whenItWasPrinted) {
+ // works similarly to Map, returning prev. value
String cardName = rules.getName();
CardRules result = rulesByName.get(cardName);
- if (result != null && result.getName().equals(cardName)){ // change properties only
+ if (result != null && result.getName().equals(cardName)) { // change properties only
result.reinitializeFromRules(rules);
return result;
}
@@ -833,34 +1021,37 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
// 1. generate all paper cards from edition data we have (either explicit, or found in res/editions, or add to unknown edition)
List paperCards = new ArrayList<>();
if (null == whenItWasPrinted || whenItWasPrinted.isEmpty()) {
- // TODO Not performant Each time we "putCard" we loop through ALL CARDS IN ALL editions
+ // @friarsol: Not performant Each time we "putCard" we loop through ALL CARDS IN ALL editions
+ // @leriomaggio: DONE! re-using here the same strategy implemented for lazy-loading!
for (CardEdition e : editions.getOrderedEditions()) {
- int artIdx = 1;
- for (CardInSet cis : e.getAllCardsInSet()) {
- if (!cis.name.equals(cardName)) {
- continue;
- }
- paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++));
- }
+ int artIdx = IPaperCard.DEFAULT_ART_INDEX;
+ for (CardInSet cis : e.getCardInSet(cardName))
+ paperCards.add(new PaperCard(rules, e.getCode(), cis.rarity, artIdx++, false,
+ cis.collectorNumber, cis.artistName));
}
- }
- else {
+ } else {
String lastEdition = null;
int artIdx = 0;
- for (Pair tuple : whenItWasPrinted){
+ for (Pair tuple : whenItWasPrinted) {
if (!tuple.getKey().equals(lastEdition)) {
- artIdx = 1;
+ artIdx = IPaperCard.DEFAULT_ART_INDEX; // reset artIndex
lastEdition = tuple.getKey();
}
CardEdition ed = editions.get(lastEdition);
- if (null == ed) {
+ if (ed == null) {
continue;
}
- paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++));
+ List cardsInSet = ed.getCardInSet(cardName);
+ if (cardsInSet.isEmpty())
+ continue;
+ int cardInSetIndex = Math.max(artIdx-1, 0); // make sure doesn't go below zero
+ CardInSet cds = cardsInSet.get(cardInSetIndex); // use ArtIndex to get the right Coll. Number
+ paperCards.add(new PaperCard(rules, lastEdition, tuple.getValue(), artIdx++, false,
+ cds.collectorNumber, cds.artistName));
}
}
if (paperCards.isEmpty()) {
- paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special, 1));
+ paperCards.add(new PaperCard(rules, CardEdition.UNKNOWN.getCode(), CardRarity.Special));
}
// 2. add them to db
for (PaperCard paperCard : paperCards) {
@@ -876,6 +1067,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
public boolean isImmediateReindex() {
return immediateReindex;
}
+
public void setImmediateReindex(boolean immediateReindex) {
this.immediateReindex = immediateReindex;
}
diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java
index 8712e8db5ac..820c6865c30 100644
--- a/forge-core/src/main/java/forge/card/CardEdition.java
+++ b/forge-core/src/main/java/forge/card/CardEdition.java
@@ -17,49 +17,27 @@
*/
package forge.card;
-import java.io.File;
-import java.io.FilenameFilter;
-import java.text.SimpleDateFormat;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Calendar;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Map.Entry;
-import java.util.Set;
-import java.util.TreeMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-import org.apache.commons.lang3.StringUtils;
-
import com.google.common.base.Function;
import com.google.common.base.Predicate;
-import com.google.common.collect.ArrayListMultimap;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Lists;
-
+import com.google.common.collect.*;
import forge.StaticData;
-import forge.card.CardDb.SetPreference;
+import forge.card.CardDb.CardArtPreference;
import forge.deck.CardPool;
import forge.item.PaperCard;
import forge.item.SealedProduct;
-import forge.util.Aggregates;
-import forge.util.FileSection;
-import forge.util.FileUtil;
-import forge.util.IItemReader;
-import forge.util.MyRandom;
+import forge.util.*;
import forge.util.storage.StorageBase;
import forge.util.storage.StorageReaderBase;
import forge.util.storage.StorageReaderFolder;
+import org.apache.commons.lang3.StringUtils;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
/**
@@ -78,19 +56,23 @@ public final class CardEdition implements Comparable {
CORE,
EXPANSION,
-
- REPRINT,
- ONLINE,
STARTER,
+ REPRINT,
+ BOXED_SET,
- DUEL_DECKS,
- PREMIUM_DECK_SERIES,
- FROM_THE_VAULT,
+ COLLECTOR_EDITION,
+ DUEL_DECK,
+ PROMO,
+ ONLINE,
- OTHER,
- PROMOS,
+ DRAFT,
+
+ COMMANDER,
+ MULTIPLAYER,
FUNNY,
- THIRDPARTY; // custom sets
+
+ OTHER, // FALLBACK CATEGORY
+ CUSTOM_SET; // custom sets
public String getBoosterBoxDefault() {
switch (this) {
@@ -101,6 +83,29 @@ public final class CardEdition implements Comparable {
return "0";
}
}
+
+ public String getFatPackDefault() {
+ switch (this) {
+ case CORE:
+ case EXPANSION:
+ return "10";
+ default:
+ return "0";
+ }
+ }
+
+ public String toString(){
+ String[] names = TextUtil.splitWithParenthesis(this.name().toLowerCase(), '_');
+ for (int i = 0; i < names.length; i++)
+ names[i] = TextUtil.capitalize(names[i]);
+ return TextUtil.join(Arrays.asList(names), " ");
+ }
+
+ public static Type fromString(String label){
+ List names = Arrays.asList(TextUtil.splitWithParenthesis(label.toUpperCase(), ' '));
+ String value = TextUtil.join(names, "_");
+ return Type.valueOf(value);
+ }
}
public enum FoilType {
@@ -133,7 +138,8 @@ public final class CardEdition implements Comparable {
BUY_A_BOX("buy a box"),
PROMO("promo"),
BUNDLE("bundle"),
- BOX_TOPPER("box topper");
+ BOX_TOPPER("box topper"),
+ DUNGEONS("dungeons");
private final String name;
@@ -157,11 +163,13 @@ public final class CardEdition implements Comparable {
public final CardRarity rarity;
public final String collectorNumber;
public final String name;
+ public final String artistName;
- public CardInSet(final String name, final String collectorNumber, final CardRarity rarity) {
+ public CardInSet(final String name, final String collectorNumber, final CardRarity rarity, final String artistName) {
this.name = name;
this.collectorNumber = collectorNumber;
this.rarity = rarity;
+ this.artistName = artistName;
}
public String toString() {
@@ -175,6 +183,10 @@ public final class CardEdition implements Comparable {
sb.append(' ');
}
sb.append(name);
+ if (artistName != null) {
+ sb.append(" @");
+ sb.append(artistName);
+ }
return sb.toString();
}
@@ -187,29 +199,36 @@ public final class CardEdition implements Comparable {
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
*/
+ private static final Map sortableCollNumberLookup = new HashMap<>();
public static String getSortableCollectorNumber(final String collectorNumber){
- String sortableCollNr = collectorNumber;
- if (sortableCollNr == null || sortableCollNr.length() == 0)
- sortableCollNr = "50000"; // very big number of 5 digits to have them in last positions
+ String inputCollNumber = collectorNumber;
+ if (collectorNumber == null || collectorNumber.length() == 0)
+ inputCollNumber = "50000"; // very big number of 5 digits to have them in last positions
+
+ String matchedCollNr = sortableCollNumberLookup.getOrDefault(inputCollNumber, null);
+ if (matchedCollNr != null)
+ return matchedCollNr;
// Now, for proper sorting, let's zero-pad the collector number (if integer)
int collNr;
+ String sortableCollNr;
try {
- collNr = Integer.parseInt(sortableCollNr);
+ collNr = Integer.parseInt(inputCollNumber);
sortableCollNr = String.format("%05d", collNr);
} catch (NumberFormatException ex) {
- String nonNumeric = sortableCollNr.replaceAll("[0-9]", "");
- String onlyNumeric = sortableCollNr.replaceAll("[^0-9]", "");
+ String nonNumSub = inputCollNumber.replaceAll("[0-9]", "");
+ String onlyNumSub = inputCollNumber.replaceAll("[^0-9]", "");
try {
- collNr = Integer.parseInt(onlyNumeric);
+ collNr = Integer.parseInt(onlyNumSub);
} catch (NumberFormatException exon) {
- collNr = 0; // this is the case of ONLY-letters collector numbers
+ collNr = 0; // this is the case of ONLY-letters collector numbers
}
- if ((collNr > 0) && (sortableCollNr.startsWith(onlyNumeric))) // e.g. 12a, 37+, 2018f,
- sortableCollNr = String.format("%05d", collNr) + nonNumeric;
- else // e.g. WS6, S1
- sortableCollNr = nonNumeric + String.format("%05d", collNr);
+ if ((collNr > 0) && (inputCollNumber.startsWith(onlyNumSub))) // e.g. 12a, 37+, 2018f,
+ sortableCollNr = String.format("%05d", collNr) + nonNumSub;
+ else // e.g. WS6, S1
+ sortableCollNr = nonNumSub + String.format("%05d", collNr);
}
+ sortableCollNumberLookup.put(inputCollNumber, sortableCollNr);
return sortableCollNr;
}
@@ -247,6 +266,8 @@ public final class CardEdition implements Comparable {
// SealedProduct
private String prerelease = null;
private int boosterBoxCount = 36;
+ private int fatPackCount = 10;
+ private String fatPackExtraSlots = "";
// Booster/draft info
private boolean smallSetOverride = false;
@@ -337,6 +358,8 @@ public final class CardEdition implements Comparable {
public String getPrerelease() { return prerelease; }
public int getBoosterBoxCount() { return boosterBoxCount; }
+ public int getFatPackCount() { return fatPackCount; }
+ public String getFatPackExtraSlots() { return fatPackExtraSlots; }
public FoilType getFoilType() { return foilType; }
public double getFoilChanceInBooster() { return foilChanceInBooster; }
@@ -356,6 +379,27 @@ public final class CardEdition implements Comparable {
return cardsInSet;
}
+ private ListMultimap cardsInSetLookupMap = null;
+
+ /**
+ * Get all the CardInSet instances with the input card name.
+ * @param cardName Name of the Card to look for.
+ * @return A List of all the CardInSet instances for a given name.
+ * If not fount, an Empty sequence (view) will be returned instead!
+ */
+ public List getCardInSet(String cardName){
+ if (cardsInSetLookupMap == null) {
+ // initialise
+ cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), CollectionSuppliers.arrayLists());
+ List cardsInSet = this.getAllCardsInSet();
+ for (CardInSet cis : cardsInSet){
+ String key = cis.name;
+ cardsInSetLookupMap.put(key, cis);
+ }
+ }
+ return this.cardsInSetLookupMap.get(cardName);
+ }
+
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
public Map getTokens() { return tokenNormalized; }
@@ -446,11 +490,11 @@ public final class CardEdition implements Comparable {
Map cardToIndex = new HashMap<>();
List sheets = Lists.newArrayList();
- for(String sectionName : cardMap.keySet()) {
+ for (String sectionName : cardMap.keySet()) {
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sectionName));
List cards = cardMap.get(sectionName);
- for(CardInSet card : cards) {
+ for (CardInSet card : cards) {
int index = 1;
if (cardToIndex.containsKey(card.name)) {
index = cardToIndex.get(card.name);
@@ -465,7 +509,7 @@ public final class CardEdition implements Comparable {
sheets.add(sheet);
}
- for(String sheetName : customPrintSheetsToParse.keySet()) {
+ for (String sheetName : customPrintSheetsToParse.keySet()) {
List sheetToParse = customPrintSheetsToParse.get(sheetName);
CardPool sheetPool = CardPool.fromCardList(sheetToParse);
PrintSheet sheet = new PrintSheet(String.format("%s %s", this.getCode(), sheetName), sheetPool);
@@ -476,8 +520,16 @@ public final class CardEdition implements Comparable {
}
public static class Reader extends StorageReaderFolder {
+ private boolean isCustomEditions;
+
public Reader(File path) {
super(path, CardEdition.FN_GET_CODE);
+ this.isCustomEditions = false;
+ }
+
+ public Reader(File path, boolean isCustomEditions) {
+ super(path, CardEdition.FN_GET_CODE);
+ this.isCustomEditions = isCustomEditions;
}
@Override
@@ -488,7 +540,7 @@ public final class CardEdition implements Comparable {
/*
The following pattern will match the WAR Japanese art entries,
it should also match the Un-set and older alternate art cards
- like Merseine from FEM (should the editions files ever be updated)
+ like Merseine from FEM.
*/
//"(^(?[0-9]+.?) )?((?[SCURML]) )?(?.*)$"
/* Ideally we'd use the named group above, but Android 6 and
@@ -501,7 +553,7 @@ public final class CardEdition implements Comparable {
* name - grouping #5
*/
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
- "(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?(.*)$"
+ "(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?([^@]*)( @(.*))?$"
);
ListMultimap cardMap = ArrayListMultimap.create();
@@ -526,7 +578,8 @@ public final class CardEdition implements Comparable {
String collectorNumber = matcher.group(2);
CardRarity r = CardRarity.smartValueOf(matcher.group(4));
String cardName = matcher.group(5);
- CardInSet cis = new CardInSet(cardName, collectorNumber, r);
+ String artistName = matcher.group(7);
+ CardInSet cis = new CardInSet(cardName, collectorNumber, r, artistName);
cardMap.put(sectionName, cis);
}
@@ -540,7 +593,7 @@ public final class CardEdition implements Comparable {
// parse tokens section
if (contents.containsKey("tokens")) {
- for(String line : contents.get("tokens")) {
+ for (String line : contents.get("tokens")) {
if (StringUtils.isBlank(line))
continue;
@@ -568,11 +621,11 @@ public final class CardEdition implements Comparable {
res.mciCode = res.code2.toLowerCase();
}
res.scryfallCode = section.get("ScryfallCode");
- if (res.scryfallCode == null){
+ if (res.scryfallCode == null) {
res.scryfallCode = res.code;
}
res.cardsLanguage = section.get("CardLang");
- if (res.cardsLanguage == null){
+ if (res.cardsLanguage == null) {
res.cardsLanguage = "en";
}
@@ -596,21 +649,28 @@ public final class CardEdition implements Comparable {
res.alias = section.get("alias");
res.borderColor = BorderColor.valueOf(section.get("border", "Black").toUpperCase(Locale.ENGLISH));
- String type = section.get("type");
Type enumType = Type.UNKNOWN;
- if (null != type && !type.isEmpty()) {
- try {
- enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH));
- } catch (IllegalArgumentException ignored) {
- // ignore; type will get UNKNOWN
- System.err.println("Ignoring unknown type in set definitions: name: " + res.name + "; type: " + type);
+ if (this.isCustomEditions){
+ enumType = Type.CUSTOM_SET; // Forcing ThirdParty Edition Type to avoid inconsistencies
+ } else {
+ String type = section.get("type");
+ if (null != type && !type.isEmpty()) {
+ try {
+ enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH));
+ } catch (IllegalArgumentException ignored) {
+ // ignore; type will get UNKNOWN
+ System.err.println("Ignoring unknown type in set definitions: name: " + res.name + "; type: " + type);
+ }
}
+
}
res.type = enumType;
res.prerelease = section.get("Prerelease", null);
res.boosterBoxCount = Integer.parseInt(section.get("BoosterBox", enumType.getBoosterBoxDefault()));
+ res.fatPackCount = Integer.parseInt(section.get("FatPack", enumType.getFatPackDefault()));
+ res.fatPackExtraSlots = section.get("FatPackExtraSlots", "");
- switch(section.get("foil", "newstyle").toLowerCase()) {
+ switch (section.get("foil", "newstyle").toLowerCase()) {
case "notsupported":
res.foilType = FoilType.NOT_SUPPORTED;
break;
@@ -743,7 +803,7 @@ public final class CardEdition implements Comparable {
@Override
public Map readAll() {
Map map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
- for(CardEdition ce : Collection.this) {
+ for (CardEdition ce : Collection.this) {
List boosterTypes = Lists.newArrayList(ce.getAvailableBoosterTypes());
for (String type : boosterTypes) {
String setAffix = type.equals("Draft") ? "" : type;
@@ -766,38 +826,35 @@ public final class CardEdition implements Comparable {
};
}
- public CardEdition getEarliestEditionWithAllCards(CardPool cards) {
+ /* @leriomaggio
+ The original name "getEarliestEditionWithAllCards" was completely misleading, as it did
+ not reflect at all what the method really does (and what's the original goal).
+
+ What the method does is to return the **latest** (as in the most recent)
+ Card Edition among all the different "Original" sets (as in "first print") were cards
+ in the Pool can be found.
+ Therefore, nothing to do with an Edition "including" all the cards.
+ */
+ public CardEdition getTheLatestOfAllTheOriginalEditionsOfCardsIn(CardPool cards) {
Set minEditions = new HashSet<>();
-
- SetPreference strictness = SetPreference.EarliestCoreExp;
-
+ CardDb db = StaticData.instance().getCommonCards();
for (Entry k : cards) {
- PaperCard cp = StaticData.instance().getCommonCards().getCardFromEdition(k.getKey().getName(), strictness);
- if( cp == null && strictness == SetPreference.EarliestCoreExp) {
- strictness = SetPreference.Earliest; // card is not found in core and expansions only (probably something CMD or C13)
- cp = StaticData.instance().getCommonCards().getCardFromEdition(k.getKey().getName(), strictness);
- }
- if ( cp == null )
- cp = k.getKey(); // it's unlikely, this code will ever run
-
+ // NOTE: Even if we do force a very stringent Policy on Editions
+ // (which only considers core, expansions, and reprint editions), the fetch method
+ // is flexible enough to relax the constraint automatically, if no card can be found
+ // under those conditions (i.e. ORIGINAL_ART_ALL_EDITIONS will be automatically used instead).
+ PaperCard cp = db.getCardFromEditions(k.getKey().getName(),
+ CardArtPreference.ORIGINAL_ART_CORE_EXPANSIONS_REPRINT_ONLY);
+ if (cp == null) // it's unlikely, this code will ever run. Only Happens if card does not exist.
+ cp = k.getKey();
minEditions.add(cp.getEdition());
}
-
- for(CardEdition ed : getOrderedEditions()) {
- if(minEditions.contains(ed.getCode()))
+ for (CardEdition ed : getOrderedEditions()) {
+ if (minEditions.contains(ed.getCode()))
return ed;
}
return UNKNOWN;
}
-
- public Date getEarliestDateWithAllCards(CardPool cardPool) {
- CardEdition earliestSet = StaticData.instance().getEditions().getEarliestEditionWithAllCards(cardPool);
-
- Calendar cal = Calendar.getInstance();
- cal.setTime(earliestSet.getDate());
- cal.add(Calendar.DATE, 1);
- return cal.getTime();
- }
}
public static class Predicates {
@@ -826,7 +883,7 @@ public final class CardEdition implements Comparable {
private static class CanMakeFatPack implements Predicate {
@Override
public boolean apply(final CardEdition subject) {
- return StaticData.instance().getFatPacks().contains(subject.getCode());
+ return subject.getFatPackCount() > 0;
}
}
diff --git a/forge-core/src/main/java/forge/card/CardRules.java b/forge-core/src/main/java/forge/card/CardRules.java
index b6989348d16..b981a8cc4ab 100644
--- a/forge-core/src/main/java/forge/card/CardRules.java
+++ b/forge-core/src/main/java/forge/card/CardRules.java
@@ -67,7 +67,7 @@ public final class CardRules implements ICardCharacteristics {
}
void reinitializeFromRules(CardRules newRules) {
- if(!newRules.getName().equals(this.getName()))
+ if (!newRules.getName().equals(this.getName()))
throw new UnsupportedOperationException("You cannot rename the card using the same CardRules object");
splitType = newRules.splitType;
@@ -91,7 +91,7 @@ public final class CardRules implements ICardCharacteristics {
}
}
int len = oracleText.length();
- for(int i = 0; i < len; i++) {
+ for (int i = 0; i < len; i++) {
char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray()
switch(c) {
case('('): isReminder = i > 0; break; // if oracle has only reminder, consider it valid rules (basic and true lands need this)
@@ -99,7 +99,7 @@ public final class CardRules implements ICardCharacteristics {
case('{'): isSymbol = true; break;
case('}'): isSymbol = false; break;
default:
- if(isSymbol && !isReminder) {
+ if (isSymbol && !isReminder) {
switch(c) {
case('W'): res |= MagicColor.WHITE; break;
case('U'): res |= MagicColor.BLUE; break;
@@ -182,7 +182,7 @@ public final class CardRules implements ICardCharacteristics {
//if card face has no cost, assume castable only by mana of its defined color
return face.getColor().hasNoColorsExcept(colorCode);
}
- return face.getManaCost().canBePaidWithAvaliable(colorCode);
+ return face.getManaCost().canBePaidWithAvailable(colorCode);
}
public boolean canCastWithAvailable(byte colorCode) {
@@ -211,11 +211,20 @@ public final class CardRules implements ICardCharacteristics {
}
public boolean canBeCommander() {
- CardType type = mainPart.getType();
- if (type.isLegendary() && type.isCreature()) {
+ if (mainPart.getOracleText().contains("can be your commander")) {
return true;
}
- return mainPart.getOracleText().contains("can be your commander");
+ CardType type = mainPart.getType();
+ boolean creature = type.isCreature();
+ for (String staticAbility : mainPart.getStaticAbilities()) { // Check for Grist
+ if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
+ creature = true;
+ }
+ }
+ if (type.isLegendary() && creature) {
+ return true;
+ }
+ return false;
}
public boolean canBePartnerCommander() {
@@ -234,12 +243,38 @@ public final class CardRules implements ICardCharacteristics {
public boolean canBeBrawlCommander() {
CardType type = mainPart.getType();
- return type.isLegendary() && (type.isCreature() || type.isPlaneswalker());
+ if (!type.isLegendary()) {
+ return false;
+ }
+ if (type.isCreature() || type.isPlaneswalker()) {
+ return true;
+ }
+
+ // Grist is checked above, but new cards might work this way
+ for (String staticAbility : mainPart.getStaticAbilities()) {
+ if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
+ return true;
+ }
+ }
+ return false;
}
public boolean canBeTinyLeadersCommander() {
CardType type = mainPart.getType();
- return type.isLegendary() && (type.isCreature() || type.isPlaneswalker());
+ if (!type.isLegendary()) {
+ return false;
+ }
+ if (type.isCreature() || type.isPlaneswalker()) {
+ return true;
+ }
+
+ // Grist is checked above, but new cards might work this way
+ for (String staticAbility : mainPart.getStaticAbilities()) {
+ if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("AddType$ Creature")) {
+ return true;
+ }
+ }
+ return false;
}
public String getMeldWith() {
@@ -282,7 +317,7 @@ public final class CardRules implements ICardCharacteristics {
/** Instantiates class, reads a card. For batch operations better create you own reader instance. */
public static CardRules fromScript(Iterable script) {
Reader crr = new Reader();
- for(String line : script) {
+ for (String line : script) {
crr.parseLine(line);
}
return crr.getCard();
@@ -379,7 +414,7 @@ public final class CardRules implements ICardCharacteristics {
String key = colonPos > 0 ? line.substring(0, colonPos) : line;
String value = colonPos > 0 ? line.substring(1+colonPos).trim() : null;
- switch(key.charAt(0)) {
+ switch (key.charAt(0)) {
case 'A':
if ("A".equals(key)) {
this.faces[curFace].addAbility(value);
@@ -388,10 +423,10 @@ public final class CardRules implements ICardCharacteristics {
String variable = colonPos > 0 ? value.substring(0, colonPos) : value;
value = colonPos > 0 ? value.substring(1+colonPos) : null;
- if ( "RemoveDeck".equals(variable) ) {
- this.removedFromAIDecks = "All".equalsIgnoreCase(value);
- this.removedFromRandomDecks = "Random".equalsIgnoreCase(value);
- this.removedFromNonCommanderDecks = "NonCommander".equalsIgnoreCase(value);
+ if ("RemoveDeck".equals(variable)) {
+ this.removedFromAIDecks |= "All".equalsIgnoreCase(value);
+ this.removedFromRandomDecks |= "Random".equalsIgnoreCase(value);
+ this.removedFromNonCommanderDecks |= "NonCommander".equalsIgnoreCase(value);
}
} else if ("AlternateMode".equals(key)) {
this.altMode = CardSplitType.smartValueOf(value);
@@ -478,14 +513,14 @@ public final class CardRules implements ICardCharacteristics {
case 'S':
if ("S".equals(key)) {
this.faces[this.curFace].addStaticAbility(value);
- } else if ( "SVar".equals(key) ) {
- if ( null == value ) throw new IllegalArgumentException("SVar has no variable name");
+ } else if ("SVar".equals(key)) {
+ if (null == value) throw new IllegalArgumentException("SVar has no variable name");
colonPos = value.indexOf(':');
String variable = colonPos > 0 ? value.substring(0, colonPos) : value;
value = colonPos > 0 ? value.substring(1+colonPos) : null;
- if ( "Picture".equals(variable) ) {
+ if ("Picture".equals(variable)) {
this.pictureUrl[this.curFace] = value;
} else
this.faces[curFace].addSVar(variable, value);
diff --git a/forge-core/src/main/java/forge/card/CardRulesPredicates.java b/forge-core/src/main/java/forge/card/CardRulesPredicates.java
index 560bfbe2e9c..935c7d56aff 100644
--- a/forge-core/src/main/java/forge/card/CardRulesPredicates.java
+++ b/forge-core/src/main/java/forge/card/CardRulesPredicates.java
@@ -26,6 +26,14 @@ public final class CardRulesPredicates {
}
};
+ /** The Constant isKeptInAiLimitedDecks. */
+ public static final Predicate IS_KEPT_IN_AI_LIMITED_DECKS = new Predicate() {
+ @Override
+ public boolean apply(final CardRules card) {
+ return !card.getAiHints().getRemAIDecks() && !card.getAiHints().getRemNonCommanderDecks();
+ }
+ };
+
/** The Constant isKeptInRandomDecks. */
public static final Predicate IS_KEPT_IN_RANDOM_DECKS = new Predicate() {
@Override
@@ -262,7 +270,6 @@ public final class CardRulesPredicates {
return new PredicateSuperType(type, isEqual);
}
-
/**
* Checks for color.
*
diff --git a/forge-core/src/main/java/forge/card/CardType.java b/forge-core/src/main/java/forge/card/CardType.java
index 3941b990124..0a332ac2371 100644
--- a/forge-core/src/main/java/forge/card/CardType.java
+++ b/forge-core/src/main/java/forge/card/CardType.java
@@ -58,7 +58,6 @@ public final class CardType implements Comparable, CardTypeView {
Conspiracy(false, "conspiracies"),
Creature(true, "creatures"),
Dungeon(false, "dungeons"),
- Emblem(false, "emblems"),
Enchantment(true, "enchantments"),
Instant(false, "instants"),
Land(true, "lands"),
@@ -437,11 +436,6 @@ public final class CardType implements Comparable, CardTypeView {
return coreTypes.contains(CoreType.Phenomenon);
}
- @Override
- public boolean isEmblem() {
- return coreTypes.contains(CoreType.Emblem);
- }
-
@Override
public boolean isTribal() {
return coreTypes.contains(CoreType.Tribal);
@@ -457,8 +451,7 @@ public final class CardType implements Comparable, CardTypeView {
if (calculatedType == null) {
if (subtypes.isEmpty()) {
calculatedType = StringUtils.join(getTypesBeforeDash(), ' ');
- }
- else {
+ } else {
calculatedType = StringUtils.join(getTypesBeforeDash(), ' ') + " - " + StringUtils.join(subtypes, " ");
}
}
@@ -484,7 +477,7 @@ public final class CardType implements Comparable, CardTypeView {
}
// we assume that changes are already correctly ordered (taken from TreeMap.values())
for (final CardChangedType ct : changedCardTypes) {
- if(null == newType)
+ if (null == newType)
newType = new CardType(CardType.this);
if (ct.isRemoveCardTypes()) {
@@ -547,7 +540,7 @@ public final class CardType implements Comparable, CardTypeView {
if (!isInstant() && !isSorcery()) {
Iterables.removeIf(subtypes, Predicates.IS_SPELL_TYPE);
}
- if (!isPlaneswalker() && !isEmblem()) {
+ if (!isPlaneswalker()) {
Iterables.removeIf(subtypes, Predicates.IS_WALKER_TYPE);
}
}
@@ -766,7 +759,6 @@ public final class CardType implements Comparable, CardTypeView {
};
}
-
///////// Utility methods
public static boolean isACardType(final String cardType) {
return CoreType.isValidEnum(cardType);
diff --git a/forge-core/src/main/java/forge/card/CardTypeView.java b/forge-core/src/main/java/forge/card/CardTypeView.java
index cfecf57a752..f89452baac8 100644
--- a/forge-core/src/main/java/forge/card/CardTypeView.java
+++ b/forge-core/src/main/java/forge/card/CardTypeView.java
@@ -42,7 +42,6 @@ public interface CardTypeView extends Iterable, Serializable {
boolean isBasicLand();
boolean isPlane();
boolean isPhenomenon();
- boolean isEmblem();
boolean isTribal();
boolean isDungeon();
CardTypeView getTypeWithChanges(Iterable changedCardTypes);
diff --git a/forge-core/src/main/java/forge/card/ICardDatabase.java b/forge-core/src/main/java/forge/card/ICardDatabase.java
index a847b0149ad..b158404d320 100644
--- a/forge-core/src/main/java/forge/card/ICardDatabase.java
+++ b/forge-core/src/main/java/forge/card/ICardDatabase.java
@@ -1,36 +1,93 @@
package forge.card;
+import com.google.common.base.Predicate;
+import forge.card.CardDb.CardArtPreference;
+import forge.item.PaperCard;
+
import java.util.Collection;
import java.util.Date;
import java.util.List;
-import com.google.common.base.Predicate;
-
-import forge.card.CardDb.SetPreference;
-import forge.item.PaperCard;
-
public interface ICardDatabase extends Iterable {
+ /**
+ * Magic Cards Database.
+ * --------------------
+ * This interface defines the general API for Database Access and Cards' Lookup.
+ *
+ * Methods for single Card's lookup currently support three alternative strategies:
+ * 1. [getCard]: Card search based on a single card's attributes
+ * (i.e. name, edition, art, collectorNumber)
+ *
+ * 2. [getCardFromSet]: Card Lookup from a single Expansion set.
+ * Particularly useful in Deck Editors when a specific Set is specified.
+ *
+ * 3. [getCardFromEditions]: Card search considering a predefined `SetPreference` policy and/or a specified Date
+ * when no expansion is specified for a card.
+ * This method is particularly useful for Re-prints whenever no specific
+ * Expansion is specified (e.g. in Deck Import) and a decision should be made
+ * on which card to pick. This methods allows to adopt a SetPreference selection
+ * policy to make this decision.
+ *
+ * The API also includes methods to fetch Collection of Card (i.e. PaperCard instances):
+ * - all cards (no filter)
+ * - all unique cards (by name)
+ * - all prints of a single card
+ * - all cards from a single Expansion Set
+ * - all cards compliant with a filter condition (i.e. Predicate)
+ *
+ * Finally, various utility methods are supported:
+ * - Get the foil version of a Card (if Any)
+ * - Get the Order Number of a Card in an Expansion Set
+ * - Get the number of Print/Arts for a card in a Set (useful for those exp. having multiple arts)
+ * */
+
+ /* SINGLE CARD RETRIEVAL METHODS
+ * ============================= */
+ // 1. Card Lookup by attributes
PaperCard getCard(String cardName);
PaperCard getCard(String cardName, String edition);
PaperCard getCard(String cardName, String edition, int artIndex);
- PaperCard getCardFromEdition(String cardName, SetPreference fromSet);
- PaperCard getCardFromEdition(String cardName, Date printedBefore, SetPreference fromSet);
- PaperCard getCardFromEdition(String cardName, Date printedBefore, SetPreference fromSet, int artIndex);
-
- PaperCard getFoiled(PaperCard cpi);
+ // [NEW Methods] Including the card CollectorNumber as criterion for DB lookup
+ PaperCard getCard(String cardName, String edition, String collectorNumber);
+ PaperCard getCard(String cardName, String edition, int artIndex, String collectorNumber);
- int getPrintCount(String cardName, String edition);
- int getMaxPrintCount(String cardName);
+ // 2. Card Lookup from a single Expansion Set
+ PaperCard getCardFromSet(String cardName, CardEdition edition, boolean isFoil); // NOT yet used, included for API symmetry
+ PaperCard getCardFromSet(String cardName, CardEdition edition, String collectorNumber, boolean isFoil);
+ PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, boolean isFoil);
+ PaperCard getCardFromSet(String cardName, CardEdition edition, int artIndex, String collectorNumber, boolean isFoil);
- int getArtCount(String cardName, String edition);
+ // 3. Card lookup based on CardArtPreference Selection Policy
+ PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference);
+ PaperCard getCardFromEditions(String cardName, CardArtPreference artPreference, int artIndex);
- Collection getUniqueCards();
+ // 4. Specialised Card Lookup on CardArtPreference Selection and Release Date
+ PaperCard getCardFromEditionsReleasedBefore(String cardName, Date releaseDate);
+ PaperCard getCardFromEditionsReleasedBefore(String cardName, int artIndex, Date releaseDate);
+ PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, Date releaseDate);
+ PaperCard getCardFromEditionsReleasedBefore(String cardName, CardArtPreference artPreference, int artIndex, Date releaseDate);
+
+ PaperCard getCardFromEditionsReleasedAfter(String cardName, Date releaseDate);
+ PaperCard getCardFromEditionsReleasedAfter(String cardName, int artIndex, Date releaseDate);
+ PaperCard getCardFromEditionsReleasedAfter(String cardName, CardArtPreference artPreference, Date releaseDate);
+ PaperCard getCardFromEditionsReleasedAfter(String cardName, CardArtPreference artPreference, int artIndex, Date releaseDate);
+
+
+
+ /* CARDS COLLECTION RETRIEVAL METHODS
+ * ================================== */
Collection getAllCards();
Collection getAllCards(String cardName);
Collection getAllCards(Predicate predicate);
+ Collection getAllCards(String cardName,Predicate predicate);
+ Collection getAllCards(CardEdition edition);
+ Collection getUniqueCards();
- List getAllCardsFromEdition(CardEdition edition);
-
+ /* UTILITY METHODS
+ * =============== */
+ int getMaxArtIndex(String cardName);
+ int getArtCount(String cardName, String edition);
+ // Utility Predicates
Predicate super PaperCard> wasPrintedInSets(List allowedSetCodes);
-
+ Predicate super PaperCard> wasPrintedAtRarity(CardRarity rarity);
}
\ No newline at end of file
diff --git a/forge-core/src/main/java/forge/card/MagicColor.java b/forge-core/src/main/java/forge/card/MagicColor.java
index 13a73fb55d1..465559faa5a 100644
--- a/forge-core/src/main/java/forge/card/MagicColor.java
+++ b/forge-core/src/main/java/forge/card/MagicColor.java
@@ -84,7 +84,7 @@ public final class MagicColor {
}
public static String toShortString(final byte color) {
- switch (color){
+ switch (color) {
case WHITE: return "W";
case BLUE: return "U";
case BLACK: return "B";
@@ -95,7 +95,7 @@ public final class MagicColor {
}
public static String toLongString(final byte color) {
- switch (color){
+ switch (color) {
case WHITE: return Constant.WHITE;
case BLUE: return Constant.BLUE;
case BLACK: return Constant.BLACK;
diff --git a/forge-core/src/main/java/forge/card/PrintSheet.java b/forge-core/src/main/java/forge/card/PrintSheet.java
index 530e9ad2717..573c834e158 100644
--- a/forge-core/src/main/java/forge/card/PrintSheet.java
+++ b/forge-core/src/main/java/forge/card/PrintSheet.java
@@ -29,8 +29,8 @@ public class PrintSheet {
public static final IStorage initializePrintSheets(File sheetsFile, CardEdition.Collection editions) {
IStorage sheets = new StorageExtendable<>("Special print runs", new PrintSheet.Reader(sheetsFile));
- for(CardEdition edition : editions) {
- for(PrintSheet ps : edition.getPrintSheetsBySection()) {
+ for (CardEdition edition : editions) {
+ for (PrintSheet ps : edition.getPrintSheetsBySection()) {
sheets.add(ps.name, ps);
}
}
@@ -40,7 +40,6 @@ public class PrintSheet {
private final ItemPool cardsWithWeights;
-
private final String name;
public PrintSheet(String name0) {
this(name0, null);
@@ -64,7 +63,7 @@ public class PrintSheet {
}
public void addAll(Iterable cards, int weight) {
- for(PaperCard card : cards)
+ for (PaperCard card : cards)
cardsWithWeights.add(card, weight);
}
@@ -78,15 +77,15 @@ public class PrintSheet {
private PaperCard fetchRoulette(int start, int roulette, Collection toSkip) {
int sum = start;
boolean isSecondRun = start > 0;
- for(Entry cc : cardsWithWeights ) {
+ for (Entry cc : cardsWithWeights ) {
sum += cc.getValue();
- if( sum > roulette ) {
- if( toSkip != null && toSkip.contains(cc.getKey()))
+ if (sum > roulette) {
+ if (toSkip != null && toSkip.contains(cc.getKey()))
continue;
return cc.getKey();
}
}
- if( isSecondRun )
+ if (isSecondRun)
throw new IllegalStateException("Print sheet does not have enough unique cards");
return fetchRoulette(sum + 1, roulette, toSkip); // start over from beginning, in case last cards were to skip
@@ -94,8 +93,8 @@ public class PrintSheet {
public List all() {
List