New feature: "Chaos Remixed" booster draft (#10328)

* Fix error in draft pick logger that was failing on chaos drafts with fewer than 3 sets

* Implement Remixed Booster Draft

* Add debug test

* minor cleanup

* Cleanup unnecessary checks

* Fix elimination tournament type

* Add note for future improvement
This commit is contained in:
xenohedron 2023-05-12 10:12:23 -04:00 committed by GitHub
parent 6d4e353867
commit 4cc9329b15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 437 additions and 47 deletions

View file

@ -18,7 +18,9 @@ public class ObjectColor implements Serializable, Copyable<ObjectColor>, Compara
public static final ObjectColor RED = new ObjectColor("R");
public static final ObjectColor GREEN = new ObjectColor("G");
public static final ObjectColor GOLD = new ObjectColor("O");
public static final ObjectColor COLORLESS = new ObjectColor();
public static final ObjectColor GOLD = new ObjectColor("O"); // Not multicolored - Sword of Dungeons & Dragons
private boolean white;
private boolean blue;

View file

@ -305,7 +305,7 @@ public abstract class ExpansionSet implements Serializable {
return true;
}
private static ObjectColor getColorForValidate(Card card) {
public 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)
@ -364,8 +364,6 @@ public abstract class ExpansionSet implements Serializable {
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)
@ -375,7 +373,7 @@ public abstract class ExpansionSet implements Serializable {
// 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;
if (uncommonColors.contains(ObjectColor.COLORLESS)) return true;
// otherwise, reject if all uncommons are the same color combination
return (new HashSet<>(uncommonColors).size() > 1);
}

View file

@ -0,0 +1,28 @@
package mage.game.draft;
import mage.cards.ExpansionSet;
import java.util.List;
public class RemixedBoosterDraft extends BoosterDraft {
final RemixedSet remixedSet;
public RemixedBoosterDraft(DraftOptions options, List<ExpansionSet> sets) {
super(options, sets);
if (sets.isEmpty()){
throw new RuntimeException("At least one set must be selected for remixed booster draft");
}
remixedSet = new RemixedSet(sets, 10, 3, 1);
}
@Override
protected void openBooster() {
if (boosterNum <= numberBoosters) {
for (DraftPlayer player: players.values()) {
player.setBooster(remixedSet.createBooster());
}
}
}
}

View file

@ -0,0 +1,191 @@
package mage.game.draft;
import mage.ObjectColor;
import mage.cards.Card;
import mage.cards.ExpansionSet;
import mage.cards.repository.CardCriteria;
import mage.cards.repository.CardInfo;
import mage.cards.repository.CardRepository;
import mage.constants.Rarity;
import mage.util.RandomUtil;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class RemixedSet implements Serializable {
protected final int numBoosterCommons;
protected final int numBoosterUncommons;
protected final int numBoosterRares;
protected final int numBoosterSpecials;
protected final List<CardInfo> commons;
protected final List<CardInfo> uncommons;
protected final List<CardInfo> rares;
protected final List<CardInfo> mythics;
protected final List<CardInfo> specials;
protected final double chanceMythic;
public RemixedSet (List<ExpansionSet> sets, int c, int u, int r) {
this(sets, c, u, r, 0);
}
public RemixedSet(List<ExpansionSet> sets, int c, int u, int r, int special) {
this.numBoosterCommons = c;
this.numBoosterUncommons = u;
this.numBoosterRares = r;
this.numBoosterSpecials = special; // TODO: add support for uploading a custom list of special cards
this.commons = new ArrayList<>();
this.uncommons = new ArrayList<>();
this.rares = new ArrayList<>();
this.mythics = new ArrayList<>();
this.specials = new ArrayList<>();
for (ExpansionSet set : sets) {
commons.addAll(findCardsBySetAndRarity(set, Rarity.COMMON));
uncommons.addAll(findCardsBySetAndRarity(set, Rarity.UNCOMMON));
rares.addAll(findCardsBySetAndRarity(set, Rarity.RARE));
mythics.addAll(findCardsBySetAndRarity(set, Rarity.MYTHIC));
}
float nMythics = mythics.size();
float nRares = rares.size();
this.chanceMythic = (nMythics / (nMythics + nRares + nRares));
}
protected List<CardInfo> findCardsBySetAndRarity(ExpansionSet set, Rarity rarity) {
List<CardInfo> cardInfos = CardRepository.instance.findCards(new CardCriteria()
.setCodes(set.getCode())
.rarities(rarity)
.maxCardNumber(set.getMaxCardNumberInBooster())); // TODO: Make sure this parameter is set appropriately where needed
cardInfos.removeIf(next -> (
next.getCardNumber().contains("*")
|| next.getCardNumber().contains("+")));
return cardInfos;
}
public List<Card> createBooster() {
List<Card> booster = new ArrayList<>();
booster.addAll(generateCommons());
booster.addAll(generateUncommons());
booster.addAll(generateRares());
// TODO: Generate special cards
return booster;
}
protected void addToBooster(List<Card> booster, List<CardInfo> cards) {
if (cards.isEmpty()) {
return;
}
CardInfo cardInfo = cards.remove(RandomUtil.nextInt(cards.size())); // so no duplicates in a booster
Card card = cardInfo.getCard();
if (card == null) {
return;
}
booster.add(card);
}
protected List<Card> generateCommons() {
List<Card> boosterCommons = new ArrayList<>(); // will be returned once valid or max attempts reached
for (int i = 0; i < 100; i++) { // don't want to somehow loop forever
boosterCommons.clear();
List<CardInfo> commonsForGenerate = new ArrayList<>(commons); // to not modify base list
for (int j = 0; j < numBoosterCommons; j++) {
addToBooster(boosterCommons, commonsForGenerate);
}
if (validateCommonColors(boosterCommons)) {
return boosterCommons;
}
}
return boosterCommons;
}
protected List<Card> generateUncommons() {
List<Card> boosterUncommons = new ArrayList<>(); // will be returned once valid or max attempts reached
for (int i = 0; i < 100; i++) { // don't want to somehow loop forever
boosterUncommons.clear();
List<CardInfo> uncommonsForGenerate = new ArrayList<>(uncommons); // to not modify base list
for (int j = 0; j < numBoosterUncommons; j++) {
addToBooster(boosterUncommons, uncommonsForGenerate);
}
if (validateUncommonColors(boosterUncommons)) {
return boosterUncommons;
}
}
return boosterUncommons;
}
protected List<Card> generateRares() {
List<Card> boosterRares = new ArrayList<>();
List<CardInfo> raresForGenerate = new ArrayList<>(rares);
List<CardInfo> mythicsForGenerate = new ArrayList<>(mythics);
for (int j = 0; j < numBoosterRares; j++) {
if (RandomUtil.nextDouble() < chanceMythic) {
addToBooster(boosterRares, mythicsForGenerate);
} else {
addToBooster(boosterRares, raresForGenerate);
}
}
return boosterRares;
}
// See ExpansionSet for original validation logic by awjackson
protected boolean validateCommonColors(List<Card> booster) {
List<ObjectColor> 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<ObjectColor> 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) {
// 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);
}
}
int colors = Math.min(colorsRepresented.getColorCount(), colorCombinations.size());
// 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;
// otherwise, stochastically treat each colorless card as 1/5 of a card of the missing color
return (RandomUtil.nextDouble() > Math.pow(0.8, colorlessCountPlusOne));
}
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(ObjectColor.COLORLESS)) return true;
// otherwise, reject if all uncommons are the same color combination
return (new HashSet<>(uncommonColors).size() > 1);
}
}

View file

@ -17,6 +17,7 @@ public class LimitedOptions implements Serializable {
protected DraftCube draftCube;
protected int numberBoosters;
protected boolean isRandom;
protected boolean isRemixed;
protected boolean isRichMan;
protected Deck cubeFromDeck;
@ -95,6 +96,14 @@ public class LimitedOptions implements Serializable {
this.isRandom = isRandom;
}
public boolean getIsRemixed() {
return isRemixed;
}
public void setIsRemixed(boolean isRemixed) {
this.isRemixed = isRemixed;
}
public boolean getIsRichMan() {
return isRichMan;
}

View file

@ -14,11 +14,12 @@ public class TournamentType implements Serializable {
protected int maxPlayers;
protected int numBoosters;
protected boolean cubeBooster; // boosters are generated from a defined cube
protected boolean draft; // or sealed
protected boolean limited; // or construced
protected boolean elimination; // or Swiss
protected boolean isRandom;
protected boolean isRichMan; // or Rich Man Draft
protected boolean draft; // else Sealed
protected boolean limited; // else Constructed
protected boolean elimination; // else Swiss
protected boolean isRandom; // chaos draft
protected boolean isRemixed; // boosters generated containing cards from multiple sets
protected boolean isRichMan; // new boosters generated for each pick
protected boolean isJumpstart;
protected TournamentType() {
@ -65,6 +66,10 @@ public class TournamentType implements Serializable {
return this.isRandom;
}
public boolean isRemixed() {
return this.isRemixed;
}
public boolean isRichMan() {
return this.isRichMan;
}