Oasis 0.9.5 con Karvan para escritorio

Rama de desarrollo experimental sobre el Oasis de epsylon, sin afiliacion con el
proyecto original. Base: 0.9.5.

Trae el modulo Karvan —salas efimeras en memoria con autodestruccion por TTL, chat
que funciona sin JavaScript y llamadas de audio y video sobre WebRTC— con el enlace
en el menu lateral por el mismo patron que el resto de modulos: renderKarvanLink
calcado de renderPollsLink, entrada en modules_view y en la lista de /modules.

Los estilos del modulo van en su propia hoja, styles/karvan.css, siguiendo el patron
de highlight.css: asi se ve igual con cualquier tema, sin depender del tema movil.

Sin servidores ICE de terceros: no hay STUN de fabrica, porque un STUN aprende la IP
publica y el momento de cada llamada. Solo candidatos host salvo que se configure un
TURN propio en oasis-config.json, con credenciales efimeras por el mecanismo REST de
coturn.

El codigo propio vive separado —views/fork/, backend/fork_routes.js,
models/karvan_model.js y translations/fork/— de modo que sobre los ficheros de
upstream solo hay enganches de una linea y cada version nueva se integra sin
arrastrar nada.

Respecto al arbol de Android: fuera el wrapper y main.js, que es su arranque; el
tema vuelve a Dark-SNH y se restauran los modulos que en movil se recortan por peso.

Verificado arrancando sin OASIS_MOBILE: nueve rutas OK, menu lateral de upstream con
27 grupos y el enlace de Karvan, cero rastro de la interfaz movil (ni hexagonos, ni
topbar, ni barra inferior), y una sala creada y servida.
This commit is contained in:
SITO 2026-08-19 00:35:08 +02:00
commit ddd2787b73
286 changed files with 157145 additions and 0 deletions

View file

@ -0,0 +1,216 @@
# 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/`).