👁️
at dev 131 lines 3.2 kB view raw
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}