|
|
# 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.
|