You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

184 lines
18 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# Formato de paquetes y protocolo de partidas — Juegoland v1
El catálogo se limita a juegos propios publicados por el equipo. La [propuesta de arquitectura Go](../platform/architecture.md) distingue este formato/protocolo de datos del SDK ejecutable TypeScript: el perfil nativo Go y un posible transporte WebSocket requieren contratos versionados propios. Este documento y los esquemas v1 no adquieren soporte Go por esa recomendación.
**Estado:** contrato propuesto para el motor propio, 5 de octubre de 2026. Este documento explica cómo se distribuye un juego y cómo se comunica una partida. El [modelo completo](../game-engine-proposal.md) define reglas, presentación, persistencia y migración. El servidor actual aún requiere el refactor para hablar este protocolo.
**Ampliación multidispositivo:** el [perfil de superficies y controles v2](multi-device-profile.md) declara `TVBoard`, `Desktop` y `Mobil`, proyecciones diferenciadas y distribución por suscripción/contrato. Distingue mesa por QR sin invitaciones y TV personal enlazada tras invitación, con cambio a móvil completo. El [perfil temporal](synchronization-profile.md) propone reloj, rondas y equidad de respuestas. No se modifica silenciosamente este formato/protocolo v1 ni se consideran implementadas estas ampliaciones por existir documentación.
**Clientes web y nativos:** el [perfil común de comunicación v2](../platform/client-communication-profile.md) contempla web Svelte y una app Flutter posterior **solo de controles**: datos/acciones neutrales del mando, SDK TypeScript/Dart, controles por runtime, acceso propio y WSS bidireccional. Tableros y vistas completas permanecen en la web; Flutter solo representa `player-controller` de `TVBoard`. Las firmas/módulos TypeScript y eventos Socket.IO de v1 no se ejecutan en Dart ni se convierten a WebSocket simple automáticamente.
**Jugadores virtuales:** el [perfil de agentes v2](virtual-player-profile.md) propone que cada juego declare esta opción en sus propiedades e incluya las instrucciones versionadas de sus agentes. La plataforma ejecuta los perfiles admitidos y el motor valida sus acciones. Es una ampliación pendiente, sin campos nuevos en los esquemas v1.
Las fuentes verificables del contrato son [schema.ts](schema.ts), los [tipos del SDK](contracts.ts), los [esquemas JSON](package.schema.json) y los [mensajes de ejemplo](examples/wire.json). Los esquemas JSON describen estructura; las comprobaciones entre campos y paquetes están en los validadores de `schema.ts`.
## 1. Un paquete de juego
El manifiesto de distribución es `game.json`. Una biblioteca de componentes o recursos usa `package.json`. Ambos tienen:
| Campo | Tipo y significado |
| ------------------ | --------------------------------------------------------------------------------- |
| `format` | Literal `juegoland.package`. |
| `formatVersion` | Entero `1`. Cambia cuando cambia la estructura del manifiesto. |
| `id` | Identificador estable, 1–80 caracteres, comienza por letra minúscula. |
| `version` | Versión exacta `mayor.menor.parche`, sin rangos ni sufijos. |
| `engineApiVersion` | `1`; versión del contrato de funciones del motor. |
| `metadata` | Nombre, descripción, autores, licencia, idioma principal y rutas de traducciones. |
| `dependencies` | Lista de `{ id, version, kind }`, con tipo `components` o `assets`. |
| `kind` | `game`, `components` o `assets`. |
Los catálogos declarados en `metadata.locales` forman parte de la distribución versionada. La publicación interna debe exigir cobertura de los idiomas activos de la [plataforma](../platform/localization.md), incluidos reglas, acciones, ayuda, textos alternativos y recursos que contengan texto. El idioma elegido solo modifica presentación; identificadores, acciones y resultados del motor son estables entre lenguas.
El manifiesto de juego añade:
| Campo | Estructura |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `classification` | `category`, `tags` y `competition`: `individual`, `teams`, `cooperative`, `solo` u `open`. |
| `players` | `min` y `max`, enteros de 1 a 64, con `min ≤ max`. |
| `capabilities` | Booleanos `hiddenInformation`, `simultaneousActions`, `deadlines`. |
| `components` | Mapa por identificador. Cada entrada declara `kind`, `definition` y `copies` (1–100). |
| `rules` | `entry`, `version`, versiones de los esquemas de estado, vista y eventos, y rutas de los esquemas de configuración, datos, vista, contexto, evento interno y evento visible. |
| `presentation` | `entry`, `apiVersion: 1`, `bundleVersion`, `resources`, `bindings` y `catalog`. |
Una biblioteca contiene `exports: { nombre: ruta }`. El código de reglas puede cargar bibliotecas `components`; la presentación puede cargar `assets`. Una biblioteca solo puede depender de otras de su mismo tipo.
### Referencias y recursos
Una definición local usa `{ "source": "local", "path": "components/board.json" }`. Una compartida usa una referencia por paquete y exportación:
```json
{
"source": "dependency",
"packageId": "playing-cards",
"export": "poker52"
}
```
La dependencia fija `playing-cards@1.0.0` en `dependencies`. El cargador rechaza exportaciones ausentes, dependencias circulares y versiones distintas del mismo paquete dentro del árbol de un juego.
Los componentes permitidos son `deck`, `dice`, `wheel`, `board`, `pieces` y `custom`. Su estructura está en [component.schema.json](component.schema.json). Una definición describe lo que existe; cada partida crea instancias con identidad propia. En Solitario, `poker52` define 52 caras y `mainDeck` crea una copia. Sus ilustraciones vienen por separado de `cards-poker/standard`. El [manifiesto de Solitario](examples/solitario.game.json) contiene todos esos campos como ejemplo estructural.
`presentation.resources` es un mapa de entradas con:
| Campo | Significado |
| ---------------------------- | ---------------------------------------------------------------------- |
| `kind` | `image`, `atlas`, `audio`, `font` o `graphic`. |
| `path` | Ruta relativa dentro de la distribución. |
| `load` | `catalog`, `board`, `phase` o `lazy`; `phase` exige el nombre de fase. |
| `required` | Si el recurso es necesario para representar el juego. |
| `fallback` | `text` con clave traducible, otro `resource` u `omit` si es opcional. |
| `width`, `height`, `regions` | Geometría de imágenes y atlas. |
| `altKey` | Clave de texto alternativo, cuando corresponda. |
`catalog` identifica obligatoriamente `cover`, `thumbnail` e `icon` entre esos recursos. `bindings` asocia componentes con arte local o de biblioteca. Un [conjunto visual](examples/poker.resource-set.json) contiene slots por carta, recursos y reverso; debe cubrir todas las caras que el renderer vaya a mostrar. Un recurso `graphic` implementa `GraphicDefinition` y devuelve un SVG a partir del slot y su definición.
Todos los `entry`, esquemas, recursos y exportaciones son rutas relativas normalizadas. El publicador debe comprobar que cada archivo existe, permanece dentro del paquete, tiene el tipo esperado y coincide con su hash. Los manifiestos de ejemplo describen esas rutas, pero **todavía no contienen los módulos ni los archivos necesarios para instalar los juegos**.
### Lock y versiones
El [lock.schema.json](lock.schema.json) fija raíz, dependencias resueltas y archivos con SHA-256, tamaño, tipo de contenido y URL pública opcional. El código privado de reglas tiene `url: null`. Los hashes se calculan de los bytes reales y de inventarios JSON canónicos; [la especificación general](../game-engine-proposal.md#publicación-y-lock) define el orden y la canonicalización.
Una partida fija `packageDigest`, `dependencyLockDigest`, versión de reglas y versión del esquema persistido. No se interpreta un estado antiguo con una versión nueva por coincidencia de identificador. `bundleVersion` identifica cambios de presentación y `protocolVersion` identifica el contrato de red.
## 2. Mensajes del protocolo
Todos los mensajes llevan `protocolVersion: 1` y `type`. Salvo `game.error` antes de identificar una partida válida, llevan `matchId` UUID. `game.sync` y `game.command-status` llevan `requestId` UUID; los comandos usan `commandId` UUID para idempotencia.
El transporte previsto es Socket.IO sobre el namespace principal. Todos los sobres v1 se envían como un objeto JSON mediante un único evento `game:message`, en ambos sentidos; los recibos usan ese evento, no el callback de acuse de Socket.IO. El campo `type` distingue el mensaje. El adaptador anterior podrá convivir hasta migrar los clientes. El cliente se autentica en la plataforma y el servidor deriva su asiento. **Ningún comando de cliente contiene `actor` ni un estado nuevo.** El límite inicial del sobre es 64 KiB de JSON y profundidad 32, además de los límites específicos de cada juego.
| `type` | Dirección | Campos específicos |
| ---------------------- | ------------------ | ----------------------------------------------------------------------------------------------------------------------- |
| `game.sync` | Cliente → servidor | `requestId`, `knownRevision` numérica o nula, `client` con versiones de API y tuplas de presentación admitidas. |
| `game.snapshot` | Servidor → cliente | `subscriptionId`, `delivery`, `revision`, `game`, `status`, `view`, `context`, `availableActions`, `outcome`, `events`. |
| `game.command` | Cliente → servidor | `commandId`, `expected`, `action: { type, payload }`. |
| `game.ack` | Servidor → cliente | `commandId`, `duplicate`, `receipt`. |
| `game.command-status` | Cliente → servidor | `requestId`, `commandId`. |
| `game.command-unknown` | Servidor → cliente | `requestId`, `commandId`. |
| `game.cursor` | Servidor → cliente | `subscriptionId`, última `revision` confirmada. |
| `game.unsubscribe` | Cliente → servidor | `subscriptionId` propio. |
| `game.error` | Servidor → cliente | `code`, `recovery`, `messageKey` y los identificadores disponibles. |
### Sincronización y vista
`game.sync.client` declara `engineApiVersion: 1`, `presentationApiVersion: 1` y al menos un contrato exacto:
```json
{
"id": "solitario",
"rulesVersion": "1.0.0",
"viewSchemaVersion": 1,
"eventSchemaVersion": 1
}
```
El servidor comprueba la tupla completa antes de entregar una vista. Un snapshot de sincronización declara `delivery: { "kind": "sync", "requestId": "…" }` y `events: []`. Su `subscriptionId` identifica el flujo nuevo. El servidor instala primero la suscripción, almacena cambios concurrentes, lee la revisión R confirmada, envía el snapshot R y después entrega los cambios posteriores a R en orden.
Un snapshot vivo declara `delivery: { "kind": "live", "fromRevision": R }` y lleva `revision: R + 1`. Contiene una vista completa autorizada para ese destinatario. `view` y `context` deben cumplir además los esquemas específicos del juego. `game` contiene la tupla de contrato usada. `status` es `active`, `completed` o `cancelled`; `outcome` es `null` exactamente mientras está activo. Las partidas terminadas no ofrecen acciones.
Cada evento visible lleva `eventId` UUID estable, revisión, `order` consecutivo desde 0, tipo y payload. Una regla puede ocultar un evento interno o proyectarlo en varios visibles. La sincronización no reproduce eventos anteriores. Si el cliente pierde una revisión, recibe una revisión duplicada, vuelve al primer plano o ve un cursor por delante de su vista, se resincroniza. Solo acepta la respuesta de su solicitud más reciente y los mensajes de su suscripción actual.
### Acción, precondiciones y recibo
Una acción usa `{ "type": "drop", "payload": { "column": 3 } }`. El módulo del juego valida ese payload. Hay dos precondiciones:
Ejemplo de comando completo para Conecta 4:
```json
{
"protocolVersion": 1,
"type": "game.command",
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9",
"expected": { "kind": "match", "revision": 12 },
"action": { "type": "drop", "payload": { "column": 3 } }
}
```
```json
{ "kind": "match", "revision": 12 }
```
```json
{ "kind": "actor", "phaseEpoch": 0, "actorRevision": 2 }
```
`match` exige la revisión global exacta. `actor` se permite solo para operaciones independientes declaradas por el módulo, como colocar un barco privado. Ambas se revalidan bajo el bloqueo de la partida; al cambiar de fase aumenta `phaseEpoch` y una colocación tardía se rechaza.
El recibo aplicado es `{ "status": "applied", "appliedRevision": 13 }`. El rechazado definitivo es `{ "status": "rejected", "code": "STALE_REVISION", "observedRevision": 13 }`; los códigos permitidos son `STALE_REVISION`, `STALE_PHASE`, `STALE_ACTOR`, `ILLEGAL_ACTION` y `MATCH_FINISHED`. Errores transitorios usan `game.error`, no un rechazo definitivo.
La identidad idempotente es partida + principal autenticado + `commandId`. El servidor guarda el recibo con el estado confirmado. Repetir **el mismo sobre completo**, incluida la precondición, devuelve el mismo recibo con `duplicate: true`; reutilizar el UUID con otro sobre da `COMMAND_ID_REUSED`. El cliente conserva los comandos pendientes y reenvía el mismo UUID después de un corte. `game.command-unknown` significa que no hay recibo almacenado al consultar: un envío retrasado todavía podría llegar.
La vista puede llegar antes o después del recibo. El recibo informa sobre la operación; la vista del snapshot determina qué se muestra. Un cambio confirmado incrementa la revisión una vez; una consulta o rechazo no la incrementa.
### Resultados, privacidad y errores
`outcome` completado contiene `reasonCode` y un mapa por asiento. Cada participante tiene `outcome: win | loss | draw | neutral`, `metrics` y puntuación, puesto o equipo opcionales. Esto admite derrota en Solitario y derrota de todos en un juego cooperativo. La cancelación usa `{ "kind": "cancelled", "reasonCode": "…" }`. El módulo decide cuándo terminan sus reglas y qué resulta de una retirada.
El servidor proyecta por destinatario `view`, `context`, ofertas, resultado y eventos. Ninguno copia automáticamente el estado privado. El esquema de red acepta objetos JSON en esos campos; los esquemas del juego y las pruebas de privacidad completan la validación.
`game.error` usa códigos del [esquema de protocolo](protocol.schema.json) y una recuperación explícita: `none`, `sync`, `retry-same-command`, `reload-client` o `reauthenticate`. No incluye datos privados ni trazas internas.
## 3. Orden de procesamiento y entrega
1. Validar sobre y límites, autenticar, autorizar asiento y buscar un recibo previo.
2. Bloquear la partida, verificar la versión fijada y las precondiciones.
3. Ejecutar reglas síncronas con servicios deterministas; validar estado y proyecciones.
4. Confirmar juntos estado, revisión, recibo, eventos, azar, plazos y publicaciones pendientes.
5. Enviar el recibo y publicar snapshots completos en orden de revisión.
6. Ante fallo de envío, reintentar la publicación pendiente. El cliente descarta duplicados y recupera huecos con `game.sync`.
El cursor periódico detecta incluso la última publicación perdida. La frecuencia inicial prevista es 15 segundos mientras existe suscripción. El servidor de producción de varias instancias necesitará coordinación de bloqueo, publicaciones y suscripciones.
## 4. Cómo comprobar el formato
Desde la raíz del repositorio:
```powershell
node docs/game-engine/verify.ts
.\node_modules\.bin\tsc.cmd --project docs/game-engine/tsconfig.json
```
El primer comando valida ejemplos, dependencias, invariantes comunes y que los seis JSON Schema coincidan con `schema.ts`. El segundo comprueba las firmas TypeScript. Véanse los [ejemplos de paquetes](examples/README.md) y los [sobres de red](examples/wire.json).
Estas comprobaciones verifican el **diseño del formato**. El compilador de paquetes, los archivos reales de cada ejemplo y las pruebas del servidor, sockets y navegador pertenecen a la implementación del motor.

Powered by TurnKey Linux.