# 3. El módulo de juegos y la anatomía de un módulo

> **Hallazgo clave que cambia las expectativas:** el módulo de juegos de Oasis
> **NO es un sistema multijugador asíncrono por SSB**. Es mucho más simple:
>
> - Cada juego es un **único `index.html` autocontenido** (HTML + CSS + JS
>   inline), 100% client-side, servido como archivo estático y embebido en un
>   `<iframe>`. No hay JS externos, ni sprites, ni carpetas de assets: cada
>   carpeta de juego tiene **solo 2 archivos**: `index.html` + `thumbnail.svg`.
> - La **única integración con SSB** es publicar un mensaje tipo `gameScore`
>   (puntuación individual) para un **Hall of Fame** (ranking global). No hay
>   retos, ni partidas, ni movimientos sincronizados. El "multijugador" se
>   reduce a comparar las puntuaciones de distintos autores leyendo el log SSB.

---

## 3.1 El modelo: `src/models/games_model.js` (67 líneas)

Minimalista. Solo gestiona puntuaciones. Patrón factory estándar:
`module.exports = ({ cooler }) => {...}`.

- **Líneas 5-9**: lista blanca `VALID_GAMES` (Set de ids permitidos). ⚠️ Incluye
  14 juegos pero **NO** `rockpaperscissors` ni `audiopendulum` (que sí están en
  la vista): esos dos **no pueden enviar score** (el modelo los rechaza).
- **Líneas 18-25** `readAll()`: vuelca todo el log con `createLogStream`.
- **Líneas 28-38** `submitScore(game, score)` — único método de escritura:

```js
async submitScore(game, score) {
  if (!VALID_GAMES.has(game)) throw new Error('invalid game');
  const n = Number(score);
  if (!Number.isFinite(n) || n < 0 || n > 9999999) throw new Error('invalid score');
  const ssbClient = await openSsb();
  return new Promise((resolve, reject) => {
    ssbClient.publish({ type: 'gameScore', game, score: Math.round(n) },
      (err, msg) => err ? reject(err) : resolve(msg));
  });
}
```

  El mensaje SSB resultante: `{ type: 'gameScore', game: '<id>', score: <int> }`.

- **Líneas 40-65** `getHallOfFame()`: recorre el log, filtra
  `content.type === 'gameScore'`, y por cada par `game:author` guarda **solo la
  mejor puntuación**. Devuelve `hall[game] = [{author, score, game, ts}, ...]`
  ordenado descendente y truncado a top-10 por juego.

---

## 3.2 La vista: `src/views/games_view.js` (151 líneas)

- **Líneas 5-22** `getGames()`: catálogo hardcodeado de **16 juegos**. Cada
  entrada es `{ id, title: () => i18n.<key>, desc: () => i18n.<key> }`. Es el
  registro maestro de la vista.
- **Línea 58** `VALID_GAME_IDS`: un **tercer** Set, usado para validar la ruta
  del shell. Incluye los 16.
- **Líneas 26-56** `renderHallOfFame(hall)`: tabla por juego con thumbnail
  (`/game-assets/${id}/thumbnail.svg`), título, descripción y filas (posición,
  `userLink(author)`, score, fecha).
- **Líneas 60-93** `gameShellView(name)`: así se **embebe cada juego**. Valida
  `name` contra `VALID_GAME_IDS` y renderiza un iframe:

```js
iframe({
  src: `/game-assets/${name}/index.html`,
  class: `game-iframe game-iframe-${name}`,
  scrolling: 'no',
  allowfullscreen: true
})
```

- **Líneas 95-151** `gamesView(filter, hall)`: el **lobby**. Barra de filtros
  (`all` / `scoring`). En modo `all` lista cada juego con thumbnail, título,
  récord top y botón PLAY → `/games/${id}`. En modo `scoring` invoca
  `renderHallOfFame`.

> ⚠️ Hay **TRES listas** que deben mantenerse sincronizadas: `getGames()` y
> `VALID_GAME_IDS` (vista) + `VALID_GAMES` (modelo). La inconsistencia de
> `rockpaperscissors`/`audiopendulum` lo demuestra.

---

## 3.3 Estructura de un juego: `src/games/<nombre>/`

17 carpetas: `8ball, arkanoid, artillery, asteroids, audiopendulum, cocoland,
cocoman, ecoinflow, flipflop, labyrinth, neoninfiltrator, pingpong,
rockpaperscissors, spaceinvaders, tetris, tiktaktoe`.

**TODOS los juegos tienen exactamente 2 archivos**:
- `index.html` (6-10 KB): documento completo y autosuficiente, con `<style>` y
  `<script>` inline. Sin dependencias externas, sin CDN.
- `thumbnail.svg` (~1-2 KB): miniatura SVG vectorial (viewBox ~`0 0 400 220`).

No existe distinción "arcade" vs "social SSB": todos son arcade HTML5/canvas o
DOM puro, client-side, en iframe. La única diferencia es si **envían score** o
no.

**Mecanismo de envío de score** (idéntico en todos los juegos puntuables;
ejemplo `tiktaktoe/index.html`):

```html
<div id="scoreSubmit" style="display:none">
  <form method="POST" action="/games/submit-score" target="_top">
    <input type="hidden" name="game" value="tiktaktoe">
    <input type="hidden" id="scoreInput" name="score" value="0">
    <button type="submit">Submit Score to Hall of Fame</button>
  </form>
</div>
```

Claves del patrón:
- `target="_top"` → el POST **rompe el iframe** y navega la ventana superior
  (necesario porque el juego corre sandboxeado dentro de `/games/<name>`).
- El JS del juego rellena `#scoreInput` al terminar
  (`document.getElementById('scoreInput').value = score;`) y muestra el `div`.
- Cada juego tiene una topbar con `<a href="/games" target="_top">← Back</a>`.

El resto del `index.html` es lógica de juego propia en JS vanilla (tiktaktoe usa
minimax con poda alfa-beta; tetris usa canvas).

`gameScore` además es un tipo SSB de primera clase **indexado en búsqueda**
(`src/views/search_view.js` lo mapea a `/games/<game>` y lo renderiza con su
thumbnail).

---

## 3.4 Cómo se sirve y enruta

**Servido estático** — `src/client/middleware.js` (líneas 109-111):

```js
const gamesStatic = new Koa();
gamesStatic.use(koaStatic(join(__dirname, "..", "games")));
app.use(mount("/game-assets", gamesStatic));
```

Sub-app Koa con `koa-static` sobre `src/games/`, montada en `/game-assets`. Por
eso `/game-assets/tetris/index.html` sirve `src/games/tetris/index.html`.
**Independiente del router principal** → una carpeta nueva queda servida
automáticamente.

**Rutas dinámicas** — `src/backend/backend.js`:
- **Línea 598**: `const gamesModel = require('../models/games_model')({ cooler });`
- **Línea 1168**: `const { gamesView } = require("../views/games_view");`
- **Líneas 1358-1373**:

```js
.get('/games', async (ctx) => {
  if (!checkMod(ctx, 'gamesMod')) { ctx.redirect('/modules'); return; }
  const filter = ctx.query.filter === 'scoring' ? 'scoring' : 'all';
  const hall = await gamesModel.getHallOfFame();
  ctx.body = gamesView(filter, hall);
})
.get('/games/:name', async (ctx) => {
  if (!checkMod(ctx, 'gamesMod')) { ctx.redirect('/modules'); return; }
  const { gameShellView } = require('../views/games_view');
  ctx.body = gameShellView(ctx.params.name);
})
.post('/games/submit-score', koaBody(), async (ctx) => {
  if (!checkMod(ctx, 'gamesMod')) { ctx.redirect('/modules'); return; }
  const { game, score } = ctx.request.body;
  try { await gamesModel.submitScore(game, score); } catch (_) {}
  ctx.redirect('/games?filter=scoring');
})
```

Flujo completo:

```
  /games  (lobby)
     └─ PLAY → /games/:name  (shell con iframe)
                  └─ iframe carga /game-assets/:name/index.html
                        └─ al terminar: POST /games/submit-score  (target="_top")
                              └─ redirect /games?filter=scoring  (Hall of Fame)
```

El guard `checkMod(ctx, 'gamesMod')` (líneas 170-177) revisa la config del
servidor + una cookie por-usuario; default `on`. Si está off → `/modules`.

---

## 3.5 Cómo se registra el módulo (puntos de integración)

| Lugar | Archivo | Qué hay |
|---|---|---|
| **Menú** | `src/views/main_views.js:898` | `renderGamesLink()` (emite el `navLink` si `gamesMod==="on"`); se invoca en línea 1280 dentro del `navGroup` "creative" |
| **Config** | `src/configs/oasis-config.json` | `"gamesMod": "on"` dentro de `modules` |
| **Página /modules** | `src/views/modules_view.js:23` | `{ name: 'games', label, description }` |
| **Listas de backend** | `src/backend/backend.js` | `'games'` en línea 1346 (GET /modules), 7054 (POST /save-modules), 7038 (`ALL_MODULES`) y presets (7040-7042) |
| **i18n** | `src/client/assets/translations/oasis_*.js` (×11) | `gamesTitle`, `gamesDescription`, `modulesGamesLabel/Description`, y por juego `games<Nombre>Title` + `games<Nombre>Desc` |

> El nombre del módulo está **hardcodeado y duplicado en muchos sitios**; no hay
> un registro central único. Por eso, al crear un módulo, cada punto es
> obligatorio. → [04-tutorial-modulo-desde-cero.md](04-tutorial-modulo-desde-cero.md)

---

## 3.6 Receta rápida: añadir un juego NUEVO al sistema existente

Para añadir un juego `foobar`:

1. **`src/games/foobar/index.html`** — HTML autocontenido (CSS+JS inline).
   Incluir la topbar `← Back` y, si puntúa, el form oculto a
   `/games/submit-score` con `target="_top"` y `name="game" value="foobar"`.
2. **`src/games/foobar/thumbnail.svg`** — miniatura.
3. **`src/views/games_view.js`**:
   - Añadir a `getGames()`:
     `{ id: 'foobar', title: () => i18n.gamesFoobarTitle, desc: () => i18n.gamesFoobarDesc }`.
   - Añadir `'foobar'` a `VALID_GAME_IDS`.
4. **`src/models/games_model.js`** — añadir `'foobar'` a `VALID_GAMES`
   (**obligatorio si puntúa**, o `submitScore` lo rechaza).
5. **i18n** — añadir `gamesFoobarTitle` y `gamesFoobarDesc` en TODOS los
   `oasis_*.js`.

No hay que tocar el backend ni el static serving (la carpeta nueva ya se sirve
por `/game-assets/`).
