👁️
1import { getPreset } from "@/lib/deck-validation/presets";
2
3export interface FormatGroup {
4 label: string;
5 formats: { value: string; label: string }[];
6}
7
8export type CommanderType =
9 | "commander"
10 | "oathbreaker"
11 | "brawl"
12 | "pauper"
13 | null;
14
15export interface FormatInfo {
16 deckSize: number | "variable";
17 singleton: boolean;
18 commanderType: CommanderType;
19 hasSignatureSpell: boolean;
20 hasSideboard: boolean;
21 tagline: string;
22 isCube: boolean;
23}
24
25/**
26 * Short taglines describing each format's identity.
27 * Focused on what makes formats unique, not rules details.
28 */
29const FORMAT_TAGLINES: Record<string, string> = {
30 // Constructed
31 standard: "Rotating · Recent sets",
32 pioneer: "Non-rotating · 2012+",
33 modern: "Non-rotating · 2003+",
34 legacy: "Eternal · All sets",
35 vintage: "Eternal · Power allowed",
36 pauper: "Commons only",
37
38 // Commander
39 commander: "Multiplayer · Casual",
40 duel: "1v1 · Competitive",
41 paupercommander: "Commons + uncommon commander",
42 predh: "Pre-2020 cards only",
43 oathbreaker: "Planeswalker + signature spell",
44
45 // Brawl
46 brawl: "Arena · Historic card pool",
47 standardbrawl: "Arena · Standard card pool",
48
49 // Arena
50 historic: "Arena · Non-rotating",
51 timeless: "Arena · No bans",
52 alchemy: "Arena · Digital cards",
53 gladiator: "Arena · 100-card singleton",
54
55 // Retro
56 premodern: "1995–2003 cards",
57 oldschool: "1993–1994 cards",
58
59 // Limited
60 draft: "40-card · Drafted cards + basic lands",
61
62 // Other
63 penny: "Budget · Rotating cheapest",
64 cube: "Draft · Custom card pool",
65
66 // Casual
67 kitchentable: "Anything goes",
68};
69
70/**
71 * Get stable format info that won't change with rules updates.
72 * Used for UI display (deck size badges, singleton indicators, etc.)
73 */
74export function getFormatInfo(format: string): FormatInfo {
75 // Special case: Cube
76 if (format === "cube") {
77 return {
78 deckSize: "variable",
79 singleton: true,
80 commanderType: null,
81 hasSignatureSpell: false,
82 hasSideboard: false,
83 tagline: FORMAT_TAGLINES.cube,
84 isCube: true,
85 };
86 }
87
88 const preset = getPreset(format);
89 if (!preset) {
90 return {
91 deckSize: 60,
92 singleton: false,
93 commanderType: null,
94 hasSignatureSpell: false,
95 hasSideboard: true,
96 tagline: "",
97 isCube: false,
98 };
99 }
100
101 const { rules, config } = preset;
102 const rulesSet = new Set(rules);
103
104 const deckSize = config.deckSize ?? config.minDeckSize ?? 60;
105 const singleton = rulesSet.has("singleton");
106 const hasSideboard = (config.sideboardSize ?? 0) > 0;
107 const hasSignatureSpell = rulesSet.has("signatureSpell");
108
109 // Determine commander type
110 let commanderType: CommanderType = null;
111 if (rulesSet.has("commanderPlaneswalker")) {
112 commanderType = "oathbreaker";
113 } else if (rulesSet.has("commanderRequired")) {
114 if (format === "brawl" || format === "standardbrawl") {
115 commanderType = "brawl";
116 } else if (format === "paupercommander") {
117 commanderType = "pauper";
118 } else {
119 commanderType = "commander";
120 }
121 }
122
123 return {
124 deckSize,
125 singleton,
126 commanderType,
127 hasSignatureSpell,
128 hasSideboard,
129 tagline: FORMAT_TAGLINES[format] ?? "",
130 isCube: false,
131 };
132}
133
134export const FORMAT_GROUPS: FormatGroup[] = [
135 {
136 label: "Constructed",
137 formats: [
138 { value: "standard", label: "Standard" },
139 { value: "pioneer", label: "Pioneer" },
140 { value: "modern", label: "Modern" },
141 { value: "legacy", label: "Legacy" },
142 { value: "vintage", label: "Vintage" },
143 { value: "pauper", label: "Pauper" },
144 ],
145 },
146 {
147 label: "Commander",
148 formats: [
149 { value: "commander", label: "Commander" },
150 { value: "duel", label: "Duel Commander" },
151 { value: "paupercommander", label: "Pauper Commander" },
152 { value: "predh", label: "PreDH" },
153 { value: "oathbreaker", label: "Oathbreaker" },
154 ],
155 },
156 {
157 label: "Brawl",
158 formats: [
159 { value: "brawl", label: "Brawl" },
160 { value: "standardbrawl", label: "Standard Brawl" },
161 ],
162 },
163 {
164 label: "Arena",
165 formats: [
166 { value: "historic", label: "Historic" },
167 { value: "timeless", label: "Timeless" },
168 { value: "alchemy", label: "Alchemy" },
169 { value: "gladiator", label: "Gladiator" },
170 ],
171 },
172 {
173 label: "Retro",
174 formats: [
175 { value: "premodern", label: "Premodern" },
176 { value: "oldschool", label: "Old School" },
177 ],
178 },
179 {
180 label: "Limited",
181 formats: [{ value: "draft", label: "Draft" }],
182 },
183 {
184 label: "Other",
185 formats: [
186 { value: "penny", label: "Penny Dreadful" },
187 { value: "cube", label: "Cube" },
188 { value: "kitchentable", label: "Kitchen Table" },
189 ],
190 },
191];
192
193const FORMAT_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
194 FORMAT_GROUPS.flatMap((group) =>
195 group.formats.map((fmt) => [fmt.value, fmt.label]),
196 ),
197);
198
199export function formatDisplayName(format: string | undefined): string {
200 if (!format) return "";
201 return FORMAT_DISPLAY_NAMES[format] ?? format;
202}