diff --git a/forge-core/src/main/java/forge/card/CardEdition.java b/forge-core/src/main/java/forge/card/CardEdition.java index b06692dd23d..14e93ea61ce 100644 --- a/forge-core/src/main/java/forge/card/CardEdition.java +++ b/forge-core/src/main/java/forge/card/CardEdition.java @@ -52,6 +52,14 @@ import java.util.stream.Collectors; */ public final class CardEdition implements Comparable { + 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 { // Booster/draft info private List 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 cardMap; @@ -373,7 +385,6 @@ public final class CardEdition implements Comparable { 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 { 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 { 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; } diff --git a/forge-core/src/main/java/forge/card/DraftOptions.java b/forge-core/src/main/java/forge/card/DraftOptions.java new file mode 100644 index 00000000000..2f034410423 --- /dev/null +++ b/forge-core/src/main/java/forge/card/DraftOptions.java @@ -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; + } +} diff --git a/forge-gui/res/editions/Marvel's Spider-Man.txt b/forge-gui/res/editions/Marvel's Spider-Man.txt index 048bb3bea38..c18605f8da6 100644 --- a/forge-gui/res/editions/Marvel's Spider-Man.txt +++ b/forge-gui/res/editions/Marvel's Spider-Man.txt @@ -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 diff --git a/forge-gui/src/main/java/forge/gamemodes/limited/BoosterDraft.java b/forge-gui/src/main/java/forge/gamemodes/limited/BoosterDraft.java index a2c6080bf14..6f406608c39 100644 --- a/forge-gui/src/main/java/forge/gamemodes/limited/BoosterDraft.java +++ b/forge-gui/src/main/java/forge/gamemodes/limited/BoosterDraft.java @@ -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 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> 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 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)");