# Slot Factory — Documentación Técnica

**Proyecto analizado:** `D:\Matias\Prototipo\BullPower\slot-machine` (copia local, análisis de solo lectura)
**Instancia actualmente configurada:** "Santos de Fortuna" (`config.game.id = "santos_de_fortuna"`)
**Fecha de análisis:** 2026-08-21
**Alcance:** JavaScript + PixiJS 8 + XState v5 + @pixi/sound, con un servidor Express local que calcula los resultados de spin.

> **Nota de encuadre, válida para todo el documento:** este proyecto es un prototipo de "fábrica de slots" — un motor genérico que construye un slot jugable a partir de un archivo de configuración (`slotConfig.json`). "Santos de Fortuna" es solo la instancia actualmente cargada: sus símbolos, paytable, paylines y features concretas son datos de configuración, no parte del mecanismo. Este documento describe el mecanismo. Donde un valor de "Santos de Fortuna" se usa como ejemplo, se marca explícitamente como tal.

---

## 0. Cómo leer este documento

Cada afirmación técnica cita el archivo (y, cuando es relevante, la línea aproximada) donde se verificó. Las secciones **"Unknowns"** y **"Hallazgos y contradicciones"** son deliberadamente explícitas: este es un prototipo real, con código muerto, duplicaciones y al menos un bug de sintaxis detectado durante el análisis. No se oculta nada de eso — es información tan útil para un desarrollador nuevo como la arquitectura "feliz".

---

## 1. Qué es la Slot Factory

La Slot Factory es un motor de slot machine para navegador compuesto por:

- Un **frontend** 100% en `public/`, sin build step (los módulos se cargan como ES modules nativos del navegador, PixiJS/XState/i18next se cargan vía `<script>`/`importmap` desde CDN — ver `public/index.html`), orquestado por **tres máquinas de estado XState v5** (`public/machines/appMachine.js`, `loadingMachine.js`, `gameMachine.js`).
- Un **servidor Express local** (`app.js`, `routes.js`, `controllers.js`) que no es un simple file-server: es la **autoridad del resultado del juego**. Calcula las paradas de reel (RNG), evalúa paylines/scatters/wilds, resuelve los loops completos de Free Games y Hold & Win, y devuelve todo precalculado al frontend. El frontend nunca recibe las tiras de reel (`reels` del JSON de config no se expone vía la API), solo resultados ya resueltos — un patrón anti-cheat típico de la industria, aunque aquí corre en `localhost:3000` sin autenticación ni persistencia.
- Un **archivo de configuración** (`server/data/jsons/slotConfig.json`) que define todo lo que hace que el slot actual sea "Santos de Fortuna": símbolos, paytable, paylines, features, denominaciones, layout de UI, textos i18n y manifiesto de assets.

La promesa de "fábrica" se sostiene en la práctica: casi todo el motor de reels, UI, animaciones y máquinas de estado lee su forma y comportamiento de `config`, no de valores hardcodeados de "Santos de Fortuna" — con algunas excepciones puntuales documentadas en la sección 15 (por ejemplo, el locale de formato de moneda está fijo a `es-AR`, y algunos colores/tamaños de layout de menús están hardcodeados).

### Diagrama de arquitectura general

```mermaid
flowchart TB
    subgraph Browser["Navegador"]
        idx["index.html"] --> mainjs["main.js<br/>createActor(appMachine).start()"]
        mainjs --> appM["appMachine<br/>(orquestador raíz)"]
        appM -->|invoke: loader| loadM["loadingMachine<br/>(carga de config/PixiJS/assets)"]
        appM -->|invoke: gameActor, systemId gameActor| gameM["gameMachine<br/>(ciclo de vida del juego)"]
        gameM -->|invoke| ticker["reelTickerActor<br/>(giro/parada de reels)"]
        gameM -->|invoke| trans["transitionActor<br/>(popups fg/hw/outro)"]
        gameM -->|invoke, provisto por appMachine| winloop["winLoopLogic<br/>(animación de líneas ganadoras)"]
        gameM -->|invoke, provisto por appMachine| wintier["winTierCelebrationActor<br/>(celebración BIG/MEGA/EPIC)"]
        gameM --> pixi["PixiJS Application / Stage"]
        gameM -.->|fetch| api1["GET /api/slotConfig"]
        gameM -.->|fetch| api2["POST /api/spinButton"]
    end
    subgraph Server["Servidor Express local (puerto 3000)"]
        appjs["app.js"] --> routesjs["routes.js"]
        routesjs --> ctrl["controllers.js<br/>RNG + evaluación de paylines<br/>+ loop Free Games + loop Hold&amp;Win"]
        ctrl --> cfgjson["server/data/jsons/slotConfig.json<br/>(incluye 'reels', NUNCA expuesto al cliente)"]
    end
    api1 --> routesjs
    api2 --> routesjs
```

---

## 2. Bootstrap: cómo arranca la aplicación

`public/index.html` carga, en orden: un `importmap` que resuelve `xstate`/`i18next` desde `esm.sh`/`jsdelivr` (comentarios en el HTML muestran alternativas de CDN probadas), PixiJS 8 (`pixi.min.js`), `pixi-filters`, `@pixi/sound` — todos como globals (`window.PIXI`), no como imports ESM — y finalmente `<script type="module" src="main.js">`.

`public/main.js` es deliberadamente mínimo:

```js
import { createActor } from 'xstate';
import { appMachine } from './machines/appMachine.js';
const appActor = createActor(appMachine);
appActor.start();
```

No hay bundler (Webpack/Vite/Rollup) en este proyecto: `package.json` solo declara dependencias runtime (`@pixi/sound`, `express`, `i18next`, `xstate`) y un script `assets` que invoca `assetpack` (herramienta de empaquetado de assets de PixiJS, config no incluida en el repo analizado). El servidor Express (`app.js`) sirve `public/` como archivos estáticos.

Un `#boot-loader` HTML/CSS puro (logo animado con `filter`/`drop-shadow`) se muestra antes de que PixiJS inicialice, como pantalla de carga inicial fuera del control de XState.

---

## 3. El servidor local: autoridad del resultado de juego

Archivos: `app.js`, `routes.js`, `controllers.js`, `server/data/jsons/slotConfig.json`.

`app.js` es un servidor Express de 15 líneas: sirve `public/` como estático y monta `routes.js` bajo `/api`, escuchando en el puerto 3000. No hay base de datos, autenticación, sesiones ni persistencia de ningún tipo — es un servidor de desarrollo/prototipo, consistente con lo indicado en el contexto del proyecto.

`routes.js` expone exactamente dos endpoints:

| Método | Ruta | Handler | Qué hace |
|---|---|---|---|
| GET | `/api/slotConfig` | `slotConfig` (`controllers.js:13-18`) | Devuelve `{ config, manifest }` de `slotConfig.json`. **`reels` (las tiras de símbolos) nunca se envía al cliente.** |
| POST | `/api/spinButton` | `spinButton` (`controllers.js:67-481`) | Recibe `{ bet }`, calcula y devuelve el resultado **completo** de un spin, incluyendo los loops enteros de Free Games y Hold & Win si se activan. |

Esto es un hallazgo arquitectónico central: **el cliente no calcula el resultado del juego.** Todo el RNG, la evaluación de paylines/wilds/scatters, y la lógica completa de features vive en `controllers.js`, ejecutándose en Node antes de que el frontend reciba una sola respuesta.

### Lógica de `spinButton` (`controllers.js`)

1. **`displayStops(reeltype)`** (líneas 24-65): para cada reel del tipo pedido (`baseGame`/`freeGames`/`holdAndWin`, filtrados de `slotConfigData.reels`), elige una parada aleatoria (`Math.floor(Math.random() * reel.length)`) y toma 3 símbolos consecutivos (con wrap-around módulo `reel.length`), formateados como `symbolNN`. Nota: hay un mecanismo de **display forzado para pruebas** en el primer spin (`firstFreeSpin`, líneas 51-55) que reemplaza el resultado real por un array hardcodeado — código de debugging dejado activo en el prototipo (ver sección 16).
2. **`pays(display, payTable, bet)`** (líneas 483-659): evalúa cada línea de pago (`payTable.lines`) recorriendo columna por columna, manejando sustitución de wilds (`payTable.wilds`) y comparando el pago de "símbolo consecutivo" vs. "wild consecutivo" para quedarse con el mayor. Cuenta scatters por tipo (`payTable.scatters`, un objeto `{freeGames:[...], holdAndWin:[...]}`) en cualquier posición del grid. Devuelve `{ Pays, totalScattersByType }`.
3. **Free Games**: si `totalScattersByType.freeGames >= config.features.freeGames.triggerAmount`, se activa. El servidor ejecuta el **loop completo** de free spins de una sola vez (no spin por spin bajo demanda del cliente): por cada spin llama de nuevo a `displayStops('freeGames')` + `pays(...)`, acumula el total ganado, aplica niveles de celebración (`BIG/SUPER/MEGA/EPIC` por umbrales de `totalPays` respecto al bet), soporta respins adicionales si vuelven a caer scatters (`freeSpinObj.amount`), y corta si se supera `maxWinCap * bet`.
4. **Hold & Win**: si `totalScattersByType.holdAndWin >= config.features.holdAndWin.triggerAmount`, se activa un loop análogo sobre una grilla `cols × rows` (de `config.display`), con símbolos "sticky" (persistentes entre respins), símbolos `multiplier`/`booster` (de `payTable.symbols` con `valueType`), un símbolo `ghost` (`config.game.ghost`, representa "celda vacía"), niveles de premio (`config.features.holdAndWin.jackpotLevels`) y un contador de respins que se reinicia (`+= additionalSpins`) cada vez que cae un símbolo nuevo. El servidor devuelve, por cada respin, un snapshot completo (`display`, `displayStickyHW`, `newSymbolsThisSpin`, `activePrize`, `hwStickyCount`, `multiplierValues`, `respins`), y al final calcula `multiplierTotal`, `prizeTotal` y `hwTotalWon`.
5. La respuesta final (`res.json(...)`, líneas 454-474) tiene esta forma:
   ```js
   {
     bet, display, pays, totalWon, wonLevel,
     freeGames: { amount, detail: [...spinResult], fgScattersCount, fgTotalWon, maxWinCapReached },
     holdAndWin: { amount, detail: [...respinResult], hwScattersCount, hwActivePrize, hwTotalWon }
   }
   ```
   El frontend consume `detail` como una lista precalculada: cada sub-spin de free games/hold&win que se "juega" visualmente en el cliente es en realidad una reproducción de un elemento de este array, no una nueva llamada de red.

### Implicación para el frontend

`public/machines/gameMachine.js` solo llama a `POST /api/spinButton` **una vez por spin del jugador** (estado `baseGame.spinning.requestingData`, ver sección 6). Todo lo que ocurre después — reels de free games, respins de hold&win — consume `context.serverData.freeGames.detail[i]` / `context.serverData.holdAndWin.detail[i]`, ya calculado. No hay una segunda ronda de peticiones HTTP durante una feature.

---

## 4. Configuración: `slotConfig.json`

Archivo: `server/data/jsons/slotConfig.json` (57 KB). Servido parcialmente al cliente vía `GET /api/slotConfig` (solo `config` y `manifest`; `reels` se queda en el servidor).

### 4.1 Esquema de nivel superior

```
{
  reels: [ {type, name, detail: [int,...]}, ... ]   // 15 tiras: 5 reels x 3 tipos (baseGame/freeGames/holdAndWin)
  config: {
    display, game, animations, autoplay, features, payTable, denominations, ui, i18n
  },
  manifest: { bundles: [ {name:'symbols'|'ui'|'sounds'|'fonts', assets:[{alias,src}]}, ... ] }
}
```

### 4.2 Detalle de `config`

- **`display`**: `reels:5, rows:3, symbolWidth:256, symbolHeight:256, paddingX:10, paddingY:10, bounceFactor:0.4, audioStopThreshold:15, minScale:0.35`. Define la grilla del juego y parámetros físicos del giro/rebote — consumido tanto por el servidor (dimensión de grilla en Hold & Win) como por el cliente (layout PixiJS, `reelTickerActor`).
- **`game`**: `id`, `atlas` (ambos sin consumidores detectados — ver Unknowns), `symbolsNames` (19 ids), `reelsSpeeds` (array de 5 floats), `timing` (perfiles `normal`/`turbo`/`quickStop` con `baseTurns`/`staggerTurns`/`speedMultiplier`), `symbolsBaseGame`/`symbolsFG`/`symbolsHW` (subconjuntos de símbolos válidos por modo), `ghost` (id del símbolo placeholder de celda vacía, usado tanto en Hold & Win server-side como client-side), `sounds` (array `{alias:'music'|'sounds', volume}` — son *buses* de volumen, no volumen por efecto individual).
- **`animations.wins`**: `minLineDuration, lineTransition, symbolSpeed`. El código también soporta `trailCount`/`particleTexture` en esta ruta (con fallback en código si faltan) pero **este JSON no los define** — en la instancia actual esos dos parámetros son efectivamente code-driven por el valor por defecto.
- **`autoplay`**: `spins` (array de conteos ofrecidos, ej. `[10,20,50,75,100]`), `lossesLimits`.
- **`features.freeGames`**: `active, triggerAmount, maxWinCap, freeSpinsAmounts:[{trigger,amount}]`.
- **`features.holdAndWin`**: `active, triggerAmount, additionalSpins, hwSpinsAmounts:{trigger,amount}` (objeto único, no array — asimetría notable respecto a `freeSpinsAmounts`), `jackpotLevels:[{name,symbol,trigger,value}]` (en la instancia actual: `mini/minor/major/grand`).
- **`payTable`**: `symbols` (19 objetos: `symbolId, type` [`baseGame`|`wild`|`scatter`|`holdAndWin`], `instancespay` [pago por 1..5 apariciones]; los símbolos de Hold & Win añaden `value`, `valueType` [`multiplier`|`booster`|`prize`], `textStyle`), `wilds` (array de ids), `scatters` (objeto `{freeGames:[...], holdAndWin:[...]}`, no un array plano), `lines` (10 arrays de 5 enteros, fila por reel).
- **`denominations`**: array plano de apuestas válidas (ej. `[100,200,...,20000]`).
- **`ui`**: objetos `desktop`/`mobile`, cada uno con ~30 claves de layout (`type, x, y, width/height, scale, anchor, rotation, color, alpha, isButton, maskPadding, ...`), más sub-objetos `textStyle`, `textColors`, `payTableLayout.pages`.
- **`i18n`**: `lng, fallbackLng, resources:{en,es,pt}` con textos traducidos por key.

### 4.3 Mapa de dependencias (extracto — la tabla completa fue construida durante el análisis y puede ampliarse a pedido)

| Propiedad | Consumidores principales | Efecto | Tipo |
|---|---|---|---|
| `display.reels/rows` | `controllers.js` (server, grilla HW), `gameMachine.js`, `slotConfigEditor/editor.js` | gameplay + render | Config-driven |
| `display.symbolWidth/Height` | `createReels.js` (sí lo lee) vs. `applyReelsLayout.js` (hardcodea `256/256` en paralelo) | render | **Mixto/inconsistente** — ver hallazgo #6 |
| `display.bounceFactor` | `reelTickerActor.js` (con `Math.min(config.display.bounceFactor||0.1, 0.7)`) | física de rebote | Mixto (config + clamp fijo en código) |
| `game.ghost` | `controllers.js` (server), `hwIsCoin.js`, `parseSymbols.js` (cliente) | gameplay | Config-driven, duplicado en server y cliente |
| `payTable.*` | `controllers.js` (autoridad de pago, server), `parseSymbols.js`, `createPayTableView.js`, módulos hw* (cliente, solo lectura) | gameplay (server) + render (cliente) | Config-driven |
| `denominations` | `controllers.js`, `denominationUpdate.js`, `createReels.js`, `syncBetUI.js` | gameplay + UI | Config-driven |
| `features.freeGames`/`holdAndWin` | `controllers.js` (server, valores) | gameplay | Mixto — datos en config, algoritmo de disparo fijo en código |
| `animations.wins.*` | `winLoopLogic.js` | render (timing) | Config-driven (con defaults en código si faltan) |
| `ui.desktop/mobile.*` | `UIManager.js`, `createUI.js`, `applyUILayout.js` | render/UI | Mixto — posiciones vienen de config, pero la interpretación "fracción vs. píxel absoluto" de `x`/`y` es una convención fija en `applyUILayout.js` |
| `i18n.resources` | `appMachine.js` (`i18next.init`), `renderConfigTab.js` | UI (localización) | Config-driven |
| `manifest.bundles` | `appMachine.js` (`PIXI.Assets.init/loadBundle`) | render + audio | Config-driven |

Propiedades sin consumidor detectado por búsqueda de texto: `config.game.id`, `config.game.atlas` (ver Unknowns, sección 18).

### 4.4 `server/slotConfigEditor/` — herramienta de desarrollo, no runtime

Es un editor visual **standalone y desconectado del juego**: una página HTML (`index.html`) que carga `editor.js`/`helpers.js` (sin build), permite abrir un `slotConfig.json` local vía `<input type="file">`, editarlo en memoria a través de un formulario (cubre solo `Display`, `Game`, `Features` parcial, `PayTable.symbols.instancespay`/`lines`, `Denominations` — no cubre `ui`, `manifest`, `reels`, `animations`, `autoplay`, `i18n`) y descargar el resultado como `slot-config.json`. No hace `fetch` a `/api/*`, no está montada por `app.js`/`routes.js`, y no escribe de vuelta al archivo real del servidor — el desarrollador debe reemplazarlo manualmente. La carpeta `sections/` contiene una versión anterior y huérfana de las mismas funciones (nunca cargada por `index.html`).

---

## 5. Las tres máquinas XState

Los tres archivos viven en `public/machines/`. `appMachine.js` es la raíz: invoca a `loadingMachine` primero y, al terminar, invoca a `gameMachine` como actor hijo con `systemId: 'gameActor'`. No hay comunicación directa entre `loadingMachine` y `gameMachine` — todo pasa por `appMachine` como intermediario.

### 5.1 `appMachine`

**Contexto:** `{ loadingResult, activeRoot, uiManager, reelsData, device }`.
**Estados:** `loading.loadingActors` (inicial) → `game`.
**Actor `loader`** (dentro de `loading`): es literalmente `loadingMachine.provide({ actors: { fetchConfig, initPixiLogic, loadAssetsLogic } })` — `appMachine` provee las tres implementaciones concretas que `loadingMachine` solo declara por nombre.
**Actor `gameActor`** (dentro de `game`, `systemId:'gameActor'`): `gameMachine.provide({ actors:{winLoopLogic, winTierCelebrationActor}, actions:{updateBet, syncBetUI, initializeGame, resetSymbols, playBgMusic} })`.
**Comunicación:** escucha `onSnapshot` de `gameActor` para copiar `activeRoot`/`uiManager`/`reelsData` hacia su propio contexto; envía `PARENT_RESIZE` a `gameActor` (recuperado vía `self.system.get('gameActor')`) en la acción `executeGlobalResize`.
**Sin `onError`** en ninguno de sus dos `invoke` — los errores internos de `initPixiLogic`/`loadAssetsLogic` solo se loguean con `console.error`, no se propagan a la máquina.

```mermaid
stateDiagram-v2
    [*] --> loading
    state loading {
        [*] --> loadingActors
        loadingActors: invoke loader = loadingMachine.provide(...)
    }
    loading --> game : onDone(loader)
    state game {
        gameActor: invoke gameActor = gameMachine.provide(...)\nsystemId: gameActor
    }
    note right of loading
        on global (cualquier estado):
        WINDOW_RESIZE, UPDATE_ACTIVE_ROOT, PIXI_READY
        -> executeGlobalResize
    end note
```

### 5.2 `loadingMachine`

**Contexto:** `{ config, loadProgress, loaderRoot, pixiApp, assets }`.
**Diseñada para ser genérica/parametrizable:** los tres actores que invoca (`fetchConfig`, `initPixiLogic`, `loadAssetsLogic`) están referenciados solo por nombre — no importa ninguna implementación propia, depende 100% de `.provide()` de su padre.
**Cadena lineal, sin estados anidados ni paralelos:** `fetchingConfig → initializingPIXI → loadingAssets → ready` (`ready` es `type:'final'`, el único estado final de las tres máquinas que usa esta primitiva explícitamente).

```mermaid
stateDiagram-v2
    [*] --> fetchingConfig
    fetchingConfig --> initializingPIXI : onDone(fetchConfig) [GET /api/slotConfig + i18next.init]
    initializingPIXI --> loadingAssets : PIXI_READY [crea PIXI.Application + pantalla de carga]
    loadingAssets --> loadingAssets : PROGRESS_UPDATE
    loadingAssets --> ready : LOAD_COMPLETE [PIXI.Assets carga bundles del manifest]
    ready --> [*]
```

### 5.3 `gameMachine` (2454 líneas — la máquina central del proyecto)

**Contexto** (resumen; ver detalle completo en sección 4 arriba para las claves derivadas de config): `pixiApp, assets, config, slot, symbolsData, bet, denominations, activeRoot, serverData, fgCounter, hwCounter, isTurbo, gameLayers, symbolsPool, spinData, freeGames{...}, holdAndWin{...}, autoplay{...}, musicVolume, soundVolume, clickCatcher, device, paytableView, menuView, translator, loaderRoot`.

**Estados de nivel superior:** `initializing` → `baseGame` ⇄ `freeGames` / `holdAndWin` (ambas features vuelven siempre a `#baseGame.idle`, nunca se pasa directo de una feature a otra).

```mermaid
stateDiagram-v2
    [*] --> initializing
    initializing --> baseGame : after 100ms [entry: initializeGame, dismissLoader, playBgMusic]
    state baseGame {
        [*] --> idle
    }
    state freeGames {
        [*] --> animateFGScatter
    }
    state holdAndWin {
        [*] --> animateHWScatter
    }
    baseGame --> freeGames : checkingWins [guard: freeGames.detail.length > 0]
    baseGame --> holdAndWin : checkingWins [guard: holdAndWin.detail.length > 0]
    freeGames --> baseGame : exitToBaseGame -> #baseGame.idle
    holdAndWin --> baseGame : exitToBaseGame -> #baseGame.idle
```

**`baseGame`** (inicial `idle`):

```
idle          — entry: setUIInteraction(true), syncBetUI; si autoplay activo, self-envía SPIN
                on: SPIN->spinning, TOGGLE_MENU->menu, OPEN_AUTOPLAY_MENU, CLOSE_AUTOPLAY_MENU, SELECT_AUTOPLAY->autoplay
menu          — entry: setUIInteraction(false), clickCatcher, showMenuView
paytable      — entry: setUIInteraction(false), showPayTable
spinning
 ├─ requestingData — invoke fromPromise: POST /api/spinButton {bet}
 │                    onDone->rolling (assign serverData/freeGames/holdAndWin) | onError->#baseGame.idle
 └─ rolling        — invoke reelTickerActor; ALL_REELS_STOPPED -> checkingWins
checkingWins  — always: wonLevel.name->celebratingWinTier | pays.length>0->showingWinsBase |
                freeGames.detail.length>0->#game.freeGames | holdAndWin.detail.length>0->#game.holdAndWin |
                autoplay.active->autoplayDelay | ->idle
celebratingWinTier — invoke winTierCelebrationActor (fromPromise); onDone->showingWinsBase
showingWinsBase    — invoke winLoopLogic
autoplayDelay / autoplay — encadenan el próximo SPIN automático (after 800ms / 500ms)
```

**`freeGames`** (inicial `animateFGScatter`), cadena: `animateFGScatter → waitingForIntro → fgIntro (invoke transitionActor mode 'fg_start') → spinning (consume serverData.freeGames.detail[currentIndex]) → checkingWins → [celebratingWinTier] → showingWinsFG (invoke winLoopLogic) → autoplay → [aditionalSpinsTransition | checkMaxWinInterruption → maxWinCappedNotice] → fgOutro (invoke transitionActor mode 'end') → exitToBaseGame`.

**`holdAndWin`** (inicial `animateHWScatter`), cadena: `animateHWScatter → hwIntro (captura monedas iniciales, crea jackpots) → prepareHW → spinning (invoke reelTickerActor con holdAndWin completo en el input) → evaluate (hwUpdateStickySymbols, hwUpdateJackpot) → [processMultipliers si hay boosters | coinLanding si hay monedas nuevas] → spinning (si quedan respins) | collectPrizes → payout → [hwCelebratingWinTier] → hwOutro (limpieza) → exitToBaseGame`.

**Guards nombrados:** `fgIsMaxWinCapped`, `hwHasMoreSpins`, `hwHasBoosters`, `hwCoinsAdded`, y `shouldStopAutoplay` (declarado pero nunca usado, ver hallazgo #4).

**Llamada HTTP:** un único `fetch('/api/spinButton', {method:'POST', body: JSON.stringify({bet: context.bet})})`, inline dentro de `baseGame.spinning.requestingData` — es el único punto de la SPA que dispara un spin contra el servidor.

**Actores importados directamente (acoplados a esta máquina):** `reelTickerActor`, `transitionActor`, `animateSymbol` (los tres vía `import` directo en `gameMachine.js`).
**Actores inyectables por nombre string (provistos por `appMachine`):** `winLoopLogic`, `winTierCelebrationActor` — los únicos dos verdaderamente "genéricos"/reemplazables sin tocar `gameMachine.js`.

---

## 6. Actores (más allá de las 3 máquinas)

XState v5 permite invocar "actores" (`fromCallback`, `fromPromise`) desde los estados de una máquina. Este proyecto usa varios, todos en `public/game/`:

| Actor | Tipo XState | Archivo | Rol |
|---|---|---|---|
| `reelTickerActor` | `fromCallback` | `game/reelTickerActor.js` | Anima el giro y la parada de cada columna de reels, frame a frame vía `pixiApp.ticker`. Recibe `serverDisplay` (resultado ya calculado) y rellena con símbolos aleatorios de `symbolsPool` durante el giro, inyectando el resultado real recién en los últimos frames. Emite `REEL_IMPACT`, `REEL_STOPPED`, `ALL_REELS_STOPPED`; recibe `FORCE_QUICK_STOP` para el modo "skip". |
| `transitionActor` | `fromCallback` | `game/transitionActor.js` | Popups de transición entre modos (`fg_start`, `hw_start`, `end`, `aditionalSpins`, `fgMaxWinReached`), con máquina de fases interna por ticker (`rising→opening→fadeText→waiting→closing→closingDescend→fadeOut→finished`). Emite `TRANSITION_COMPLETE`; recibe `CLOSE_TRANSITION`. |
| `winTierCelebrationActor` | `fromPromise` | `game/winTierCelebrationActor.js` | Celebración visual de tiers altos (`BIG/SUPER/MEGA/EPIC`): monedas explotando, un "dado" 3D (mesh manual con proyección) que rota entre caras, shockwave filter sobre el stage. Resuelve la promesa al terminar la animación (dispara `onDone`). El archivo tiene 1409 líneas, de las cuales **solo las últimas ~440 (desde la línea 966) son código activo**; el resto son tres versiones anteriores comentadas en bloque. |
| `winLoopLogic` | `fromCallback` | `game/winLoopLogic.js` | Anima cada línea ganadora de `context.serverData.pays`: atenúa el resto del display, resalta símbolos, dibuja una partícula recorriendo la línea de pago, muestra un popup de texto (`playWinPopUp.js`). En modo "feature especial" (autoplay/FG/HW) recorre `pays` una vez y emite `WIN_LOOP_FINISHED`; **en modo juego base normal, entra en loop infinito y nunca emite ese evento por sí solo** — ver Unknowns. |
| `animateSymbol` | `fromCallback` | `game/animateSymbol.js` | Resalta uno o varios símbolos específicos (usado para la animación de scatter que dispara FG/HW), con manejo de errores que igual emite el evento de completado para no bloquear la máquina. |

---

## 7. Slot Construction Pipeline

Orden real verificado por imports y llamadas, disparado por la acción `initializeGame` en el `entry` del estado `initializing` de `gameMachine.js`:

```
initializeGame()                         [game/initializeGame.js]
 ├─ installBitmapFont() x4                [utils/installBitmapFont.js]     — instala fuentes bitmap (Morpheus, Outfit *)
 ├─ getScreenSize()                       [utils/viewport.js]
 ├─ buildFontTextureMap()                 [utils/buildFontTextureMap.js]
 ├─ initializeSoundVolumes()              [game/initializeSoundVolumes.js] — aplica config.game.sounds a los buses PIXI.sound
 ├─ parseSymbols(slotConfig, assets.symbols) [game/parseSymbols.js]       — construye symbolsData (texturas + metadata de paytable)
 └─ setupSlot(pixiApp, slotConfig, symbolsData, bet, assetsUi, device, handlers, translator)  [game/setupSlot.js]
     ├─ createRoot(pixiApp)                [game/gameRootContainer.js]    — crea el container 'gameRoot', lo agrega a app.stage
     ├─ createBackground(pixiApp, device)  [game/createBackground.js]     — capa de fondo con máscara alfa + partículas de polvo
     ├─ createUI(config, root, device, pixiApp, handlers, translator)  [ui/createUI.js]  — construye botones/paneles vía UIManager
     └─ createReels({root, config, symbolsData, frame, width, height, pixiApp})  [game/createReels.js]
         └─ createSymbolInstance() x (reels · (rows+1))  [utils/createSymbolInstance.js]
             └─ applySymbolData()          [game/applySymbolData.js]      — asigna texturas/valor a cada instancia de símbolo
```

`parseSymbols` recorre `config.game.symbolsNames`, ubica el spritesheet cargado de cada uno en `assets.symbols[name]`, ordena sus frames y construye `{ symbolBG, symbolFrame, staticTexture, animationFrames }`, cruzando además con `config.payTable.symbols` para anexar `value/valueType/textStyle/type` y marcar `isGhost` (comparando contra `config.game.ghost`). El diccionario resultante (`symbolsData`) es la fuente única de verdad de apariencia+metadata de cada símbolo para el resto del pipeline.

`createReels` crea, por columna, `rows + 1` instancias de símbolo (una fila extra oculta arriba, usada como buffer del giro) con un símbolo aleatorio inicial de `config.game.symbolsBaseGame`. Expone `updateLayout` (que internamente ejecuta `applyReelsLayout.js` para reposicionar/reescalar tras un resize).

Tras `setupSlot`, `initializeGame.js` cablea los handlers de UI a eventos de la máquina: `spinButton→SPIN`, `betUpButton/betDownButton→UPDATE_BET`, `turboButton→TOGGLE_TURBO`, `autoplayButton→OPEN_AUTOPLAY_MENU`.

---

## 8. Reels y símbolos

- **Dimensión:** `config.display.reels × config.display.rows` (5×3 en la instancia actual).
- **Buffer de giro:** cada columna tiene `rows+1` sprites (una fila extra arriba, fuera de pantalla).
- **Ticker de giro** (`updateReelLogic` en `game/reelSpin.js`, ejecutado cada frame por `reelTickerActor`): mueve los símbolos hacia abajo; cuando el penúltimo alcanza el límite, recicla el último símbolo, decide su nuevo contenido (aleatorio de `symbolsPool` durante el giro; en las últimas `rows+2` vueltas, inyecta el símbolo real de `context.serverData.display`), le reasigna textura/valor vía `applySymbolData`, y lo reinserta arriba. Al llegar a `spinTurnsMax`, pasa a modo rebote (`isBouncing`) e interpola hacia la posición final con `bounceFactor` (de config, clamp `≤0.7` en código).
- **Turbo/quick-stop:** perfiles de tiempo en `config.game.timing.normal|turbo|quickStop`; el "skip" (`FORCE_QUICK_STOP`) recalcula los turnos restantes para saltar directo al resultado final.
- **Reset entre spins:** `resetSymbols({reelsSprites})` (`game/resetSymbols.js`) restaura `alpha`/`scale`/frame de animación — se llama al entrar a `spinning`, antes de pedir datos al servidor.
- **Visibilidad de reels (Hold & Win):** la función dedicada `game/syncReelVisibility.js` **no se usa en ningún punto activo del código** — la misma lógica (ocultar símbolos bajo celdas Hold & Win llenas) está duplicada e inline dentro de `updateReelLogic` en `reelSpin.js`.

---

## 9. Ciclo de vida de un spin

1. Click en el botón de spin → `self.send({type:'SPIN'})` (cableado en `initializeGame.js`).
2. `gameMachine` en `baseGame.idle` recibe `SPIN` → `spinning.requestingData`.
3. Entry de `spinning`: `resetSymbols`, desactiva interacción de UI, sonido de giro.
4. `requestingData` invoca el `fetch POST /api/spinButton` con `{bet}` → al resolver, `context.serverData = data` (más se derivan `context.freeGames`/`context.holdAndWin` de `data.freeGames`/`data.holdAndWin`) → transición a `rolling`.
5. `rolling` invoca `reelTickerActor` con `serverDisplay: context.serverData.display` — el reel gira con relleno aleatorio y en los últimos frames inyecta el resultado real.
6. Al recibir `ALL_REELS_STOPPED` → `checkingWins`.
7. `checkingWins` decide, en este orden: celebración de tier alto (si `wonLevel.name`) → animación de líneas ganadoras (si `pays.length>0`) → transición a Free Games (si `freeGames.detail.length>0`) → transición a Hold & Win (si `holdAndWin.detail.length>0`) → continuar autoplay → volver a `idle`.
8. `showingWinsBase` invoca `winLoopLogic`, que anima cada línea de `pays`. En juego base normal, este loop es infinito y **no se pudo determinar con los archivos analizados qué evento exacto lo corta** para volver a `idle` (ver Unknowns) — probablemente el siguiente `SPIN` del jugador, pero no está confirmado.
9. Cambio de apuesta (fuera del ciclo de spin): `UPDATE_BET`, bloqueado por guard si hay una feature activa (`freeGames.detail.length>0 || holdAndWin.detail.length>0`), ejecuta `updateBet` (calcula la siguiente denominación válida) + `resetSymbols` + `syncBetUI`.

---

## 10. Win system

- **Origen de los datos:** `pays`/`display`/`wonLevel` vienen 100% de la respuesta del servidor (`context.serverData`). El cliente no recalcula nada, solo anima.
- **`winLoopLogic`** (ver sección 6): por cada línea ganadora, resuelve el patrón de posiciones desde `config.payTable.lines[win.line]`, muestra un popup de texto (`playWinPopUp.js`, animación de escala con easing), resalta y anima los símbolos ganadores, y dibuja una partícula recorriendo la línea (`utils/animationPaylinePoints.js`, que delega en `animationPaylineBorder.js` — modo contorno perimetral, el único usado actualmente — o `animationPaylineCenter.js` — modo centro, no usado por este archivo). Las coordenadas se calculan en vivo con `toGlobal`/`toLocal` sobre los sprites reales, no con tamaños fijos asumidos.
- **Celebración de tier:** si `wonLevel.name` existe (niveles `BIG/SUPER/MEGA/EPIC` calculados server-side por umbral de `totalWon` respecto al bet), corre primero `winTierCelebrationActor` y solo después `winLoopLogic`.
- **Rolling win (contador numérico animado):** `utils/animateRollingWin.js`, una función genérica que anima un `BitmapText` con easing cúbico durante `duration` (no es un actor XState); usada por `transitionActor` en modo `'end'` para el total de free games.

---

## 11. Arquitectura de UI

- **`UIManager`** (`ui/UIManager.js`): clase instanciada (no singleton), guarda `config`/`root`/`device`/`pixiApp`/`translator` y un diccionario interno de displayObjects. Su método `add(name, layoutKey, factory?)` resuelve el layout desde `config.ui[device][layoutKey]` (con fallback a `desktop`), crea el objeto (`panel`→`Graphics`, `text`→`BitmapText` traducido, resto→`Sprite`), y marca automáticamente como botón (`eventMode:'dynamic'`, `cursor:'pointer'`) cualquier elemento cuyo nombre contenga `"button"`.
- **`createUI`** (`ui/createUI.js`): orquestador de alto nivel. Define una lista hardcodeada de ~17 elementos "esenciales" del juego base (frame, spinButton, controlsPanel, balancePanel, betPanel, botones de bet, turbo, autoplay, menu) y los crea vía `UIManager`, saltando silenciosamente (try/catch + log) cualquiera que falte en el config del device — tolerante a configs incompletas por diseño.
- **Sincronización UI↔estado:** el patrón dominante es **llamada explícita desde hooks `entry`/acciones de `gameMachine.js`** (`setUIInteraction(ui, enabled)`, `syncBetUI({context})`), no una suscripción reactiva automática. La única excepción real es `createSpinCounter.js`, que sí usa `self.subscribe((snapshot) => ...)` sobre el actor XState, con un `snapshotFilter`/`contextSelector` inyectados para desacoplarse de la forma exacta del contexto.
- **Menús:** `createDesktopMenu.js`/`createMobileMenu.js` montan un `createTabsView.js` genérico con 3 tabs hardcodeadas (`TP`=Paytable, `RJ`=Rules, `CFG`=Config, con labels como keys i18n). `createPayTableView.js` lee `config.ui[device].payTableLayout` y `config.payTable.symbols`, escalando la tabla de pagos en vivo según `bet/betMin` — sin valores de paytable hardcodeados en el código. `renderConfigTab.js` es el único punto donde una tab envía eventos directos a la máquina (`SET_SOUND_VOLUME`, `SET_MUSIC_VOLUME`, `CHANGE_LANGUAGE`).
- **Autoplay:** `createAutoplayMenu.js` (panel de configuración: cantidad de spins, límite de pérdida, toggles "stop on loss"/"stop on feature") y `createAutoplayStatus.js` (widget flotante con botón STOP + contador). Ver hallazgo #2/#3 sobre "stop on loss" incompleto.

---

## 12. Audio

`utils/playSound.js` es genérico: recibe `(context, key, options)`, busca `context.assets.sounds[key]` (pobladas en algún punto del bundle `sounds` del manifest, fuera del alcance exacto verificado), aplica volumen por defecto desde `context.config.config.game.soundsVolumes?.[key]` — **esta ruta de config no existe en el JSON real** (el JSON usa `game.sounds`, un array de buses `music`/`sounds`, no un diccionario `soundsVolumes` por efecto), por lo que el volumen individual de efectos cae siempre al fallback `1` salvo que se pase explícito (ver hallazgo #7). `initializeSoundVolumes.js` sí lee correctamente `config.game.sounds` para fijar el volumen de los buses `music`/`spinButtonClick`/`reelSpinning`/`reelStop`. La librería usada es `@pixi/sound` (confirmado por `package.json` y la carga por `<script>` en `index.html`), cargada como global, no como import ESM.

---

## 13. Subsistema Hold & Win (HW)

**"HW" = "Hold and Win"** (confirmado textualmente: comentario `// hold and win` en `controllers.js:128`, y la clave de config/contexto en todo el proyecto es `holdAndWin`, sin abreviar). Es el mecanismo estándar de industria de grid de "monedas" persistentes + respins + niveles de jackpot — no es una integración de hardware ni un modo de juego separado que el jugador elija: es una **feature bonus** disparada por el servidor (conteo de scatters ≥ `config.features.holdAndWin.triggerAmount`) y manejada como un estado top-level más de `gameMachine`, estructuralmente análogo a `freeGames`.

**Flujo:** `animateHWScatter → hwIntro (hwCaptureInitialCoins, hwCreateJackpot, hwUpdateJackpotInitial) → prepareHW → spinning (reelTickerActor) → evaluate (hwUpdateStickySymbols, hwUpdateJackpot) → [processMultipliers si hay boosters | coinLanding si hay monedas nuevas] → spinning|collectPrizes (hwAnimateCollectAllPrizes) → payout → [hwCelebratingWinTier] → hwOutro (hwCleanupStickyLayer, hwCleanupJackpot) → exitToBaseGame`.

Módulos activos: `game/hwCaptureInitialCoins.js` (captura monedas del spin de disparo), `game/hwCreateStickyCoin.js` (crea la instancia visual sticky, usado tanto para la captura inicial como desde la acción `hwUpdateStickySymbols` de `gameMachine.js` en cada respin), `game/hwCleanupStickyLayer.js` (al terminar, "graba" el estado visual final de las monedas de vuelta en los sprites del reel base — usa deliberadamente la capa visual `stickyLayer` como fuente de verdad en vez de `serverData`/`grid`, según su propio comentario), `game/hwCleanUpJackpot.js`, `game/hwUpdateJackpot.js` / `hwUpdateJackpotInitial.js` (resaltan visualmente el nivel de jackpot activo), `ui/hwAnimateMultipliers.js` (procesa símbolos "booster": suma su valor a todos los multiplicadores activos del tablero), `ui/hwAnimateCollectAllPrizes.js` (animación final: cada moneda-multiplicador vuela hacia el acumulador de premio; si hay jackpot activo, se recolecta también; al terminar envía `COLLECT_FINISHED`), `ui/hwCreateJackpot.js` (crea los 4 sprites de nivel de jackpot desde `config.features.holdAndWin.jackpotLevels`), `utils/hwIsCoin.js` (predicado: símbolo de tipo `holdAndWin` y no es el `ghost`).

**Código muerto/prototipo detectado en este subsistema** (ver también sección 16): `game/createHWGrid.js` solo se usa desde `hwCaptureInitialCoins.js`, mientras que la acción `hwCreateGrid` de `gameMachine.js` reimplementa la misma lógica de creación de grid inline en vez de reutilizarlo. `game/hwUpdateGrid.js` no está importado en ningún lugar del proyecto y, si se ejecutara, fallaría (llama a `hwCreateStickyCoin` sin importarla, con una firma de argumentos obsoleta). `utils/hwGetPrizeValue.js` se importa en `hwAnimateCollectAllPrizes.js` pero nunca se invoca (el cálculo real está inline ahí mismo).

---

## 14. Reusable (Slot Factory genérica) vs. específico de "Santos de Fortuna"

**Genérico / reusable, confirmado por dependencia exclusiva de `config` y ausencia de valores hardcodeados de este juego:**
- Las tres máquinas XState (`appMachine`, `loadingMachine`, `gameMachine`) — su estructura de estados no menciona símbolos ni paytable específicos.
- El pipeline de construcción completo (`initializeGame → setupSlot → createReels`).
- El motor de reels/ticker (`reelSpin.js`, `reelTickerActor.js`) — lee dimensiones, velocidades y timing de `config`.
- `winLoopLogic.js`, `animationPaylinePoints/Border/Center.js`, `playWinPopUp.js`, `animateRollingWin.js` — leen `config.payTable.lines`/`config.animations.wins`, sin datos de símbolos hardcodeados.
- `UIManager.js`, `createUI.js`, `applyUILayout.js`, `createTabsView.js`, `createDropdownSelector.js`, `createAutoplayToggle.js`, `createSliderToggle.js`, `createShineEffect.js`, `clickCatcher.js` — mecanismos de UI agnósticos de contenido.
- El servidor (`controllers.js`): el algoritmo de evaluación de líneas/wilds/scatters y los loops de FG/HW operan sobre cualquier `payTable`/`reels` que se les dé.

**Específico del juego actual (parámetros de `slotConfig.json`, no código):**
- Todos los símbolos, paytable, paylines, niveles de jackpot, denominaciones — viven 100% en `slotConfig.json`.
- Assets visuales/sonoros referenciados por `manifest.bundles`.

**Zona gris / mezcla de mecanismo genérico + skin del juego actual:**
- `createAutoplayMenu.js` (colores/texturas hardcodeados propios de la skin visual actual, aunque la estructura del panel es genérica).
- `createDustParticles.js` (partículas ambientales, mecanismo genérico pero temáticamente decorativo).
- `formatMoney.js` — locale fijo `es-AR`, no parametrizable desde config (fricción real si la fábrica se usara para otro mercado).
- Los 3 tabs de menú (`TP/RJ/CFG`) están hardcodeados en código, no en config — el *conjunto de secciones de menú* no es configurable, aunque su contenido sí lo es.

---

## 15. Configuration-driven vs. code-driven vs. mixto (resumen)

- **Config-driven puro:** `denominations`, `game.reelsSpeeds`, `manifest.bundles`, dimensiones de grilla (`display.reels/rows`), `payTable` completo como datos.
- **Code-driven puro:** el algoritmo de recorrido de líneas de pago y sustitución de wilds (`controllers.js`, función `pays`); la convención de interpretación de `x`/`y` como fracción vs. píxel absoluto en `applyUILayout.js`; el locale de `formatMoney.js`.
- **Mixto:** las features (`freeGames`/`holdAndWin`) — el JSON aporta umbrales y tablas de premio, pero la mecánica de cuándo/cómo disparar y acumular vive fija en `controllers.js`; `display.bounceFactor` (config, con clamp fijo en código); el layout de UI (posiciones desde config, interpretación fija en código).

---

## 16. Hallazgos y contradicciones (código muerto, bugs, inconsistencias)

Esta sección documenta desviaciones reales detectadas en el código durante el análisis — relevantes para cualquiera que vaya a tocar este proyecto.

1. **`holdAndWin.evaluate` en `gameMachine.js` usa `after: { always: [...] }`** — `always` no es una clave estándar dentro de un bloque `after` en XState v5 (`after` espera claves numéricas/ids de delay). Es probable que sea un error de sintaxis heredado de refactors; no se pudo confirmar en runtime si XState lo ignora silenciosamente o si rompe la evaluación de ese estado.
2. **Autoplay "stop on loss" incompleto:** `showAutoplayMenu` envía `SET_LOSS_LIMIT`/`SET_STOP_ON_LOSS`, pero ningún estado de `gameMachine.js` maneja esos dos tipos de evento (solo `SET_STOP_ON_FEATURE` está cableado).
3. **Acción `checkLossInterruption` y guard `shouldStopAutoplay`** están definidos pero nunca referenciados por ninguna transición — código muerto, consistente con el punto anterior.
4. **`resetSymbols` provisto por nombre en `appMachine.provide({actions:{resetSymbols:...}}))` nunca se usa así** — `gameMachine.js` siempre llama a la función importada directamente.
5. **`import { resize } from '../utils/resize.js'`** en `appMachine.js` no se usa en ningún punto visible del archivo.
6. **Inconsistencia `display.symbolWidth/Height` vs. `applyReelsLayout.js`:** `createReels.js` lee correctamente `config.display.symbolWidth/Height`, pero `applyReelsLayout.js` hardcodea `nativeWidth=256; nativeHeight=256` en paralelo. Hoy coinciden (256/256 en el JSON actual), pero cambiar esos valores en config desincronizaría el layout tras un resize.
7. **`playSound.js` lee `context.config.config.game.soundsVolumes`, ruta que no existe en el esquema real** (`game.sounds`, no `game.soundsVolumes`) — el volumen individual de efectos siempre cae al fallback.
8. **`getActiveConfig.js` no tiene ningún consumidor detectado** en todo el árbol — probablemente código muerto o pensado para un uso no cableado aún. `UIManager.js`/`createUI.js` resuelven el fallback desktop/mobile con una estrategia distinta (por clave individual), no con el merge de objeto completo que implementa `getActiveConfig`.
9. **`createMenuView.js` es código muerto (casi seguro):** implementa una jerarquía de menú alternativa completa, pero `gameMachine.js` siempre elige entre `createDesktopMenu`/`createMobileMenu`. Además usa `PIXI.BitMapText` (typo, "M" mayúscula) que lanzaría `TypeError` si se ejecutara.
10. **`infoButton` referenciado en `createUI.js` pero nunca creado** (no está en la lista de elementos esenciales) — el binding de `handlers.onInfo` nunca se activa.
11. **Constantes de "lienzo virtual" (1100×720 / 720×1100) duplicadas** en `applyUILayout.js` y `viewport.js` — riesgo de desincronización si se cambia en un solo lugar.
12. **`resize.js` contiene lógica muerta/vestigial** (un clamp de escala mínima calculado pero no aplicado) y una expression statement huérfana sin efecto (`('screen ', ...)`— probablemente un `console.log` al que se le borró la función por error).
13. **`winTierCelebrationActor` no puede recibir `FORCE_SKIP`** — `gameMachine.js` intenta `sendTo('winTierActor', {type:'FORCE_SKIP'})`, pero el actor está implementado con `fromPromise` (sin `receive()`). El mecanismo de skip real y activo es un listener nativo `pixiApp.stage.once('pointerdown', ...)`, desacoplado del sistema de eventos de XState.
14. **`winTierCelebrationActor` recibe `gameLayers`/`gameRoot` como input pero no los usa** — anima directamente sobre `pixiApp.stage`, no sobre esos containers.
15. **`applySymbolData.js` crea 4 filtros PixiJS (`Glow`, `DropShadow`, `BulgePinch`, `OldFilm`) en cada llamada, nunca asignados a `.filters`** (la línea que lo haría está comentada) — basura de memoria en cada reciclado de símbolo, sin efecto visual.
16. **`server/data/jsons/slotConfig.json` no define `animations.wins.trailCount`/`particleTexture`**, aunque el código los soporta con fallback — hoy son efectivamente code-driven, no config-driven, en esta instancia.
17. **`controllers.js` tiene un "display forzado para pruebas" activo en el primer spin** (`firstFreeSpin`, líneas 51-55) que ignora el RNG real y devuelve un resultado hardcodeado — código de debugging que un despliegue de producción debería revisar/eliminar.
18. **`hwUpdateJackpotInitial.js` mezcla nombres inconsistentes:** trata `hwScattersCount` (nombre que sugiere un número) como si fuera un objeto premio con `.name`, y su mensaje de warning interno menciona el nombre del otro archivo (`hwUpdateJackpot`) por copy-paste.
19. **`server/slotConfigEditor/sections/*.js`** duplican nombres de función de `editor.js` con firmas distintas, pero `index.html` nunca los carga — carpeta huérfana dentro de una herramienta ya de por sí desconectada del runtime.

---

## 17. Preguntas abiertas para el equipo (no técnicas, de proceso)

`AGENTS.md` (raíz del repo) no es parte de la arquitectura del juego: es un protocolo de flujo Git para colaboración (rama `dev`, prefijo `codex/` para ramas de Codex, despliegue a `https://santosdefortuna.bullpower.ar`). Se documenta su existencia aquí solo para que quede registrado, no se usó como fuente para el resto de este documento.

---

## 18. Unknowns — no determinable con el material analizado

| Pregunta | Por qué importa | Qué se necesitaría |
|---|---|---|
| ¿Qué evento saca a `gameMachine` del loop infinito de `showingWinsBase` (juego base) para volver a `idle`? `winLoopLogic` no emite `WIN_LOOP_FINISHED` en ese modo. | Cierra el ciclo de vida completo del spin. | Releer el bloque `on:` completo de `showingWinsBase` en `gameMachine.js` con foco específico en ese punto, o probar la app en runtime. |
| ¿`winLoopLogic` está registrado en algún `.provide()` de `appMachine.js` igual que `winTierCelebrationActor`? Solo se confirmó el `src:'winLoopLogic'` (string ref) en `gameMachine.js`. | Confirma qué implementación corre realmente. | Releer `appMachine.js` con foco en el objeto de actores pasado a `provide()`. |
| Forma exacta de `serverData.holdAndWin.hwActivePrize` (¿objeto `{name,symbol,value}` u otra cosa?). | `hwUpdateJackpotInitial.js` asume una forma que su propio nombre de variable interna contradice. | Releer `controllers.js` línea por línea en la sección que construye `paysResult.hwActivePrize`. |
| ¿Dónde se crea el `PIXI.Application` raíz y cuál es la relación exacta entre `root`/`rootContainer`/`activeRoot`/`context.slot.container`? Se usan como parámetros con nombres distintos en distintos módulos. | Completa el árbol de jerarquía PixiJS con certeza total. | Cruce fino entre `appMachine.js` (`initPixiLogic`), `gameRootContainer.js` y los puntos donde `gameMachine.js` construye/pasa estos containers. |
| `config.game.id` y `config.game.atlas` — ningún `.js` los referencia por búsqueda de texto. | Podrían ser vestigiales o consumidos de forma dinámica no detectable por grep. | Confirmar con el equipo, o revisar accesos dinámicos de propiedad. |
| ¿Por qué `manifest.bundles.symbols` mapea los alias `symbol10`–`symbol13` al mismo `src` que `symbol09`? | Podría ser optimización de atlas compartido o un placeholder de arte pendiente. | Confirmar con el equipo de arte/assets. |
| Catálogo completo de sonidos y su mapeo a eventos de juego — solo se confirmó un call-site (`reelSpinning`). | Documentación completa de audio. | Lectura dedicada y exhaustiva de `gameMachine.js` en busca de todas las llamadas a `playSound`. |
| Comportamiento real de `holdAndWin.grid` tras `hwCleanupStickyLayer` (el comentario del propio archivo dice que se preserva "por si algún estado posterior lo necesita", pero no se identificó qué estado lo consumiría). | Podría ser vestigial o una dependencia no documentada. | Grep dirigido de `holdAndWin.grid` fuera de los archivos ya analizados. |
| ¿Existe algún mecanismo (script npm, servidor ad hoc) que sirva `server/slotConfigEditor/` en la práctica? | Aclara si la herramienta se usa activamente o quedó abandonada. | Revisar `package.json` "scripts" y preguntar al equipo. |

---

## 19. Índice de archivos citados

**Bootstrap/servidor:** `app.js`, `routes.js`, `controllers.js`, `package.json`, `public/index.html`, `public/main.js`, `public/loaderScreen.js`, `AGENTS.md` (protocolo Git, no arquitectura).
**Configuración:** `server/data/jsons/slotConfig.json`, `server/slotConfigEditor/` (editor.js, helpers.js, index.html, sections/*.js).
**Máquinas:** `public/machines/appMachine.js`, `loadingMachine.js`, `gameMachine.js`.
**Motor de juego:** `public/game/` — `animateSymbol.js`, `applySymbolData.js`, `createBackground.js`, `createDesktopMenu.js`, `createHWGrid.js`, `createMenuView.js` (muerto), `createMobileMenu.js`, `createPayTableView.js`, `createReels.js`, `createTabsView.js`, `gameRootContainer.js`, `hwCaptureInitialCoins.js`, `hwCleanUpJackpot.js`, `hwCleanupStickyLayer.js`, `hwCreateStickyCoin.js`, `hwUpdateGrid.js` (muerto), `hwUpdateJackpot.js`, `hwUpdateJackpotInitial.js`, `initializeGame.js`, `initializeSoundVolumes.js`, `parseSymbols.js`, `playWinPopUp.js`, `reelSpin.js`, `reelTickerActor.js`, `renderConfigTab.js`, `renderPaytableTab.js`, `renderRulesTab.js`, `resetSymbols.js`, `setupSlot.js`, `syncBetUI.js`, `syncReelVisibility.js` (muerto), `transitionActor.js`, `updateBet.js`, `winLoopLogic.js`, `winTierCelebrationActor.js`.
**UI:** `public/ui/` — `createAccumulator.js`, `createAutoplayStatus.js`, `createSpinCounter.js`, `createUI.js`, `getActiveConfig.js` (muerto), `hwAnimateCollectAllPrizes.js`, `hwAnimateMultipliers.js`, `hwCreateJackpot.js`, `setUIInteraction.js`, `spinCounter.js`, `UIManager.js`.
**Utilidades:** `public/utils/` — `animateParticles.js`, `animateRollingWin.js`, `animationPaylineBorder.js`, `animationPaylineCenter.js`, `animationPaylinePoints.js`, `applyReelsLayout.js`, `applyUILayout.js`, `betChangeAction.js`, `buildFontTextureMap.js`, `clickCatcher.js`, `createAutoplayMenu.js`, `createAutoplayToggle.js`, `createDropdownSelector.js`, `createDustParticles.js`, `createDynamicText.js`, `createShineEffect.js`, `createSliderToggle.js`, `createSymbolInstance.js`, `denominationUpdate.js`, `formatMoney.js`, `hwGetPrizeValue.js` (muerto), `hwIsCoin.js`, `installBitmapFont.js`, `playSound.js`, `pulseEffect.js`, `resize.js`, `viewport.js`.

*("muerto" = código no importado/invocado desde ningún punto activo detectado, o roto si se invocara — ver sección 16 para el detalle de cada caso.)*
