mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 17:58:01 +00:00
Add ability to draft with less than 8 players
This commit is contained in:
@@ -52,6 +52,14 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
public DraftOptions getDraftOptions() {
|
||||
return draftOptions;
|
||||
}
|
||||
|
||||
public void setDraftOptions(DraftOptions draftOptions) {
|
||||
this.draftOptions = draftOptions;
|
||||
}
|
||||
|
||||
// immutable
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
@@ -275,18 +283,22 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
// Booster/draft info
|
||||
private List<BoosterSlot> boosterSlots = null;
|
||||
private boolean smallSetOverride = false;
|
||||
private boolean foilAlwaysInCommonSlot = false;
|
||||
private String additionalUnlockSet = "";
|
||||
private FoilType foilType = FoilType.NOT_SUPPORTED;
|
||||
|
||||
// Replace all of these things with booster slots
|
||||
private boolean foilAlwaysInCommonSlot = false;
|
||||
private double foilChanceInBooster = 0;
|
||||
private double chanceReplaceCommonWith = 0;
|
||||
private String slotReplaceCommonWith = "Common";
|
||||
private String additionalSheetForFoils = "";
|
||||
private String additionalUnlockSet = "";
|
||||
private String boosterMustContain = "";
|
||||
private String boosterReplaceSlotFromPrintSheet = "";
|
||||
private String sheetReplaceCardFromSheet = "";
|
||||
private String sheetReplaceCardFromSheet2 = "";
|
||||
private String doublePickDuringDraft = "";
|
||||
|
||||
// Draft options
|
||||
private DraftOptions draftOptions = null;
|
||||
private String[] chaosDraftThemes = new String[0];
|
||||
|
||||
private final ListMultimap<String, EditionEntry> cardMap;
|
||||
@@ -373,7 +385,6 @@ public final class CardEdition implements Comparable<CardEdition> {
|
||||
public String getSlotReplaceCommonWith() { return slotReplaceCommonWith; }
|
||||
public String getAdditionalSheetForFoils() { return additionalSheetForFoils; }
|
||||
public String getAdditionalUnlockSet() { return additionalUnlockSet; }
|
||||
public String getDoublePickDuringDraft() { return doublePickDuringDraft; }
|
||||
public String getBoosterMustContain() { return boosterMustContain; }
|
||||
public String getBoosterReplaceSlotFromPrintSheet() { return boosterReplaceSlotFromPrintSheet; }
|
||||
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.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.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.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;
|
||||
}
|
||||
|
||||
|
||||
57
forge-core/src/main/java/forge/card/DraftOptions.java
Normal file
57
forge-core/src/main/java/forge/card/DraftOptions.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,8 @@ Date=2025-09-26
|
||||
Name=Marvel's Spider-Man
|
||||
Type=Expansion
|
||||
ScryfallCode=SPM
|
||||
DoublePick=Always
|
||||
RecommendedPodSize=4
|
||||
|
||||
[cards]
|
||||
1 M Anti-Venom, Horrifying Healer @Néstor Ossandón Leal
|
||||
|
||||
@@ -19,6 +19,7 @@ package forge.gamemodes.limited;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.card.CardEdition;
|
||||
import forge.card.DraftOptions;
|
||||
import forge.deck.CardPool;
|
||||
import forge.deck.Deck;
|
||||
import forge.deck.DeckBase;
|
||||
@@ -54,15 +55,17 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
private int nextId = 0;
|
||||
private static final int N_PLAYERS = 8;
|
||||
public static final String FILE_EXT = ".draft";
|
||||
|
||||
int podSize;
|
||||
private final List<LimitedPlayer> players = new ArrayList<>();
|
||||
private final LimitedPlayer localPlayer;
|
||||
private boolean readyForComputerPick = false;
|
||||
|
||||
private IDraftLog draftLog = null;
|
||||
|
||||
private String doublePickDuringDraft = ""; // "FirstPick" or "Always"
|
||||
private boolean shouldShowDraftLog = false;
|
||||
|
||||
private DraftOptions.DoublePick doublePickDuringDraft;
|
||||
protected int nextBoosterGroup = 0;
|
||||
private int currentBoosterSize = 0;
|
||||
private int currentBoosterPick = 0;
|
||||
@@ -80,6 +83,9 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
if (!draft.generateProduct()) {
|
||||
return null;
|
||||
}
|
||||
// Choose the amount of players
|
||||
|
||||
|
||||
draft.initializeBoosters();
|
||||
return draft;
|
||||
}
|
||||
@@ -157,11 +163,14 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
CardEdition edition = FModel.getMagicDb().getEditions().get(setCode);
|
||||
// If this is metaset, edtion will be 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);
|
||||
|
||||
for (int i = 0; i < nPacks; i++) {
|
||||
this.product.add(product1);
|
||||
}
|
||||
@@ -296,10 +305,11 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
|
||||
protected BoosterDraft(final LimitedPoolType draftType, int numPlayers) {
|
||||
this.draftFormat = draftType;
|
||||
this.podSize = numPlayers;
|
||||
|
||||
localPlayer = new LimitedPlayer(0, this);
|
||||
players.add(localPlayer);
|
||||
for (int i = 1; i < numPlayers; i++) {
|
||||
for (int i = 1; i < this.podSize; i++) {
|
||||
players.add(new LimitedPlayerAI(i, this));
|
||||
}
|
||||
}
|
||||
@@ -309,6 +319,22 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
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
|
||||
public boolean isPileDraft() {
|
||||
return false;
|
||||
@@ -336,7 +362,7 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
|
||||
@Override
|
||||
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) {
|
||||
@@ -438,7 +464,7 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
public void initializeBoosters() {
|
||||
|
||||
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++);
|
||||
this.players.get(i).receiveUnopenedPack(pack);
|
||||
}
|
||||
@@ -467,8 +493,8 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
|
||||
@Override
|
||||
public Deck[] getComputerDecks() {
|
||||
Deck[] decks = new Deck[7];
|
||||
for (int i = 1; i < N_PLAYERS; i++) {
|
||||
Deck[] decks = new Deck[this.podSize - 1];
|
||||
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);
|
||||
}
|
||||
return decks;
|
||||
@@ -476,7 +502,7 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
|
||||
@Override
|
||||
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
|
||||
@@ -501,9 +527,9 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
public void passPacks() {
|
||||
// Alternate direction of pack passing
|
||||
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;
|
||||
} 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
|
||||
// But it probably doesn't matter since Conspiracy doesn't have double pick?
|
||||
adjust = 0;
|
||||
@@ -518,7 +544,7 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
}
|
||||
|
||||
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);
|
||||
DraftPack passingPack = pl.passPack();
|
||||
|
||||
@@ -555,7 +581,7 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -573,7 +599,7 @@ public class BoosterDraft implements IBoosterDraft {
|
||||
|
||||
protected void computerChoose() {
|
||||
// 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);
|
||||
if (pl.shouldSkipThisPick()) {
|
||||
pl.debugPrint("Skipped (shouldSkipThisPick)");
|
||||
|
||||
Reference in New Issue
Block a user