diff --git a/Mage.Sets/src/mage/sets/AlaraReborn.java b/Mage.Sets/src/mage/sets/AlaraReborn.java index 5e8c9b528b..c4fcf0254f 100644 --- a/Mage.Sets/src/mage/sets/AlaraReborn.java +++ b/Mage.Sets/src/mage/sets/AlaraReborn.java @@ -28,6 +28,7 @@ public final class AlaraReborn extends ExpansionSet { this.numBoosterUncommon = 3; this.numBoosterRare = 1; this.ratioBoosterMythic = 8; + this.hasOnlyMulticolorCards = true; cards.add(new SetCardInfo("Anathemancer", 33, Rarity.UNCOMMON, mage.cards.a.Anathemancer.class)); cards.add(new SetCardInfo("Architects of Will", 17, Rarity.COMMON, mage.cards.a.ArchitectsOfWill.class)); cards.add(new SetCardInfo("Ardent Plea", 1, Rarity.UNCOMMON, mage.cards.a.ArdentPlea.class)); diff --git a/Mage.Sets/src/mage/sets/Judgment.java b/Mage.Sets/src/mage/sets/Judgment.java index c21637ed78..9a1bcdd866 100644 --- a/Mage.Sets/src/mage/sets/Judgment.java +++ b/Mage.Sets/src/mage/sets/Judgment.java @@ -28,6 +28,7 @@ public final class Judgment extends ExpansionSet { this.numBoosterUncommon = 3; this.numBoosterRare = 1; this.ratioBoosterMythic = 0; + this.hasUnbalancedColors = true; cards.add(new SetCardInfo("Ancestor's Chosen", 1, Rarity.UNCOMMON, mage.cards.a.AncestorsChosen.class)); cards.add(new SetCardInfo("Anger", 77, Rarity.UNCOMMON, mage.cards.a.Anger.class)); cards.add(new SetCardInfo("Anurid Barkripper", 104, Rarity.COMMON, mage.cards.a.AnuridBarkripper.class)); diff --git a/Mage.Sets/src/mage/sets/Torment.java b/Mage.Sets/src/mage/sets/Torment.java index 6ede6aff0a..e9eb791d21 100644 --- a/Mage.Sets/src/mage/sets/Torment.java +++ b/Mage.Sets/src/mage/sets/Torment.java @@ -28,6 +28,7 @@ public final class Torment extends ExpansionSet { this.numBoosterUncommon = 3; this.numBoosterRare = 1; this.ratioBoosterMythic = 0; + this.hasUnbalancedColors = true; cards.add(new SetCardInfo("Accelerate", 90, Rarity.COMMON, mage.cards.a.Accelerate.class)); cards.add(new SetCardInfo("Acorn Harvest", 118, Rarity.COMMON, mage.cards.a.AcornHarvest.class)); cards.add(new SetCardInfo("Ambassador Laquatus", 23, Rarity.RARE, mage.cards.a.AmbassadorLaquatus.class)); diff --git a/Mage/src/main/java/mage/cards/ExpansionSet.java b/Mage/src/main/java/mage/cards/ExpansionSet.java index f6205e90fe..169a27541a 100644 --- a/Mage/src/main/java/mage/cards/ExpansionSet.java +++ b/Mage/src/main/java/mage/cards/ExpansionSet.java @@ -8,6 +8,7 @@ import mage.collation.BoosterCollator; import mage.constants.CardType; import mage.constants.Rarity; import mage.constants.SetType; +import mage.filter.FilterMana; import mage.util.CardUtil; import mage.util.RandomUtil; import org.apache.log4j.Logger; @@ -124,9 +125,8 @@ public abstract class ExpansionSet implements Serializable { protected int numBoosterDoubleFaced; // -1 = include normally 0 = exclude 1-n = include explicit protected double ratioBoosterMythic; - protected boolean validateBoosterColors = true; - protected double rejectMissingColorProbability = 0.8; - protected double rejectSameColorUncommonsProbability = 0.8; + protected boolean hasUnbalancedColors = false; + protected boolean hasOnlyMulticolorCards = false; protected int maxCardNumberInBooster; // used to omit cards with collector numbers beyond the regular cards in a set for boosters @@ -276,10 +276,12 @@ public abstract class ExpansionSet implements Serializable { } protected boolean boosterIsValid(List booster) { - if (validateBoosterColors) { - if (!validateColors(booster)) { - return false; - } + if (!validateCommonColors(booster)) { + return false; + } + + if (!validateUncommonColors(booster)) { + return false; } // TODO: add partner check @@ -287,56 +289,79 @@ public abstract class ExpansionSet implements Serializable { return true; } - protected boolean validateColors(List booster) { - List magicColors - = Arrays.asList(ObjectColor.WHITE, ObjectColor.BLUE, ObjectColor.BLACK, ObjectColor.RED, ObjectColor.GREEN); - - // all cards colors - Map colorWeight = new HashMap<>(); - // uncommon/rare/mythic cards colors - Map uncommonWeight = new HashMap<>(); - - for (ObjectColor color : magicColors) { - colorWeight.put(color, 0); - uncommonWeight.put(color, 0); + private static ObjectColor getColorForValidate(Card card) { + ObjectColor color = card.getColor(); + // treat colorless nonland cards with exactly one ID color as cards of that color + // (e.g. devoid, emerge, spellbombs... but not mana fixing artifacts) + if (color.isColorless() && !card.isLand()) { + FilterMana colorIdentity = card.getColorIdentity(); + if (colorIdentity.getColorCount() == 1) { + return new ObjectColor(colorIdentity.toString()); + } } + return color; + } - // count colors in the booster - for (Card card : booster) { - ObjectColor cardColor = card.getColor(null); - if (cardColor != null) { - List colors = cardColor.getColors(); - // todo: do we need gold color? - colors.remove(ObjectColor.GOLD); - if (!colors.isEmpty()) { - // 60 - full card weight - // multicolored cards add part of the weight to each color - int cardColorWeight = 60 / colors.size(); - for (ObjectColor color : colors) { - colorWeight.put(color, colorWeight.get(color) + cardColorWeight); - if (card.getRarity() != Rarity.COMMON) { - uncommonWeight.put(color, uncommonWeight.get(color) + cardColorWeight); - } - } - } + protected boolean validateCommonColors(List booster) { + List commonColors = booster.stream() + .filter(card -> card.getRarity() == Rarity.COMMON) + .map(ExpansionSet::getColorForValidate) + .collect(Collectors.toList()); + + // for multicolor sets, count not just the colors present at common, + // but also the number of color combinations (guilds/shards/wedges) + // e.g. a booster with three UB commons, three RW commons and four G commons + // has all five colors but isn't "balanced" + ObjectColor colorsRepresented = new ObjectColor(); + Set colorCombinations = new HashSet<>(); + int colorlessCountPlusOne = 1; + + for (ObjectColor color : commonColors) { + colorCombinations.add(color); + int colorCount = color.getColorCount(); + if (colorCount == 0) { + ++colorlessCountPlusOne; + } else if (colorCount > 1 && !hasOnlyMulticolorCards) { + // to prevent biasing toward multicolor over monocolor cards, + // count them as one of their colors chosen at random + List multiColor = color.getColors(); + colorsRepresented.addColor(multiColor.get(RandomUtil.nextInt(multiColor.size()))); + } else { + colorsRepresented.addColor(color); } } - // check that all colors are present - if (magicColors.stream().anyMatch(color -> colorWeight.get(color) < 60)) { - // reject only part of the boosters - if (RandomUtil.nextDouble() < rejectMissingColorProbability) { - return false; - } - } + int colors = Math.min(colorsRepresented.getColorCount(), colorCombinations.size()); - // check that we don't have 3 or more uncommons/rares of the same color - if (magicColors.stream().anyMatch(color -> uncommonWeight.get(color) >= 180)) { - // reject only part of the boosters - return !(RandomUtil.nextDouble() < rejectSameColorUncommonsProbability); - } + // if booster has all five colors in five unique combinations, or if it has + // one card per color and all but one of the rest are colorless, accept it + // ("all but one" adds some leeway for sets with small boosters) + if (colors >= Math.min(5, commonColors.size() - colorlessCountPlusOne)) return true; + // otherwise, if booster is missing more than one color, reject it + if (colors < 4) return false; + // for Torment and Judgment, always accept boosters with four out of five colors + if (hasUnbalancedColors) return true; + // if a common was replaced by a special card, increase the chance to accept four colors + if (commonColors.size() < numBoosterCommon) ++colorlessCountPlusOne; - return true; + // otherwise, stochiastically treat each colorless card as 1/5 of a card of the missing color + return (RandomUtil.nextDouble() > Math.pow(0.8, colorlessCountPlusOne)); + } + + private static final ObjectColor COLORLESS = new ObjectColor(); + + protected boolean validateUncommonColors(List booster) { + List uncommonColors = booster.stream() + .filter(card -> card.getRarity() == Rarity.UNCOMMON) + .map(ExpansionSet::getColorForValidate) + .collect(Collectors.toList()); + + // if there are only two uncommons, they can be the same color + if (uncommonColors.size() < 3) return true; + // boosters of artifact sets can have all colorless uncommons + if (uncommonColors.contains(COLORLESS)) return true; + // otherwise, reject if all uncommons are the same color combination + return (new HashSet<>(uncommonColors).size() > 1); } protected boolean checkMythic() {