Spaces:
Running
Running
Download web/src/lib/deck.ts from nsaroiu/mtg-deck-completion: direct link, hf CLI and curl.
- Browser
- Download file 5.75 kB
-
https://huggingface.co/spaces/nsaroiu/mtg-deck-completion/resolve/main/web/src/lib/deck.ts
- Command line
-
hf download hf://spaces/nsaroiu/mtg-deck-completion/web/src/lib/deck.ts
-
curl -L -o deck.ts https://huggingface.co/spaces/nsaroiu/mtg-deck-completion/resolve/main/web/src/lib/deck.ts
5.75 kB
| import type { CardInfo, Completion, DeckCard } from '../api' | |
| export const DECK_SIZE = 100 | |
| // Display order for result groups (the server assigns each card's category). | |
| export const CATEGORY_ORDER = [ | |
| 'Creature', 'Planeswalker', 'Battle', 'Instant', 'Sorcery', 'Artifact', 'Enchantment', 'Land', 'Other', | |
| ] | |
| const CATEGORY_LABELS: Record<string, string> = { | |
| Creature: 'Creatures', Planeswalker: 'Planeswalkers', Battle: 'Battles', Instant: 'Instants', | |
| Sorcery: 'Sorceries', Artifact: 'Artifacts', Enchantment: 'Enchantments', Land: 'Lands', Other: 'Other', | |
| } | |
| /** Cards the user can lock and ban: everything except basic | |
| * lands, which are either the user's own (always kept) or recalculated by | |
| * the server to hit the land target. */ | |
| export function isAdjustable(card: DeckCard): boolean { | |
| return card.source !== 'basic' && !card.type_line.startsWith('Basic') | |
| } | |
| export function categoryLabel(category: string): string { | |
| return CATEGORY_LABELS[category] ?? category | |
| } | |
| export interface CardGroup { | |
| category: string | |
| cards: DeckCard[] | |
| count: number | |
| } | |
| /** Groups cards by category; within a group, nonland cards sort by mana | |
| * value then name, lands by name with basics last. The user's basics and | |
| * added basics of the same name are merged into one row. */ | |
| export function groupCards(cards: DeckCard[]): CardGroup[] { | |
| const merged: DeckCard[] = [] | |
| for (const card of cards) { | |
| const existing = card.category === 'Land' && merged.find((c) => c.name === card.name) | |
| if (existing) { | |
| existing.quantity += card.quantity | |
| if (card.source === 'basic') existing.source = 'basic' | |
| } else { | |
| merged.push({ ...card }) | |
| } | |
| } | |
| const groups = new Map<string, DeckCard[]>() | |
| for (const card of merged) { | |
| const key = CATEGORY_ORDER.includes(card.category) ? card.category : 'Other' | |
| groups.set(key, [...(groups.get(key) ?? []), card]) | |
| } | |
| const isBasic = (c: DeckCard) => c.type_line.startsWith('Basic') | |
| return CATEGORY_ORDER.filter((cat) => groups.has(cat)).map((category) => { | |
| const list = groups.get(category)! | |
| list.sort((a, b) => | |
| category === 'Land' | |
| ? Number(isBasic(a)) - Number(isBasic(b)) || a.name.localeCompare(b.name) | |
| : a.cmc - b.cmc || a.name.localeCompare(b.name), | |
| ) | |
| return { category, cards: list, count: list.reduce((n, c) => n + c.quantity, 0) } | |
| }) | |
| } | |
| /** Plain "1 Card Name" list, commander(s) first -- the format Moxfield, | |
| * Archidekt and most other deckbuilders accept on import. */ | |
| export function toDecklistText(completion: Completion): string { | |
| const lines = completion.commanders.map((c) => `1 ${c.name}`) | |
| lines.push('') | |
| for (const group of groupCards(completion.cards)) { | |
| for (const card of group.cards) lines.push(`${card.quantity} ${card.name}`) | |
| } | |
| return lines.join('\n') + '\n' | |
| } | |
| /** "Meren of Clan Nel Toth" -> "Meren decklist"; partners joined with "+". */ | |
| export function exportFileName(completion: Completion, extension: string): string { | |
| return `${completion.commanders.map((c) => c.name.split(',')[0]).join(' + ')} decklist.${extension}` | |
| } | |
| export interface DeckSummary { | |
| total: number | |
| lands: number | |
| /** Sum of the cheapest printing of every card, commanders included. */ | |
| price_usd: number | |
| /** Cards (by quantity) with no known price, left out of price_usd. */ | |
| unpriced: number | |
| } | |
| /** The deck header's numbers, from the cards as they are now: the server's | |
| * `summary` describes the deck it returned, which hand edits change. Same | |
| * math as the server's Completion.to_dict. */ | |
| export function summarize(completion: Completion): DeckSummary { | |
| const everything = [...completion.commanders, ...completion.cards] | |
| const priced = everything.reduce((sum, c) => sum + (c.price ? c.price.usd * c.quantity : 0), 0) | |
| return { | |
| total: everything.reduce((n, c) => n + c.quantity, 0), | |
| lands: completion.cards.reduce((n, c) => n + (c.category === 'Land' ? c.quantity : 0), 0), | |
| price_usd: Math.round(priced * 100) / 100, | |
| unpriced: everything.reduce((n, c) => n + (c.price ? 0 : c.quantity), 0), | |
| } | |
| } | |
| /** Sets how many copies of `name` the deck has, as the user's own card | |
| * (kept by every reroll). Rows of the same card (your basics plus the ones | |
| * the server added) collapse into one; 0 removes it. */ | |
| export function withQuantity(cards: DeckCard[], name: string, quantity: number): DeckCard[] { | |
| const first = cards.find((c) => c.name === name) | |
| const rest = cards.filter((c) => c.name !== name) | |
| if (!first || quantity <= 0) return rest | |
| return [...rest, { ...first, quantity, source: 'input', score: null }] | |
| } | |
| /** Adds one copy of a card as the user's own (locked by the caller). */ | |
| export function withCardAdded(cards: DeckCard[], card: CardInfo): DeckCard[] { | |
| const copies = cards.filter((c) => c.name === card.name).reduce((n, c) => n + c.quantity, 0) | |
| if (copies > 0) return withQuantity(cards, card.name, copies + 1) | |
| return [...cards, { ...card, quantity: 1, source: 'input', score: null, locked: true }] | |
| } | |
| export function withoutCard(cards: DeckCard[], name: string): DeckCard[] { | |
| return cards.filter((c) => c.name !== name) | |
| } | |
| /** How many cards can be locked: the deck minus its commanders (99, or 98 | |
| * with partners), so a deck over 100 can always be cut down to 100 without | |
| * touching a locked card. Mirrors the server's check. */ | |
| export function lockCap(completion: Completion): number { | |
| return DECK_SIZE - completion.commanders.length | |
| } | |
| /** Locked cards, counting copies (5 locked Relentless Rats use 5 of the cap). */ | |
| export function lockedCount(cards: DeckCard[], lockedNames: Set<string>, banned: Set<string>): number { | |
| return cards.reduce((n, c) => n + (isAdjustable(c) && lockedNames.has(c.name) && !banned.has(c.name) ? c.quantity : 0), 0) | |
| } | |