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.

16 KiB

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 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 define reglas, presentación, persistencia y migración. El servidor actual aún requiere el refactor para hablar este protocolo.

Las fuentes verificables del contrato son schema.ts, los tipos del SDK, los esquemas JSON y los mensajes de ejemplo. 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.

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:

{
  "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. 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 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 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 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 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:

{
  "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:

{
  "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 } }
}
{ "kind": "match", "revision": 12 }
{ "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 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:

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 y los sobres de red.

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.