This commit is contained in:
Grimm
2021-09-09 03:56:39 +02:00
2408 changed files with 83293 additions and 56626 deletions

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.43-SNAPSHOT</version>
<version>1.6.45-SNAPSHOT</version>
</parent>
<artifactId>forge-ai</artifactId>

View File

@@ -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;
}

View File

@@ -917,7 +917,6 @@ public class AiBlockController {
}
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
final List<Card> 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) {

View File

@@ -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<Card> memActivatedThisTurn;
private final Set<Card> memChosenFogEffect;
private final Set<Card> memMarkedToAvoidReentry;
private final Set<Card> memPaysTapCost;
private final Set<Card> 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<Card> 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<Card> 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;

View File

@@ -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> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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<CounterType> 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;
}

View File

@@ -89,7 +89,7 @@ public class ComputerUtilCombat {
return ComputerUtilCombat.canAttackNextTurn(attacker, input);
}
});
} // canAttackNextTurn(Card)
}
/**
* <p>
@@ -176,7 +176,6 @@ public class ComputerUtilCombat {
return n;
}
// Returns the damage an unblocked attacker would deal
/**
* <p>
@@ -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<Card> attackers = combat.getAttackersOf(ai);
final List<Card> unblocked = Lists.newArrayList();
for (final Card attacker : attackers) {
final List<Card> 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<Card> unblocked = Lists.newArrayList();
for (final Card attacker : attackers) {
final List<Card> 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<Card> threateningCommanders = getLifeThreateningCommanders(ai,combat);
for (final Card attacker : attackers) {
final List<Card> 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<Card> attackers = combat.getAttackersOf(ai);
for (final Card attacker : attackers) {
final List<Card> 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;
}
/**
* <p>
* 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);
}

View File

@@ -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);
}
/**
* <p>
* 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<String> getAvailableManaColors(Player ai, Card additionalLand) {
return getAvailableManaColors(ai, Lists.newArrayList(additionalLand));
}
public static Set<String> getAvailableManaColors(Player ai, List<Card> additionalLands) {
CardCollection cardsToConsider = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Presets.UNTAPPED);
Set<String> 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;
}

View File

@@ -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<Mana> 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<SpellAbility> 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<SpellAbility> filteredList = Lists.newArrayList(saList);
switch (manaSourceType) {
case "Snow":
filteredList.sort(new Comparator<SpellAbility>() {
@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<SpellAbility>() {
@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<SpellAbility> 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<SpellAbility>() {
@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<Mana> 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);
}
}

View File

@@ -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<CounterType, Integer> kv : counters.entrySet()) {
for (Entry<CounterType, Integer> kv : counters.entrySet()) {
if (!first) {
counterString.append(",");
}
@@ -470,7 +470,7 @@ public abstract class GameState {
}
public void parse(List<String> 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<Card, Integer> entry : cardToAttachId.entrySet()) {
for (Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
attachedTo.unAttachAllCards();
}
// Attach permanents by ID
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
for (Entry<Card, Integer> 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<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
for (Entry<Card, Integer> 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<Card, List<String>> entry : cardToMergedCards.entrySet()) {
for (Entry<Card, List<String>> 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<ZoneType, CardCollectionView> playerCards = new EnumMap<>(ZoneType.class);
for (Entry<ZoneType, String> 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<ZoneType, CardCollectionView> 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<Card> 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);

View File

@@ -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<PaperCard> dungeonCards, String message) {
// TODO: improve the conditions that define which dungeon is a viable option to choose
List<String> 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<Card> chooseCardsForSplice(SpellAbility sa, List<Card> cards) {
// sort from best to worst

View File

@@ -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();

View File

@@ -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
* <p>
@@ -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 {

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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<Card> 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"))) {

View File

@@ -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) {

View File

@@ -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<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {

View File

@@ -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<SpellAbility> spells,
Map<String, Object> params) {
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
return spells.get(0);
}
}

View File

@@ -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);

View File

@@ -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<AbilitySub> 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<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
List<AbilitySub> 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) {

View File

@@ -63,7 +63,6 @@ public class ChooseCardNameAi extends SpellAbilityAi {
*/
@Override
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
return ComputerUtilCard.getBestAI(options);
}

View File

@@ -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();
}

View File

@@ -33,7 +33,6 @@ public class ClashAi extends SpellAbilityAi {
return legalAction;
}
/*
* (non-Javadoc)
*

View File

@@ -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()) {

View File

@@ -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 {
}
}

View File

@@ -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<SpellAbilityStackInstance> it = game.getStack().iterator();
SpellAbilityStackInstance si = null;
while(it.hasNext()) {
while (it.hasNext()) {
si = it.next();
tgtSA = si.getSpellAbility(true);
if (!sa.canTargetSpellAbility(tgtSA)) {

View File

@@ -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

View File

@@ -27,7 +27,6 @@ public class CountersProliferateAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final List<Card> cperms = Lists.newArrayList();
final List<Player> allies = ai.getAllies();
allies.add(ai);
@@ -87,7 +86,6 @@ public class CountersProliferateAi extends SpellAbilityAi {
}));
}
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
}

View File

@@ -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 {

View File

@@ -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;
}
}
}
}

View File

@@ -47,7 +47,6 @@ public class DamageEachAi extends DamageAiBase {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
}

View File

@@ -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) {

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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) {

View File

@@ -69,4 +69,3 @@ public class ExploreAi extends SpellAbilityAi {
}
}

View File

@@ -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

View File

@@ -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")) {

View File

@@ -24,4 +24,3 @@ public class InvestigateAi extends SpellAbilityAi {
return true;
}
}

View File

@@ -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<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> 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?

View File

@@ -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) {

View File

@@ -167,7 +167,7 @@ public class ManaEffectAi extends SpellAbilityAi {
List<SpellAbility> 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<Card>)null)).getColor());
if (cost.getCMC() == 0 && cost.countX() == 0) {

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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<Card> itr = cards.iterator();
while (itr.hasNext()) {
final Card c = itr.next();
final List<SpellAbility> 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;
}
/**
* <p>
* 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<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
@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<Card> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<Card>() {
@@ -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;
}

View File

@@ -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))

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -66,7 +66,6 @@ public class StoreSVarAi extends SpellAbilityAi {
return false;
}
return true;
}

View File

@@ -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);
}

View File

@@ -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);
}
}
}

View File

@@ -311,7 +311,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
}
final List<Card> 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)) {

View File

@@ -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"));

View File

@@ -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);

View File

@@ -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;

View File

@@ -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),

View File

@@ -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<ICardFace> faces) {
// TODO: improve the conditions that define which dungeon is a viable option to choose
List<String> 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);
}
}

View File

@@ -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());

View File

@@ -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 {

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.43-SNAPSHOT</version>
<version>1.6.45-SNAPSHOT</version>
</parent>
<artifactId>forge-core</artifactId>

View File

@@ -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;
/**
* <p>
@@ -138,7 +119,7 @@ public class CardStorageReader {
final CardRules.Reader rulesReader = new CardRules.Reader();
final List<CardRules> 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<CardRules> result, final List<Callable<List<CardRules>>> tasks, final CountDownLatch cdl) {
try {
if ( useThreadPool ) {
if (useThreadPool) {
final ExecutorService executor = ThreadUtil.getComputingPool(0.5f);
final List<Future<List<CardRules>>> parts = executor.invokeAll(tasks);
executor.shutdown();
cdl.await();
for(final Future<List<CardRules>> pp : parts) {
for (final Future<List<CardRules>> pp : parts) {
result.addAll(pp.get());
}
} else {
for(final Callable<List<CardRules>> c : tasks) {
for (final Callable<List<CardRules>> c : tasks) {
result.addAll(c.call());
}
}

View File

@@ -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<String, HashSet<String>> 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<String> 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<String> 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<String> cache, String filename){
if (cache == null || cache.isEmpty())
return false;
final String keyPrefix = filename.split("\\.")[0];
return cache.contains(keyPrefix);
}
}

View File

@@ -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<SealedProduct.Template> boosters;
@@ -62,21 +58,28 @@ public class StaticData {
private IStorage<FatPack.Template> fatPacks;
private IStorage<BoosterBox.Template> boosterBoxes;
private IStorage<PrintSheet> printSheets;
private final Map<String, List<String>> setLookup = new HashMap<>();
private List<String> 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<String> funnyCards = new ArrayList<>();
List<String> 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<String, CardRules> 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<String, List<String>> getSetLookup() {
return setLookup;
}
public final CardEdition.Collection getEditions() {
return this.editions;
}
@@ -174,6 +191,22 @@ public class StaticData {
return sortedEditions;
}
private TreeMap<CardEdition.Type, List<CardEdition>> editionsTypeMap;
public final Map<CardEdition.Type, List<CardEdition>> 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<CardEdition> 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<FatPack.Template> 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<SealedProduct.Template> getTournamentPacks() {
if (tournaments == null)
@@ -275,9 +291,24 @@ public class StaticData {
return variantCards;
}
public Map<String, CardDb> getAvailableDatabases(){
Map<String, CardDb> databases = new HashMap<>();
databases.put("Common", commonCards);
databases.put("Custom", customCards);
databases.put("Variant", variantCards);
return databases;
}
public List<String> getBlockLands() {
return blocksLandCodes;
}
public TokenDb getAllTokens() { return allTokens; }
public boolean allowCustomCardsInDecksConformance() {
return this.allowCustomCardsInDecksConformance;
}
public void setStandardPredicate(Predicate<PaperCard> standardPredicate) { this.standardPredicate = standardPredicate; }
@@ -303,88 +334,241 @@ public class StaticData {
public Predicate<PaperCard> 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 <code>setReleaseDate</code> will be used depending on the desired Card Art Preference policy to apply
* when looking for alternative card, namely <code>Latest Art</code> and <i>with</i> or <i>without</i> 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 <code>PaperCard</code> 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 <code>PaperCard</code> that is the selected alternative candidate, or <code>null</code>
* 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 <code>getAlternativeCardPrint</code> with extra settings to be used for
* alternative card print.
*
* <p>
* 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 <code>PaperCard</code> 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 <code>PaperCard</code> that is the selected alternative candidate, or <code>null</code>
* 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 <code>PaperCard</code> looking for a candidate in all
* available databases.
*
* @param card Instance of target <code>PaperCard</code>
* @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<CardDb> 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;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<CardEdition> {
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<CardEdition> {
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<String> 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<CardEdition> {
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<CardEdition> {
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<CardEdition> {
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<CardEdition> {
* @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<String, String> 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<CardEdition> {
// 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<CardEdition> {
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<CardEdition> {
return cardsInSet;
}
private ListMultimap<String, CardInSet> 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<CardInSet> getCardInSet(String cardName){
if (cardsInSetLookupMap == null) {
// initialise
cardsInSetLookupMap = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), CollectionSuppliers.arrayLists());
List<CardInSet> 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<String, Integer> getTokens() { return tokenNormalized; }
@@ -446,11 +490,11 @@ public final class CardEdition implements Comparable<CardEdition> {
Map<String, Integer> cardToIndex = new HashMap<>();
List<PrintSheet> 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<CardInSet> 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<CardEdition> {
sheets.add(sheet);
}
for(String sheetName : customPrintSheetsToParse.keySet()) {
for (String sheetName : customPrintSheetsToParse.keySet()) {
List<String> 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<CardEdition> {
}
public static class Reader extends StorageReaderFolder<CardEdition> {
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<CardEdition> {
/*
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.
*/
//"(^(?<cnum>[0-9]+.?) )?((?<rarity>[SCURML]) )?(?<name>.*)$"
/* Ideally we'd use the named group above, but Android 6 and
@@ -501,7 +553,7 @@ public final class CardEdition implements Comparable<CardEdition> {
* 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<String, CardInSet> cardMap = ArrayListMultimap.create();
@@ -526,7 +578,8 @@ public final class CardEdition implements Comparable<CardEdition> {
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<CardEdition> {
// 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<CardEdition> {
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<CardEdition> {
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<CardEdition> {
@Override
public Map<String, SealedProduct.Template> readAll() {
Map<String, SealedProduct.Template> map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
for(CardEdition ce : Collection.this) {
for (CardEdition ce : Collection.this) {
List<String> 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<CardEdition> {
};
}
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<String> minEditions = new HashSet<>();
SetPreference strictness = SetPreference.EarliestCoreExp;
CardDb db = StaticData.instance().getCommonCards();
for (Entry<PaperCard, Integer> 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<CardEdition> {
private static class CanMakeFatPack implements Predicate<CardEdition> {
@Override
public boolean apply(final CardEdition subject) {
return StaticData.instance().getFatPacks().contains(subject.getCode());
return subject.getFatPackCount() > 0;
}
}

View File

@@ -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<String> 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);

View File

@@ -26,6 +26,14 @@ public final class CardRulesPredicates {
}
};
/** The Constant isKeptInAiLimitedDecks. */
public static final Predicate<CardRules> IS_KEPT_IN_AI_LIMITED_DECKS = new Predicate<CardRules>() {
@Override
public boolean apply(final CardRules card) {
return !card.getAiHints().getRemAIDecks() && !card.getAiHints().getRemNonCommanderDecks();
}
};
/** The Constant isKeptInRandomDecks. */
public static final Predicate<CardRules> IS_KEPT_IN_RANDOM_DECKS = new Predicate<CardRules>() {
@Override
@@ -262,7 +270,6 @@ public final class CardRulesPredicates {
return new PredicateSuperType(type, isEqual);
}
/**
* Checks for color.
*

View File

@@ -58,7 +58,6 @@ public final class CardType implements Comparable<CardType>, 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<CardType>, 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<CardType>, 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<CardType>, 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<CardType>, 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<CardType>, CardTypeView {
};
}
///////// Utility methods
public static boolean isACardType(final String cardType) {
return CoreType.isValidEnum(cardType);

View File

@@ -42,7 +42,6 @@ public interface CardTypeView extends Iterable<String>, Serializable {
boolean isBasicLand();
boolean isPlane();
boolean isPhenomenon();
boolean isEmblem();
boolean isTribal();
boolean isDungeon();
CardTypeView getTypeWithChanges(Iterable<CardChangedType> changedCardTypes);

View File

@@ -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<PaperCard> {
/**
* 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<PaperCard> 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<PaperCard> getAllCards();
Collection<PaperCard> getAllCards(String cardName);
Collection<PaperCard> getAllCards(Predicate<PaperCard> predicate);
Collection<PaperCard> getAllCards(String cardName,Predicate<PaperCard> predicate);
Collection<PaperCard> getAllCards(CardEdition edition);
Collection<PaperCard> getUniqueCards();
List<PaperCard> getAllCardsFromEdition(CardEdition edition);
/* UTILITY METHODS
* =============== */
int getMaxArtIndex(String cardName);
int getArtCount(String cardName, String edition);
// Utility Predicates
Predicate<? super PaperCard> wasPrintedInSets(List<String> allowedSetCodes);
Predicate<? super PaperCard> wasPrintedAtRarity(CardRarity rarity);
}

View File

@@ -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;

View File

@@ -29,8 +29,8 @@ public class PrintSheet {
public static final IStorage<PrintSheet> initializePrintSheets(File sheetsFile, CardEdition.Collection editions) {
IStorage<PrintSheet> 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<PaperCard> cardsWithWeights;
private final String name;
public PrintSheet(String name0) {
this(name0, null);
@@ -64,7 +63,7 @@ public class PrintSheet {
}
public void addAll(Iterable<PaperCard> 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<PaperCard> toSkip) {
int sum = start;
boolean isSecondRun = start > 0;
for(Entry<PaperCard, Integer> cc : cardsWithWeights ) {
for (Entry<PaperCard, Integer> 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<PaperCard> all() {
List<PaperCard> result = new ArrayList<>();
for(Entry<PaperCard, Integer> kv : cardsWithWeights) {
for(int i = 0; i < kv.getValue(); i++) {
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
for (int i = 0; i < kv.getValue(); i++) {
result.add(kv.getKey());
}
}
@@ -106,26 +105,26 @@ public class PrintSheet {
List<PaperCard> 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<PaperCard, Integer> kv : cardsWithWeights) {
while (number >= uniqueCards) {
for (Entry<PaperCard, Integer> kv : cardsWithWeights) {
result.add(kv.getKey());
}
number -= uniqueCards;
}
List<PaperCard> 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;

View File

@@ -353,7 +353,7 @@ public final class ManaCost implements Comparable<ManaCost>, Iterable<ManaCostSh
* @param colorCode
* @return
*/
public boolean canBePaidWithAvaliable(byte colorCode) {
public boolean canBePaidWithAvailable(byte colorCode) {
for (ManaCostShard shard : shards) {
if (!shard.isPhyrexian() && !shard.canBePaidWithManaOfColor(colorCode)) {
return false;

View File

@@ -17,24 +17,26 @@
*/
package forge.deck;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimaps;
import forge.StaticData;
import forge.card.CardDb;
import forge.card.CardEdition;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.util.CollectionSuppliers;
import forge.util.ItemPool;
import forge.util.ItemPoolSorter;
import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CardPool extends ItemPool<PaperCard> {
@@ -49,57 +51,70 @@ public class CardPool extends ItemPool<PaperCard> {
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<String, CardDb> 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<String, CardDb> 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<PaperCard> {
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<PaperCard> list) {
for (PaperCard cp : list) {
@@ -132,13 +144,14 @@ public class CardPool extends ItemPool<PaperCard> {
/**
* returns n-th card from this DeckSection. LINEAR time. No fixed order between changes
*
* @param n
* @return
*/
public PaperCard get(int n) {
for (Entry<PaperCard, Integer> 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<PaperCard> {
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<CardEdition, Integer>
* An HashMap structure mapping each CardEdition in Pool to its corresponding frequency count
*/
public Map<CardEdition, Integer> getCardEditionStatistics(boolean includeBasicLands) {
Map<CardEdition, Integer> editionStatistics = new HashMap<>();
for(Entry<PaperCard, Integer> 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 <code>getCardEditionStatistics</code>
*
* @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<Integer, CardEdition> getCardEditionsGroupedByNumberOfCards(boolean includeBasicLands){
Map<CardEdition, Integer> editionsFrequencyMap = this.getCardEditionStatistics(includeBasicLands);
ListMultimap<Integer, CardEdition> reverseMap = Multimaps.newListMultimap(new HashMap<>(), CollectionSuppliers.arrayLists());
for (Map.Entry<CardEdition, Integer> 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 <code>CardEdition.Type</code> found among
* cards in the Pool, and their corresponding (card) count.
*
* @see CardPool#getCardEditionStatistics(boolean)
*/
public Map<CardEdition.Type, Integer> getCardEditionTypeStatistics(boolean includeBasicLands){
Map<CardEdition.Type, Integer> editionTypeStats = new HashMap<>();
Map<CardEdition, Integer> editionStatistics = this.getCardEditionStatistics(includeBasicLands);
for(Entry<CardEdition, Integer> 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 <code>CardEdition.Type</code> 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<CardEdition.Type, Integer> editionTypeStats = this.getCardEditionTypeStatistics(false);
Integer mostFrequentType = 0;
List<CardEdition.Type> mostFrequentEditionTypes = new ArrayList<>();
for (Map.Entry<CardEdition.Type, Integer> 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<CardEdition, Integer> editionStats = this.getCardEditionStatistics(false);
for (Map.Entry<CardEdition, Integer> 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.
* <p>
* The Pivot Edition refers to the <code>CardEdition</code> for cards in the pool that sets the
* <i>reference boundary</i> for cards in the pool.
* Therefore, the <i>Pivot Edition</i> 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).
* <p>
* 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.
* <p>
* 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<Integer, CardEdition> editionsStatistics = this.getCardEditionsGroupedByNumberOfCards(false);
List<Integer> frequencyValues = new ArrayList<>(editionsStatistics.keySet());
// Sort in descending order
frequencyValues.sort(new Comparator<Integer>() {
@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<CardEdition> pivotCandidates = new ArrayList<>(editionsStatistics.get(pivotFrequency));
// Now Sort candidates chronologically
pivotCandidates.sort(new Comparator<CardEdition>() {
@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<Integer> 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<PaperCard> {
for (Entry<PaperCard, Integer> 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<PaperCard> {
}
private final static Pattern p = Pattern.compile("((\\d+)\\s+)?(.*?)");
public static CardPool fromCardList(final Iterable<String> lines) {
CardPool pool = new CardPool();
if (lines == null) {
return pool;
}
final Iterator<String> 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<Pair<String, Integer>> cardRequests = processCardList(lines);
for (Pair<String, Integer> pair : cardRequests) {
String cardRequest = pair.getLeft();
int count = pair.getRight();
pool.add(cardRequest, count);
}
return pool;
}
public static List<Pair<String, Integer>> processCardList(final Iterable<String> lines){
List<Pair<String, Integer>> 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<Entry<PaperCard, Integer>> main2sort = Lists.newArrayList(this);
Collections.sort(main2sort, ItemPoolSorter.BY_NAME_THEN_SET);
@@ -207,7 +442,7 @@ public class CardPool extends ItemPool<PaperCard> {
boolean isFirst = true;
for (final Entry<PaperCard, Integer> e : main2sort) {
if(!isFirst)
if (!isFirst)
sb.append(separator);
else
isFirst = false;
@@ -222,13 +457,17 @@ public class CardPool extends ItemPool<PaperCard> {
/**
* 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<PaperCard> predicate){
public CardPool getFilteredPool(Predicate<PaperCard> predicate) {
CardPool filteredPool = new CardPool();
for(PaperCard pc : this.items.keySet()){
if(predicate.apply(pc)) filteredPool.add(pc);
Iterator<PaperCard> 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;
}

View File

@@ -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;
/**
* <p>
@@ -51,14 +44,12 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
private final Set<String> 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<String, List<String>> deferredSections;
private Map<String, List<String>> deferredSections = null;
private Map<String, List<String>> loadedSections = null;
private String lastCardArtPreferenceUsed = "";
private Boolean lastCardArtOptimisationOptionUsed = null;
private boolean includeCardsFromUnspecifiedSet = false;
// gameType is from Constant.GameType, like GameType.Regular
/**
* <p>
* Decks have their named finalled.
* </p>
*/
public Deck() {
this("");
}
@@ -183,9 +174,8 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
// will return new if it was absent
public CardPool getOrCreate(DeckSection deckSection) {
CardPool p = get(deckSection);
if (p != null) {
if (p != null)
return p;
}
p = new CardPool();
this.parts.put(deckSection, p);
return p;
@@ -225,83 +215,203 @@ public class Deck extends DeckBase implements Iterable<Entry<DeckSection, CardPo
}
private void loadDeferredSections() {
if (deferredSections == null) {
if ((deferredSections == null) && (loadedSections == null))
return;
}
boolean hasExplicitlySpecifiedSet = false;
for (Entry<String, List<String>> 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<String, List<String>> referenceDeckLoadingMap;
if (deferredSections != null)
referenceDeckLoadingMap = new HashMap<>(deferredSections);
else
referenceDeckLoadingMap = new HashMap<>(loadedSections);
loadedSections = new HashMap<>();
lastCardArtPreferenceUsed = cardArtPreference;
lastCardArtOptimisationOptionUsed = smartCardArtSelection;
Map<DeckSection, ArrayList<String>> cardsWithNoEdition = null;
if (smartCardArtSelection)
cardsWithNoEdition = new EnumMap<>(DeckSection.class);
for (Entry<String, List<String>> 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<String> cardsInSection = s.getValue();
for (String k : cardsInSection)
if (k.indexOf(CardDb.NameSetSeparator) > 0)
hasExplicitlySpecifiedSet = true;
ArrayList<String> 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<String> getAllCardNamesWithNoSpecifiedEdition(List<String> cardsInSection) {
ArrayList<String> cardNamesWithNoEdition = new ArrayList<>();
List<Pair<String, Integer>> cardRequests = CardPool.processCardList(cardsInSection);
for (Pair<String, Integer> 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<DeckSection, CardPool> p : parts.entrySet()) {
if( p.getKey() == DeckSection.Planes || p.getKey() == DeckSection.Schemes || p.getKey() == DeckSection.Avatar)
private void optimiseCardArtSelectionInDeckSections(Map<DeckSection, ArrayList<String>> cardsWithNoEdition) {
StaticData data = StaticData.instance();
// Get current Card Art Preference Settings
boolean isCardArtPreferenceLatestArt = data.cardArtPreferenceIsLatest();
boolean cardArtPreferenceHasFilter = data.isCoreExpansionOnlyFilterSet();
for(Entry<DeckSection, CardPool> 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<String> 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<PaperCard, Integer> cp : p.getValue()){
for (Entry<PaperCard, Integer> 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<Deck, String> FN_NAME_SELECTOR = new Function<Deck, String>() {
@Override
public String apply(Deck arg1) {

View File

@@ -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<String, Integer> 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());

View File

@@ -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;
/**
* <p>
* 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) {

View File

@@ -43,9 +43,8 @@ public class DeckGenPool implements IDeckGenPool {
Iterable<PaperCard> editionCards=Iterables.filter(cards.values(), filter);
if (editionCards.iterator().hasNext()){
return editionCards.iterator().next();
}else {
return getCard(name);
}
return getCard(name);
}
@Override

View File

@@ -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;
/**
* <p>
@@ -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;

View File

@@ -87,7 +87,6 @@ public class DeckGeneratorMonoColor extends DeckGeneratorBase {
}
}
@Override
public final CardPool getDeck(final int size, final boolean forAi) {
addCreaturesAndSpells(size, cmcLevels, forAi);

View File

@@ -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<CardEdition, FatPack> FN_FROM_SET = new Function<CardEdition, FatPack>() {
@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<PaperCard> generate() {
List<PaperCard> result = new ArrayList<PaperCard>();
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<Pair<String, Integer>> itrSlots)
{
super(edition, itrSlots);
cntBoosters = boosters;
}
private Template(CardEdition edition) {
super(edition.getCode(), edition.getFatPackExtraSlots());
public static final class Reader extends StorageReaderFile<Template> {
public Reader(String pathname) {
super(pathname, FN_GET_NAME);
}
@Override
protected Template read(String line, int i) {
String[] headAndData = TextUtil.split(line, ':', 2);
final String edition = headAndData[0];
final String[] data = TextUtil.splitWithParenthesis(headAndData[1], ',');
int nBoosters = 6;
List<Pair<String, Integer>> slots = new ArrayList<>();
for(String slotDesc : data) {
String[] kv = TextUtil.split(slotDesc, ' ', 2);
if (kv[1].startsWith("Booster"))
nBoosters = Integer.parseInt(kv[0]);
else
slots.add(ImmutablePair.of(kv[1], Integer.parseInt(kv[0])));
}
return new FatPack.Template(edition, nBoosters, slots);
}
cntBoosters = edition.getFatPackCount();
}
@Override

View File

@@ -1,26 +1,26 @@
package forge.item;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.card.CardRarity;
import forge.card.CardRules;
import forge.card.CardType.CoreType;
import forge.card.MagicColor;
import forge.util.PredicateCard;
import forge.util.PredicateString;
import org.apache.commons.lang3.StringUtils;
import java.io.Serializable;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public interface IPaperCard extends InventoryItem, Serializable {
String NO_COLLECTOR_NUMBER = "N.A."; // Placeholder for No-Collection number available
int DEFAULT_ART_INDEX = 1;
int NO_ART_INDEX = -1; // Placeholder when NO ArtIndex is Specified
String NO_ARTIST_NAME = "";
/**
* Number of filters based on CardPrinted values.
@@ -235,7 +235,10 @@ public interface IPaperCard extends InventoryItem, Serializable {
boolean isToken();
CardRules getRules();
CardRarity getRarity();
String getArtist();
String getItemType();
boolean hasBackFace();
String getCardImageKey();
String getCardAltImageKey();
}

View File

@@ -3,9 +3,6 @@ package forge.item;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
/**
* Filtering conditions for miscellaneous InventoryItems.
*/

View File

@@ -17,22 +17,19 @@
*/
package forge.item;
import com.google.common.base.Function;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.*;
import forge.util.CardTranslation;
import forge.util.ImageUtil;
import forge.util.Localizer;
import forge.util.TextUtil;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import com.google.common.base.Function;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.CardDb;
import forge.card.CardEdition;
import forge.card.CardRarity;
import forge.card.CardRules;
import forge.util.CardTranslation;
import forge.util.Localizer;
import forge.util.TextUtil;
/**
* A lightweight version of a card that matches real-world cards, to use outside of games (eg. inventory, decks, trade).
* <br><br>
@@ -53,7 +50,8 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
By default the attribute is marked as "unset" so that it could be retrieved and set.
(see getCollectorNumber())
*/
private String collectorNumber = null;
private String collectorNumber;
private String artist;
private final int artIndex;
private final boolean foil;
private Boolean hasImage;
@@ -75,18 +73,8 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
@Override
public String getCollectorNumber() {
/* The collectorNumber attribute is managed in a property-like fashion.
By default it is marked as "unset" (-1), which integrates with all constructors
invocations not including this as an extra parameter. In this way, the new
attribute could be added to the API with minimum disruption to code
throughout the other packages.
If "unset", the corresponding collectorNumber will be retrieved
from the corresponding CardEdition (see retrieveCollectorNumber)
* */
if (collectorNumber == null) {
collectorNumber = this.retrieveCollectorNumber();
}
if (collectorNumber == null)
collectorNumber = IPaperCard.NO_COLLECTOR_NUMBER;
return collectorNumber;
}
@@ -115,6 +103,13 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
return rarity;
}
@Override
public String getArtist() {
if (this.artist == null)
artist = IPaperCard.NO_ARTIST_NAME;
return artist;
}
/* FIXME: At the moment, every card can get Foiled, with no restriction on the
corresponding Edition - so we could Foil even Alpha cards.
*/
@@ -124,7 +119,7 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
if (this.foiledVersion == null) {
this.foiledVersion = new PaperCard(this.rules, this.edition, this.rarity,
this.artIndex, true, String.valueOf(collectorNumber));
this.artIndex, true, String.valueOf(collectorNumber), this.artist);
}
return this.foiledVersion;
}
@@ -163,36 +158,24 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
}
};
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0) {
this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false);
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0){
this(rules0, edition0, rarity0, IPaperCard.DEFAULT_ART_INDEX, false,
IPaperCard.NO_COLLECTOR_NUMBER, IPaperCard.NO_ARTIST_NAME);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0) {
this(rules0, edition0, rarity0, artIndex0, false);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0,
final boolean foil0) {
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0, final String artist0) {
if (rules0 == null || edition0 == null || rarity0 == null) {
throw new IllegalArgumentException("Cannot create card without rules, edition or rarity");
}
rules = rules0;
name = rules0.getName();
edition = edition0;
artIndex = artIndex0;
artIndex = Math.max(artIndex0, IPaperCard.DEFAULT_ART_INDEX);
foil = foil0;
rarity = rarity0;
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0, final int artIndex0,
final String collectorNumber0) {
this(rules0, edition0, rarity0, artIndex0, false, collectorNumber0);
}
public PaperCard(final CardRules rules0, final String edition0, final CardRarity rarity0,
final int artIndex0, final boolean foil0, final String collectorNumber0) {
this(rules0, edition0, rarity0, artIndex0, foil0);
collectorNumber = collectorNumber0;
artist = (artist0 != null ? TextUtil.normalizeText(artist0) : IPaperCard.NO_ARTIST_NAME);
collectorNumber = (collectorNumber0 != null) && (collectorNumber0.length() > 0) ? collectorNumber0 : IPaperCard.NO_COLLECTOR_NUMBER;
}
// Want this class to be a key for HashTable
@@ -264,10 +247,14 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
return CardEdition.CardInSet.getSortableCollectorNumber(collectorNumber);
}
private String sortableCNKey = null;
public String getCollectorNumberSortingKey(){
// Hardly the case, but just invoke getter rather than direct
// attribute to be sure that collectorNumber has been retrieved already!
return makeCollectorNumberSortingKey(getCollectorNumber());
if (sortableCNKey == null) {
// Hardly the case, but just invoke getter rather than direct
// attribute to be sure that collectorNumber has been retrieved already!
sortableCNKey = makeCollectorNumberSortingKey(getCollectorNumber());
}
return sortableCNKey;
}
@@ -305,31 +292,6 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
rarity = pc.getRarity();
}
private String retrieveCollectorNumber() {
StaticData data = StaticData.instance();
CardEdition edition = data.getEditions().get(this.edition);
if (edition == null) {
edition = data.getCustomEditions().get(this.edition);
if (edition == null) // don't bother continuing - non-existing card!
return NO_COLLECTOR_NUMBER;
}
int artIndexCount = 0;
String collectorNumberInEdition = "";
for (CardEdition.CardInSet card : edition.getAllCardsInSet()) {
if (card.name.equalsIgnoreCase(this.name)) {
artIndexCount += 1;
if (artIndexCount == this.artIndex) {
collectorNumberInEdition = card.collectorNumber;
break;
}
}
}
// CardEdition stores collectorNumber as a String, which is null if there isn't any.
// In this case, the NO_COLLECTOR_NUMBER value (i.e. 0) is returned.
return ((collectorNumberInEdition != null) && (collectorNumberInEdition.length() > 0)) ?
collectorNumberInEdition : NO_COLLECTOR_NUMBER;
}
@Override
public String getImageKey(boolean altState) {
String imageKey = ImageKeys.CARD_PREFIX + name + CardDb.NameSetSeparator
@@ -340,6 +302,32 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
return imageKey;
}
private String cardImageKey = null;
@Override
public String getCardImageKey() {
if (this.cardImageKey == null)
this.cardImageKey = ImageUtil.getImageKey(this, false, true);
return cardImageKey;
}
private String cardAltImageKey = null;
@Override
public String getCardAltImageKey() {
if (this.cardAltImageKey == null){
if (this.hasBackFace())
this.cardAltImageKey = ImageUtil.getImageKey(this, true, true);
else // altImageKey will be the same as cardImageKey
this.cardAltImageKey = ImageUtil.getImageKey(this, false, true);
}
return cardAltImageKey;
}
@Override
public boolean hasBackFace(){
CardSplitType cst = this.rules.getSplitType();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld || cst == CardSplitType.Modal;
}
// Return true if card is one of the five basic lands that can be added for free
public boolean isVeryBasicLand() {
return (this.getName().equals("Swamp"))
@@ -349,4 +337,3 @@ public final class PaperCard implements Comparable<IPaperCard>, InventoryItemFro
|| (this.getName().equals("Mountain"));
}
}

View File

@@ -95,7 +95,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
build.add(subtypes);
// Are these keywords sorted?
for(String keyword : rules.getMainPart().getKeywords()) {
for (String keyword : rules.getMainPart().getKeywords()) {
build.add(keyword);
}
@@ -122,7 +122,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
String formatEdition = null == edition || CardEdition.UNKNOWN == edition ? "" : "_" + edition.getCode().toLowerCase();
this.imageFileName.add(String.format("%s%s", imageFileName, formatEdition));
for(int idx = 2; idx <= this.artIndex; idx++) {
for (int idx = 2; idx <= this.artIndex; idx++) {
this.imageFileName.add(String.format("%s%d%s", imageFileName, idx, formatEdition));
}
}
@@ -143,6 +143,7 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override public CardRules getRules() { return card; }
@Override public CardRarity getRarity() { return CardRarity.None; }
@Override public String getArtist() { /*TODO*/ return ""; }
// Unfortunately this is a property of token, cannot move it outside of class
public String getImageFilename() { return getImageFilename(1); }
@@ -150,8 +151,25 @@ public class PaperToken implements InventoryItemFromSet, IPaperCard {
@Override public String getItemType() { return "Token"; }
@Override
public boolean hasBackFace() {
return false;
}
@Override public boolean isToken() { return true; }
// IPaperCard
@Override
public String getCardImageKey() {
return this.getImageKey(false);
}
@Override
public String getCardAltImageKey() {
return getImageKey(true);
}
// InventoryItem
@Override
public String getImageKey(boolean altState) {
int idx = MyRandom.getRandom().nextInt(artIndex);

View File

@@ -58,9 +58,9 @@ public abstract class SealedProduct implements InventoryItemFromSet {
}
public SealedProduct(String name0, Template boosterData) {
if (null == name0) { throw new IllegalArgumentException("name0 must not be null"); }
if (null == name0) { throw new IllegalArgumentException("name0 must not be null"); }
if (null == boosterData) {
throw new IllegalArgumentException("boosterData must not be null");
throw new IllegalArgumentException("boosterData for " + name0 + " must not be null");
}
contents = boosterData;
name = name0;
@@ -193,7 +193,6 @@ public abstract class SealedProduct implements InventoryItemFromSet {
public String toString() {
StringBuilder s = new StringBuilder();
s.append("consisting of ");
for(Pair<String, Integer> p : slots) {
s.append(p.getRight()).append(" ").append(p.getLeft()).append(", ");
@@ -236,9 +235,9 @@ public abstract class SealedProduct implements InventoryItemFromSet {
public static List<Pair<String, Integer>> parseSlots(String data) {
final String[] dataz = TextUtil.splitWithParenthesis(data, ',');
List<Pair<String, Integer>> slots = new ArrayList<>();
for(String slotDesc : dataz) {
for (String slotDesc : dataz) {
String[] kv = TextUtil.splitWithParenthesis(slotDesc, ' ', 2);
slots.add(ImmutablePair.of(kv[1], Integer.parseInt(kv[0])));
slots.add(ImmutablePair.of(kv[1].replace(";", ","), Integer.parseInt(kv[0])));
}
return slots;
}

View File

@@ -61,10 +61,9 @@ import forge.util.TextUtil;
*/
public class BoosterGenerator {
private final static Map<String, PrintSheet> cachedSheets = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private static synchronized PrintSheet getPrintSheet(String key) {
if( !cachedSheets.containsKey(key) )
if (!cachedSheets.containsKey(key))
cachedSheets.put(key, makeSheet(key, StaticData.instance().getCommonCards().getAllCards()));
return cachedSheets.get(key);
}
@@ -370,7 +369,7 @@ public class BoosterGenerator {
}
String boosterReplaceSlotFromPrintSheet = edition.getBoosterReplaceSlotFromPrintSheet();
if(!boosterReplaceSlotFromPrintSheet.isEmpty()) {
if (!boosterReplaceSlotFromPrintSheet.isEmpty()) {
replaceCardFromExtraSheet(result, boosterReplaceSlotFromPrintSheet);
}
}
@@ -449,7 +448,7 @@ public class BoosterGenerator {
*/
public static void replaceCard(List<PaperCard> booster, PaperCard toAdd) {
Predicate<PaperCard> rarityPredicate = null;
switch(toAdd.getRarity()){
switch (toAdd.getRarity()) {
case BasicLand:
rarityPredicate = Presets.IS_BASIC_LAND;
break;
@@ -470,7 +469,7 @@ public class BoosterGenerator {
PaperCard toReplace = null;
// Find first card in booster that matches the rarity
for (PaperCard card : booster) {
if(rarityPredicate.apply(card)) {
if (rarityPredicate.apply(card)) {
toReplace = card;
break;
}
@@ -507,7 +506,6 @@ public class BoosterGenerator {
@SuppressWarnings("unchecked")
public static PrintSheet makeSheet(String sheetKey, Iterable<PaperCard> src) {
PrintSheet ps = new PrintSheet(sheetKey);
String[] sKey = TextUtil.splitWithParenthesis(sheetKey, ' ', 2);
Predicate<PaperCard> setPred = (Predicate<PaperCard>) (sKey.length > 1 ? IPaperCard.Predicates.printedInSets(sKey[1].split(" ")) : Predicates.alwaysTrue());
@@ -517,8 +515,7 @@ public class BoosterGenerator {
// source replacement operators - if one is applied setPredicate will be ignored
Iterator<String> itMod = operators.iterator();
while(itMod.hasNext()) {
while (itMod.hasNext()) {
String mainCode = itMod.next();
if (mainCode.regionMatches(true, 0, "fromSheet", 0, 9) ||
@@ -530,43 +527,38 @@ public class BoosterGenerator {
setPred = Predicates.alwaysTrue();
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet
String list = StringUtils.strip(mainCode.substring(5), "() ");
String[] cardNames = TextUtil.splitWithParenthesis(list, ',', '"', '"');
List<PaperCard> srcList = new ArrayList<>();
for(String cardName: cardNames) {
for (String cardName: cardNames) {
srcList.add(StaticData.instance().getCommonCards().getCard(cardName));
}
src = srcList;
setPred = Predicates.alwaysTrue();
} else {
continue;
}
itMod.remove();
}
// only special operators should remain by now - the ones that could not be turned into one predicate
String mainCode = operators.isEmpty() ? null : operators.get(0).trim();
if( null == mainCode || mainCode.equalsIgnoreCase(BoosterSlots.ANY) ) { // no restriction on rarity
if (null == mainCode || mainCode.equalsIgnoreCase(BoosterSlots.ANY)) { // no restriction on rarity
Predicate<PaperCard> predicate = Predicates.and(setPred, extraPred);
ps.addAll(Iterables.filter(src, predicate));
} else if ( mainCode.equalsIgnoreCase(BoosterSlots.UNCOMMON_RARE) ) { // for sets like ARN, where U1 cards are considered rare and U3 are uncommon
} else if (mainCode.equalsIgnoreCase(BoosterSlots.UNCOMMON_RARE)) { // for sets like ARN, where U1 cards are considered rare and U3 are uncommon
Predicate<PaperCard> predicateRares = Predicates.and(setPred, IPaperCard.Predicates.Presets.IS_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateRares));
Predicate<PaperCard> predicateUncommon = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_UNCOMMON, extraPred);
ps.addAll(Iterables.filter(src, predicateUncommon), 3);
} else if ( mainCode.equalsIgnoreCase(BoosterSlots.RARE_MYTHIC) ) {
} else if (mainCode.equalsIgnoreCase(BoosterSlots.RARE_MYTHIC)) {
// Typical ratio of rares to mythics is 53:15, changing to 35:10 in smaller sets.
// To achieve the desired 1:8 are all mythics are added once, and all rares added twice per print sheet.
@@ -575,32 +567,28 @@ public class BoosterGenerator {
Predicate<PaperCard> predicateRare = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateRare), 2);
} else {
throw new IllegalArgumentException("Booster generator: operator could not be parsed - " + mainCode);
}
return ps;
}
/**
* This method also modifies passed parameter
*/
private static Predicate<PaperCard> buildExtraPredicate(List<String> operators) {
List<Predicate<PaperCard>> conditions = new ArrayList<>();
Iterator<String> itOp = operators.iterator();
while(itOp.hasNext()) {
while (itOp.hasNext()) {
String operator = itOp.next();
if(StringUtils.isEmpty(operator)) {
if (StringUtils.isEmpty(operator)) {
itOp.remove();
continue;
}
if(operator.endsWith("s")) {
if (operator.endsWith("s")) {
operator = operator.substring(0, operator.length() - 1);
}
@@ -670,7 +658,6 @@ public class BoosterGenerator {
toAdd = Predicates.not(toAdd);
}
conditions.add(toAdd);
}
if (conditions.isEmpty()) {

View File

@@ -31,7 +31,6 @@ public class UnOpenedProduct implements IUnOpenedProduct {
this.poolLimited = considerNumbersInPool; // TODO: Add 0 to parameter's name.
}
// Means to select from all unique cards (from base game, ie. no schemes or avatars)
public UnOpenedProduct(SealedProduct.Template template) {
tpl = template;

View File

@@ -1,21 +1,20 @@
package forge.token;
import com.google.common.base.Predicate;
import forge.card.CardDb;
import forge.item.PaperToken;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import com.google.common.base.Predicate;
import forge.card.CardDb;
import forge.item.PaperToken;
public interface ITokenDatabase extends Iterable<PaperToken> {
PaperToken getToken(String tokenName);
PaperToken getToken(String tokenName, String edition);
PaperToken getToken(String tokenName, String edition, int artIndex);
PaperToken getTokenFromEdition(String tokenName, CardDb.SetPreference fromSet);
PaperToken getTokenFromEdition(String tokenName, Date printedBefore, CardDb.SetPreference fromSet);
PaperToken getTokenFromEdition(String tokenName, Date printedBefore, CardDb.SetPreference fromSet, int artIndex);
PaperToken getTokenFromEditions(String tokenName, CardDb.CardArtPreference fromSet);
PaperToken getTokenFromEditions(String tokenName, Date printedBefore, CardDb.CardArtPreference fromSet);
PaperToken getTokenFromEditions(String tokenName, Date printedBefore, CardDb.CardArtPreference fromSet, int artIndex);
PaperToken getFoiled(PaperToken cpi);

View File

@@ -1,20 +1,14 @@
package forge.token;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import forge.card.CardDb;
import forge.card.CardEdition;
import forge.card.CardRules;
import forge.item.PaperToken;
import java.util.*;
public class TokenDb implements ITokenDatabase {
// Expected naming convention of scripts
// token_name
@@ -46,7 +40,7 @@ public class TokenDb implements ITokenDatabase {
}
public void preloadTokens() {
for(CardEdition edition : this.editions) {
for (CardEdition edition : this.editions) {
for (String name : edition.getTokens().keySet()) {
try {
getToken(name, edition.getCode());
@@ -80,17 +74,17 @@ public class TokenDb implements ITokenDatabase {
}
@Override
public PaperToken getTokenFromEdition(String tokenName, CardDb.SetPreference fromSet) {
public PaperToken getTokenFromEditions(String tokenName, CardDb.CardArtPreference fromSet) {
return null;
}
@Override
public PaperToken getTokenFromEdition(String tokenName, Date printedBefore, CardDb.SetPreference fromSet) {
public PaperToken getTokenFromEditions(String tokenName, Date printedBefore, CardDb.CardArtPreference fromSet) {
return null;
}
@Override
public PaperToken getTokenFromEdition(String tokenName, Date printedBefore, CardDb.SetPreference fromSet, int artIndex) {
public PaperToken getTokenFromEditions(String tokenName, Date printedBefore, CardDb.CardArtPreference fromSet, int artIndex) {
return null;
}

View File

@@ -34,7 +34,12 @@ public class CardTranslation {
translatedtypes.put(matches[0], matches[2]);
}
if (matches.length >= 4) {
translatedoracles.put(matches[0], matches[3].replace("\\n", "\r\n\r\n"));
String toracle = matches[3];
// Workaround to remove additional //Level_2// and //Level_3// lines from non-English Class cards
toracle = toracle.replace("//Level_2//\\n", "").replace("//Level_3//\\n", "");
// Workaround for roll dice cards
toracle = toracle.replace("\\n", "\r\n\r\n").replace("VERT", "|");
translatedoracles.put(matches[0], toracle);
}
}
} catch (IOException e) {
@@ -91,7 +96,7 @@ public class CardTranslation {
public static void preloadTranslation(String language, String languagesDirectory) {
languageSelected = language;
if (needsTranslation()) {
translatednames = new HashMap<>();
translatedtypes = new HashMap<>();
@@ -137,9 +142,10 @@ public class CardTranslation {
public static String translateMultipleDescriptionText(String descText, String cardName) {
if (!needsTranslation()) return descText;
String [] splitDescText = descText.split("\r\n");
String [] splitDescText = descText.split("\n");
String result = descText;
for (String text : splitDescText) {
text = text.trim();
if (text.isEmpty()) continue;
String translated = translateSingleDescriptionText(text, cardName);
if (!text.equals(translated)) {

View File

@@ -78,8 +78,10 @@ public final class FileUtil {
}
public static boolean isDirectoryWithFiles(final String path) {
if (path == null) return false;
final File f = new File(path);
return f.exists() && f.isDirectory() && f.list().length > 0;
final String[] fileList = f.list();
return fileList!=null && fileList.length > 0;
}
public static boolean ensureDirectoryExists(final String path) {

View File

@@ -14,7 +14,7 @@ public class ImageUtil {
}
public static PaperCard getPaperCardFromImageKey(String key) {
if ( key == null ) {
if (key == null) {
return null;
}
@@ -44,20 +44,19 @@ public class ImageUtil {
final boolean hasManyPictures;
final CardDb db = !card.isVariant() ? StaticData.instance().getCommonCards() : StaticData.instance().getVariantCards();
if (includeSet) {
cntPictures = db.getPrintCount(card.getName(), edition);
cntPictures = db.getArtCount(card.getName(), edition);
hasManyPictures = cntPictures > 1;
} else {
cntPictures = 1;
// raise the art index limit to the maximum of the sets this card was printed in
int maxCntPictures = db.getMaxPrintCount(card.getName());
int maxCntPictures = db.getMaxArtIndex(card.getName());
hasManyPictures = maxCntPictures > 1;
}
int artIdx = cp.getArtIndex() - 1;
if (hasManyPictures) {
if ( cntPictures <= artIdx ) // prevent overflow
artIdx = cntPictures == 0 ? 0 : artIdx % cntPictures;
if (cntPictures <= artIdx) // prevent overflow
artIdx = artIdx % cntPictures;
s.append(artIdx + 1);
}
@@ -84,15 +83,10 @@ public class ImageUtil {
}
}
public static boolean hasBackFacePicture(PaperCard cp) {
CardSplitType cst = cp.getRules().getSplitType();
return cst == CardSplitType.Transform || cst == CardSplitType.Flip || cst == CardSplitType.Meld || cst == CardSplitType.Modal;
}
public static String getNameToUse(PaperCard cp, boolean backFace) {
final CardRules card = cp.getRules();
if (backFace ) {
if ( hasBackFacePicture(cp) )
if (backFace) {
if (cp.hasBackFace())
if (card.getOtherPart() != null) {
return card.getOtherPart().getName();
} else if (!card.getMeldWith().isEmpty()) {
@@ -118,7 +112,7 @@ public class ImageUtil {
return getImageRelativePath(cp, backFace, true, true);
}
public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode, String langCode){
public static String getScryfallDownloadUrl(PaperCard cp, boolean backFace, String setCode, String langCode, boolean useArtCrop){
String editionCode;
if ((setCode != null) && (setCode.length() > 0))
editionCode = setCode;
@@ -127,12 +121,13 @@ public class ImageUtil {
String cardCollectorNumber = cp.getCollectorNumber();
// Hack to account for variations in Arabian Nights
cardCollectorNumber = cardCollectorNumber.replace("+", "");
String versionParam = useArtCrop ? "art_crop" : "normal";
String faceParam = "";
if (cp.getRules().getOtherPart() != null) {
faceParam = (backFace ? "&face=back" : "&face=front");
}
return String.format("%s/%s/%s?format=image&version=normal%s", editionCode, cardCollectorNumber,
langCode, faceParam);
return String.format("%s/%s/%s?format=image&version=%s%s", editionCode, cardCollectorNumber,
langCode, versionParam, faceParam);
}
public static String toMWSFilename(String in) {

View File

@@ -30,6 +30,7 @@ import java.util.concurrent.ConcurrentHashMap;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.Maps;
import forge.item.InventoryItem;
/**
@@ -136,24 +137,25 @@ public class ItemPool<T extends InventoryItem> implements Iterable<Entry<T, Inte
return count;
}
public final int countAll(Predicate<T> condition) {
public int countAll(Predicate<T> condition){
int count = 0;
for (Entry<T, Integer> e : this) {
if (condition.apply(e.getKey())) {
count += e.getValue();
}
}
for (Integer v : Maps.filterKeys(this.items, condition).values())
count += v;
return count;
}
@SuppressWarnings("unchecked")
public final <U extends InventoryItem> int countAll(Predicate<U> condition, Class<U> cls) {
int count = 0;
for (Entry<T, Integer> e : this) {
T item = e.getKey();
if (cls.isInstance(item) && condition.apply((U)item)) {
count += e.getValue();
Map<T, Integer> matchingKeys = Maps.filterKeys(this.items, new Predicate<T>() {
@Override
public boolean apply(T item) {
return cls.isInstance(item) && (condition.apply((U)item));
}
});
for (Integer i : matchingKeys.values()) {
count += i;
}
return count;
}

Some files were not shown because too many files have changed in this diff Show More