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