magarena/src/magic/model/MagicManaCost.java

485 lines
16 KiB
Java

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<String,MagicManaCost> COSTS_MAP=new HashMap<>();
private static final Map<String,MagicCondition> 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<MagicCostManaType> order = new ArrayList<>();
private volatile MagicBuilderManaCost builderCost;
private List<MagicIcon> 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<MagicCostManaType> getCostManaTypes(final int x) {
final List<MagicCostManaType> 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<MagicCostManaType> getCanonicalOrder(final int[] amounts) {
final List<MagicCostManaType> 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<MagicCostManaType> 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<MagicIcon> 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<MagicCostManaType> 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;
}
}