# DeckCraft — Conclusiones sobre la generalización del modelo

Documento de trabajo. Resume el análisis hecho a partir de `reference-structures.html`,
`findings-game-probes.html` y las probes de Dominion / Race for the Galaxy.

**Contexto del producto:** creador de juegos de cartas de mesa para imprimir. La partida
real se juega en vivo; el simulador existe para que el usuario pueda testear el diseño
antes de imprimir. Posible expansión futura a app jugable y/o TCG digital.

---

## 0 · Postura general

**Compromiso total con el formato, pragmatismo con el motor.**

Los cambios de *forma* (esquema) son casi imposibles de revertir una vez haya juegos
autorados por usuarios: obligan a migrar contenido escrito por gente que ya no está.
Los cambios de *ejecución* (motor) se pueden rehacer cuantas veces haga falta sin tocar
los datos.

Por tanto: hacer los cambios de esquema ahora y completos, aunque el motor solo consuma
una fracción. Es lo que ya se hizo con `players[].modifiers`, `action_bindings` y
`card_families` — andamio vacío. La intuición era correcta; falta completar el conjunto.

| Decisión | Coste hoy | Coste con 5.000 juegos autorados |
|---|---|---|
| Valor puede ser expresión | pequeño | migración de todo |
| `trigger` es patrón, no enum | pequeño | migración + semántica ambigua |
| `modifiers[]` separado de `effects[]` | pequeño | reinterpretar efectos ya autorados |
| Efecto lleva `subject`/`from`/`to` | pequeño | catálogo fosilizado |
| Zona tiene `parent` (aunque siempre sea null) | trivial | cambio de esquema |
| Cola de pasos heterogénea | trivial | — |
| Motor que suspende y reanuda | caro | igual de caro |
| Resolutor de modificadores | caro | igual de caro |
| Bots buenos | caro | igual de caro |

**Regla sobre el andamio:** un campo reservado necesita esquema y semántica escritos,
aunque nadie lo lea. Reservar el nombre con la forma equivocada es peor que no reservarlo
(caso actual de `card_families`).

---

## 1 · Valores como expresión

**Problema:** todo número es un literal autorado. Gardens ("1 PV por cada 10 cartas de tu
mazo") no cabe: en `dominion.json` acabó como un `effect_key` con `trigger: on_play`, que
es un error de categoría — la puntuación no es un efecto y no ocurre al jugar la carta.

**Cambio:** tipo unión `valor | expresión` en atributos, costes, cantidades de parámetros,
`copies`, `starts_at`, umbrales.

```json
"attributes": { "vp": 1 }
"attributes": { "vp": { "floor_div": [ { "count_cards": {} }, 10 ] } }
```

**Desbloquea:** G12 (score-from-cards), G4 (Gardens), trackers derivados, costes computados,
cantidades de setup escaladas por número de jugadores (Province 8/12, Curse 10/20/30).

**Motor:** extender el evaluador que ya existe para `ending.when`.

**Aviso:** la validación cambia de naturaleza. `validateCardAgainstGame()` pasa de
comprobar rangos a comprobar tipos y existencia de referencias en expresiones. Es un
componente nuevo, no una extensión — contarlo en el esfuerzo.

**Es innegociable:** sin esto los juegos terminan 0–0 y el simulador no puede decir quién gana.

---

## 2 · Selector de sujeto en el efecto

**Problema:** el "a quién" está soldado dentro del nombre del efecto (`_to_self`,
`_to_opponent`). 5 verbos × 6 alcances = 30 entradas de catálogo. Crece multiplicando.

**Matiz importante:** el parámetro que falta no es la zona, es **de qué jugador** es esa
zona. `draw_to_self` y `draw_to_opponent` no se diferencian en la zona.

**Cambio:** bloque de destino en el efecto de la carta.

```json
{
  "effect_key": "move_cards",
  "trigger": "on_play",
  "parameters": { "amount": 1 },
  "subject": { "who": "each_opponent" },
  "from": "draw_pile",
  "to": "hand"
}
```

El catálogo sobrevive como **presets que se materializan** en esta forma — mismo patrón
que ya se usa con los subtipos de carta.

**Terminología:** las cartas tienen *effects*, no *actions*. `actions` son los verbos de
juego que el bot elige en una fase. Son dos catálogos distintos; esto consolida el de
efectos.

**Desbloquea:** G2 (Council Room: "cada rival roba 1" = verbo + selector).
No basta para Militia — eso necesita el punto 3.

---

## 3 · La decisión como estructura

**Problema:** el motor ejecuta los efectos de principio a fin sin poder preguntar nada a
nadie. "Cada rival descarta hasta quedarse con 3" es imposible, no por el targeting sino
porque no hay forma de detenerse a que alguien elija.

**Ya existe media pieza:** `params[].source: design | player` en el catálogo de efectos.
Falta que `player` describa *qué* se elige y *quién* elige.

**Cambio en la carta** — la spec de selección:

```json
"parameters": {
  "cards": {
    "source": "player",
    "chooser": "subject",
    "from": "hand",
    "count": { "expr": "size_of(hand) - 3" },
    "filter": null,
    "optional": false
  }
}
```

Cinco campos cubren: descartar hasta N, pagar eligiendo cartas, elegir objetivo, robar y
quedarse con M, cartas modales, elecciones del reparto inicial.

**Cambio en el runtime:**
- `pending_decisions[]` en el Game object
- entradas de tipo `decision` en `history`
- estado `awaiting_decision` en `flow`

**Rebaja por el contexto de producto:** si el simulador es solo bots, una decisión es una
llamada a la política del bot en línea — **no hace falta suspender ni reanudar nada**. Se
implementa la estructura (barata) y se aplaza el motor (caro). Mantener el diseño abierto
para un futuro modo "jugar contra bots" con humano.

**Crítico:** hoy el replay es determinista solo con `seed`. En cuanto haya decisiones,
depende también del log de decisiones. Si no se guarda, se pierde la reproducibilidad que
ya se tiene. Con un TCG competitivo detrás, ese historial deja de ser comodidad de
depuración y pasa a ser el mecanismo de resolución de disputas.

---

## 4 · El flujo como cola mutable de pasos

**Problema:** el turno es una lista fija de fases que se ejecuta entera. Nada puede
insertar nada.

**Dos correcciones a la formulación inicial ("crear la pila al inicio del turno"):**

1. No basta con crearla al inicio del turno — tiene que ser modificable **durante** la
   resolución (una carta puede empujar un paso a mitad de un efecto).
2. La cola no es de fases, es de **pasos heterogéneos**.

Hoy `flow.phase_queue: ["cleanup"]` son strings. Pasa a objetos:

```json
"step_queue": [
  { "kind": "phase",    "id": "cleanup" },
  { "kind": "effect",   "ref": "eff_3", "source_card": "c12" },
  { "kind": "decision", "ref": "d17" }
]
```

**Del lado de la definición:** `scope` de fase gana `all_players_sequential` y
`all_players_simultaneous`. Hoy solo hay `per_player`, que implícitamente significa "el
jugador activo".

**Unificar el setup aquí:** deja de ser un compilador aparte y pasa a ser los primeros
pasos de la cola. Una máquina en vez de dos, y hereda gratis las decisiones del punto 3.

**Cambio de estructura mínimo** — el hueco ya existe. Casi todo es motor.

---

## 5 · Modificadores (efectos permanentes)

**La clave conceptual:** un permanente **no es un efecto**. Un efecto es un verbo que
ocurre en un instante y cambia el estado. Un permanente es una regla vigente que no cambia
nada — altera cómo se *lee* el estado. Meterlos en el mismo array es lo que bloquea.

**Cambio en la carta** — array separado:

```json
{
  "effects": [ ... ],
  "modifiers": [
    {
      "active_while": { "in_zone": "in_play", "owner": "self" },
      "property": "action_cost",
      "target": { "who": "controller", "cards": "*" },
      "operation": "add",
      "value": -1
    }
  ]
}
```

Cuatro conceptos: cuándo está activo, qué propiedad toca, sobre quién, cómo la altera.

**El runtime ya tiene el hueco:** `players[].modifiers` y `card_instances[].modifiers`
existen vacíos. Ahí registra el motor los modificadores activos al entrar la carta en zona,
y los retira al salir.

**La parte cara, y es de estructura:** hay que escribir el **catálogo de propiedades
modificables**. Lista cerrada y explícita: coste de compra, coste de jugar, cantidad
robada, valor de atributo, legalidad de una acción, límite de mano, usos por turno. Cada
propiedad es un sitio donde el motor deja de leer el número crudo. Empezar con cinco o seis;
si crece sin control el motor se vuelve inauditable.

**Decidir desde el día uno el orden de capas:** sumas → multiplicaciones → reemplazos, y
las prohibiciones ganan a todo. Si no, dos cartas juntas dan resultados distintos según
cuál se jugó antes, y son bugs irreproducibles.

**Es irrenunciable para el simulador:** el desequilibrio vive en las combinaciones. Un
simulador que ignora los permanentes dirá que todo está balanceado justo en los juegos
donde no lo está.

---

## 6 · Triggers como patrón de evento

**Problema:** `trigger` es un enum cerrado (`on_play`, `on_destroy`, `at_start_of_turn`).
No se puede escribir "cuando un rival te obligue a descartar". Peor: un trigger
desconocido se reescribe en silencio a `on_play` (hallazgo A2 de Ascension).

**Cambio:**

```json
"trigger": { "event": "card_moved", "to_zone": "discard", "owner": "self" }
```

Los valores actuales sobreviven como presets (`on_play` = "carta movida a in_play por su
dueño").

**Regla de higiene:** patrón desconocido **avisa**, nunca se reescribe en silencio.

**Analogía WordPress (correcta):** el punto 6 son las *actions* (ocurre X, ejecuta esto),
el punto 5 son los *filters* (un valor pasa por aquí, devuélvelo modificado). Misma
división, por eso van juntos en el motor.

Tres diferencias con WP:
- El orden debe ser determinista (capas por tipo de operación, no prioridades ad hoc).
- Todo es dato declarativo, no código — lo tiene que poder emitir un formulario y una IA.
- Se guardan las suscripciones activas en el runtime, no las definiciones.

**Bonus:** junto con el punto 4, una reacción o interrupción es empujar una ventana de
respuesta a la cola. El techo declarado (G3, resolution stack) deja de ser una reescritura.

---

## 7 · Acciones compuestas — corregido

**Corrección:** la presuposición de género **no está en las zonas** (que las define el
usuario). Está en `bindings`. El juego se autora abierto y en el setup se colapsa a roles
fijos que el motor sabe leer:

```json
"market": "trade_row",
"life_tracker": "authority",
"attack_cost_tracker": "combat",
"currency_source": "catalog_default:buy_from_market"
```

Un juego sin vida, sin mercado, o con dos vías de adquirir la misma carta, pierde
información en ese colapso. El catálogo lo refuerza: `buy_from_market` trae
`default_cost_tracker` y `requires: { shared_zone_present: true }`.

**La estructura ya lo soporta:** `game_meta['action_bindings']` está reservado y vacío.
En vez de que el motor deduzca los roles, el juego los declara por acción:

```json
"action_bindings": {
  "buy_from_market": {
    "source_zones": ["copper_pile", "silver_pile"],
    "cost_tracker": "coins",
    "destination": "discard",
    "uses_per_turn": { "tracker": "buys" }
  }
}
```

**Cero estructura nueva.** Es motor + poblar un campo existente. Cierra G9, G10, G11 y da
soporte a la Rules page (P7).

**Irrenunciable para el simulador:** si las acciones son gratis e ilimitadas, la duración y
la economía salen mal por construcción (mazos finales de 100 cartas en la probe de Dominion).

---

## 8 · Zonas anidadas y posicionales

**Cambio:**

```json
{
  "id": "pyramid",
  "parent": null,
  "slots": [
    { "id": "r1c1", "face": "down", "covered_by": ["r2c1", "r2c2"] }
  ]
}
```

- `parent` (mesa / jugador / **instancia de carta**) → contenedores anidados: fichas o
  cartas colocadas encima de otra carta concreta.
- `slots` con relaciones → pirámide de 7 Wonders Duel (`covered_by`) o tablero
  (`adjacent_to`). Mismo mecanismo.

**Visibilidad:** sacarla del enum de la zona y convertirla en relación observador→objeto
evaluable. Cubre casos como "ves las manos ajenas pero no la tuya".

**Lo que NO cubre de 7WD:** costes dependientes de lo que tenga el rival y victoria por
colección de símbolos — ambos caen en el punto 1 (expresiones), no en éste.

**Tier aislado.** Se puede dejar el último. Si el producto se mantiene en juegos de cartas
puros, el `parent` (trivial) vale la pena y los `slots` son opcionales.

---

## Nota · Aproximación de cartas no modelables

**Este es el cambio estructural que hace viable todo lo demás, y no es ninguno de los ocho.**

Separar lo impreso de lo simulado:

```json
{
  "name": "Bazar",
  "rules_text": "Descarta una carta. Si es un tesoro, roba dos.",
  "sim_model": { },
  "sim_fidelity": "exact | approximate | unmodelled"
}
```

Hoy el esquema asume que el efecto estructurado **es** la carta, y el texto se deriva del
`render` del catálogo. Invertir la relación: el texto es la carta, el modelo es una lectura
de la carta.

Tres consecuencias:

1. **El modelo deja de bloquear la autoría.** El usuario siempre puede imprimir lo que
   quiera. Nunca se le dice "esa carta no se puede crear" — veneno en un producto creativo.
2. **Se puede aproximar a propósito.** "Esta carta es difícil de modelar; para simular,
   trátala como *roba 1*." La aproximación pasa a ser una decisión de diseño explícita en
   vez de un fallo silencioso.
3. **La cobertura se vuelve una métrica de producto.** El informe de simulación abre con:
   *"He entendido 47 de 52 cartas. Estas 5 se han tratado como sin efecto. Los resultados
   de estrategias que dependan de ellas son poco fiables."*

Sin esto, cada agujero del modelo es un muro. Con esto, cada agujero es una nota al pie.

**Y es lo que permite medir:** sustituir el objetivo "modelar casi todo" (que no dice
nunca cuándo parar) por "% de cartas autoradas que se simulan sin aproximar". Con datos
reales de usuarios, que autoran cosas que ninguna probe contiene.

**Importante:** la aproximación es una válvula de escape, no una arquitectura. Solo es
segura porque existe el modelo fiel debajo.

---

## Nota · Familias

Confirmado: **familia = tag clasificatorio, cero motor de simulación.** No dispara ni
modifica nada. Es un adjetivo que otras cosas leen — igual que `type`, que el motor no
ejecuta pero las zonas filtran con `accepts_types`.

Tres puntos a resolver:

1. **Formato abierto a N, UI limitada a 1.** El campo en la carta ya es array
   (`families: []`). La restricción "v1 max 1" tiene sentido para el estilo de imagen, pero
   se queda corta en cuanto la familia es target de efectos (cartas de dos bandos, temática
   + facción). Dejar el formato abierto y limitar en el formulario; la primera familia manda
   para la imagen. Gratis ahora, migración después.

2. **Entidad, no string suelto.** Si lleva estilo de imagen, es un objeto autorado a nivel
   de juego: `{ id, label, color, description, style_prompt }`. La carta guarda el id. El
   scaffold actual (`family_1` / `enabled` / `name`) es una forma **distinta y peor** que la
   forma "intended" del propio D6. Corregirlo ahora que no cuesta nada.

3. **La familia es un criterio del filtro genérico, no un target especial.** Modelarla como
   caso particular obliga a escribir lo mismo tres veces:

```json
"filter": { "family": "gremio_chatarrero" }
"filter": { "type": "unit", "cost": { "<=": 4 } }
"filter": { "behaviour": "Permanent", "owner": "self" }
```

Mismo mecanismo que "elige 3 cartas de tu mano" (punto 3) y "gana una carta de coste ≤ 4".
Del lado de contar, `count_cards` ya filtra por campos de carta — es posible que familias
como condición **ya funcione hoy** en cuanto las familias existan de verdad.

**S1 se descompone en:** definir la entidad + UI (pequeño, y desbloquea el estilo de
imagen, que es valor de producto inmediato aunque el motor no la mire) · aceptar `family`
en el filtro genérico (mínimo) · dar contexto a la IA generadora para asignar familias con
coherencia temática (aquí está el trabajo real).

---

## Resumen: estructura vs motor

| # | Cambio de estructura | Alcance | Motor |
|---|---|---|---|
| 1 | Unión `valor \| expresión` en atributos, costes, cantidades, copies, starts_at | **Grande** — toca casi todos los esquemas | Extender el evaluador de `ending.when` |
| 2 | `subject`/`from`/`to` en el efecto; catálogo pasa a presets | Media | Reescribir el ejecutor de efectos |
| 3 | Spec de selección en params; `pending_decisions` + `history` de decisiones | Pequeña pero nueva | Grande con humanos; **trivial si solo hay bots** |
| 4 | `phase_queue` → cola de objetos heterogéneos; `scope` simultáneo/secuencial | **Mínima** — el hueco ya existe | Bucle de turno |
| 5 | `modifiers[]` en la carta + catálogo de propiedades + orden de capas | Media — el runtime ya tiene el hueco | Resolutor en cada lectura de valor |
| 6 | `trigger` de enum a patrón de evento | Pequeña | Bus de eventos |
| 7 | **Ninguna** — `action_bindings` ya está reservado | — | Dejar de derivar bindings; leerlos |
| 8 | `parent` + `slots` con relaciones en la zona | Media, aislada | Legalidad posicional |
| — | `rules_text` / `sim_model` / `sim_fidelity` en la carta | Pequeña | Informe de cobertura |

**Orden sugerido:** 1 primero (es el que más esquemas toca; hacerlo antes de que haya
juegos autorados encima). Luego 2 y 6 juntos — son el mismo refactor del catálogo de
efectos, uno por el lado del destino y otro por el del disparo. Luego 3 y 4. El 7 cuando
convenga, porque la estructura ya está. El 5 y el 8 al final, cada uno es un tier propio.

Los tres irrenunciables para que el número que ve el usuario signifique algo: **1
(expresiones)**, **5 (modificadores)** y **7 (costes y límites de uso reales)**.

---

## Pendiente de decidir

**Versionado de cartas.** Si el futuro incluye app o TCG con propiedad digital, "Bazar
v1.2" tiene que ser una entidad distinta de "Bazar v1.0", los errata no pueden reescribir
en silencio lo que alguien poseía, y un mazo debe apuntar a versiones concretas. Hoy las
cartas viven en filas de `game_cards` sin versión. Añadir versionado después de que exista
propiedad es una pesadilla; ahora es una columna.

**Niveles de autoría.** Formalizar tres, y guardar el nivel en el dato:
preset ("Roba 3", ~90%) → preset parametrizado ("Roba 1 por cada X", ~9%) → expresión libre
(~1%, con aviso de validación más débil). La IA generadora emite **solo presets**, nunca
expresiones libres.

**Calidad de los bots.** Probablemente el mayor retorno por hora de todo el proyecto, y no
es una feature de modelo. Un simulador vale lo que valga el jugador que lo juega; un bot
codicioso dice lo mismo de un juego bien diseñado que de uno roto. Hacen falta varias
políticas (codicioso, económico, agresivo, aleatorio — comparar contra el aleatorio es el
mejor detector de juegos donde las decisiones no importan) y muchas semillas, para pasar de
"terminó en 41 turnos" a "termina entre 28 y 46, y el que empieza gana el 58%".

---

## Techo declarado

Fuera de alcance, ahora y siempre: **tiempo real, negociación entre jugadores, destreza
física y azar externo al mazo**. No por dificultad, sino porque no son juegos de cartas por
turnos. Acotar "casi todo" es lo que hace que la promesa sea cumplible.

**Métricas que el simulador debe responder** (y criterio para decidir si algo merece
modelarse: *¿mueve una de estas seis?*):

1. ¿La partida termina?
2. ¿Dura lo que el diseñador cree?
3. ¿Hay una carta rota?
4. ¿Hay cartas que nadie compra nunca?
5. ¿La economía crece o se atasca?
6. ¿Gana siempre el que empieza?
