forked from External/mage
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:
parent
6d4e353867
commit
4cc9329b15
20 changed files with 437 additions and 47 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
28
Mage/src/main/java/mage/game/draft/RemixedBoosterDraft.java
Normal file
28
Mage/src/main/java/mage/game/draft/RemixedBoosterDraft.java
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
191
Mage/src/main/java/mage/game/draft/RemixedSet.java
Normal file
191
Mage/src/main/java/mage/game/draft/RemixedSet.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue