Spaces:
Running
Running
File size: 4,981 Bytes
8538f10 2fd2eab 15c3f7a 8538f10 15c3f7a 8538f10 08b6c63 8538f10 15c3f7a 8538f10 15c3f7a 8538f10 08b6c63 8538f10 08b6c63 8538f10 08b6c63 8538f10 | 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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | // Typed client for the FastAPI server (server/app.py).
export type CardSource = 'input' | 'synergy' | 'staple' | 'basic'
/** Real-deck-frequency classification, independent of `source` -- the
* synergy phase is unrestricted and routinely picks real staples on their
* own merit, so a card can be source: 'synergy' and tier: 'staple' at
* once. null for basics (outside the tokenizer vocab, so untiered). */
export type CardTier = 'staple' | 'mid' | 'long_tail'
/** What it takes to show a card: its row (cost) and hover preview (text,
* stats, price). Deck cards and search results both carry it. */
export interface CardInfo {
name: string
type_line: string
mana_cost: string
cmc: number
category: string
oracle_text: string
tier: CardTier | null
/** Per-face power/toughness, loyalty, or defense, e.g. ["4/3", null]. */
stats: (string | null)[]
/** Cheapest paper printing, if Scryfall has a price for it. */
price: CardPrice | null
color_identity: string[]
/** A deck may hold more than one copy (basics, Relentless Rats, ...). */
multiples_allowed: boolean
}
export interface DeckCard extends CardInfo {
quantity: number
source: CardSource
score: number | null
locked: boolean
}
export interface CardPrice {
usd: number
set: string
finish: 'nonfoil' | 'foil' | 'etched'
}
export interface Completion {
commanders: DeckCard[]
cards: DeckCard[]
warnings: string[]
summary: {
total: number
lands: number
target_lands: number
added: number
basics_added: 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
}
/** When the price data was built (ISO time), or null if there is none yet. */
prices_updated: string | null
}
/** A search result: a full CardInfo, so it can be shown anywhere a deck card is. */
export interface CardSummary extends CardInfo {
banned: boolean
}
export interface SearchOptions {
commanderOnly?: boolean
/** Only cards within this color identity, e.g. "BG" ("" = colorless). */
colors?: string
/** Also offer basic lands (they're not otherwise searchable). */
basics?: boolean
}
export interface UnresolvedCard {
name: string
line: number | null // null = a commander field, not a decklist line
suggestions: string[]
}
export interface LockedCard {
name: string
quantity: number
source: CardSource
/** False: kept by this request without being locked (your basics, or the
* rest of a deck over 100), so it can be cut. Default true. */
locked?: boolean
}
export interface CompleteRequest {
commanders: string[]
decklist: string
target_lands: number | null
prune: boolean
/** Cards kept from the current deck when rerolling. */
locked?: LockedCard[]
/** Cards the model must never suggest (the user's ban list). */
exclude?: string[]
}
export class ApiError extends Error {
unresolved: UnresolvedCard[]
constructor(message: string, unresolved: UnresolvedCard[] = []) {
super(message)
this.unresolved = unresolved
}
}
async function parseError(res: Response): Promise<ApiError> {
try {
const body = await res.json()
if (typeof body.error === 'string') return new ApiError(body.error, body.unresolved ?? [])
// FastAPI request-validation errors: {detail: [{msg, loc}, ...]}
if (Array.isArray(body.detail)) return new ApiError(body.detail.map((d: { msg: string }) => d.msg).join('; '))
} catch {
// fall through
}
return new ApiError(`Request failed (${res.status}).`)
}
export async function completeDeck(req: CompleteRequest, signal?: AbortSignal): Promise<Completion> {
const res = await fetch('/api/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(req),
signal,
})
if (!res.ok) throw await parseError(res)
return res.json()
}
export interface Config {
default_target_lands: number
min_target_lands: number
/** When the price data was built (ISO time), or null if there is none yet. */
prices_updated: string | null
}
export async function getConfig(signal?: AbortSignal): Promise<Config> {
const res = await fetch('/api/config', { signal })
if (!res.ok) throw await parseError(res)
return res.json()
}
export async function searchCards(q: string, options: SearchOptions = {}, signal?: AbortSignal): Promise<CardSummary[]> {
const params = new URLSearchParams({ q, commander: String(options.commanderOnly ?? false), limit: '8' })
if (options.colors !== undefined) params.set('colors', options.colors)
if (options.basics) params.set('basics', 'true')
const res = await fetch(`/api/cards?${params}`, { signal })
if (!res.ok) throw await parseError(res)
return (await res.json()).cards
}
export function scryfallPageUrl(name: string): string {
return `https://scryfall.com/search?q=${encodeURIComponent(`!"${name}"`)}`
}
|