package magic.model; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; import magic.data.MagicIcon; import magic.data.TextImages; import magic.model.choice.MagicBuilderManaCost; import magic.model.condition.MagicCondition; import magic.model.condition.MagicManaCostCondition; public class MagicManaCost implements MagicCopyable { private static final Map COSTS_MAP=new HashMap<>(); private static final Map CONDS_MAP=new HashMap<>(); private static final Pattern PATTERN=Pattern.compile("\\{[A-Z\\d/]+\\}"); private static final int[] SINGLE_PENALTY={0,1,1,3,6,9,12,15,18}; private static final int[] DOUBLE_PENALTY={0,0,1,2,4,6, 8,10,12}; private static final MagicIcon[] GENERIC_ICONS={ MagicIcon.MANA_0, MagicIcon.MANA_1, MagicIcon.MANA_2, MagicIcon.MANA_3, MagicIcon.MANA_4, MagicIcon.MANA_5, MagicIcon.MANA_6, MagicIcon.MANA_7, MagicIcon.MANA_8, MagicIcon.MANA_9, MagicIcon.MANA_10, MagicIcon.MANA_11, MagicIcon.MANA_12, MagicIcon.MANA_13, MagicIcon.MANA_14, MagicIcon.MANA_15, MagicIcon.MANA_16 }; public static final MagicManaCost ZERO = MagicManaCost.create("{0}"); public static final MagicManaCost NONE = new MagicManaCost(new int[MagicCostManaType.NR_OF_TYPES], 0); public static final int MAXIMUM_MANA_COST = 16; private final String costText; private final int[] amounts = new int[MagicCostManaType.NR_OF_TYPES]; private final int converted; private final int XCount; private final List order = new ArrayList<>(); private volatile MagicBuilderManaCost builderCost; private List icons; private MagicManaCost(final String aCostText) { costText = aCostText; final int[] XCountArr = {0}; final int[] convertedArr = {0}; final Matcher matcher = PATTERN.matcher(costText); int matched = 0; while (matcher.find()) { final String group = matcher.group(); matched += group.length(); addType(group, XCountArr, convertedArr); } if (matched != costText.length()) { throw new RuntimeException("unknown mana cost \"" + aCostText + "\""); } XCount = XCountArr[0]; converted = convertedArr[0]; //assert getCanonicalText().equals(costText) : "canonical: " + getCanonicalText() + " != cost: " + costText; } private MagicManaCost(final int[] aAmounts, final int aXCount) { int total = 0; for (int i = 0; i < aAmounts.length; i++) { amounts[i] = aAmounts[i]; total += amounts[i]; if (amounts[i] != 0) { order.add(MagicCostManaType.values()[i]); } } XCount = aXCount; converted = total; costText = getCanonicalText(amounts, XCount); } private void addType(final MagicCostManaType type,final int amount,final int[] convertedArr) { convertedArr[0] += amount; amounts[type.ordinal()] += amount; if (!order.contains(type)) { order.add(type); } } private void addType(final String typeText, final int[] XCountArr, final int[] convertedArr) { final String symbol = typeText.substring(1, typeText.length() - 1); if ("X".equals(symbol)) { XCountArr[0]++; } else if (isNumeric(symbol)) { addType(MagicCostManaType.Generic,Integer.parseInt(symbol),convertedArr); } else { for (final MagicCostManaType type : MagicCostManaType.values()) { if (type.getText().equals(typeText)) { addType(type,1,convertedArr); return; } } throw new RuntimeException("unknown mana cost \"" + costText + "\""); } } private static boolean isNumeric(final String str) { for (final char c : str.toCharArray()) { if (!Character.isDigit(c)) { return false; } } return true; } public String getText() { return costText; } @Override public String toString() { return costText; } public int getXCount() { return XCount; } public boolean hasX() { return XCount > 0; } /** * @param x value for {X} in the mana cost (if it is present) * @return list of mana costs, with each element representing exactly 1 mana of some kind */ public List getCostManaTypes(final int x) { final List types = new ArrayList<>(); int generic=x * XCount; for (final MagicCostManaType type : order) { int amount=amounts[type.ordinal()]; if (type == MagicCostManaType.Generic) { generic+=amount; } else { for (;amount>0;amount--) { types.add(type); } } } for (;generic>0;generic--) { types.add(MagicCostManaType.Generic); } return types; } public int getDevotion(final MagicColor... colors) { int devotion = 0; for (final MagicCostManaType mt : getCostManaTypes(0)) { if (mt == MagicCostManaType.Generic) { continue; } for (final MagicColor c : colors) { if (mt.getTypes().contains(c.getManaType())) { devotion++; break; } } } return devotion; } public int getColorFlags() { int colorFlags = 0; for (final MagicCostManaType costType : order) { if (costType != MagicCostManaType.Generic && costType != MagicCostManaType.Colorless) { for (final MagicManaType manaType : costType.getTypes()) { colorFlags |= manaType.getColor().getMask(); } } } return colorFlags; } private static String getCanonicalText(final int[] amounts, final int XCount) { final StringBuilder sb = new StringBuilder(); //add X for (int i = 0; i < XCount; i++) { sb.append('{').append('X').append('}'); } //add others for (final MagicCostManaType type : getCanonicalOrder(amounts)) { final int amt = amounts[type.ordinal()]; if (type == MagicCostManaType.Generic && amt > 0) { sb.append('{').append(amt).append('}'); continue; } for (int i = 0; i < amt ; i++) { sb.append(type.getText()); } } if (sb.length() == 0) { return "{0}"; } else { return sb.toString(); } } private static List getCanonicalOrder(final int[] amounts) { final List manaOrder = new ArrayList<>(); for (final MagicCostManaType type : MagicCostManaType.NON_MONO) { final int amt = amounts[type.ordinal()]; if (amt > 0) { manaOrder.add(type); } } //add mono MagicCostManaType curr = findFirstMonoSymbol(amounts); for (int i = 0; i < 5; i++, curr = curr.next()) { final int amt = amounts[curr.ordinal()]; if (amt > 0) { manaOrder.add(curr); } } return manaOrder; } //find the first mono color in the mana cost order private static MagicCostManaType findFirstMonoSymbol(final int[] amounts) { //keep color with amount > 0 and has no prev final List cand = new ArrayList<>(); for (final MagicCostManaType color : MagicCostManaType.MONO) { final int amt_c = amounts[color.ordinal()]; final int amt_p = amounts[color.prev().ordinal()]; if (amt_c > 0 && amt_p == 0) { cand.add(color); } } if (cand.isEmpty()) { //WUBRG return MagicCostManaType.White; } else if (cand.size() == 1) { //Linear chain return cand.get(0); } else { //cand.size() == 2 //Wedge final MagicCostManaType t1 = cand.get(0); final MagicCostManaType t2 = cand.get(1); return (t1.prev().prev() == t2) ? t2 : t1; } } private void buildIcons() { for (int x = XCount; x > 0; x--) { icons.add(MagicIcon.MANA_X); } for (final MagicCostManaType type : order) { int amount = amounts[type.ordinal()]; if (type == MagicCostManaType.Generic) { while (amount > 16) { icons.add(GENERIC_ICONS[16]); amount-=16; } if (amount >= 0) { icons.add(GENERIC_ICONS[amount]); } } else { final MagicIcon icon = TextImages.getIcon(type.getText()); for (int a=amount;a>0;a--) { icons.add(icon); } } } } public List getIcons() { if (icons == null) { icons = new ArrayList<>(); buildIcons(); } return icons; } int getCostScore(final MagicDeckProfile profile) { final int[] singleCounts=new int[MagicManaType.NR_OF_TYPES]; int doubleCount=0; int maxSingleCount=0; for (final MagicCostManaType type : order) { final int amount = amounts[type.ordinal()]; if (type == MagicCostManaType.Generic || amount == 0) { continue; } final MagicManaType[] profileTypes = type.getTypes(profile); switch (profileTypes.length) { case 0: return 0; case 1: final int typeIndex=profileTypes[0].ordinal(); singleCounts[typeIndex]+=amount; maxSingleCount=Math.max(maxSingleCount,singleCounts[typeIndex]); break; case 2: doubleCount+=amount; break; } } return 2*converted+3*(10-SINGLE_PENALTY[maxSingleCount]-DOUBLE_PENALTY[doubleCount]); } public int getConvertedCost() { return converted; } public int getConvertedCost(final int x) { return converted + x * XCount; } public MagicBuilderManaCost getBuilderCost() { if (builderCost == null) { synchronized (this) { if (builderCost == null) { builderCost = new MagicBuilderManaCost(this); } } } return builderCost; } public void addTo(final MagicBuilderManaCost aBuilderCost) { for (final MagicCostManaType type : order) { aBuilderCost.addType(type,amounts[type.ordinal()]); } if (hasX()) { aBuilderCost.setXCount(XCount); } aBuilderCost.compress(); } public void addTo(final MagicBuilderManaCost aBuilderCost,final int x) { for (final MagicCostManaType type : order) { if (type == MagicCostManaType.Generic) { //skip } else { aBuilderCost.addType(type,amounts[type.ordinal()]); } } final int generic = Math.max(0, amounts[MagicCostManaType.Generic.ordinal()] + x * XCount); aBuilderCost.addType(MagicCostManaType.Generic, generic); aBuilderCost.compress(); } public MagicCondition getCondition() { MagicCondition cond = CONDS_MAP.get(costText); if (cond == null) { cond = new MagicManaCostCondition(this); CONDS_MAP.put(costText, cond); } return cond; } public static MagicManaCost create(final String costText) { MagicManaCost cost = COSTS_MAP.get(costText); if (cost == null) { cost = new MagicManaCost(costText); COSTS_MAP.put(costText,cost); } return cost; } public static MagicManaCost create(final int amount) { return create("{"+amount+"}"); } public MagicManaCost reduce(final MagicCostManaType type, final int amt) { return increase(type, -amt); } public MagicManaCost reduce(final int amt) { return increase(MagicCostManaType.Generic, -amt); } public MagicManaCost increase(final int amt) { return increase(MagicCostManaType.Generic, amt); } public MagicManaCost increase(final MagicCostManaType type, final int amt) { if (amt == 0) { return this; } final int[] reducedAmounts = Arrays.copyOf(amounts, amounts.length); final int idx = type.ordinal(); reducedAmounts[idx] += amt; if (XCount > 0 && type == MagicCostManaType.Generic && reducedAmounts[idx] < 0) { // If cost contains {X}, we store even negative value for colorless as possible "discount" return new MagicManaCost(reducedAmounts, XCount); } else if (amounts[idx] == 0 && reducedAmounts[idx] < 0) { return this; } else { return MagicManaCost.create(getCanonicalText(reducedAmounts, XCount)); } } /** Return cost increased by some other cost. Basically sum of those two costs */ public MagicManaCost increasedBy(MagicManaCost extraCost) { MagicManaCost res = this; for (final MagicCostManaType cmt : extraCost.getCostManaTypes(0)) { res = res.increase(cmt, 1); } return res; } /** * Return cost decreased by some other cost. Only identical mana is removed from the cost. * For example, cost {R}{B} reduced by {B}{B}{1} will become {R} * * When removing colored mana ({R}, {G}, {B}, {U}, {W}), primarily remove the specified mana. * If that is not present, look for mana with choice (hybrid, phyrexian, or colorless-hybrid in that order) * and see if the reduction can be deducted from some of that cost. * * First possible encountered cost is used for reduction. * That may not necessarily be optimal choice if more hybrid costs are present. * * (i.e. removing {R}{G} from {R/G}{R/U} may remove first {R/G} via {R} even if the result would not be optimal) */ public MagicManaCost reducedBy(MagicManaCost extraCost) { final int[] reducedAmounts = Arrays.copyOf(amounts, amounts.length); boolean changed = false; List remaining = new ArrayList<>(); for (final MagicCostManaType type : extraCost.getCostManaTypes(0)) { final int idx = type.ordinal(); if (reducedAmounts[idx] >= 1) { reducedAmounts[idx] -= 1; changed = true; } else if (XCount > 0 && type == MagicCostManaType.Generic) { // For Generic: if X present, store even negative reducedAmounts[idx] -= 1; changed = true; } else if (MagicCostManaType.MONO.contains(type)) { // Try in second round remaining.add(type); } } // Try to remove monocolored costs from hybrid costs for (final MagicCostManaType type : remaining) { MagicManaType thisColor = type.getTypes().get(0); for (MagicCostManaType candidate : MagicCostManaType.CHOICE) { final int idx = candidate.ordinal(); if (reducedAmounts[idx] >= 1 && candidate.getTypes().contains(thisColor)) { reducedAmounts[idx] -= 1; changed = true; break; } } } if (changed) { return new MagicManaCost(reducedAmounts, XCount); } return this; } }