Skip to content

Estadísticas

Pablo Martínez López edited this page Oct 21, 2026 · 2 revisions

Estadísticas

English · Español

Las estadísticas del mazo (curva de maná, colores, tipos, funciones, probabilidades de robo, precios, legalidad por formato, problemas de construcción, estimación del bracket de Commander) se calculan por completo en Rust. El diálogo solo pinta un valor DeckStats. El core es puro recuento sobre una lista de StatInput; la capa de la app reúne los datos de Scryfall (primero la caché, la red para lo que falte o esté caducado) y se los pasa.

Código fuente: src-tauri/crates/proxy-core/src/stats/ (mod.rs, funciones.rs, mana.rs, prob.rs), src-tauri/src/commands/stats.rs, src/features/stats/ y la regla cuentan en src-tauri/crates/proxy-core/src/model.rs.

Resumen de estadísticas

Arquitectura

flowchart LR
    UI["StatsDialog (7 pestañas)"] -->|"llama a deck_stats(cards, kind)"| CMD["commands/stats.rs: calcular"]
    CMD --> CU["model::cuentan(cards)"]
    CU --> CACHE["caché de Scryfall (SQLite)"]
    CACHE -->|"falta / más de 24 h / sin texto de reglas"| NET["/cards/collection, 75 por lote"]
    CMD --> IN["Vec<StatInput>"]
    IN --> CORE["proxy_core::stats::stats()"]
    CORE --> DS["DeckStats"]
    DS --> UI
    LIB["biblioteca.rs detalles_de"] -->|"calcular_sin_red (solo caché)"| CMD
Loading

Regla de frontera (Decisión): el core (proxy-core) no tiene Tauri, ni red, ni base de datos. stats() es una función pura (&[StatInput], Option<DeckKind>) -> DeckStats, lo que la hace trivial de probar. Todo lo impuro (lecturas de caché, HTTP, precio alternativo) está en commands/stats.rs.

La regla cuentan y setAside

model::cuentan(cards) -> (Vec<&DeckCard>, Apartadas) decide qué copias físicas cuentan para el tamaño del mazo, los límites de copias, las estadísticas y la lista exportada del mazo. Las marcas salen de DeckCard.marca (MarcaCarta::{Variante, Opcional, Banquillo}; se ponen en el menú de la vista previa, véase Vista previa en vivo). Todas las cartas marcadas se imprimen igualmente; la marca solo cambia lo que se cuenta.

Marca ¿Cuenta?
ninguna sí
Opcional (opcional) no (opcionales)
Banquillo (banquillo) no (banquillo)
Variante (variante de diseño) Si existe sin marca una copia de la misma carta (nombre sin distinguir mayúsculas y recortado): no cuenta ninguna de las variantes. Si todas las copias de ese nombre son variantes: cuenta exactamente una (la primera). El resto va a variantes.

Apartadas { variantes, opcionales, banquillo } (+ total()) se devuelve y calcular lo copia en DeckStats.setAside (s.set_aside = apartadas), porque el propio stats() solo ve la entrada ya filtrada y deja set_aside en su valor por defecto. La pestaña de resumen muestra un aviso cuando setAside no es cero. El mismo cuentan lo usa proxy_core::decklist::exportar para que la lista exportada coincida con las estadísticas (Exportación). Test: las_marcadas_no_cuentan en model.rs y las_cartas_marcadas_no_cuentan_para_el_mazo en commands/stats.rs.

Reunir los datos (src-tauri/src/commands/stats.rs)

Puntos de entrada:

  • deck_stats(cards: Vec<DeckCard>, kind: Option<DeckKind>) -> AppResult<DeckStats> (comando de Tauri asíncrono; usa el cliente real de Scryfall).
  • calcular_sin_red(db, cards) (visible en el crate): el mismo cálculo con SinRed, un CardApi cuyas llamadas fallan todas con Network, de modo que solo se usa la caché. La biblioteca de mazos (detalles_de de biblioteca.rs) lo usa para calcular colores, precio, etc. sin red y deprisa (Biblioteca de mazos).
  • calcular(db, api, cards, kind) es genérica sobre CardApi para que los tests inyecten un Scryfall falso.

Pasos de calcular:

  1. cuentan(cards).
  2. Para cada carta contada que enlaza con una carta de Scryfall (id_scryfall: las fuentes Scryfall y Token, y Custom con un scryfallId; las cartas en blanco y las propias sin vincular no lo tienen), la busca en la caché (scryfall::cache::leer). Una carta cacheada está «fresca» si se descargó hace menos de 24 h (PRECIOS_MS) Y keywords.is_some() (una señal de que la entrada se guardó después de que la app empezara a conservar el texto de reglas; las entradas más antiguas se vuelven a pedir). Los ids caducados o ausentes van a una lista de peticiones; mientras tanto se siguen usando los datos caducados.
  3. Pide la lista por /cards/collection en lotes de 75 (POR_LOTE), guardando los resultados de vuelta en la caché. Ante cualquier error (sin conexión) el bucle se detiene con un log de aviso y las estadísticas se calculan con lo que tiene la caché: el diálogo nunca falla por culpa de la red.
  4. Precio alternativo inglés. Scryfall no da precios a las impresiones que no están en inglés. Para cada carta que no esté en inglés y no tenga ni EUR ni USD (los precios foil cuentan, véase más abajo) y tenga un set conocido, busca la impresión inglesa con el mismo set y número de coleccionista (cache::leer_impresion(set, number, "en"), fresca dentro de 24 h, si no se pide por Identifier::SetNumber). Entonces entrada() usa los precios de la impresión inglesa solo cuando la propia carta no tiene ninguno (test con_precio_propio_no_se_usa_el_ingles).
  5. Elección del precio (precios()): eur y si no eur_foil, usd y si no usd_foil (algunas impresiones existen solo en foil).
  6. Construye un StatInput por cada carta contada que no sea en blanco: a partir de la carta de Scryfall (entrada(): colores de la carta o de su primera cara, texto_completo() como texto de reglas, coste() como coste de maná de la cara frontal, poder/resistencia con la primera cara como alternativa, game_changer, legalidades...). Fichas: la fuente Token, o una carta vinculada cuya línea de tipo empieza por Token, o una carta propia de una fuente externa con una línea de tipo de ficha. Las cartas sin datos de Scryfall se convierten en un StatInput vacío con solo id, name e is_token: se cuentan como «desconocidas».
  7. stats(&entrada, kind); después se rellena set_aside.

El core: stats() (proxy-core/src/stats/mod.rs)

Entrada y salida

StatInput (no se exporta a TS) contiene una copia: 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 (exportado con #[ts(export)], camelCase) es una estructura grande; los grupos:

Grupo Campos
Clase y tamaño kind, detectedKind, setAside, cards (copias con datos, sin fichas), deckSize (todas las copias que no son ficha, incluidas las desconocidas), lands, nonlands, tokens, unknown, unknownCards, tokenCards
Curva averageCmc (sin tierras), averageCmcWithLands, medianCmc, totalManaValue, curve: TramoCurva[8] (cmc 0..7+, byType en el orden de TIPOS_CURVA, permanents, spells)
Colores colors: ColorMana[6] (W U B R G C: cards, pips, landSources, otherSources), colorIdentity, colorCount (0..5 colores, sin tierras), xSpells
Tipos types, supertypes, subtypes (los 30 primeros), keywords (los 30 primeros), creatures { count, averagePower, averageToughness, power[0..7+] }, rarity, sets, oldest, newest
Funciones roles: Rol[14] (key, count, cards)
Precios prices { totalEur, totalUsd, withoutEur, withoutUsd, cards, byType, buckets } (tramos <1, 1-5, 5-20, 20-50, 50+ en EUR)
Probabilidad probability { deckSize, lands, openingLands[0..7], expectedLands, landDrops[1..8], cards, roles }
Construcción recommendedLands, cheapRampDraw, legality[9 formats], problems, bracket

Reglas que conviene conocer

  • Desconocida frente a ficha frente a contada. Una ficha va a tokenCards y se excluye de todo lo demás. Una carta que no es ficha y no tiene ni línea de tipo ni cmc es unknown (cuenta para deckSize y las probabilidades, para nada más).
  • Tipo principal (tipo_principal): mira solo la cara frontal y lo anterior a la raya larga, y toma la PRIMERA coincidencia en este orden de prioridad: Creature, Planeswalker, Battle, Instant, Sorcery, Artifact, Enchantment, Land, y si no «Other». Consecuencias visibles en el código: una «Artifact Creature» es una Creature; una «Artifact Land» se clasifica como Artifact (así que es_tierra, que comprueba tipo_principal == "Land", no la cuenta como tierra); una «Land Creature» es una Creature.
  • Curva excluye las tierras; los costes de 7 o más comparten el último tramo; los permanentes son todo lo que no es Instant ni Sorcery.
  • Pips (mana.rs leer): cuenta los símbolos de color del coste de la cara frontal; el híbrido {U/B} cuenta para ambos colores, el fyrexiano {G/P} para su color; {X} activa un indicador (xSpells). Las cartas partidas {1}{R} // {1}{U} cuentan ambas mitades.
  • Fuentes de maná: a partir de produced_mana, las tierras van a landSources y las que no son tierras a otherSources. La pestaña «Maná» avisa cuando la proporción de pips de un color supera su proporción de fuentes de tierra en más de 10 puntos (AVISO_FUENTES = 0.1 en src/features/stats/mana.tsx).
  • Clase de mazo (deducir): menos de 50 cartas que no son ficha es Limited; de 90 a 110 cartas donde se repiten como mucho dos nombres que no sean tierras básicas ni de «cualquier número de copias» es Commander; en otro caso, Constructed. El usuario puede sobrescribir la clase detectada en el diálogo (argumento kind); detectedKind siempre informa de la deducida.
  • Tierras recomendadas (tierras_recomendadas): fórmula al estilo Karsten. Commander: 31.42 + 3.13 * avgCmc - 0.28 * cheap; Constructed (60): 19.59 + 1.90 * avgCmc - 0.28 * cheap; Limited: el valor de 60 cartas escalado por 40/60. cheap = cartas que no son tierra con cmc <= 2 que son rampa o robo (cheapRampDraw). Redondeado a un decimal.
  • Problemas: copies (más de 1 copia en Commander o 4 en Constructed, excluyendo las tierras básicas y las cartas con texto «any number of cards named» o «up to», sin_limite_de_copias); size (Commander debe ser exactamente 100, Constructed al menos 60, Limited al menos 40).
  • Legalidad: para los 9 formatos de FORMATOS (standard, pioneer, modern, legacy, vintage, commander, pauper, historic, brawl) una carta es un problema salvo que su estado sea legal o restricted. Un formato es legal solo si hay cartas y ninguna infractora; un mazo vacío no es legal en nada (un_deck_vacio_no_es_legal_en_nada).
  • Precios: el precio por nombre se toma de la primera copia; los totales suman cada copia; la lista se ordena por count * price (EUR, y si no USD). Redondeado a céntimos.
  • Bracket (solo Commander, Bracket): nivel 4 si hay alguna denegación masiva de tierras o más de 3 Game Changers; si no, nivel 3 si hay algún Game Changer, 2 o más cartas de turno extra o más de 3 tutores; si no, 2. No puede detectar combos, y el bracket 1 depende de la temática, así que nunca se devuelve. Los textos de la pestaña en src/features/stats/textos.ts (BRACKETS) lo dicen.

Funciones (funciones.rs)

14 claves en FUNCIONES: ramp, draw, removal, wipe, counter, tutor, protection, recursion, tokens, anthem, lifegain, mill, extraTurn, landDenial. Una carta puede tener varias. Se deducen mediante expresiones regulares sobre el texto de reglas en inglés (FUENTES, compiladas una vez en un LazyLock), después de que normalizar pase a minúsculas y quite el texto recordatorio entre paréntesis. Es una heurística: orientativa, como una persona clasificando a ojo.

  • ramp: cartas que no son tierra y producen maná (produced_mana no vacío) o coinciden con un patrón de rampa (tierra buscada al campo de batalla, jugada de tierra extra, Treasure, reducción de coste). Las tierras nunca son rampa.
  • removal y wipe usan alguno_ajeno: una coincidencia con el grupo con nombre tuyo («you control») se ignora, así que «exile target creature you control» (un parpadeo) no es eliminación.
  • tutor (es_tutor): «search your library for» algo que no sea un tipo de tierra.
  • lifegain/mill también aceptan las palabras clave Lifelink/Mill.
  • todos_los_patrones_compilan es un test de protección: los patrones que no compilan se descartarían en silencio (compilar usa filter_map), así que mantén ese test en verde al añadir patrones.

Probabilidades (prob.rs)

Distribución hipergeométrica con log-binomiales (ln_comb). exacta(total, successes, n, k) es P(X = k); al_menos es P(X >= k); por_vistas(total, copies) es P(al menos una copia) tras ver 7, 8, ... 7 + ROBOS (12) cartas; valores redondeados a 4 decimales (redondear). Probabilidades:

  • openingLands[k]: P(exactamente k tierras en las 7 iniciales), k = 0..7. expectedLands = 7 * lands / deckSize.
  • landDrops[t] para el turno t = 1..8: P(al menos t tierras en el turno t): play (jugando primero) ve 7 + t - 1 cartas, draw ve 7 + t.
  • cards y roles: curvas bySeen por nombre de carta (ordenadas por nombre) y por función con al menos una carta. La calculadora del diálogo indexa bySeen por turno: indice(turn, onThePlay) = 0 para la mano inicial, turn - 1 jugando primero, turn robando primero.
  • deckSize aquí es cada copia que no es ficha (incluidas las desconocidas), así que las cartas propias sin vincular siguen acortando bien las probabilidades. No se modela ningún mulligan (la interfaz lo dice).

Pantalla: src/features/stats/

StatsDialog (stats-dialog.tsx) se abre cuando useUi.dialogo.tipo === "estadisticas" (botón de la barra de herramientas de la vista previa, desactivado con un mazo vacío). Al abrirse (y siempre que cambien las cartas del mazo o la clase elegida) llama a deck_stats con la lista de cartas del store del mazo; una bandera vigente descarta las respuestas tardías. Muestra un spinner (y sigue mostrando el resultado anterior mientras se actualiza), un selector de clase de mazo («Automático: », «Commander (100)», «Construido (60)», «Limitado (40)») y un interruptor EUR/USD que solo afecta a la visualización.

Pestaña Fichero Muestra
Resumen resumen.tsx Cifras (cartas, tierras frente a las recomendadas, coste medio, valor de maná, precio, bracket o probabilidad de tierras iniciales), aviso de setAside, lista de cartas desconocidas, problemas de copias/tamaño, curva de maná (por tipo o permanentes frente a hechizos), colores, fichas usadas
Maná mana.tsx Pips frente a fuentes de tierra por color, aviso de pocas fuentes, distribución del número de colores
Tipos tipos.tsx Tipos, rareza, criaturas (poder/resistencia medios, histograma de poder), subtipos, palabras clave, sets y rango de fechas
Funciones funciones.tsx Recuento por función con listas de cartas desplegables
Probabilidades probabilidades.tsx Distribución de tierras de la mano inicial, jugadas de tierra por turno, calculadora de robo (función o carta, jugando primero/robando primero, turno)
Precios precios.tsx Total, media, la más cara, sin precio, por tipo, tramos de precio, tabla de cartas ordenada por precio total
Legalidad legalidad.tsx Legalidad por formato con infractoras, problemas de construcción, detalles del bracket de Commander

Las piezas compartidas están en piezas.tsx (Cifra, Bloque, Lista, ListaCartas). Los precios los formatea formatoMoneda(lang, moneda) (textos.ts) y las fechas de salida de tipos.tsx, toLocaleDateString, los dos con localeDe(lang) (src/lib/idiomas.ts), así que siguen el idioma de la interfaz. ListaCartas y la tabla de precios tienen un botón «seleccionar estas cartas en el deck»: seleccionarEnDeck(ids, text) selecciona esas copias en la vista previa, cierra el diálogo y lo anuncia (por eso CartaCuenta/CartaPrecio llevan los ids de las copias). textos.ts contiene los nombres de visualización y los colores (COLORES, TIPOS, RAREZAS, FUNCIONES, CLASES_MAZO, BRACKETS, formatoMoneda) y todas las cadenas pasan por t(); Rust devuelve claves, nunca prosa. Capturas: docs/presentacion/estadisticas-{resumen,funciones,probabilidades,precios}.jpg.

Tests

  • Core: tests de stats/mod.rs (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: tests de commands/stats.rs con un Scryfall falso (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.

Por dónde empezar para ampliarlo

  • Nueva estadística: añade el campo a DeckStats (con el derive TS), calcúlalo en stats() con un test, ejecuta pnpm bindings, añade la interfaz en la pestaña adecuada y las cadenas (fuente en español en t(), traducciones en src/i18n/<código>/ de cada idioma).
  • Nueva categoría de función: añade la clave a FUNCIONES, una lista de patrones a FUENTES/Patrones, una entrada pares en funciones() y una entrada en FUNCIONES de textos.ts.
  • Nuevo dato necesario de Scryfall: añádelo a StatInput, rellénalo en entrada() y recuerda que las cartas cacheadas que no lo tengan deben volver a pedirse (véase la señal de frescura keywords.is_some()).

Véase también

Documentation · Documentación

Language: English · Español

Use and build · Uso y desarrollo

Architecture · Arquitectura

Data · Datos

Getting cards · Conseguir cartas

Layout, images, PDF · Maquetación, imagen y PDF

Card designer · Diseñador de cartas

Game table · Mesa de juego

Frontend · Frontend

Reference · Referencia

Clone this wiki locally