👁️
1import type { Deck } from "@/lib/deck-types";
2import { getCommanderColorIdentity } from "@/lib/deck-types";
3import type { Card, OracleId, ScryfallId } from "@/lib/scryfall-types";
4import { getPreset } from "./presets";
5import { RULES, type RuleId } from "./rules";
6import type {
7 FormatConfig,
8 RuleNumber,
9 ValidationContext,
10 ValidationOptions,
11 ValidationResult,
12 Violation,
13} from "./types";
14
15export interface ValidateDeckParams {
16 deck: Deck;
17 cardLookup: (id: ScryfallId) => Card | undefined;
18 oracleLookup: (id: OracleId) => Card | undefined;
19 getPrintings: (id: OracleId) => Card[];
20 options?: ValidationOptions;
21}
22
23/**
24 * Validate a deck against format rules.
25 *
26 * Uses the deck's format field to determine which rules to apply.
27 * Returns violations grouped by card and rule for easy display.
28 */
29export function validateDeck(params: ValidateDeckParams): ValidationResult {
30 const { deck, options = {} } = params;
31
32 const format = deck.format;
33 const preset = format ? getPreset(format) : undefined;
34
35 if (!preset) {
36 return {
37 valid: true,
38 violations: [],
39 byCard: new Map(),
40 byRule: new Map(),
41 };
42 }
43
44 return validateDeckWithRules({
45 ...params,
46 rules: preset.rules,
47 config: preset.config,
48 options,
49 });
50}
51
52/**
53 * Validate a deck with custom rules instead of format preset.
54 *
55 * Use this when you need to apply specific rules regardless of format,
56 * or when the deck doesn't have a format set.
57 */
58export function validateDeckWithRules(
59 params: ValidateDeckParams & {
60 rules: readonly RuleId[];
61 config: FormatConfig;
62 },
63): ValidationResult {
64 const {
65 deck,
66 cardLookup,
67 oracleLookup,
68 getPrintings,
69 rules,
70 config,
71 options = {},
72 } = params;
73
74 const commanderColors = getCommanderColorIdentity(deck, cardLookup);
75
76 const ctx: ValidationContext = {
77 deck,
78 cardLookup,
79 oracleLookup,
80 getPrintings,
81 format: deck.format,
82 commanderColors,
83 config: { ...config, ...options.configOverrides },
84 };
85
86 const violations: Violation[] = [];
87
88 for (const ruleId of rules) {
89 if (options.disabledRules?.has(ruleId)) continue;
90
91 const rule = RULES[ruleId];
92 if (options.disabledCategories?.has(rule.category)) continue;
93
94 const ruleViolations = rule.validate(ctx);
95 violations.push(...ruleViolations);
96 }
97
98 const validityViolations = options.includeMaybeboard
99 ? violations
100 : violations.filter((v) => v.section !== "maybeboard");
101
102 const hasErrors = validityViolations.some((v) => v.severity === "error");
103
104 return {
105 valid: !hasErrors,
106 violations,
107 byCard: groupByCard(violations),
108 byRule: groupByRule(violations),
109 };
110}
111
112function groupByCard(violations: Violation[]): Map<OracleId, Violation[]> {
113 const map = new Map<OracleId, Violation[]>();
114 for (const v of violations) {
115 if (!v.oracleId) continue;
116 const existing = map.get(v.oracleId) ?? [];
117 existing.push(v);
118 map.set(v.oracleId, existing);
119 }
120 return map;
121}
122
123function groupByRule(violations: Violation[]): Map<RuleNumber, Violation[]> {
124 const map = new Map<RuleNumber, Violation[]>();
125 for (const v of violations) {
126 const existing = map.get(v.rule) ?? [];
127 existing.push(v);
128 map.set(v.rule, existing);
129 }
130 return map;
131}