New booster color validation algorithm, fixes #8177 (#8413)

This commit is contained in:
Alex W. Jackson 2021-10-29 11:51:37 -04:00 committed by GitHub
parent 76e1ee84c7
commit 1e6709de46
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 78 additions and 50 deletions

View file

@ -28,6 +28,7 @@ public final class AlaraReborn extends ExpansionSet {
this.numBoosterUncommon = 3; this.numBoosterUncommon = 3;
this.numBoosterRare = 1; this.numBoosterRare = 1;
this.ratioBoosterMythic = 8; this.ratioBoosterMythic = 8;
this.hasOnlyMulticolorCards = true;
cards.add(new SetCardInfo("Anathemancer", 33, Rarity.UNCOMMON, mage.cards.a.Anathemancer.class)); 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("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)); cards.add(new SetCardInfo("Ardent Plea", 1, Rarity.UNCOMMON, mage.cards.a.ArdentPlea.class));

View file

@ -28,6 +28,7 @@ public final class Judgment extends ExpansionSet {
this.numBoosterUncommon = 3; this.numBoosterUncommon = 3;
this.numBoosterRare = 1; this.numBoosterRare = 1;
this.ratioBoosterMythic = 0; 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("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("Anger", 77, Rarity.UNCOMMON, mage.cards.a.Anger.class));
cards.add(new SetCardInfo("Anurid Barkripper", 104, Rarity.COMMON, mage.cards.a.AnuridBarkripper.class)); cards.add(new SetCardInfo("Anurid Barkripper", 104, Rarity.COMMON, mage.cards.a.AnuridBarkripper.class));

View file

@ -28,6 +28,7 @@ public final class Torment extends ExpansionSet {
this.numBoosterUncommon = 3; this.numBoosterUncommon = 3;
this.numBoosterRare = 1; this.numBoosterRare = 1;
this.ratioBoosterMythic = 0; this.ratioBoosterMythic = 0;
this.hasUnbalancedColors = true;
cards.add(new SetCardInfo("Accelerate", 90, Rarity.COMMON, mage.cards.a.Accelerate.class)); 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("Acorn Harvest", 118, Rarity.COMMON, mage.cards.a.AcornHarvest.class));
cards.add(new SetCardInfo("Ambassador Laquatus", 23, Rarity.RARE, mage.cards.a.AmbassadorLaquatus.class)); cards.add(new SetCardInfo("Ambassador Laquatus", 23, Rarity.RARE, mage.cards.a.AmbassadorLaquatus.class));

View file

@ -8,6 +8,7 @@ import mage.collation.BoosterCollator;
import mage.constants.CardType; import mage.constants.CardType;
import mage.constants.Rarity; import mage.constants.Rarity;
import mage.constants.SetType; import mage.constants.SetType;
import mage.filter.FilterMana;
import mage.util.CardUtil; import mage.util.CardUtil;
import mage.util.RandomUtil; import mage.util.RandomUtil;
import org.apache.log4j.Logger; 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 int numBoosterDoubleFaced; // -1 = include normally 0 = exclude 1-n = include explicit
protected double ratioBoosterMythic; protected double ratioBoosterMythic;
protected boolean validateBoosterColors = true; protected boolean hasUnbalancedColors = false;
protected double rejectMissingColorProbability = 0.8; protected boolean hasOnlyMulticolorCards = false;
protected double rejectSameColorUncommonsProbability = 0.8;
protected int maxCardNumberInBooster; // used to omit cards with collector numbers beyond the regular cards in a set for boosters 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<Card> booster) { protected boolean boosterIsValid(List<Card> booster) {
if (validateBoosterColors) { if (!validateCommonColors(booster)) {
if (!validateColors(booster)) { return false;
return false; }
}
if (!validateUncommonColors(booster)) {
return false;
} }
// TODO: add partner check // TODO: add partner check
@ -287,56 +289,79 @@ public abstract class ExpansionSet implements Serializable {
return true; return true;
} }
protected boolean validateColors(List<Card> booster) { private static ObjectColor getColorForValidate(Card card) {
List<ObjectColor> magicColors ObjectColor color = card.getColor();
= Arrays.asList(ObjectColor.WHITE, ObjectColor.BLUE, ObjectColor.BLACK, ObjectColor.RED, ObjectColor.GREEN); // treat colorless nonland cards with exactly one ID color as cards of that color
// (e.g. devoid, emerge, spellbombs... but not mana fixing artifacts)
// all cards colors if (color.isColorless() && !card.isLand()) {
Map<ObjectColor, Integer> colorWeight = new HashMap<>(); FilterMana colorIdentity = card.getColorIdentity();
// uncommon/rare/mythic cards colors if (colorIdentity.getColorCount() == 1) {
Map<ObjectColor, Integer> uncommonWeight = new HashMap<>(); return new ObjectColor(colorIdentity.toString());
}
for (ObjectColor color : magicColors) {
colorWeight.put(color, 0);
uncommonWeight.put(color, 0);
} }
return color;
}
// count colors in the booster protected boolean validateCommonColors(List<Card> booster) {
for (Card card : booster) { List<ObjectColor> commonColors = booster.stream()
ObjectColor cardColor = card.getColor(null); .filter(card -> card.getRarity() == Rarity.COMMON)
if (cardColor != null) { .map(ExpansionSet::getColorForValidate)
List<ObjectColor> colors = cardColor.getColors(); .collect(Collectors.toList());
// todo: do we need gold color?
colors.remove(ObjectColor.GOLD); // for multicolor sets, count not just the colors present at common,
if (!colors.isEmpty()) { // but also the number of color combinations (guilds/shards/wedges)
// 60 - full card weight // e.g. a booster with three UB commons, three RW commons and four G commons
// multicolored cards add part of the weight to each color // has all five colors but isn't "balanced"
int cardColorWeight = 60 / colors.size(); ObjectColor colorsRepresented = new ObjectColor();
for (ObjectColor color : colors) { Set<ObjectColor> colorCombinations = new HashSet<>();
colorWeight.put(color, colorWeight.get(color) + cardColorWeight); int colorlessCountPlusOne = 1;
if (card.getRarity() != Rarity.COMMON) {
uncommonWeight.put(color, uncommonWeight.get(color) + cardColorWeight); 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<ObjectColor> multiColor = color.getColors();
colorsRepresented.addColor(multiColor.get(RandomUtil.nextInt(multiColor.size())));
} else {
colorsRepresented.addColor(color);
} }
} }
// check that all colors are present int colors = Math.min(colorsRepresented.getColorCount(), colorCombinations.size());
if (magicColors.stream().anyMatch(color -> colorWeight.get(color) < 60)) {
// reject only part of the boosters
if (RandomUtil.nextDouble() < rejectMissingColorProbability) {
return false;
}
}
// check that we don't have 3 or more uncommons/rares of the same color // if booster has all five colors in five unique combinations, or if it has
if (magicColors.stream().anyMatch(color -> uncommonWeight.get(color) >= 180)) { // one card per color and all but one of the rest are colorless, accept it
// reject only part of the boosters // ("all but one" adds some leeway for sets with small boosters)
return !(RandomUtil.nextDouble() < rejectSameColorUncommonsProbability); 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<Card> booster) {
List<ObjectColor> 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() { protected boolean checkMythic() {