Spaces:
Running
Running
File size: 5,748 Bytes
08b6c63 8538f10 08b6c63 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 | 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)
}
|