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)
}