nsaroiu's picture
Deploy 41a05c7
08b6c63 verified
Raw History Blame Contribute Delete
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)
}