Repository navigation
Statistics
English · Español
Deck statistics (mana curve, colours, types, functions, draw probabilities, prices, format legality, construction problems, Commander bracket estimate) are computed entirely in Rust. The dialog only paints a DeckStats value. The core is pure counting over a list of StatInput; the app layer gathers Scryfall data (cache first, network for what is missing or stale) and feeds it in.
Source: src-tauri/crates/proxy-core/src/stats/ (mod.rs, funciones.rs, mana.rs, prob.rs), src-tauri/src/commands/stats.rs, src/features/stats/, and the cuentan rule in src-tauri/crates/proxy-core/src/model.rs.

flowchart LR
UI["StatsDialog (7 tabs)"] -->|"call deck_stats(cards, kind)"| CMD["commands/stats.rs: calcular"]
CMD --> CU["model::cuentan(cards)"]
CU --> CACHE["scryfall cache (SQLite)"]
CACHE -->|"missing / older than 24 h / no oracle text"| NET["/cards/collection, 75 per batch"]
CMD --> IN["Vec<StatInput>"]
IN --> CORE["proxy_core::stats::stats()"]
CORE --> DS["DeckStats"]
DS --> UI
LIB["biblioteca.rs detalles_de"] -->|"calcular_sin_red (cache only)"| CMD
Boundary rule (Decision): the core (proxy-core) has no Tauri, no network, no database. stats() is a pure function (&[StatInput], Option<DeckKind>) -> DeckStats, which makes it trivially testable. Everything impure (cache reads, HTTP, price fallback) is in commands/stats.rs.
model::cuentan(cards) -> (Vec<&DeckCard>, Apartadas) decides which physical copies count for deck size, copy limits, statistics and the exported deck list. Marks come from DeckCard.marca (MarcaCarta::{Variante, Opcional, Banquillo}; set in the preview menu, see Live Preview). All marked cards still print; the mark only changes what is counted.
| Mark | Counted? |
|---|---|
| none | yes |
Opcional (optional) |
no (opcionales) |
Banquillo (sideboard) |
no (banquillo) |
Variante (design variant) |
If a copy of the same card (case-insensitive trimmed name) exists without a mark: none of the variants count. If all copies of that name are variants: exactly one counts (the first). The rest go to variantes. |
Apartadas { variantes, opcionales, banquillo } (+ total()) is returned and copied into DeckStats.setAside by calcular (s.set_aside = apartadas), because stats() itself only sees the already-filtered input and leaves set_aside at its default. The summary tab shows a notice when setAside is non-zero. The same cuentan is used by proxy_core::decklist::exportar so the exported list matches the statistics (Export). Test: las_marcadas_no_cuentan in model.rs, and las_cartas_marcadas_no_cuentan_para_el_mazo in commands/stats.rs.
Entry points:
-
deck_stats(cards: Vec<DeckCard>, kind: Option<DeckKind>) -> AppResult<DeckStats>(async Tauri command; uses the real Scryfall client). -
calcular_sin_red(db, cards)(crate-visible): same computation withSinRed, aCardApiwhose every call fails withNetwork, so only the cache is used. The deck library (biblioteca.rsdetalles_de) uses this to compute colours, price and so on without network and quickly (Deck Library). -
calcular(db, api, cards, kind)is generic overCardApiso tests inject a fake Scryfall.
Steps in calcular:
-
cuentan(cards). - For each counted card that links to a Scryfall card (
id_scryfall:ScryfallandTokensources, andCustomwith ascryfallId; blanks and unlinked customs have none), look it up in the cache (scryfall::cache::leer). A cached card is "fresh" if fetched less than 24 h ago (PRECIOS_MS) ANDkeywords.is_some()(a marker that the entry was stored after the app started keeping oracle text; older entries are refetched). Stale or missing ids go into a request list; stale data is still used meanwhile. - Request the list through
/cards/collectionin batches of 75 (POR_LOTE), saving results back to the cache. On any error (offline) the loop stops with a warning log and the stats are computed from what the cache has: the dialog never fails because of the network. -
English price fallback. Scryfall gives no prices to non-English printings. For each non-English card with neither EUR nor USD (foil prices count, see below) and a known set, look up the English printing with the same set and collector number (
cache::leer_impresion(set, number, "en"), fresh within 24 h, otherwise requested byIdentifier::SetNumber).entrada()then uses the English printing's prices only when the card itself has none (testcon_precio_propio_no_se_usa_el_ingles). - Price choice (
precios()):eurelseeur_foil,usdelseusd_foil(some printings exist only in foil). - Build one
StatInputper counted card that is not a blank: from the Scryfall card (entrada(): colours from the card or its first face,texto_completo()as oracle text,coste()as mana cost of the front face, power/toughness fallback to the first face,game_changer, legalities...). Tokens:Tokensource, or a linked card whose type line starts withToken, or a custom card from an external source with a token type line. Cards without Scryfall data become an emptyStatInputwith onlyid,nameandis_token: counted as "unknown". -
stats(&entrada, kind); then setset_aside.
StatInput (not exported to TS) holds one copy: id, name, cmc, mana_cost, colors, color_identity, type_line, oracle_text, keywords, produced_mana, power, toughness, rarity, set, set_name, released_at, price_eur, price_usd, legalities, game_changer, is_token. DeckStats (exported with #[ts(export)], camelCase) is a large struct; the groups:
| Group | Fields |
|---|---|
| Kind and size |
kind, detectedKind, setAside, cards (copies with data, no tokens), deckSize (all non-token copies, including unknown), lands, nonlands, tokens, unknown, unknownCards, tokenCards
|
| Curve |
averageCmc (no lands), averageCmcWithLands, medianCmc, totalManaValue, curve: TramoCurva[8] (cmc 0..7+, byType in TIPOS_CURVA order, permanents, spells) |
| Colours |
colors: ColorMana[6] (W U B R G C: cards, pips, landSources, otherSources), colorIdentity, colorCount (0..5 colours, non-lands), xSpells
|
| Types |
types, supertypes, subtypes (top 30), keywords (top 30), creatures { count, averagePower, averageToughness, power[0..7+] }, rarity, sets, oldest, newest
|
| Functions |
roles: Rol[14] (key, count, cards) |
| Prices |
prices { totalEur, totalUsd, withoutEur, withoutUsd, cards, byType, buckets } (buckets <1, 1-5, 5-20, 20-50, 50+ in EUR) |
| Probability | probability { deckSize, lands, openingLands[0..7], expectedLands, landDrops[1..8], cards, roles } |
| Construction |
recommendedLands, cheapRampDraw, legality[9 formats], problems, bracket
|
-
Unknown vs token vs counted. A token goes to
tokenCardsand is excluded from everything else. A non-token with neither type line nor cmc isunknown(counts towardsdeckSizeand probabilities, nothing else). -
Main type (
tipo_principal): looks only at the front face and before the em dash, and takes the FIRST match in this priority order: Creature, Planeswalker, Battle, Instant, Sorcery, Artifact, Enchantment, Land, else "Other". Consequences visible in the code: "Artifact Creature" is a Creature; an "Artifact Land" is classified Artifact (so it is not counted as a land byes_tierra, which teststipo_principal == "Land"); a "Land Creature" is a Creature. - Curve excludes lands; costs of 7 or more share the last bucket; permanents are everything that is not Instant or Sorcery.
-
Pips (
mana.rsleer): counts coloured symbols of the front-face cost; hybrid{U/B}counts for both colours, Phyrexian{G/P}for its colour;{X}sets a flag (xSpells). Split cards{1}{R} // {1}{U}count both halves. -
Mana sources: from
produced_mana, lands go tolandSources, non-lands tootherSources. The "Mana" tab warns when a colour's share of pips exceeds its share of land sources by more than 10 points (AVISO_FUENTES = 0.1insrc/features/stats/mana.tsx). -
Deck kind (
deducir): fewer than 50 non-token cards is Limited; 90 to 110 cards where at most two non-basic, non-"any number of copies" names repeat is Commander; otherwise Constructed. The user can override the detected kind in the dialog (kindargument);detectedKindalways reports the deduced one. -
Recommended lands (
tierras_recomendadas): Karsten-style formula. Commander:31.42 + 3.13 * avgCmc - 0.28 * cheap; Constructed (60):19.59 + 1.90 * avgCmc - 0.28 * cheap; Limited: the 60-card value scaled by 40/60.cheap= non-land cards with cmc <= 2 that are ramp or draw (cheapRampDraw). Rounded to one decimal. -
Problems:
copies(more than 1 copy in Commander or 4 in Constructed, excluding basic lands and cards with "any number of cards named" or "up to" text,sin_limite_de_copias);size(Commander must be exactly 100, Constructed at least 60, Limited at least 40). -
Legality: for the 9 formats in
FORMATOS(standard, pioneer, modern, legacy, vintage, commander, pauper, historic, brawl) a card is a problem unless its status islegalorrestricted. A format is legal only if there are cards and no offenders; an empty deck is legal in nothing (un_deck_vacio_no_es_legal_en_nada). -
Prices: per-name price taken from the first copy; totals sum every copy; list sorted by
count * price(EUR, else USD). Rounded to cents. -
Bracket (Commander only,
Bracket): level 4 if any mass land denial or more than 3 Game Changers; else level 3 if any Game Changer, 2 or more extra-turn cards, or more than 3 tutors; else 2. It cannot detect combos, and bracket 1 depends on theme so it is never returned. The tab texts insrc/features/stats/textos.ts(BRACKETS) say so.
14 keys in FUNCIONES: ramp, draw, removal, wipe, counter, tutor, protection, recursion, tokens, anthem, lifegain, mill, extraTurn, landDenial. A card can have several. They are deduced by regular expressions on the English oracle text (FUENTES, compiled once in a LazyLock), after normalizar lowercases and strips reminder text in parentheses. It is a heuristic: orientative, like a person classifying by eye.
-
ramp: non-lands that produce mana (produced_mananot empty) or match a ramp pattern (fetch land to battlefield, extra land drop, Treasure, cost reduction). Lands are never ramp. -
removalandwipeusealguno_ajeno: a match with the named grouptuyo("you control") is ignored, so "exile target creature you control" (a blink) is not removal. -
tutor(es_tutor): "search your library for" something that is not a land type. -
lifegain/millalso accept the keywordsLifelink/Mill. -
todos_los_patrones_compilanis a test guard: patterns that fail to compile would be silently dropped (compilarusesfilter_map), so keep that test green when adding patterns.
Hypergeometric distribution with log-binomials (ln_comb). exacta(total, successes, n, k) is P(X = k); al_menos is P(X >= k); por_vistas(total, copies) is P(at least one copy) after 7, 8, ... 7 + ROBOS (12) cards seen; values rounded to 4 decimals (redondear). Probabilidades:
-
openingLands[k]: P(exactly k lands in the opening 7), k = 0..7.expectedLands = 7 * lands / deckSize. -
landDrops[t]for turn t = 1..8: P(at least t lands by turn t):play(on the play) sees7 + t - 1cards,drawsees7 + t. -
cardsandroles:bySeencurves per card name (sorted by name) and per function with at least one card. The calculator in the dialog indexesbySeenby turn:indice(turn, onThePlay)= 0 for the opening hand,turn - 1on the play,turnon the draw. -
deckSizehere is every non-token copy (including unknown ones), so unlinked custom cards still shorten the odds correctly. No mulligan is modelled (the UI says so).
StatsDialog (stats-dialog.tsx) opens when useUi.dialogo.tipo === "estadisticas" (button in the preview toolbar, disabled for an empty deck). On open (and whenever the deck's cards or the chosen kind change) it calls deck_stats with the card list from the deck store; a vigente flag discards late answers. It shows a spinner (and keeps showing the previous result while refreshing), a deck-kind selector ("Automatic: ", Commander 100, Constructed 60, Limited 40), and a EUR/USD toggle that only affects display.
| Tab | File | Shows |
|---|---|---|
| Summary | resumen.tsx |
Figures (cards, lands vs recommended, average cost, mana value, price, bracket or opening-land odds), setAside notice, unknown-card list, copy/size problems, mana curve (by type or permanents vs spells), colours, tokens used |
| Mana | mana.tsx |
Pips vs land sources per colour, low-sources warning, colour-count distribution |
| Types | tipos.tsx |
Types, rarity, creatures (average power/toughness, power histogram), subtypes, keywords, sets and date range |
| Functions | funciones.tsx |
Count per function with expandable card lists |
| Probabilities | probabilidades.tsx |
Opening-hand land distribution, land drops by turn, draw calculator (function or card, play/draw, turn) |
| Prices | precios.tsx |
Total, average, most expensive, without price, by type, price buckets, card table sorted by total price |
| Legality | legalidad.tsx |
Per-format legality with offenders, construction problems, Commander bracket details |
Shared pieces are in piezas.tsx (Cifra, Bloque, Lista, ListaCartas). Prices are formatted by formatoMoneda(lang, moneda) (textos.ts) and the release dates in tipos.tsx by toLocaleDateString, both with localeDe(lang) (src/lib/idiomas.ts), so they follow the interface language. ListaCartas and the price table have a "select these cards in the deck" button: seleccionarEnDeck(ids, text) selects those copies in the preview, closes the dialog and announces it (this is why CartaCuenta/CartaPrecio carry copy ids). textos.ts holds the display names and colours (COLORES, TIPOS, RAREZAS, FUNCIONES, CLASES_MAZO, BRACKETS, formatoMoneda) and all strings go through t(); Rust returns keys, never prose. Screenshots: docs/presentacion/en/estadisticas-{resumen,funciones,probabilidades,precios}.jpg.
- Core:
stats/mod.rstests (cuenta_curva_colores_tipos_y_precio,probabilidades_del_deck,la_clase_de_mazo_y_sus_problemas,el_bracket,las_tierras_de_karsten,el_tipo_principal,un_deck_vacio_no_es_legal_en_nada),funciones.rs(rampa,robo_eliminacion_y_barrido,tutores_y_lo_demas,casos_de_un_deck_real,todos_los_patrones_compilan,copias_sin_limite),prob.rs(la_hipergeometrica,por_vistas_crece,mas_cartas_de_las_que_hay),mana.rs(lee_los_simbolos). - App:
commands/stats.rstests with a fake Scryfall (pide_de_una_vez_solo_lo_que_no_esta_fresco,los_precios_viejos_y_la_cache_de_antes_se_vuelven_a_pedir,sin_red_se_cuenta_con_la_cache,mas_de_setenta_y_cinco_cartas_van_en_varios_lotes,una_impresion_sin_precio_usa_el_de_la_inglesa,fichas_propias_y_huecos,las_cartas_marcadas_no_cuentan_para_el_mazo,un_deck_vacio_no_pide_nada). - Frontend:
src/features/stats/__tests__/textos.test.ts.
- New statistic: add the field to
DeckStats(withTSderive), compute it instats()with a test, runpnpm bindings, add the UI in the right tab and the strings (Spanish source int(), translations insrc/i18n/<code>/of every language). - New function category: add the key to
FUNCIONES, a pattern list toFUENTES/Patrones, aparesentry infunciones(), and an entry inFUNCIONESoftextos.ts. - New data needed from Scryfall: add it to
StatInput, fill it inentrada(), and remember that cached cards lacking it must be refetched (see thekeywords.is_some()freshness marker).
Use and build · Uso y desarrollo
- User-Guide · Guía de usuario
- Development-Setup · Preparación del entorno de desarrollo
- Testing-and-CI · Tests y CI
- Releases-and-Auto-Updates · Versiones y actualizaciones automáticas
- Conventions · Convenciones
Architecture · Arquitectura
- Architecture · Arquitectura
- IPC-and-Error-Handling · IPC y manejo de errores
- Rust-App-Layer · Capa de aplicación Rust
Data · Datos
- Deck-Model · Modelo del mazo
- Storage-and-Blobs · Almacenamiento y blobs
- Deck-Package-Format · Formato del paquete de mazo
- Deck-Library · Biblioteca de mazos
Getting cards · Conseguir cartas
- Decklist-Parsing · Parseo de la lista del mazo
- Scryfall-Integration · Integración con Scryfall
- External-Sources · Fuentes externas
- Card-Identification-and-OCR · Identificación de cartas y OCR
Layout, images, PDF · Maquetación, imagen y PDF
- Layout-Engine · Motor de maquetación
- Imaging-and-PDF · Imagen y PDF
- Export · Exportación
- Live-Preview · Vista previa en vivo
- Statistics · Estadísticas
Card designer · Diseñador de cartas
- Designer-Overview · Visión general del diseñador
- Designer-Engine · Motor del diseñador
- Designer-Composition · Composición del diseñador
- Frame-Packs · Packs de marcos
- Designer-App-Layer · Capa de aplicación del diseñador
- Designer-UI · Interfaz del diseñador
- Card-Editor-Dialog · Diálogo Editar carta
Frontend · Frontend
- Frontend-Architecture · Arquitectura del frontend
- State-Stores · Stores de estado
- Internationalization · Internacionalización
- Themes-and-Skins · Temas y skins
- Settings-and-Shell · Ajustes y shell
- Tutorials · Tutoriales
- Decklist-Panel · Panel de la lista del mazo
- Add-Card-and-Variants · Añadir carta y variantes
- Set-Browser · Explorador de sets
Reference · Referencia