diff --git a/forge-ai/pom.xml b/forge-ai/pom.xml index 4598d8cf7e6..74b400846f2 100644 --- a/forge-ai/pom.xml +++ b/forge-ai/pom.xml @@ -6,7 +6,7 @@ forge forge - 1.6.43-SNAPSHOT + 1.6.45-SNAPSHOT forge-ai diff --git a/forge-ai/src/main/java/forge/ai/AiAttackController.java b/forge-ai/src/main/java/forge/ai/AiAttackController.java index a0dd1b7bbbc..10c511ed5c6 100644 --- a/forge-ai/src/main/java/forge/ai/AiAttackController.java +++ b/forge-ai/src/main/java/forge/ai/AiAttackController.java @@ -208,7 +208,6 @@ public class AiAttackController { * @return a boolean. */ public final boolean isEffectiveAttacker(final Player ai, final Card attacker, final Combat combat) { - // if the attacker will die when attacking don't attack if ((attacker.getNetToughness() + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, null, combat, true)) <= 0) { return false; @@ -717,7 +716,7 @@ public class AiAttackController { } if (attackMax == 0) { - // can't attack anymore + // can't attack anymore return; } diff --git a/forge-ai/src/main/java/forge/ai/AiBlockController.java b/forge-ai/src/main/java/forge/ai/AiBlockController.java index a6b16cb648d..56a8514c9d8 100644 --- a/forge-ai/src/main/java/forge/ai/AiBlockController.java +++ b/forge-ai/src/main/java/forge/ai/AiBlockController.java @@ -917,7 +917,6 @@ public class AiBlockController { } private void clearBlockers(final Combat combat, final List possibleBlockers) { - final List oldBlockers = combat.getAllBlockers(); for (final Card blocker : oldBlockers) { if (blocker.getController() == ai) // don't touch other player's blockers @@ -1270,7 +1269,7 @@ public class AiBlockController { return false; } - int numSteps = ai.getStartingLife() - 5; // e.g. 15 steps between 5 life and 20 life + int numSteps = Math.max(1, ai.getStartingLife() - 5); // e.g. 15 steps between 5 life and 20 life float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps; int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep)); if (chance > maxRandomTradeChance) { diff --git a/forge-ai/src/main/java/forge/ai/AiCardMemory.java b/forge-ai/src/main/java/forge/ai/AiCardMemory.java index 062489e3db2..791058d65e1 100644 --- a/forge-ai/src/main/java/forge/ai/AiCardMemory.java +++ b/forge-ai/src/main/java/forge/ai/AiCardMemory.java @@ -57,7 +57,9 @@ public class AiCardMemory { BOUNCED_THIS_TURN, // These cards were bounced this turn ACTIVATED_THIS_TURN, // These cards had their ability activated this turn CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn - MARKED_TO_AVOID_REENTRY // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash + MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash + PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part + PAYS_SAC_COST // These cards will be sacrificed as part of a cost and cannot be chosen in another part //REVEALED_CARDS // stub, not linked to AI code yet } @@ -73,6 +75,8 @@ public class AiCardMemory { private final Set memActivatedThisTurn; private final Set memChosenFogEffect; private final Set memMarkedToAvoidReentry; + private final Set memPaysTapCost; + private final Set memPaysSacCost; public AiCardMemory() { this.memMandatoryAttackers = new HashSet<>(); @@ -87,6 +91,8 @@ public class AiCardMemory { this.memChosenFogEffect = new HashSet<>(); this.memMarkedToAvoidReentry = new HashSet<>(); this.memHeldManaSourcesForNextSpell = new HashSet<>(); + this.memPaysTapCost = new HashSet<>(); + this.memPaysSacCost = new HashSet<>(); } private Set getMemorySet(MemorySet set) { @@ -115,6 +121,10 @@ public class AiCardMemory { return memChosenFogEffect; case MARKED_TO_AVOID_REENTRY: return memMarkedToAvoidReentry; + case PAYS_TAP_COST: + return memPaysTapCost; + case PAYS_SAC_COST: + return memPaysSacCost; //case REVEALED_CARDS: // return memRevealedCards; default: @@ -313,6 +323,12 @@ public class AiCardMemory { } // Static functions to simplify access to AI card memory of a given AI player. + public static Set getMemorySet(Player ai, MemorySet set) { + if (!ai.getController().isAI()) { + return null; + } + return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().getMemorySet(set); + } public static void rememberCard(Player ai, Card c, MemorySet set) { if (!ai.getController().isAI()) { return; diff --git a/forge-ai/src/main/java/forge/ai/AiController.java b/forge-ai/src/main/java/forge/ai/AiController.java index 45d40041ace..c22a95b8c66 100644 --- a/forge-ai/src/main/java/forge/ai/AiController.java +++ b/forge-ai/src/main/java/forge/ai/AiController.java @@ -138,11 +138,11 @@ public class AiController { public void setUseSimulation(boolean value) { this.useSimulation = value; } - + public SpellAbilityPicker getSimulationPicker() { return simPicker; } - + public Game getGame() { return game; } @@ -253,7 +253,10 @@ public class AiController { } boolean rightapi = false; Player activatingPlayer = sa.getActivatingPlayer(); - + + // for xPaid stuff + card.setCastSA(sa); + // Trigger play improvements for (final Trigger tr : card.getTriggers()) { // These triggers all care for ETB effects @@ -386,7 +389,7 @@ public class AiController { final List spellAbility = Lists.newArrayList(); for (final Card c : l) { for (final SpellAbility sa : c.getNonManaAbilities()) { - // Check if this AF is a Counterpsell + // Check if this AF is a Counterspell if (sa.getApi() == ApiType.Counter) { spellAbility.add(sa); } @@ -510,7 +513,6 @@ public class AiController { landList = unreflectedLands; } - //try to skip lands that enter the battlefield tapped if (!nonLandsInHand.isEmpty()) { CardCollection nonTappedLands = new CardCollection(); @@ -534,6 +536,7 @@ public class AiController { } } + // TODO if this is the only source for a color we need badly prioritize it instead if (foundTapped) { continue; } @@ -680,18 +683,16 @@ public class AiController { return null; } - public boolean reserveManaSources(SpellAbility sa) { - return reserveManaSources(sa, PhaseType.MAIN2, false, false, null); - } - public boolean reserveManaSourcesForNextSpell(SpellAbility sa, SpellAbility exceptForSa) { return reserveManaSources(sa, null, false, true, exceptForSa); } + public boolean reserveManaSources(SpellAbility sa) { + return reserveManaSources(sa, PhaseType.MAIN2, false, false, null); + } public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) { return reserveManaSources(sa, phaseType, enemy, true, null); } - public boolean reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy, boolean forNextSpell, SpellAbility exceptForThisSa) { ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0); CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player); @@ -743,14 +744,22 @@ public class AiController { return AiPlayDecision.CantPlaySa; } - boolean xCost = sa.getPayCosts().hasXInAnyCostPart(); + boolean xCost = sa.getPayCosts().hasXInAnyCostPart() || sa.getHostCard().hasStartOfKeyword("Strive"); if (!xCost && !ComputerUtilCost.canPayCost(sa, player)) { // for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check // when the AI won't even be able to play the spell in the first place (even if it could afford it) return AiPlayDecision.CantAfford; } + // state needs to be switched here so API checks evaluate the right face + if (sa.getCardState() != null && !sa.getHostCard().isInPlay() && sa.getCardState().getStateName() == CardStateName.Modal) { + sa.getHostCard().setState(CardStateName.Modal, false); + } AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc. + if (sa.getCardState() != null && !sa.getHostCard().isInPlay() && sa.getCardState().getStateName() == CardStateName.Modal) { + sa.getHostCard().setState(CardStateName.Original, false); + } + if (canPlay != AiPlayDecision.WillPlay) { return canPlay; } @@ -810,10 +819,9 @@ public class AiController { if (!canPlay) { return AiPlayDecision.CantPlayAi; } - } - else { + } else { Cost payCosts = sa.getPayCosts(); - if(payCosts != null) { + if (payCosts != null) { ManaCost mana = payCosts.getTotalMana(); if (mana != null) { if (mana.countX() > 0) { @@ -879,7 +887,7 @@ public class AiController { public boolean isNonDisabledCardInPlay(final String cardName) { for (Card card : player.getCardsIn(ZoneType.Battlefield)) { if (card.getName().equals(cardName)) { - // TODO - Better logic to detemine if a permanent is disabled by local effects + // TODO - Better logic to determine if a permanent is disabled by local effects // currently assuming any permanent enchanted by another player // is disabled and a second copy is necessary // will need actual logic that determines if the enchantment is able @@ -1747,10 +1755,10 @@ public class AiController { } public boolean doTrigger(SpellAbility spell, boolean mandatory) { - if (spell.getApi() != null) - return SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory); if (spell instanceof WrappedAbility) return doTrigger(((WrappedAbility)spell).getWrappedAbility(), mandatory); + if (spell.getApi() != null) + return SpellApiToAi.Converter.get(spell.getApi()).doTriggerAI(player, spell, mandatory); if (spell.getPayCosts() == Cost.Zero && spell.getTargetRestrictions() == null) { // For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about return true; @@ -1916,7 +1924,7 @@ public class AiController { if (sa.hasParam("AIMaxAmount")) { max = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("AIMaxAmount"), sa); } - switch(sa.getApi()) { + switch (sa.getApi()) { case TwoPiles: // TODO: improve AI Card biggest = null; @@ -2013,7 +2021,7 @@ public class AiController { final CardCollection library = new CardCollection(in); CardLists.shuffle(library); - + // remove all land, keep non-basicland in there, shuffled CardCollection land = CardLists.filter(library, CardPredicates.Presets.LANDS); for (Card c : land) { @@ -2021,7 +2029,7 @@ public class AiController { library.remove(c); } } - + try { // mana weave, total of 7 land // The Following have all been reduced by 1, to account for the @@ -2038,19 +2046,14 @@ public class AiController { System.err.println("Error: cannot smooth mana curve, not enough land"); return in; } - + // add the rest of land to the end of the deck for (int i = 0; i < land.size(); i++) { if (!library.contains(land.get(i))) { library.add(land.get(i)); } } - - // check - for (int i = 0; i < library.size(); i++) { - System.out.println(library.get(i)); - } - + return library; } // smoothComputerManaCurve() @@ -2218,13 +2221,13 @@ public class AiController { return null; } - public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount) { + public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount, final CardCollectionView exclude) { if (simPicker != null) { - return simPicker.chooseSacrificeType(type, ability, amount); + return simPicker.chooseSacrificeType(type, ability, amount, exclude); } - return ComputerUtil.chooseSacrificeType(player, type, ability, ability.getTargetCard(), amount); + return ComputerUtil.chooseSacrificeType(player, type, ability, ability.getTargetCard(), amount, exclude); } - + private boolean checkAiSpecificRestrictions(final SpellAbility sa) { // AI-specific restrictions specified as activation parameters in spell abilities @@ -2276,5 +2279,5 @@ public class AiController { // AI logic for choosing which replacement effect to apply happens here. return Iterables.getFirst(list, null); } - + } diff --git a/forge-ai/src/main/java/forge/ai/AiCostDecision.java b/forge-ai/src/main/java/forge/ai/AiCostDecision.java index 8058d6d973c..060a1a34231 100644 --- a/forge-ai/src/main/java/forge/ai/AiCostDecision.java +++ b/forge-ai/src/main/java/forge/ai/AiCostDecision.java @@ -60,7 +60,6 @@ public class AiCostDecision extends CostDecisionMakerBase { return PaymentDecision.number(c); } - @Override public PaymentDecision visit(CostChooseCreatureType cost) { String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(), @@ -78,15 +77,13 @@ public class AiCostDecision extends CostDecisionMakerBase { return null; } return PaymentDecision.card(player.getLastDrawnCard()); - } - else if (cost.payCostFromSource()) { + } else if (cost.payCostFromSource()) { if (!hand.contains(source)) { return null; } return PaymentDecision.card(source); - } - else if (type.equals("Hand")) { + } else if (type.equals("Hand")) { if (hand.size() > 1 && ability.getActivatingPlayer() != null) { hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard, ability); } @@ -107,8 +104,7 @@ public class AiCostDecision extends CostDecisionMakerBase { randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard, ability); } return PaymentDecision.card(randomSubset); - } - else if (type.equals("DifferentNames")) { + } else if (type.equals("DifferentNames")) { CardCollection differentNames = new CardCollection(); CardCollection discardMe = CardLists.filter(hand, CardPredicates.hasSVar("DiscardMe")); while (c > 0) { @@ -125,8 +121,7 @@ public class AiCostDecision extends CostDecisionMakerBase { c--; } return PaymentDecision.card(differentNames); - } - else { + } else { final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); CardCollection result = aic.getCardsToDiscard(c, type.split(";"), ability, discarded); @@ -183,8 +178,7 @@ public class AiCostDecision extends CostDecisionMakerBase { else if (cost.sameZone) { // TODO Determine exile from same zone for AI return null; - } - else { + } else { CardCollectionView chosen = ComputerUtil.chooseExileFrom(player, cost.getFrom(), cost.getType(), source, ability.getTargetCard(), c, ability); return null == chosen ? null : PaymentDecision.card(chosen); } @@ -192,7 +186,6 @@ public class AiCostDecision extends CostDecisionMakerBase { @Override public PaymentDecision visit(CostExileFromStack cost) { - Integer c = cost.convertAmount(); if (c == null) { c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); @@ -267,6 +260,15 @@ public class AiCostDecision extends CostDecisionMakerBase { return PaymentDecision.number(c); } + @Override + public PaymentDecision visit(CostRollDice cost) { + Integer c = cost.convertAmount(); + if (c == null) { + c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability); + } + return PaymentDecision.number(c); + } + @Override public PaymentDecision visit(CostGainControl cost) { if (cost.payCostFromSource()) { @@ -366,8 +368,7 @@ public class AiCostDecision extends CostDecisionMakerBase { if (cost.isSameZone()) { list = new CardCollection(game.getCardsIn(cost.getFrom())); - } - else { + } else { list = new CardCollection(player.getCardsIn(cost.getFrom())); } @@ -415,7 +416,6 @@ public class AiCostDecision extends CostDecisionMakerBase { return PaymentDecision.card(card); } - @Override public PaymentDecision visit(CostTap cost) { return PaymentDecision.number(0); @@ -474,14 +474,13 @@ public class AiCostDecision extends CostDecisionMakerBase { return PaymentDecision.card(totap); } - @Override public PaymentDecision visit(CostSacrifice cost) { if (cost.payCostFromSource()) { return PaymentDecision.card(source); } if (cost.getType().equals("OriginalHost")) { - return PaymentDecision.card(ability.getHostCard()); + return PaymentDecision.card(ability.getOriginalHost()); } if (cost.getAmount().equals("All")) { // Does the AI want to use Sacrifice All? @@ -495,7 +494,7 @@ public class AiCostDecision extends CostDecisionMakerBase { c = AbilityUtils.calculateAmount(source, amount, ability); } final AiController aic = ((PlayerControllerAi)player.getController()).getAi(); - CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c); + CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c, null); return PaymentDecision.card(list); } @@ -533,7 +532,7 @@ public class AiCostDecision extends CostDecisionMakerBase { return null; } - if (cost.getRevealFrom().equals(ZoneType.Exile)) { + if (cost.getRevealFrom().get(0).equals(ZoneType.Exile)) { hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability); return PaymentDecision.card(getBestCreatureAI(hand)); } @@ -597,7 +596,6 @@ public class AiCostDecision extends CostDecisionMakerBase { // currently if amount is bigger than one, // it tries to remove all counters from one source and type at once - int toRemove = 0; final GameEntityCounterTable table = new GameEntityCounterTable(); @@ -862,4 +860,3 @@ public class AiCostDecision extends CostDecisionMakerBase { return false; } } - diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtil.java b/forge-ai/src/main/java/forge/ai/ComputerUtil.java index cbbde47a278..0a4b0174478 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtil.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtil.java @@ -555,10 +555,14 @@ public class ComputerUtil { return -1; } - public static CardCollection chooseSacrificeType(final Player ai, final String type, final SpellAbility ability, final Card target, final int amount) { + public static CardCollection chooseSacrificeType(final Player ai, final String type, final SpellAbility ability, final Card target, final int amount, final CardCollectionView exclude) { final Card source = ability.getHostCard(); CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, ability); + if (exclude != null) { + typeList.removeAll(exclude); + } + typeList = CardLists.filter(typeList, CardPredicates.canBeSacrificedBy(ability)); // don't sacrifice the card we're pumping @@ -736,6 +740,7 @@ public class ComputerUtil { CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa); // don't bounce the card we're pumping + // TODO unless it can be used as a save typeList = ComputerUtilCost.paymentChoicesWithoutTargets(typeList, sa, ai); if (typeList.size() < amount) { @@ -947,7 +952,7 @@ public class ComputerUtil { canRegen = true; } - } catch (final Exception ex) { + } catch (final Exception ex) { throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex); } } @@ -1307,7 +1312,7 @@ public class ComputerUtil { } if (abCost.hasTapCost() && source.hasSVar("AITapDown")) { return true; - } else if (sa.hasParam("Planeswalker") && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) { + } else if (sa.isPwAbility() && ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) { for (final CostPart part : abCost.getCostParts()) { if (part instanceof CostPutCounter) { return true; @@ -1554,7 +1559,7 @@ public class ComputerUtil { Iterables.addAll(objects, ComputerUtil.predictThreatenedObjects(ai, sa, spell)); } if (top) { - break; // only evaluate top-stack + break; // only evaluate top-stack } } @@ -2066,7 +2071,7 @@ public class ComputerUtil { // Computer mulligans if there are no cards with converted mana cost of 0 in its hand public static boolean wantMulligan(Player ai, int cardsToReturn) { final CardCollectionView handList = ai.getCardsIn(ZoneType.Hand); - return scoreHand(handList, ai, cardsToReturn) <= 0; + return !handList.isEmpty() && scoreHand(handList, ai, cardsToReturn) <= 0; } public static CardCollection getPartialParisCandidates(Player ai) { @@ -2699,7 +2704,6 @@ public class ComputerUtil { for (Trigger trigger : theTriggers) { final Card source = trigger.getHostCard(); - if (!trigger.zonesCheck(game.getZoneOf(source))) { continue; } @@ -2832,7 +2836,6 @@ public class ComputerUtil { } public static boolean lifegainPositive(final Player player, final Card source) { - if (!player.canGainLife()) { return false; } @@ -2860,7 +2863,6 @@ public class ComputerUtil { public static boolean lifegainNegative(final Player player, final Card source) { return lifegainNegative(player, source, 1); } - public static boolean lifegainNegative(final Player player, final Card source, final int n) { if (!player.canGainLife()) { return false; @@ -2891,10 +2893,10 @@ public class ComputerUtil { return false; } - public static boolean targetPlayableSpellCard(final Player ai, CardCollection options, final SpellAbility sa, final boolean withoutPayingManaCost) { + public static boolean targetPlayableSpellCard(final Player ai, CardCollection options, final SpellAbility sa, final boolean withoutPayingManaCost, boolean mandatory) { // determine and target a card with a SA that the AI can afford and will play AiController aic = ((PlayerControllerAi) ai.getController()).getAi(); - Card targetSpellCard = null; + CardCollection targets = new CardCollection(); for (Card c : options) { if (withoutPayingManaCost && c.getManaCost() != null && c.getManaCost().countX() > 0) { // The AI will otherwise cheat with the mana payment, announcing X > 0 for spells like Heat Ray when replaying them @@ -2914,18 +2916,19 @@ public class ComputerUtil { // at this point, we're assuming that card will be castable from whichever zone it's in by the AI player. abTest.setActivatingPlayer(ai); abTest.getRestrictions().setZone(c.getZone().getZoneType()); - final boolean play = AiPlayDecision.WillPlay == aic.canPlaySa(abTest); - final boolean pay = ComputerUtilCost.canPayCost(abTest, ai); - if (play && pay) { - targetSpellCard = c; - break; + if (AiPlayDecision.WillPlay == aic.canPlaySa(abTest) && ComputerUtilCost.canPayCost(abTest, ai)) { + targets.add(c); } } } - if (targetSpellCard == null) { - return false; + if (targets.isEmpty()) { + if (mandatory && !options.isEmpty()) { + targets = options; + } else { + return false; + } } - sa.getTargets().add(targetSpellCard); + sa.getTargets().add(ComputerUtilCard.getBestAI(targets)); return true; } diff --git a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java index e3234956d5c..0e8c2ea385e 100644 --- a/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java +++ b/forge-ai/src/main/java/forge/ai/ComputerUtilCard.java @@ -716,6 +716,7 @@ public class ComputerUtilCard { int bigCMC = -1; for (final Card card : all) { + // TODO when PlayAi can consider MDFC this should also look at the back face (if not on stack or battlefield) int curCMC = card.getCMC(); // Add all cost of all auras with the same controller @@ -1684,8 +1685,8 @@ public class ComputerUtilCard { pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp); pumped.setPTBoost(c.getPTBoostTable()); - pumped.addPTBoost(power + berserkPower, toughness, timestamp, null); - pumped.addChangedCardKeywords(kws, null, false, false, timestamp); + pumped.addPTBoost(power + berserkPower, toughness, timestamp, 0); + pumped.addChangedCardKeywords(kws, null, false, false, timestamp, 0); Set 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 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 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 wasPrintedInSets(List allowedSetCodes); - + Predicate 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 result = new ArrayList<>(); - for(Entry kv : cardsWithWeights) { - for(int i = 0; i < kv.getValue(); i++) { + for (Entry kv : cardsWithWeights) { + for (int i = 0; i < kv.getValue(); i++) { result.add(kv.getKey()); } } @@ -106,26 +105,26 @@ public class PrintSheet { List result = new ArrayList<>(); int totalWeight = cardsWithWeights.countAll(); - if( totalWeight == 0) { + if (totalWeight == 0) { System.err.println("No cards were found on sheet " + name); return result; } // If they ask for 40 unique basic lands (to make a fatpack) out of 20 distinct possible, add the whole print run N times. int uniqueCards = cardsWithWeights.countDistinct(); - while ( number >= uniqueCards ) { - for(Entry kv : cardsWithWeights) { + while (number >= uniqueCards) { + for (Entry kv : cardsWithWeights) { result.add(kv.getKey()); } number -= uniqueCards; } List uniques = wantUnique ? new ArrayList<>() : null; - for(int iC = 0; iC < number; iC++) { + for (int iC = 0; iC < number; iC++) { int index = MyRandom.getRandom().nextInt(totalWeight); PaperCard toAdd = fetchRoulette(0, index, wantUnique ? uniques : null); result.add(toAdd); - if( wantUnique ) + if (wantUnique) uniques.add(toAdd); } return result; diff --git a/forge-core/src/main/java/forge/card/mana/ManaCost.java b/forge-core/src/main/java/forge/card/mana/ManaCost.java index 34a0387b61b..85ce4c528a7 100644 --- a/forge-core/src/main/java/forge/card/mana/ManaCost.java +++ b/forge-core/src/main/java/forge/card/mana/ManaCost.java @@ -353,7 +353,7 @@ public final class ManaCost implements Comparable, Iterable { @@ -49,57 +51,70 @@ public class CardPool extends ItemPool { this.addAll(cards); } - public void add(final String cardName, final int amount) { - if (cardName.contains("|")) { - // an encoded cardName with set and possibly art index was passed, split it and pass in full - String[] splitCardName = StringUtils.split(cardName, "|"); - if (splitCardName.length == 2) { - // set specified - this.add(splitCardName[0], splitCardName[1], amount); - } else if (splitCardName.length == 3) { - // set and art specified - this.add(splitCardName[0], splitCardName[1], Integer.parseInt(splitCardName[2]), amount); - } - } else { - this.add(cardName, null, -1, amount); - } + public void add(final String cardRequest, final int amount) { + CardDb.CardRequest request = CardDb.CardRequest.fromString(cardRequest); + this.add(request.cardName, request.edition, request.artIndex, amount); } public void add(final String cardName, final String setCode) { - this.add(cardName, setCode, -1, 1); + this.add(cardName, setCode, IPaperCard.DEFAULT_ART_INDEX, 1); } public void add(final String cardName, final String setCode, final int amount) { - this.add(cardName, setCode, -1, amount); + this.add(cardName, setCode, IPaperCard.DEFAULT_ART_INDEX, amount); + } + + public void add(final String cardName, final String setCode, final int amount, boolean addAny) { + this.add(cardName, setCode, IPaperCard.NO_ART_INDEX, amount, addAny); } // NOTE: ART indices are "1" -based - public void add(String cardName, String setCode, final int artIndex, final int amount) { - - PaperCard paperCard = StaticData.instance().getCommonCards().getCard(cardName, setCode, artIndex); - final boolean isCommonCard = paperCard != null; - - if (!isCommonCard) { - paperCard = StaticData.instance().getVariantCards().getCard(cardName, setCode); - if (paperCard == null) { + public void add(String cardName, String setCode, int artIndex, final int amount) { + this.add(cardName, setCode, artIndex, amount, false); + } + public void add(String cardName, String setCode, int artIndex, final int amount, boolean addAny) { + Map dbs = StaticData.instance().getAvailableDatabases(); + PaperCard paperCard = null; + String selectedDbName = ""; + artIndex = Math.max(artIndex, IPaperCard.DEFAULT_ART_INDEX); + int loadAttempt = 0; + while (paperCard == null && loadAttempt < 2) { + for (Map.Entry entry: dbs.entrySet()){ + String dbName = entry.getKey(); + CardDb db = entry.getValue(); + paperCard = db.getCard(cardName, setCode, artIndex); + if (paperCard != null) { + selectedDbName = dbName; + break; + } + } + loadAttempt += 1; + if (paperCard == null && loadAttempt < 2) { + /* Attempt to load the card first, and then try again all the three available DBs + as we simply don't know which db the card has been added to (in case). */ StaticData.instance().attemptToLoadCard(cardName, setCode); - paperCard = StaticData.instance().getVariantCards().getCard(cardName, setCode); + artIndex = IPaperCard.DEFAULT_ART_INDEX; // Reset Any artIndex passed in, at this point } } - - int artCount = 1; - if (paperCard != null) { - setCode = paperCard.getEdition(); - cardName = paperCard.getName(); - artCount = isCommonCard ? StaticData.instance().getCommonCards().getArtCount(cardName, setCode) : 1; - } else { - System.err.print("An unsupported card was requested: \"" + cardName + "\" from \"" + setCode + "\". "); - paperCard = StaticData.instance().getCommonCards().createUnsupportedCard(cardName); + if (addAny && paperCard == null) { + paperCard = StaticData.instance().getCommonCards().getCard(cardName); + selectedDbName = "Common"; } + if (paperCard == null){ + // after all still null + System.err.println("An unsupported card was requested: \"" + cardName + "\" from \"" + setCode + "\". \n"); + paperCard = StaticData.instance().getCommonCards().createUnsupportedCard(cardName); + selectedDbName = "Common"; + } + CardDb cardDb = dbs.getOrDefault(selectedDbName, StaticData.instance().getCommonCards()); + // Determine Art Index + setCode = paperCard.getEdition(); + cardName = paperCard.getName(); + int artCount = cardDb.getArtCount(cardName, setCode); + boolean artIndexExplicitlySet = (artIndex > IPaperCard.DEFAULT_ART_INDEX) || + (CardDb.CardRequest.fromString(cardName).artIndex > IPaperCard.NO_ART_INDEX); - boolean artIndexExplicitlySet = artIndex > 0 || Character.isDigit(cardName.charAt(cardName.length() - 1)) && cardName.charAt(cardName.length() - 2) == CardDb.NameSetSeparator; - - if (artIndexExplicitlySet || artCount <= 1) { + if ((artIndexExplicitlySet || artCount == 1) && !addAny) { // either a specific art index is specified, or there is only one art, so just add the card this.add(paperCard, amount); } else { @@ -107,22 +122,19 @@ public class CardPool extends ItemPool { int[] artGroups = MyRandom.splitIntoRandomGroups(amount, artCount); for (int i = 1; i <= artGroups.length; i++) { int cnt = artGroups[i - 1]; - if (cnt <= 0) { + if (cnt <= 0) continue; - } - PaperCard randomCard = StaticData.instance().getCommonCards().getCard(cardName, setCode, i); + PaperCard randomCard = cardDb.getCard(cardName, setCode, i); this.add(randomCard, cnt); } } } - /** * Add all from a List of CardPrinted. * - * @param list - * CardPrinteds to add + * @param list CardPrinteds to add */ public void add(final Iterable list) { for (PaperCard cp : list) { @@ -132,13 +144,14 @@ public class CardPool extends ItemPool { /** * returns n-th card from this DeckSection. LINEAR time. No fixed order between changes + * * @param n * @return */ public PaperCard get(int n) { for (Entry e : this) { n -= e.getValue(); - if ( n <= 0 ) return e.getKey(); + if (n <= 0) return e.getKey(); } return null; } @@ -151,9 +164,220 @@ public class CardPool extends ItemPool { return this.count(pc); } + /** + * Get the Map of frequencies (i.e. counts) for all the CardEdition found + * among cards in the Pool. + * + * @param includeBasicLands determines whether or not basic lands should be counted in or + * not when gathering statistics + * @return Map + * An HashMap structure mapping each CardEdition in Pool to its corresponding frequency count + */ + public Map getCardEditionStatistics(boolean includeBasicLands) { + Map editionStatistics = new HashMap<>(); + for(Entry cp : this.items.entrySet()) { + PaperCard card = cp.getKey(); + // Check whether or not including basic land in stats count + if (card.getRules().getType().isBasicLand() && !includeBasicLands) + continue; + int count = cp.getValue(); + CardEdition edition = StaticData.instance().getCardEdition(card.getEdition()); + int currentCount = editionStatistics.getOrDefault(edition, 0); + currentCount += count; + editionStatistics.put(edition, currentCount); + } + return editionStatistics; + } + + /** + * Returns the map of card frequency indexed by frequency value, rather than single card edition. + * Therefore, all editions with the same card count frequency will be grouped together. + * + * Note: This method returns the reverse map generated by getCardEditionStatistics + * + * @param includeBasicLands Decide to include or not basic lands in gathered statistics + * + * @return a ListMultimap structure matching each unique frequency value to its corresponding list + * of CardEditions + * + * @see CardPool#getCardEditionStatistics(boolean) + */ + public ListMultimap getCardEditionsGroupedByNumberOfCards(boolean includeBasicLands){ + Map editionsFrequencyMap = this.getCardEditionStatistics(includeBasicLands); + ListMultimap reverseMap = Multimaps.newListMultimap(new HashMap<>(), CollectionSuppliers.arrayLists()); + for (Map.Entry entry : editionsFrequencyMap.entrySet()) + reverseMap.put(entry.getValue(), entry.getKey()); + return reverseMap; + } + + /** + * Gather Statistics per Edition Type from cards included in the CardPool. + * + * @param includeBasicLands Determine whether or not basic lands should be included in gathered statistics + * + * @return an HashMap structure mapping each CardEdition.Type found among + * cards in the Pool, and their corresponding (card) count. + * + * @see CardPool#getCardEditionStatistics(boolean) + */ + public Map getCardEditionTypeStatistics(boolean includeBasicLands){ + Map editionTypeStats = new HashMap<>(); + Map editionStatistics = this.getCardEditionStatistics(includeBasicLands); + for(Entry entry : editionStatistics.entrySet()) { + CardEdition edition = entry.getKey(); + int count = entry.getValue(); + CardEdition.Type key = edition.getType(); + int currentCount = editionTypeStats.getOrDefault(key, 0); + currentCount += count; + editionTypeStats.put(key, currentCount); + } + return editionTypeStats; + } + + /** + * Returns the CardEdition.Type that is the most frequent among cards' editions + * in the pool. In case of more than one candidate, Expansion Type will be preferred (if available). + * + * @return The most frequent CardEdition.Type in the pool, or null if the Pool is empty + */ + public CardEdition.Type getTheMostFrequentEditionType(){ + Map editionTypeStats = this.getCardEditionTypeStatistics(false); + Integer mostFrequentType = 0; + List mostFrequentEditionTypes = new ArrayList<>(); + for (Map.Entry entry : editionTypeStats.entrySet()){ + if (entry.getValue() > mostFrequentType) { + mostFrequentType = entry.getValue(); + mostFrequentEditionTypes.add(entry.getKey()); + } + } + if (mostFrequentEditionTypes.isEmpty()) + return null; + CardEdition.Type mostFrequentEditionType = mostFrequentEditionTypes.get(0); + for (int i=1; i < mostFrequentEditionTypes.size(); i++){ + CardEdition.Type frequentType = mostFrequentEditionTypes.get(i); + if (frequentType == CardEdition.Type.EXPANSION) + return frequentType; + } + return mostFrequentEditionType; + } + + /** + * Determines whether (the majority of the) cards in the Pool are modern framed + * (that is, cards are from Modern Card Edition). + * + * @return True if the majority of cards in Pool are from Modern Edition, false otherwise. + * If the count of Modern and PreModern cards is tied, the return value is determined + * by the preferred Card Art Preference settings, namely True if Latest Art, False otherwise. + */ + public boolean isModern(){ + int modernEditionsCount = 0; + int preModernEditionsCount = 0; + Map editionStats = this.getCardEditionStatistics(false); + for (Map.Entry entry: editionStats.entrySet()){ + CardEdition edition = entry.getKey(); + if (edition.isModern()) + modernEditionsCount += entry.getValue(); + else + preModernEditionsCount += entry.getValue(); + } + if (modernEditionsCount == preModernEditionsCount) + return StaticData.instance().cardArtPreferenceIsLatest(); + return modernEditionsCount > preModernEditionsCount; + } + + /** + * Determines the Pivot Edition for cards in the Pool. + *

+ * The Pivot Edition refers to the CardEdition for cards in the pool that sets the + * reference boundary for cards in the pool. + * Therefore, the Pivot Edition will be selected considering the per-edition distribution of + * cards in the Pool. + * If the majority of the cards in the pool corresponds to a single edition, this edition will be the Pivot. + * The majority exists if the highest card frequency accounts for at least a third of the whole Pool + * (i.e. 1 over 3 cards - not including basic lands). + *

+ * However, there are cases in which cards in a Pool are gathered from several editions, so that there is + * no clear winner for a single edition of reference. + * In these cases, the Pivot will be selected as the "Median Edition", that is the edition whose frequency + * is the closest to the average. + *

+ * In cases where multiple candidates could be selected (most likely to occur when the average frequency + * is considered) pivot candidates will be first sorted in ascending (earliest edition first) or + * descending (latest edition first) order depending on whether or not the selected Card Art Preference policy + * and the majority of cards in the Pool are compliant. This is to give preference more likely to + * the best candidate for alternative card art print search. + * + * @param isLatestCardArtPreference Determines whether the Card Art Preference to consider should + * prefer or not Latest Card Art Editions first. + * @return CardEdition instance representing the Pivot Edition + * + * @see #isModern() + */ + public CardEdition getPivotCardEdition(boolean isLatestCardArtPreference) { + ListMultimap editionsStatistics = this.getCardEditionsGroupedByNumberOfCards(false); + List frequencyValues = new ArrayList<>(editionsStatistics.keySet()); + // Sort in descending order + frequencyValues.sort(new Comparator() { + @Override + public int compare(Integer f1, Integer f2) { + return (f1.compareTo(f2)) * -1; + } + }); + float weightedMean = 0; + int sumWeights = 0; + for (Integer freq : frequencyValues) { + int editionsCount = editionsStatistics.get(freq).size(); + int weightedFrequency = freq * editionsCount; + sumWeights += editionsCount; + weightedMean += weightedFrequency; + } + int totalNoCards = (int)weightedMean; + weightedMean /= sumWeights; + + int topFrequency = frequencyValues.get(0); + float ratio = ((float) topFrequency) / totalNoCards; + // determine the Pivot Frequency + int pivotFrequency; + if (ratio >= 0.33) // 1 over 3 cards are from the most frequent edition(s) + pivotFrequency = topFrequency; + else + pivotFrequency = getMedianFrequency(frequencyValues, weightedMean); + + // Now Get editions corresponding to pivot frequency + List pivotCandidates = new ArrayList<>(editionsStatistics.get(pivotFrequency)); + // Now Sort candidates chronologically + pivotCandidates.sort(new Comparator() { + @Override + public int compare(CardEdition ed1, CardEdition ed2) { + return ed1.compareTo(ed2); + } + }); + boolean searchPolicyAndPoolAreCompliant = isLatestCardArtPreference == this.isModern(); + if (!searchPolicyAndPoolAreCompliant) + Collections.reverse(pivotCandidates); // reverse to have latest-first. + return pivotCandidates.get(0); + } + + /* Utility (static) method to return the median value given a target mean. */ + private static int getMedianFrequency(List frequencyValues, float meanFrequency) { + int medianFrequency = frequencyValues.get(0); + float refDelta = Math.abs(meanFrequency - medianFrequency); + for (int i = 1; i < frequencyValues.size(); i++){ + int currentFrequency = frequencyValues.get(i); + float delta = Math.abs(meanFrequency - currentFrequency); + if (delta < refDelta) { + medianFrequency = currentFrequency; + refDelta = delta; + } + } + return medianFrequency; + } + @Override public String toString() { - if (this.isEmpty()) { return "[]"; } + if (this.isEmpty()) { + return "[]"; + } boolean isFirst = true; StringBuilder sb = new StringBuilder(); @@ -161,8 +385,7 @@ public class CardPool extends ItemPool { for (Entry e : this) { if (isFirst) { isFirst = false; - } - else { + } else { sb.append(", "); } sb.append(e.getValue()).append(" x ").append(e.getKey().getName()); @@ -171,33 +394,45 @@ public class CardPool extends ItemPool { } private final static Pattern p = Pattern.compile("((\\d+)\\s+)?(.*?)"); + public static CardPool fromCardList(final Iterable lines) { CardPool pool = new CardPool(); - - if (lines == null) { return pool; } - - final Iterator lineIterator = lines.iterator(); - while (lineIterator.hasNext()) { - final String line = lineIterator.next(); - if (line.startsWith(";") || line.startsWith("#")) { continue; } // that is a comment or not-yet-supported card - - final Matcher m = p.matcher(line.trim()); - m.matches(); - final String sCnt = m.group(2); - final String cardName = m.group(3); - if (StringUtils.isBlank(cardName)) { - continue; - } - - final int count = sCnt == null ? 1 : Integer.parseInt(sCnt); - pool.add(cardName, count); + List> cardRequests = processCardList(lines); + for (Pair pair : cardRequests) { + String cardRequest = pair.getLeft(); + int count = pair.getRight(); + pool.add(cardRequest, count); } return pool; } + public static List> processCardList(final Iterable lines){ + List> cardRequests = new ArrayList<>(); + if (lines == null) + return cardRequests; // empty list + + for (String line : lines) { + if (line.startsWith(";") || line.startsWith("#")) { + continue; + } // that is a comment or not-yet-supported card + + final Matcher m = p.matcher(line.trim()); + boolean matches = m.matches(); + if (!matches) + continue; + final String sCnt = m.group(2); + final String cardRequest = m.group(3); + if (StringUtils.isBlank(cardRequest)) + continue; + final int count = sCnt == null ? 1 : Integer.parseInt(sCnt); + cardRequests.add(Pair.of(cardRequest, count)); + } + return cardRequests; + } + public String toCardList(String separator) { List> main2sort = Lists.newArrayList(this); Collections.sort(main2sort, ItemPoolSorter.BY_NAME_THEN_SET); @@ -207,7 +442,7 @@ public class CardPool extends ItemPool { boolean isFirst = true; for (final Entry e : main2sort) { - if(!isFirst) + if (!isFirst) sb.append(separator); else isFirst = false; @@ -222,13 +457,17 @@ public class CardPool extends ItemPool { /** * Applies a predicate to this CardPool's cards. + * * @param predicate the Predicate to apply to this CardPool * @return a new CardPool made from this CardPool with only the cards that agree with the provided Predicate */ - public CardPool getFilteredPool(Predicate predicate){ + public CardPool getFilteredPool(Predicate predicate) { CardPool filteredPool = new CardPool(); - for(PaperCard pc : this.items.keySet()){ - if(predicate.apply(pc)) filteredPool.add(pc); + Iterator cardsInPool = this.items.keySet().iterator(); + while (cardsInPool.hasNext()){ + PaperCard c = cardsInPool.next(); + if (predicate.apply(c)) + filteredPool.add(c, this.items.get(c)); } return filteredPool; } diff --git a/forge-core/src/main/java/forge/deck/Deck.java b/forge-core/src/main/java/forge/deck/Deck.java index 24eccbae1b2..9d15cf578bb 100644 --- a/forge-core/src/main/java/forge/deck/Deck.java +++ b/forge-core/src/main/java/forge/deck/Deck.java @@ -17,24 +17,17 @@ */ package forge.deck; -import java.util.Collections; -import java.util.Comparator; -import java.util.Date; -import java.util.EnumMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.TreeSet; - import com.google.common.base.Function; import com.google.common.collect.Lists; - import forge.StaticData; import forge.card.CardDb; +import forge.card.CardEdition; import forge.item.IPaperCard; import forge.item.PaperCard; +import org.apache.commons.lang3.tuple.Pair; + +import java.util.*; +import java.util.Map.Entry; /** *

@@ -51,14 +44,12 @@ public class Deck extends DeckBase implements Iterable tags = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); // Supports deferring loading a deck until we actually need its contents. This works in conjunction with // the lazy card load feature to ensure we don't need to load all cards on start up. - private Map> deferredSections; + private Map> deferredSections = null; + private Map> loadedSections = null; + private String lastCardArtPreferenceUsed = ""; + private Boolean lastCardArtOptimisationOptionUsed = null; + private boolean includeCardsFromUnspecifiedSet = false; - // gameType is from Constant.GameType, like GameType.Regular - /** - *

- * Decks have their named finalled. - *

- */ public Deck() { this(""); } @@ -183,9 +174,8 @@ public class Deck extends DeckBase implements Iterable> s : deferredSections.entrySet()) { + if (loadedSections != null && !includeCardsFromUnspecifiedSet) + return; // deck loaded, and does not include ANY card with no specified edition: all good! + + String cardArtPreference = StaticData.instance().getCardArtPreferenceName(); + boolean smartCardArtSelection = StaticData.instance().isEnabledCardArtSmartSelection(); + + if (lastCardArtOptimisationOptionUsed == null) // first time here + lastCardArtOptimisationOptionUsed = smartCardArtSelection; + + if (loadedSections != null && cardArtPreference.equals(lastCardArtPreferenceUsed) && + lastCardArtOptimisationOptionUsed == smartCardArtSelection) + return; // deck loaded already - card with no set have been found, but no change since last time: all good! + + Map> referenceDeckLoadingMap; + if (deferredSections != null) + referenceDeckLoadingMap = new HashMap<>(deferredSections); + else + referenceDeckLoadingMap = new HashMap<>(loadedSections); + + loadedSections = new HashMap<>(); + lastCardArtPreferenceUsed = cardArtPreference; + lastCardArtOptimisationOptionUsed = smartCardArtSelection; + Map> cardsWithNoEdition = null; + if (smartCardArtSelection) + cardsWithNoEdition = new EnumMap<>(DeckSection.class); + + for (Entry> s : referenceDeckLoadingMap.entrySet()) { + // first thing, update loaded section + loadedSections.put(s.getKey(), s.getValue()); DeckSection sec = DeckSection.smartValueOf(s.getKey()); - if (sec == null) { + if (sec == null) continue; - } - final List cardsInSection = s.getValue(); - for (String k : cardsInSection) - if (k.indexOf(CardDb.NameSetSeparator) > 0) - hasExplicitlySpecifiedSet = true; + ArrayList cardNamesWithNoEdition = getAllCardNamesWithNoSpecifiedEdition(cardsInSection); + if (cardNamesWithNoEdition.size() > 0){ + includeCardsFromUnspecifiedSet = true; + if (smartCardArtSelection) + cardsWithNoEdition.put(sec, cardNamesWithNoEdition); + } CardPool pool = CardPool.fromCardList(cardsInSection); + // TODO: @Leriomaggio + // this will need improvements with a validation schema for each section to avoid + // accidental additions and/or sanitise the content of each section. // I used to store planes and schemes under sideboard header, so this will assign them to a correct section IPaperCard sample = pool.get(0); - if (sample != null && ( sample.getRules().getType().isPlane() || sample.getRules().getType().isPhenomenon())) { + if (sample != null && (sample.getRules().getType().isPlane() || sample.getRules().getType().isPhenomenon())) sec = DeckSection.Planes; - } - if (sample != null && sample.getRules().getType().isScheme()) { + if (sample != null && sample.getRules().getType().isScheme()) sec = DeckSection.Schemes; - } putSection(sec, pool); } + deferredSections = null; // set to null, just in case! + if (includeCardsFromUnspecifiedSet && smartCardArtSelection) + optimiseCardArtSelectionInDeckSections(cardsWithNoEdition); - deferredSections = null; - if (!hasExplicitlySpecifiedSet) { - convertByXitaxMethod(); - } } - private void convertByXitaxMethod() { - Date dateWithAllCards = StaticData.instance().getEditions().getEarliestDateWithAllCards(getAllCardsInASinglePool()); - String artOption = StaticData.instance().getPrefferedArtOption(); + private ArrayList getAllCardNamesWithNoSpecifiedEdition(List cardsInSection) { + ArrayList cardNamesWithNoEdition = new ArrayList<>(); + List> cardRequests = CardPool.processCardList(cardsInSection); + for (Pair pair : cardRequests) { + String requestString = pair.getLeft(); + CardDb.CardRequest request = CardDb.CardRequest.fromString(requestString); + if (request.edition == null) + cardNamesWithNoEdition.add(request.cardName); + } + return cardNamesWithNoEdition; + } - for(Entry p : parts.entrySet()) { - if( p.getKey() == DeckSection.Planes || p.getKey() == DeckSection.Schemes || p.getKey() == DeckSection.Avatar) + private void optimiseCardArtSelectionInDeckSections(Map> cardsWithNoEdition) { + StaticData data = StaticData.instance(); + // Get current Card Art Preference Settings + boolean isCardArtPreferenceLatestArt = data.cardArtPreferenceIsLatest(); + boolean cardArtPreferenceHasFilter = data.isCoreExpansionOnlyFilterSet(); + + for(Entry part : parts.entrySet()) { + DeckSection deckSection = part.getKey(); + if(deckSection == DeckSection.Planes || deckSection == DeckSection.Schemes || deckSection == DeckSection.Avatar) continue; + // == 0. First Off, check if there is anything at all to do for the current section + ArrayList cardNamesWithNoEditionInSection = cardsWithNoEdition.getOrDefault(deckSection, null); + if (cardNamesWithNoEditionInSection == null || cardNamesWithNoEditionInSection.size() == 0) + continue; // nothing to do here + + CardPool pool = part.getValue(); + // Set options for the alternative card print search + boolean isExpansionTheMajorityInThePool = (pool.getTheMostFrequentEditionType() == CardEdition.Type.EXPANSION); + boolean isPoolModernFramed = pool.isModern(); + + // == Get the most representative (Pivot) Edition in the Pool + // Note: Card Art Updates (if any) will be determined based on the Pivot Edition. + CardEdition pivotEdition = pool.getPivotCardEdition(isCardArtPreferenceLatestArt); + + // == Inspect and Update the Pool + Date releaseDatePivotEdition = pivotEdition.getDate(); CardPool newPool = new CardPool(); - - for(Entry cp : p.getValue()){ + for (Entry cp : pool) { PaperCard card = cp.getKey(); - int count = cp.getValue(); - - PaperCard replacementCard; - switch (artOption) { - case "Latest": - replacementCard = StaticData.instance().getCardFromLatestorEarliest(card); - break; - case "Earliest": - replacementCard = StaticData.instance().getCardFromEarliestCoreExp(card); - break; - default: - replacementCard = StaticData.instance().getCardByEditionDate(card, dateWithAllCards); + int totalToAddToPool = cp.getValue(); + // A. Skip cards not requiring any update, because they add the edition specified! + if (!cardNamesWithNoEditionInSection.contains(card.getName())) { + addCardToPool(newPool, card, totalToAddToPool, card.isFoil()); + continue; } - - if (replacementCard.getArtIndex() == card.getArtIndex()) { - if (card.hasImage()) - newPool.add(card, count); - else - newPool.add(replacementCard, count); - } else { - if (card.hasImage()) - newPool.add(card.getName(), card.getEdition(), count); // this is to randomize art - else - newPool.add(replacementCard, count); + // B. Determine if current card requires update + boolean cardArtNeedsOptimisation = this.isCardArtUpdateRequired(card, releaseDatePivotEdition); + if (!cardArtNeedsOptimisation) { + addCardToPool(newPool, card, totalToAddToPool, card.isFoil()); + continue; } + PaperCard alternativeCardPrint = data.getAlternativeCardPrint(card, releaseDatePivotEdition, + isCardArtPreferenceLatestArt, + cardArtPreferenceHasFilter, + isExpansionTheMajorityInThePool, + isPoolModernFramed); + if (alternativeCardPrint == null) // no alternative found, add original card in Pool + addCardToPool(newPool, card, totalToAddToPool, card.isFoil()); + else + addCardToPool(newPool, alternativeCardPrint, totalToAddToPool, card.isFoil()); } - - parts.put(p.getKey(), newPool); + parts.put(deckSection, newPool); } } + private void addCardToPool(CardPool pool, PaperCard card, int totalToAdd, boolean isFoil) { + StaticData data = StaticData.instance(); + if (card.getArtIndex() != IPaperCard.NO_ART_INDEX && card.getArtIndex() != IPaperCard.DEFAULT_ART_INDEX) + pool.add(isFoil ? card.getFoiled() : card, totalToAdd); // art index requested, keep that way! + else { + int artCount = data.getCardArtCount(card); + if (artCount > 1) + addAlternativeCardPrintInPoolWithMultipleArt(card, pool, totalToAdd, artCount); + else + pool.add(isFoil ? card.getFoiled() : card, totalToAdd); + } + } + + private void addAlternativeCardPrintInPoolWithMultipleArt(PaperCard alternativeCardPrint, CardPool pool, + int totalNrToAdd, int nrOfAvailableArts) { + StaticData data = StaticData.instance(); + + // distribute available card art + String cardName = alternativeCardPrint.getName(); + String setCode = alternativeCardPrint.getEdition(); + boolean isFoil = alternativeCardPrint.isFoil(); + int cardsPerArtIndex = totalNrToAdd / nrOfAvailableArts; + cardsPerArtIndex = Math.max(1, cardsPerArtIndex); // make sure is never zero + int restOfCardsToAdd = totalNrToAdd % nrOfAvailableArts; + int cardsAdded = 0; + PaperCard alternativeCardArt = null; + for (int artIndex = 1; artIndex <= nrOfAvailableArts; artIndex++){ + alternativeCardArt = data.getOrLoadCommonCard(cardName, setCode, artIndex, isFoil); + cardsAdded += cardsPerArtIndex; + pool.add(alternativeCardArt, cardsPerArtIndex); + if (cardsAdded == totalNrToAdd) + break; + } + if (restOfCardsToAdd > 0) + pool.add(alternativeCardArt, restOfCardsToAdd); + } + + private boolean isCardArtUpdateRequired(PaperCard card, Date referenceReleaseDate) { + /* A Card Art update is required ONLY IF the current edition of the card is either + newer (older) than pivot edition when LATEST ART (ORIGINAL ART) Card Art Preference + is selected. + This is because what we're trying to "FIX" is the card art selection that is + "too new" wrt. PivotEdition (or, "too old" with ORIGINAL ART Preference, respectively). + Example: + - Case 1: [Latest Art] + We don't want Lands automatically selected from AFR (too new) within a Deck of mostly Core21 (Pivot) + - Case 2: [Original Art] + We don't want an Atog from LEA (too old) in a Deck of Mirrodin (Pivot) + + NOTE: the control implemented in release date also consider the case when the input PaperCard + is exactly from the Pivot Edition. In this case, NO update will be required! + */ + + if (card.getRules().isVariant()) + return false; // skip variant cards + boolean isLatestCardArtPreference = StaticData.instance().cardArtPreferenceIsLatest(); + CardEdition cardEdition = StaticData.instance().getCardEdition(card.getEdition()); + if (cardEdition == null) return false; + Date releaseDate = cardEdition.getDate(); + if (releaseDate == null) return false; + if (isLatestCardArtPreference) // Latest Art + return releaseDate.compareTo(referenceReleaseDate) > 0; + // Original Art + return releaseDate.compareTo(referenceReleaseDate) < 0; + } + + + public static final Function FN_NAME_SELECTOR = new Function() { @Override public String apply(Deck arg1) { diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 4097f15b2cd..76de92988d4 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -17,20 +17,10 @@ */ package forge.deck; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Map.Entry; -import java.util.Set; - -import org.apache.commons.lang3.Range; -import org.apache.commons.lang3.tuple.ImmutablePair; - import com.google.common.base.Predicate; import com.google.common.base.Predicates; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; - import forge.StaticData; import forge.card.CardRules; import forge.card.CardRulesPredicates; @@ -43,6 +33,14 @@ import forge.item.IPaperCard; import forge.item.PaperCard; import forge.util.Aggregates; import forge.util.TextUtil; +import org.apache.commons.lang3.Range; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map.Entry; +import java.util.Set; /** * GameType is an enum to determine the type of current game. :) @@ -337,7 +335,12 @@ public enum DeckFormat { // should group all cards by name, so that different editions of same card are really counted as the same card for (final Entry cp : Aggregates.groupSumBy(allCards, PaperCard.FN_GET_NAME)) { - final IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey()); + IPaperCard simpleCard = StaticData.instance().getCommonCards().getCard(cp.getKey()); + if (simpleCard == null) { + simpleCard = StaticData.instance().getCustomCards().getCard(cp.getKey()); + if (simpleCard != null && !StaticData.instance().allowCustomCardsInDecksConformance()) + return TextUtil.concatWithSpace("contains a Custom Card:", cp.getKey(), "\nPlease Enable Custom Cards in Forge Preferences to use this deck."); + } // Might cause issues since it ignores "Special" Cards if (simpleCard == null) { return TextUtil.concatWithSpace("contains the nonexisting card", cp.getKey()); diff --git a/forge-core/src/main/java/forge/deck/DeckRecognizer.java b/forge-core/src/main/java/forge/deck/DeckRecognizer.java index 1d7aa3b1cd2..6469dd18c68 100644 --- a/forge-core/src/main/java/forge/deck/DeckRecognizer.java +++ b/forge-core/src/main/java/forge/deck/DeckRecognizer.java @@ -17,18 +17,17 @@ */ package forge.deck; +import forge.card.CardDb; +import forge.card.CardDb.CardArtPreference; +import forge.card.ICardDatabase; +import forge.item.PaperCard; +import org.apache.commons.lang3.StringUtils; + import java.util.Calendar; import java.util.Date; import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.apache.commons.lang3.StringUtils; - -import forge.card.CardDb; -import forge.card.CardDb.SetPreference; -import forge.card.ICardDatabase; -import forge.item.PaperCard; - /** *

* DeckRecognizer class. @@ -105,7 +104,7 @@ public class DeckRecognizer { //private static final Pattern READ_SEPARATED_EDITION = Pattern.compile("[[\\(\\{]([a-zA-Z0-9]){1,3})[]*\\s+(.*)"); private static final Pattern SEARCH_SINGLE_SLASH = Pattern.compile("(?<=[^/])\\s*/\\s*(?=[^/])"); - private final SetPreference useLastSet; + private final CardArtPreference useLastSet; private final ICardDatabase db; private Date recognizeCardsPrintedBefore = null; @@ -114,10 +113,10 @@ public class DeckRecognizer { useLastSet = null; } else if (onlyCoreAndExp) { - useLastSet = SetPreference.LatestCoreExp; + useLastSet = CardArtPreference.LATEST_ART_CORE_EXPANSIONS_REPRINT_ONLY; } else { - useLastSet = SetPreference.Latest; + useLastSet = CardArtPreference.LATEST_ART_ALL_EDITIONS; } this.db = db; } @@ -156,7 +155,7 @@ public class DeckRecognizer { } private PaperCard tryGetCard(String text) { - return db.getCardFromEdition(text, recognizeCardsPrintedBefore, useLastSet); + return db.getCardFromEditionsReleasedBefore(text, useLastSet, recognizeCardsPrintedBefore); } private Token recognizePossibleNameAndNumber(final String name, final int n) { diff --git a/forge-core/src/main/java/forge/deck/generation/DeckGenPool.java b/forge-core/src/main/java/forge/deck/generation/DeckGenPool.java index 870c6472107..3bdd0619e7f 100644 --- a/forge-core/src/main/java/forge/deck/generation/DeckGenPool.java +++ b/forge-core/src/main/java/forge/deck/generation/DeckGenPool.java @@ -43,9 +43,8 @@ public class DeckGenPool implements IDeckGenPool { Iterable editionCards=Iterables.filter(cards.values(), filter); if (editionCards.iterator().hasNext()){ return editionCards.iterator().next(); - }else { - return getCard(name); } + return getCard(name); } @Override diff --git a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java index a326b3c9a3b..da65b9c0d6b 100644 --- a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java +++ b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorBase.java @@ -17,29 +17,12 @@ */ package forge.deck.generation; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.TreeMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import org.apache.commons.lang3.tuple.ImmutablePair; - 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 forge.StaticData; -import forge.card.CardRules; -import forge.card.CardRulesPredicates; -import forge.card.CardType; -import forge.card.ColorSet; -import forge.card.MagicColor; +import forge.card.*; import forge.card.mana.ManaCost; import forge.deck.CardPool; import forge.deck.DeckFormat; @@ -49,6 +32,12 @@ import forge.util.Aggregates; import forge.util.DebugTrace; import forge.util.ItemPool; import forge.util.MyRandom; +import org.apache.commons.lang3.tuple.ImmutablePair; + +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** *

@@ -236,7 +225,7 @@ public abstract class DeckGeneratorBase { } for (int i = 0; i < nLand; i++) { - tDeck.add(landPool.getCard(basicLandName, edition != null ? edition : basicLandEdition, -1), 1); + tDeck.add(landPool.getCard(basicLandName, edition != null ? edition : basicLandEdition), 1); } landsLeft -= nLand; diff --git a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorMonoColor.java b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorMonoColor.java index 551c36b8231..4abc1aa9dde 100644 --- a/forge-core/src/main/java/forge/deck/generation/DeckGeneratorMonoColor.java +++ b/forge-core/src/main/java/forge/deck/generation/DeckGeneratorMonoColor.java @@ -87,7 +87,6 @@ public class DeckGeneratorMonoColor extends DeckGeneratorBase { } } - @Override public final CardPool getDeck(final int size, final boolean forAi) { addCreaturesAndSpells(size, cmcLevels, forAi); diff --git a/forge-core/src/main/java/forge/item/FatPack.java b/forge-core/src/main/java/forge/item/FatPack.java index 2ee80d07309..3d0f65a3b7b 100644 --- a/forge-core/src/main/java/forge/item/FatPack.java +++ b/forge-core/src/main/java/forge/item/FatPack.java @@ -18,10 +18,8 @@ package forge.item; -import java.util.ArrayList; import java.util.List; -import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; import com.google.common.base.Function; @@ -30,16 +28,17 @@ import forge.ImageKeys; import forge.StaticData; import forge.card.CardEdition; import forge.item.generation.BoosterGenerator; -import forge.util.TextUtil; -import forge.util.storage.StorageReaderFile; public class FatPack extends BoxedProduct { public static final Function FN_FROM_SET = new Function() { @Override - public FatPack apply(final CardEdition arg1) { - FatPack.Template d = StaticData.instance().getFatPacks().get(arg1.getCode()); + public FatPack apply(final CardEdition edition) { + int boosters = edition.getFatPackCount(); + if (boosters <= 0) { return null; } + + FatPack.Template d = new Template(edition); if (d == null) { return null; } - return new FatPack(arg1.getName(), d, d.cntBoosters); + return new FatPack(edition.getName(), d, d.cntBoosters); } }; @@ -68,17 +67,6 @@ public class FatPack extends BoxedProduct { return BoosterGenerator.getBoosterPack(fpData); } - /*@Override - protected List generate() { - List result = new ArrayList(); - for (int i = 0; i < fpData.getCntBoosters(); i++) { - result.addAll(super.generate()); - } - // Add any extra cards that may come in the fatpack after Boosters - result.addAll(BoosterGenerator.getBoosterPack(fpData)); - return result; - }*/ - @Override public final Object clone() { return new FatPack(name, fpData, fpData.cntBoosters); @@ -92,38 +80,12 @@ public class FatPack extends BoxedProduct { public static class Template extends SealedProduct.Template { private final int cntBoosters; - public int getCntBoosters() { return cntBoosters; } - private Template(String edition, int boosters, Iterable> itrSlots) - { - super(edition, itrSlots); - cntBoosters = boosters; - } + private Template(CardEdition edition) { + super(edition.getCode(), edition.getFatPackExtraSlots()); - public static final class Reader extends StorageReaderFile