Add ability to draft with less than 8 players

This commit is contained in:
Chris H
2025-07-30 23:32:29 -04:00
parent 6a84a7a6f0
commit b8a5668db6
4 changed files with 131 additions and 19 deletions

View File

@@ -52,6 +52,14 @@ import java.util.stream.Collectors;
*/ */
public final class CardEdition implements Comparable<CardEdition> { public final class CardEdition implements Comparable<CardEdition> {
public DraftOptions getDraftOptions() {
return draftOptions;
}
public void setDraftOptions(DraftOptions draftOptions) {
this.draftOptions = draftOptions;
}
// immutable // immutable
public enum Type { public enum Type {
UNKNOWN, UNKNOWN,
@@ -275,18 +283,22 @@ public final class CardEdition implements Comparable<CardEdition> {
// Booster/draft info // Booster/draft info
private List<BoosterSlot> boosterSlots = null; private List<BoosterSlot> boosterSlots = null;
private boolean smallSetOverride = false; private boolean smallSetOverride = false;
private boolean foilAlwaysInCommonSlot = false; private String additionalUnlockSet = "";
private FoilType foilType = FoilType.NOT_SUPPORTED; private FoilType foilType = FoilType.NOT_SUPPORTED;
// Replace all of these things with booster slots
private boolean foilAlwaysInCommonSlot = false;
private double foilChanceInBooster = 0; private double foilChanceInBooster = 0;
private double chanceReplaceCommonWith = 0; private double chanceReplaceCommonWith = 0;
private String slotReplaceCommonWith = "Common"; private String slotReplaceCommonWith = "Common";
private String additionalSheetForFoils = ""; private String additionalSheetForFoils = "";
private String additionalUnlockSet = "";
private String boosterMustContain = ""; private String boosterMustContain = "";
private String boosterReplaceSlotFromPrintSheet = ""; private String boosterReplaceSlotFromPrintSheet = "";
private String sheetReplaceCardFromSheet = ""; private String sheetReplaceCardFromSheet = "";
private String sheetReplaceCardFromSheet2 = ""; private String sheetReplaceCardFromSheet2 = "";
private String doublePickDuringDraft = "";
// Draft options
private DraftOptions draftOptions = null;
private String[] chaosDraftThemes = new String[0]; private String[] chaosDraftThemes = new String[0];
private final ListMultimap<String, EditionEntry> cardMap; private final ListMultimap<String, EditionEntry> cardMap;
@@ -373,7 +385,6 @@ public final class CardEdition implements Comparable<CardEdition> {
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; } public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; } public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
public String getAdditionalUnlockSet() { return additionalUnlockSet; } public String getAdditionalUnlockSet() { return additionalUnlockSet; }
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
public String getBoosterMustContain() { return boosterMustContain; } public String getBoosterMustContain() { return boosterMustContain; }
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; } public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; } public String getSheetReplaceCardFromSheet() { return sheetReplaceCardFromSheet; }
@@ -808,7 +819,6 @@ public final class CardEdition implements Comparable<CardEdition> {
res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral res.additionalUnlockSet = metadata.get("AdditionalSetUnlockedInQuest", ""); // e.g. Time Spiral Timeshifted (TSB) for Time Spiral
res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon) res.smallSetOverride = metadata.getBoolean("TreatAsSmallSet", false); // for "small" sets with over 200 cards (e.g. Eldritch Moon)
res.doublePickDuringDraft = metadata.get("DoublePick", ""); // "FirstPick" or "Always"
res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature res.boosterMustContain = metadata.get("BoosterMustContain", ""); // e.g. Dominaria guaranteed legendary creature
res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card res.boosterReplaceSlotFromPrintSheet = metadata.get("BoosterReplaceSlotFromPrintSheet", ""); // e.g. Zendikar Rising guaranteed double-faced card
@@ -816,6 +826,23 @@ public final class CardEdition implements Comparable<CardEdition> {
res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", ""); res.sheetReplaceCardFromSheet2 = metadata.get("SheetReplaceCardFromSheet2", "");
res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names res.chaosDraftThemes = metadata.get("ChaosDraftThemes", "").split(";"); // semicolon separated list of theme names
// Draft options
String doublePick = metadata.get("DoublePick", "Never");
int maxPodSize = metadata.getInt("MaxPodSize", 8);
int recommendedPodSize = metadata.getInt("RecommendedPodSize", 8);
int maxMatchPlayers = metadata.getInt("MaxMatchPlayers", 2);
String deckType = metadata.get("DeckType", "Normal");
String freeCommander = metadata.get("FreeCommander", "");
res.draftOptions = new DraftOptions(
doublePick,
maxPodSize,
recommendedPodSize,
maxMatchPlayers,
deckType,
freeCommander
);
return res; return res;
} }

View File

@@ -0,0 +1,57 @@
package forge.card;
public class DraftOptions {
public enum DoublePick {
NEVER,
FIRST_PICK, // only first pick each pack
ALWAYS // each time you receive a pack, you can pick two cards
};
public enum DeckType {
Normal, // Standard deck, usually 40 cards
Commander // Special deck type for Commander format. Important for selection/construction
}
private DoublePick doublePick = DoublePick.NEVER;
private final int maxPodSize; // Usually 8, but could be smaller for cubes. I guess it could be larger too
private final int recommendedPodSize; // Usually 8, but is 4 for new double pick
private final int maxMatchPlayers; // Usually 2, but 4 for things like Commander or Conspiracy
private final DeckType deckType; // Normal or Commander
private final String freeCommander;
public DraftOptions(String doublePickOption, int maxPodSize, int recommendedPodSize, int maxMatchPlayers, String deckType, String freeCommander) {
this.maxPodSize = maxPodSize;
this.recommendedPodSize = recommendedPodSize;
this.maxMatchPlayers = maxMatchPlayers;
this.deckType = DeckType.valueOf(deckType);
this.freeCommander = freeCommander;
if (doublePickOption != null) {
switch (doublePickOption.toLowerCase()) {
case "firstpick":
doublePick = DoublePick.FIRST_PICK;
break;
case "always":
doublePick = DoublePick.ALWAYS;
break;
}
}
}
public int getMaxPodSize() {
return maxPodSize;
}
public int getRecommendedPodSize() {
return recommendedPodSize;
}
public DoublePick getDoublePick() {
return doublePick;
}
public int getMaxMatchPlayers() {
return maxMatchPlayers;
}
public DeckType getDeckType() {
return deckType;
}
public String getFreeCommander() {
return freeCommander;
}
}

View File

@@ -4,6 +4,8 @@ Date=2025-09-26
Name=Marvel's Spider-Man Name=Marvel's Spider-Man
Type=Expansion Type=Expansion
ScryfallCode=SPM ScryfallCode=SPM
DoublePick=Always
RecommendedPodSize=4
[cards] [cards]
1 M Anti-Venom, Horrifying Healer @Néstor Ossandón Leal 1 M Anti-Venom, Horrifying Healer @Néstor Ossandón Leal

View File

@@ -19,6 +19,7 @@ package forge.gamemodes.limited;
import forge.StaticData; import forge.StaticData;
import forge.card.CardEdition; import forge.card.CardEdition;
import forge.card.DraftOptions;
import forge.deck.CardPool; import forge.deck.CardPool;
import forge.deck.Deck; import forge.deck.Deck;
import forge.deck.DeckBase; import forge.deck.DeckBase;
@@ -54,15 +55,17 @@ public class BoosterDraft implements IBoosterDraft {
private int nextId = 0; private int nextId = 0;
private static final int N_PLAYERS = 8; private static final int N_PLAYERS = 8;
public static final String FILE_EXT = ".draft"; public static final String FILE_EXT = ".draft";
int podSize;
private final List<LimitedPlayer> players = new ArrayList<>(); private final List<LimitedPlayer> players = new ArrayList<>();
private final LimitedPlayer localPlayer; private final LimitedPlayer localPlayer;
private boolean readyForComputerPick = false; private boolean readyForComputerPick = false;
private IDraftLog draftLog = null; private IDraftLog draftLog = null;
private String doublePickDuringDraft = ""; // "FirstPick" or "Always"
private boolean shouldShowDraftLog = false; private boolean shouldShowDraftLog = false;
private DraftOptions.DoublePick doublePickDuringDraft;
protected int nextBoosterGroup = 0; protected int nextBoosterGroup = 0;
private int currentBoosterSize = 0; private int currentBoosterSize = 0;
private int currentBoosterPick = 0; private int currentBoosterPick = 0;
@@ -80,6 +83,9 @@ public class BoosterDraft implements IBoosterDraft {
if (!draft.generateProduct()) { if (!draft.generateProduct()) {
return null; return null;
} }
// Choose the amount of players
draft.initializeBoosters(); draft.initializeBoosters();
return draft; return draft;
} }
@@ -157,11 +163,14 @@ public class BoosterDraft implements IBoosterDraft {
CardEdition edition = FModel.getMagicDb().getEditions().get(setCode); CardEdition edition = FModel.getMagicDb().getEditions().get(setCode);
// If this is metaset, edtion will be null // If this is metaset, edtion will be null
if (edition != null) { if (edition != null) {
doublePickDuringDraft = edition.getDoublePickDuringDraft(); doublePickDuringDraft = edition.getDraftOptions().getDoublePick();
// Let's set the pod size to the recommended one for this edition
if (podSize != edition.getDraftOptions().getRecommendedPodSize()) {
setPodSize(edition.getDraftOptions().getRecommendedPodSize());
}
} }
final IUnOpenedProduct product1 = block.getBooster(setCode); final IUnOpenedProduct product1 = block.getBooster(setCode);
for (int i = 0; i < nPacks; i++) { for (int i = 0; i < nPacks; i++) {
this.product.add(product1); this.product.add(product1);
} }
@@ -296,10 +305,11 @@ public class BoosterDraft implements IBoosterDraft {
protected BoosterDraft(final LimitedPoolType draftType, int numPlayers) { protected BoosterDraft(final LimitedPoolType draftType, int numPlayers) {
this.draftFormat = draftType; this.draftFormat = draftType;
this.podSize = numPlayers;
localPlayer = new LimitedPlayer(0, this); localPlayer = new LimitedPlayer(0, this);
players.add(localPlayer); players.add(localPlayer);
for (int i = 1; i < numPlayers; i++) { for (int i = 1; i < this.podSize; i++) {
players.add(new LimitedPlayerAI(i, this)); players.add(new LimitedPlayerAI(i, this));
} }
} }
@@ -309,6 +319,22 @@ public class BoosterDraft implements IBoosterDraft {
return new DraftPack(product.get(), nextId++); return new DraftPack(product.get(), nextId++);
} }
public void setPodSize(int size) {
if (size < 2 || size > N_PLAYERS) {
throw new IllegalArgumentException("BoosterDraft : invalid pod size " + size);
}
this.podSize = size;
// Resize players list if it was already generated
while (this.players.size() < this.podSize) {
this.players.add(new LimitedPlayerAI(this.players.size(), this));
}
while (this.players.size() > this.podSize) {
this.players.remove(this.players.size() - 1);
}
}
@Override @Override
public boolean isPileDraft() { public boolean isPileDraft() {
return false; return false;
@@ -336,7 +362,7 @@ public class BoosterDraft implements IBoosterDraft {
@Override @Override
public LimitedPlayer getNeighbor(LimitedPlayer player, boolean left) { public LimitedPlayer getNeighbor(LimitedPlayer player, boolean left) {
return players.get((player.order + (left ? 1 : -1) + N_PLAYERS) % N_PLAYERS); return players.get((player.order + (left ? 1 : -1) + this.podSize) % this.podSize);
} }
private void setupCustomDraft(final CustomLimited draft) { private void setupCustomDraft(final CustomLimited draft) {
@@ -438,7 +464,7 @@ public class BoosterDraft implements IBoosterDraft {
public void initializeBoosters() { public void initializeBoosters() {
for (Supplier<List<PaperCard>> boosterRound : this.product) { for (Supplier<List<PaperCard>> boosterRound : this.product) {
for (int i = 0; i < N_PLAYERS; i++) { for (int i = 0; i < this.podSize; i++) {
DraftPack pack = new DraftPack(boosterRound.get(), nextId++); DraftPack pack = new DraftPack(boosterRound.get(), nextId++);
this.players.get(i).receiveUnopenedPack(pack); this.players.get(i).receiveUnopenedPack(pack);
} }
@@ -467,8 +493,8 @@ public class BoosterDraft implements IBoosterDraft {
@Override @Override
public Deck[] getComputerDecks() { public Deck[] getComputerDecks() {
Deck[] decks = new Deck[7]; Deck[] decks = new Deck[this.podSize - 1];
for (int i = 1; i < N_PLAYERS; i++) { for (int i = 1; i < this.podSize; i++) {
decks[i - 1] = ((LimitedPlayerAI) this.players.get(i)).buildDeck(IBoosterDraft.LAND_SET_CODE[0] != null ? IBoosterDraft.LAND_SET_CODE[0].getCode() : null); decks[i - 1] = ((LimitedPlayerAI) this.players.get(i)).buildDeck(IBoosterDraft.LAND_SET_CODE[0] != null ? IBoosterDraft.LAND_SET_CODE[0].getCode() : null);
} }
return decks; return decks;
@@ -476,7 +502,7 @@ public class BoosterDraft implements IBoosterDraft {
@Override @Override
public LimitedPlayer[] getOpposingPlayers() { public LimitedPlayer[] getOpposingPlayers() {
return this.players.subList(1, players.size()).toArray(new LimitedPlayer[7]); return this.players.subList(1, players.size()).toArray(new LimitedPlayer[this.podSize - 1]);
} }
@Override @Override
@@ -501,9 +527,9 @@ public class BoosterDraft implements IBoosterDraft {
public void passPacks() { public void passPacks() {
// Alternate direction of pack passing // Alternate direction of pack passing
int adjust = this.nextBoosterGroup % 2 == 1 ? 1 : -1; int adjust = this.nextBoosterGroup % 2 == 1 ? 1 : -1;
if ("FirstPick".equals(this.doublePickDuringDraft) && currentBoosterPick == 1) { if (DraftOptions.DoublePick.FIRST_PICK.equals(this.doublePickDuringDraft) && currentBoosterPick == 0) {
adjust = 0; adjust = 0;
} else if (currentBoosterPick % 2 == 1 && "Always".equals(this.doublePickDuringDraft)) { } else if (currentBoosterPick % 2 == 0 && DraftOptions.DoublePick.ALWAYS.equals(this.doublePickDuringDraft)) {
// This may not work with Conspiracy cards that mess with the draft // This may not work with Conspiracy cards that mess with the draft
// But it probably doesn't matter since Conspiracy doesn't have double pick? // But it probably doesn't matter since Conspiracy doesn't have double pick?
adjust = 0; adjust = 0;
@@ -518,7 +544,7 @@ public class BoosterDraft implements IBoosterDraft {
} }
Map<DraftPack, LimitedPlayer> toPass = new HashMap<>(); Map<DraftPack, LimitedPlayer> toPass = new HashMap<>();
for (int i = 0; i < N_PLAYERS; i++) { for (int i = 0; i < this.podSize; i++) {
LimitedPlayer pl = this.players.get(i); LimitedPlayer pl = this.players.get(i);
DraftPack passingPack = pl.passPack(); DraftPack passingPack = pl.passPack();
@@ -555,7 +581,7 @@ public class BoosterDraft implements IBoosterDraft {
} }
if (passToPlayer == null) { if (passToPlayer == null) {
passToPlayer = this.players.get((i + adjust + N_PLAYERS) % N_PLAYERS); passToPlayer = this.players.get((i + adjust + this.podSize) % this.podSize);
} }
assert(!toPass.containsKey(passingPack)); assert(!toPass.containsKey(passingPack));
@@ -573,7 +599,7 @@ public class BoosterDraft implements IBoosterDraft {
protected void computerChoose() { protected void computerChoose() {
// Loop through players 1-7 to draft their current pack // Loop through players 1-7 to draft their current pack
for (int i = 1; i < N_PLAYERS; i++) { for (int i = 1; i < this.podSize; i++) {
LimitedPlayer pl = this.players.get(i); LimitedPlayer pl = this.players.get(i);
if (pl.shouldSkipThisPick()) { if (pl.shouldSkipThisPick()) {
pl.debugPrint("Skipped (shouldSkipThisPick)"); pl.debugPrint("Skipped (shouldSkipThisPick)");