mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-17 19:28:01 +00:00
Merge branch 'master' of https://git.cardforge.org/core-developers/forge into adventure
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ public class ClashAi extends SpellAbilityAi {
|
||||
return legalAction;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ public class DamageEachAi extends DamageAiBase {
|
||||
*/
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
return mandatory || canPlayAI(ai, sa);
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -69,4 +69,3 @@ public class ExploreAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -24,4 +24,3 @@ public class InvestigateAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -66,7 +66,6 @@ public class StoreSVarAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -87,7 +87,6 @@ public class DeckGeneratorMonoColor extends DeckGeneratorBase {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public final CardPool getDeck(final int size, final boolean forAi) {
|
||||
addCreaturesAndSpells(size, cmcLevels, forAi);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -3,9 +3,6 @@ package forge.item;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Filtering conditions for miscellaneous InventoryItems.
|
||||
*/
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user