👁️
1import type { Card } from "@/lib/scryfall-types";
2import { getOracleText, getTypeLine } from "./utils";
3
4/**
5 * Copy exception types for cards that bypass normal deck construction limits
6 */
7export type CopyException =
8 | { type: "unlimited" }
9 | { type: "limited"; max: number };
10
11/**
12 * Pattern for "a deck can have any number of cards named X"
13 */
14const UNLIMITED_PATTERN = /a deck can have any number of cards named/i;
15
16/**
17 * Pattern for "a deck can have up to X cards named Y"
18 */
19const LIMITED_PATTERN = /a deck can have up to (\w+) cards named/i;
20
21/**
22 * Word to number mapping for limited patterns
23 */
24const WORD_TO_NUMBER: Record<string, number> = {
25 one: 1,
26 two: 2,
27 three: 3,
28 four: 4,
29 five: 5,
30 six: 6,
31 seven: 7,
32 eight: 8,
33 nine: 9,
34 ten: 10,
35};
36
37/**
38 * Detect copy limit exception from oracle text.
39 * Returns undefined if no exception found.
40 */
41export function detectCopyException(card: Card): CopyException | undefined {
42 const text = getOracleText(card);
43
44 if (UNLIMITED_PATTERN.test(text)) {
45 return { type: "unlimited" };
46 }
47
48 const limitedMatch = text.match(LIMITED_PATTERN);
49 if (limitedMatch) {
50 const word = limitedMatch[1].toLowerCase();
51 const num = WORD_TO_NUMBER[word] ?? parseInt(word, 10);
52 if (!Number.isNaN(num)) {
53 return { type: "limited", max: num };
54 }
55 }
56
57 return undefined;
58}
59
60/**
61 * Check if card is a basic land (unlimited copies always allowed)
62 */
63export function isBasicLand(card: Card): boolean {
64 const typeLine = getTypeLine(card);
65 return typeLine.includes("Basic") && typeLine.includes("Land");
66}
67
68/**
69 * Get the maximum allowed copies for a card given format rules.
70 * @param card The card to check
71 * @param defaultLimit The default limit (1 for singleton, 4 for playset)
72 * @returns The maximum copies allowed (Infinity for unlimited)
73 */
74export function getCopyLimit(card: Card, defaultLimit: number): number {
75 if (isBasicLand(card)) {
76 return Infinity;
77 }
78
79 const exception = detectCopyException(card);
80 if (exception?.type === "unlimited") {
81 return Infinity;
82 }
83 if (exception?.type === "limited") {
84 return exception.max;
85 }
86
87 return defaultLimit;
88}