51 KiB
Motor propio de Juegoland: especificación v1
Estado: diseño consolidado el 5 de octubre de 2026, con contratos y validadores ejecutables. El motor de producción conserva todavía su implementación actual. Esta especificación define el objetivo del refactor; no certifica que el servidor ya lo implemente.
La decisión es construir un motor propio con paquetes de juego, reglas autoritativas en el servidor y presentación independiente. El primer alcance son los juegos actuales por turnos y las acciones simultáneas independientes de Hundido. El modelo también representa solitarios, equipos y cooperación, sin obligar a implementar ahora nuevos juegos, un editor visual, partidas masivas o una plataforma de código subido por terceros.
Actualización de alcance: el usuario confirma que todos los juegos serán propios y los publicará exclusivamente el equipo. La arquitectura de plataforma fija API y servidor/motor propio Go para games2; esta especificación v1 conserva por ahora su SDK TypeScript y perfil de módulos. Las funciones TS no se ejecutan nativamente en Go: se requiere SDK equivalente y perfil versionado de paquete antes de portar reglas. El protocolo JSON y los recursos pueden conservar su semántica; cambiar lenguaje no demuestra equivalencia ni cambia automáticamente el transporte Socket.IO.
Ampliación decidida el 6 de octubre: un juego podrá admitir plazas controladas por agentes IA y deberá declararlo en sus propiedades, incluyendo sus instrucciones. El perfil de jugadores virtuales v2 concreta la propuesta de paquete, privacidad y ejecución; todavía no cambia los contratos ejecutables v1. El controlador decide una acción fuera de la transacción y las reglas conservan su autoridad.
1. Qué constituye la especificación
| Artefacto | Responsabilidad |
|---|---|
| schema.ts | Fuente de los esquemas estructurales y validadores semánticos comunes. |
| contracts.ts | SDK de funciones de reglas, componentes, persistencia y presentación. |
| package.schema.json | Manifiestos de juegos y bibliotecas. |
| component.schema.json | Barajas, dados, ruletas, tableros, piezas y extensiones. |
| resource-set.schema.json | Recursos visuales asociados a las definiciones de un componente. |
| lock.schema.json | Versiones resueltas y hashes de archivos del paquete publicado. |
| protocol.schema.json | Mensajes del protocolo de partidas. |
| Formato y protocolo | Documento independiente del paquete y de los mensajes de red. |
| presentation-plan.schema.json | Planes de animación y sonido. |
| Ejemplos y perfiles | Manifiestos completos como registros de datos y escenarios de conformidad. |
| verify.ts y type-check.ts | Comprobaciones de estructura, referencias, invariantes y tipos. |
Los seis esquemas JSON se generan desde Zod, con dialecto JSON Schema 2020-12; no se mantienen a mano. Sus identificadores urn:juegoland:…:1 son identificadores del contrato, no servicios web. El SDK infiere de esos esquemas las estructuras compartidas cuando procede. Las firmas de las funciones permanecen en TypeScript: JSON no contiene funciones.
Hay cuatro niveles de validación: estructura del documento, coherencia entre sus campos y dependencias, existencia y contenido de los archivos del paquete, y comportamiento de las reglas y del runtime. Los validadores adjuntos cubren los dos primeros en los casos indicados; no sustituyen el futuro compilador de paquetes ni las pruebas del motor.
2. Referencias publicadas y decisiones propias
No hay en las fuentes consultadas un único formato que resuelva conjuntamente los requisitos de esta plataforma. Se toman ideas concretas, conservando un contrato propio y pequeño.
| Fuente primaria | Qué aporta al diseño |
|---|---|
| Foundry VTT: Game System Development | Manifiesto de sistema, compatibilidad, dependencias, módulos, recursos y evolución de modelos. |
| Tabletop Playground: Packages | Paquetes con manifiesto, miniatura, objetos, texturas y scripts; reutilización de componentes. |
| boardgame.io: contratos de tipos | Separación entre configuración inicial, movimientos, fases, etapas y vistas por jugador. |
| Board Game Arena: estados | Participación de un jugador, varios jugadores y estados automáticos como conceptos diferentes. |
| Board Game Arena: interfaz y notificaciones | Secuenciar efectos mediante promesas y resolverlos también cuando se desactivan las animaciones. |
| Tabletop Simulator: Save File Format | Serialización de objetos, recursos y scripts. Útil para componentes; no equivale al contrato de reglas autoritativas de Juegoland. |
| Ludii: Language Reference | Separación declarativa de jugadores, equipamiento y reglas. No se adopta su lenguaje ni se crea otro DSL universal en v1. |
| Socket.IO: Delivery guarantees | El orden de los mensajes entregados no garantiza su entrega. La fiabilidad de comandos y sincronización corresponde al protocolo de aplicación. |
| JSON Schema 2020-12 y Zod: JSON Schema | Formato verificable y generación de esquemas a partir de la definición canónica. |
Las políticas de concurrencia, proyección, recursos, idempotencia y presentación descritas a continuación son decisiones de Juegoland. No se atribuyen a esos proyectos ni requieren importar sus motores.
3. Responsabilidades y límites
| Capa | Es responsable de |
|---|---|
| Plataforma | Identidad, salas, permisos, pertenencia, transporte y almacenamiento. |
| Núcleo del motor | Carga de versiones, validación, serialización de comandos, revisiones, azar, transacciones, recibos y plazos. |
| Módulo de reglas | Legalidad, datos del juego, fases, actores, resultados, retiradas y proyección de información. |
| Biblioteca de componentes | Definiciones reutilizables y operaciones genéricas sin reglas particulares de un juego. |
| Paquete de presentación | Renderizado, recursos, accesibilidad y planes de efectos. |
| Coordinador de presentación | Vista confirmada y mostrada, ejecución de planes, cancelación, audio y recuperación. |
La especificación de plataforma v2 y su API/eventos concretan identidad, salas, chat, invitaciones y operación. La plataforma proporciona almacenamiento y transporte; aplicación y núcleo comparten una única unidad de trabajo para cambios coordinados de sala/partida. El núcleo controla la transición y proyección de partida, sin confirmar una transacción independiente de la sala. La adaptación del SDK debe permitir participar en esa unidad de trabajo.
flowchart LR
U[Acción del usuario] --> C[Comando validado]
C --> N[Núcleo y reglas]
N --> T[Transacción: estado, recibo y publicación]
T --> P[Proyección por destinatario]
P --> V[Vista confirmada]
V --> Q[Coordinador de presentación]
Q --> R[Vista mostrada]
El servidor decide el resultado de una tirada o un movimiento antes de cualquier animación. Ningún efecto, sonido ni confirmación visual del cliente determina una regla o impide que el servidor avance.
4. Formato de paquete
El manifiesto se llama game.json para juegos y package.json para bibliotecas dentro de su directorio de distribución. Su campo format es siempre juegoland.package; el nombre del archivo no lo convierte en un paquete npm.
Un juego contiene obligatoriamente:
- Identidad, versión exacta, autores, licencia, descripción e idiomas.
- Clasificación: categoría, etiquetas y modalidad competitiva, cooperativa, por equipos, individual, solitaria o abierta.
- Intervalo de participantes y capacidades de información oculta, acciones simultáneas y plazos.
- Componentes declarados: tipo, definición local o exportación de una dependencia y número de copias.
- Entrada de reglas, versión de reglas y esquemas de configuración, datos, vista, contexto, eventos internos y visibles.
- Entrada de presentación, versión del bundle, recursos, asociaciones visuales de componentes y portada, miniatura e icono.
En el futuro formato v2, el soporte opcional de jugadores virtuales pertenece también al paquete: perfiles, modalidades, dificultades, instrucciones incluidas y límites. No se deduce de la categoría del juego ni se activa desde un prompt genérico de plataforma.
Las bibliotecas tienen kind: components o kind: assets y exportaciones con nombre. Una referencia a dependencia es un objeto inequívoco:
{
"source": "dependency",
"packageId": "playing-cards",
"export": "poker52"
}
La versión se resuelve en dependencies: por ejemplo, playing-cards@1.0.0. No se admiten latest, rangos, @1 ni referencias implícitas. Una referencia local tiene source: local y una ruta relativa normalizada.
Las bibliotecas de componentes solo dependen de componentes; las de recursos, de recursos. El juego puede depender de ambas. Esto permite cargar las reglas sin cargar imágenes y compilar el cliente sin incorporar el módulo privado de reglas.
Se rechazan dependencias circulares, autorreferencias, exportaciones inexistentes y dos versiones del mismo identificador dentro del árbol de un paquete. El registro general sí puede alojar diferentes versiones utilizadas por juegos distintos. Los campos desconocidos del contrato se rechazan; las extensiones futuras requerirán un campo o una versión explícitos.
Publicación y lock
El proceso de publicación debe resolver todas las dependencias y producir un lock inmutable con hashes SHA-256 reales, tamaño, tipo de contenido y URL pública de cada archivo cuando corresponda. Los archivos privados de reglas tienen url: null y no se entregan al navegador. La API pública de recursos expone únicamente el subconjunto autorizado del lock.
Para evitar hashes circulares, el digest de un paquete se calcula sobre su manifiesto y el inventario ordenado de sus archivos propios, excluyendo el lock. Cada archivo se identifica por sus bytes exactos. La representación del digest es { manifest, files: [{ path, sha256, bytes, mediaType }] }, con archivos ordenados por ruta, serializada en UTF-8 mediante JCS, RFC 8785. El digest del lock se calcula después de resolver los paquetes con la misma canonicalización; sus dependencias se ordenan por identificador y sus archivos por identificador y ruta. Nunca se usa una URL mutable como identidad de contenido.
El publicador comprobará rutas resueltas dentro del paquete, archivos, exportaciones, tipos de contenido, traducciones, hashes y compatibilidad. Las entradas TypeScript se compilan a artefactos separados de servidor y cliente; el inventario de distribución refleja esas rutas finales. Un ejemplo de manifiesto de fuente no se considera instalable sin ese proceso.
5. Versiones y partidas existentes
| Campo | Qué cambia |
|---|---|
formatVersion |
Forma del manifiesto; v1 en esta especificación. |
engineApiVersion |
Contrato del SDK de reglas y servicios. |
version del paquete |
Cualquier cambio publicado en el contenido del paquete. |
rules.version |
Comportamiento de las reglas; incluye correcciones que cambien resultados. |
stateSchemaVersion |
Forma persistida de los datos privados del juego. |
viewSchemaVersion |
Vista y contexto entregados al cliente. |
eventSchemaVersion |
Eventos internos y visibles, con sus respectivos esquemas. |
presentation.apiVersion |
Contrato del renderer y los efectos. |
presentation.bundleVersion |
Recursos y comportamiento visual. |
protocolVersion |
Sobres y semántica de los mensajes de red. |
Una partida fija el paquete, las reglas, las dependencias y sus digests al crearse. Una corrección visual no actualiza silenciosamente ese lock durante la partida. Una publicación nueva puede reutilizar la misma versión de reglas solo si el artefacto de reglas y sus dependencias lógicas son idénticos.
Los módulos antiguos permanecen disponibles mientras existan partidas que los requieran. Una migración de estado es una operación administrativa explícita, con origen, destino, validación y posibilidad de restauración; nunca un alias que interprete datos antiguos con reglas nuevas. La migración concreta de partidas actuales se decidirá después de disponer de fixtures de sus estados reales.
La negociación del cliente declara tuplas completas { id, rulesVersion, viewSchemaVersion, eventSchemaVersion }. No se combinan listas independientes de versiones que puedan producir una compatibilidad inexistente. Un cliente incompatible recibe INCOMPATIBLE_CLIENT; no recibe una vista que no sabe interpretar.
6. Componentes: definiciones, instancias y zonas
Una baraja compartida define las cartas posibles, no una partida ni su aspecto. Solitario referencia playing-cards/poker52; su presentación referencia cards-poker/standard. Brisca puede usar otra definición y otro conjunto de ilustraciones.
| Tipo | Definición |
|---|---|
deck |
Cartas con identificador, palo, rango y atributos. |
dice |
Dados, caras, valores y pesos enteros positivos. |
wheel |
Sectores y pesos; las reglas interpretan el sector seleccionado. |
board |
Cuadrícula o grafo de casillas y conexiones dirigidas. |
pieces |
Tipos de piezas, cantidades y atributos. |
custom |
Extensión con typeId, datos y validador registrado por código de confianza. |
No se presupone que el rango de una carta determine su valor en todos los juegos. Brisca y Solitario asignan orden y puntuación en sus propias reglas. Las conexiones de un grafo no se hacen bidireccionales automáticamente.
Las instancias tienen identificador estable dentro de la partida, definitionId, componentId e índice de copia. Dos barajas generan 104 instancias de 52 definiciones; dos ases iguales no comparten identidad. La creación usa un espacio de nombres determinista por componente y copia.
Cada carta pertenece a exactamente una zona. Una zona tiene propietario opcional, orden, visibilidad declarada e identificadores de sus cartas. Los movimientos entre zonas son atómicos: no pierden, duplican ni crean instancias. La creación inicial las coloca en una zona supply; las reglas distribuyen desde ella. Los ayudantes devuelven estructuras nuevas.
La visibilidad de una zona ayuda a validar, pero no sustituye a project. Una carta oculta no debe revelar su identidad estable: incluso sin mostrar su cara, permitiría seguirla después de barajar. El rival recibe cantidades o representaciones opacas adecuadas a la regla.
El perfil multidispositivo v2 distingue TVBoard (TV pública + mando por etapa), Desktop y Mobil (vista completa autorizada + controles). Cada juego declara presentaciones/proyecciones versionadas y combinaciones de modos: mesa presencial con QR sin invitaciones, o admisión convencional que puede enlazar una TV personal. El último recorrido exige cambiar a móvil completo sin nueva plaza ni consumo de invitación. La distribución negocia contrato por suscripción; no reutiliza vista privada ni observador v1 en TV. El gesto de agitar solo solicita una acción cuya aleatoriedad sigue en el servidor.
El perfil de sincronización temporal v2 propone reloj de servidor, rondas y recogida duradera de inputs; revisiones y reconexión no bastan para ordenar reacción humana. La primera llegada no decide automáticamente quién respondió antes. Ventanas de acierto y carreras con compensación/empates son modelos distintos; las últimas siguen experimentales. Cambiar de presentación conserva recibos y se coordina con la fuente temporal fijada para la ronda. Todo ello requiere contratos ejecutables nuevos y no modifica el SDK v1 implícitamente.
Dados y ruletas usan el azar del servidor. Las caras o sectores visuales representan ese resultado; una simulación física del navegador no lo elige. Un componente custom no registrado impide cargar el juego, con un error explícito.
7. Recursos, ilustraciones y catálogo
El bundle declara todos los recursos propios y dependencias necesarias: imágenes, atlas, sonidos, fuentes o gráficos generados mediante código de presentación. Portada, miniatura e icono son referencias obligatorias a recursos de catálogo; pueden ser imágenes o gráficos. No se deducen del nombre del juego.
Cada recurso declara ruta, tipo, estrategia de carga, si es necesario y un fallback. Imágenes y atlas declaran dimensiones; las regiones del atlas deben quedar dentro de ellas. Los textos alternativos y fallbacks de texto usan claves de traducción.
Un recurso graphic exporta GraphicDefinition: genera un SVG nuevo a partir del identificador de recurso, slot, definición e idioma. Su propietario inserta y retira ese SVG. El gráfico no crea temporizadores ni envía comandos; la animación pertenece al coordinador. Así quedan definidos también los recursos procedurales y se puede usar un mismo módulo para todas las caras de una baraja.
Las estrategias son catalog, board, phase y lazy. Una carga por fase declara exactamente su fase. Un recurso necesario puede sustituirse por texto o un recurso alternativo, pero su cadena de fallbacks no puede terminar en omisión. Un sonido opcional puede omitirse. Los fallbacks no excusan archivos ausentes en una publicación: permiten recuperarse de fallos de carga en el cliente.
Una asociación visual de componente apunta a un ResourceSet: recursos, slots por identificador de definición, reverso opcional y proporción visual. El publicador valida la cobertura de las definiciones que deban poder mostrarse y la existencia de las regiones referenciadas. Una baraja oculta necesita reverso; una baraja completa necesita una representación para cada cara, aunque proceda de un renderer común.
Un juego de rol debe declarar sus mapas, personajes, objetos, iconos y sonidos propios o referenciarlos en dependencias exactas. Sus entidades y reglas pueden usar componentes custom. El formato admite el paquete de recursos; un sistema de rol concreto necesitará además su modelo de personajes, acciones y proyección. No se pretende que un campo category: roleplaying implemente esas reglas.
Los archivos públicos pueden permitir conocer las ilustraciones disponibles. La posición secreta de un barco o el reparto real de cartas nunca se codifican en nombres de URL, recursos solicitados observables por rivales ni manifiestos por partida compartidos.
8. Esquema fijo de funciones de reglas
Las firmas exactas están en contracts.ts. Los datos cruzan el límite del módulo como JSON finito, sin funciones, fechas implícitas, mapas, referencias cíclicas ni instancias de clases.
| Función | Obligación |
|---|---|
setup(input, services) |
Crear la transición inicial con datos, flujo, resultado, eventos y plazos. |
validatePosition(position) |
Comprobar invariantes globales del juego; lanzar un error si hay corrupción. |
actions[type].schema |
Validar el comando específico, incluidos sus parámetros y campos permitidos. |
actions[type].apply(position, command, services) |
Rechazar una acción ilegal o devolver la siguiente transición completa. |
availableActions(position, recipient) |
Proponer acciones concretas o parametrizadas visibles para ese destinatario. |
project(position, recipient) |
Construir vista, contexto y resultado autorizados. |
projectEvent({ before, after, event, recipient }) |
Emitir cero o más eventos visibles derivados de cada evento interno. |
onLifecycle(position, event, services) |
Resolver una retirada o un plazo vencido según las reglas del juego. |
Cada manejador declara conflictPolicy: match | actor. El núcleo añade las precondiciones correctas a las acciones ofrecidas; no confía en precondiciones inventadas por la presentación. Las ofertas parametrizadas identifican el esquema registrado de la acción y sus restricciones visibles; no enumeran millones de coordenadas o combinaciones.
Todas las funciones de reglas son síncronas. No acceden a red, base de datos, DOM, archivos, Math.random() ni Date.now(). apply devuelve { ok: false, code: ILLEGAL_ACTION } para una jugada inválida; una excepción significa fallo interno y no confirma cambios parciales.
Los argumentos usan DeepReadonly. Esto protege también arrays y objetos anidados en TypeScript, pero no es una barrera en ejecución: el núcleo debe aislar las estructuras y congelarlas en desarrollo, validar las salidas y probar ausencia de mutación. La limitación de readonly está documentada en TypeScript.
Servicios deterministas
RuleServices expone definiciones inmutables, un instante capturado para la operación, azar etiquetado y ayudantes de cartas. Las etiquetas identifican cada extracción y son únicas en la operación. Las opciones y límites del azar se validan; el resultado real se registra en una traza privada junto con su orden, etiqueta y parámetros.
En ejecución normal, el servidor obtiene entropía segura. En reproducción consume la traza registrada y falla si cambian las peticiones. No se publica una semilla que permita predecir futuras cartas. shuffle no modifica su entrada; las selecciones ponderadas respetan los pesos sin sesgo por reducción modular.
project, projectEvent, availableActions y validatePosition son puras y no reciben azar. La misma posición y destinatario producen la misma salida.
Plazos y retiradas
Un plazo es una intención persistida con identificador, vencimiento y payload interno. El núcleo asigna una generación a cada programación; reemplazar o cancelar invalida disparos anteriores. Al vencer, genera una operación interna idempotente para onLifecycle, valida la generación bajo el bloqueo de la partida y confirma consumo y transición juntos.
Una retirada es una operación de plataforma autorizada. El módulo recibe la posición anterior y el evento; el núcleo conserva el participante con estado withdrawn en la posición siguiente. Las reglas deben retirarlo de los actores habilitados y decidir continuidad o resultado. Una desconexión de transporte no equivale por defecto a una retirada.
Las operaciones automáticas inmediatas se resuelven dentro de la función que las desencadena, con ejecución acotada. Una fase automática persistida debe tener un plazo duradero que la haga avanzar; v1 no tiene callbacks automáticos ocultos ni llamadas recursivas del núcleo a un supuesto next.
9. Estado, fases y resultados
La posición separa configuración inmutable, participantes, datos del juego, flujo y resultado. El sobre persistido añade versiones fijadas, revisión global, época de fase y revisiones por actor. Los identificadores p0, p1, etc. representan asientos; el vínculo con cuentas pertenece a la plataforma.
El flujo contiene fase, ronda, turno y actors, un mapa de asiento a etapa. Las fases declaran participación single, multiple o automatic; sus etapas enumeran tipos de acción permitidos. El núcleo valida esas referencias y que los actores sigan activos. single admite exactamente un actor en posiciones activas estables; multiple, los actores habilitados; automatic, ninguno. Un estado terminal no tiene actores.
Las reglas definen cuándo aumentan ronda y turno; no disminuyen dentro de la partida. No hay un currentSeat adicional que pueda contradecir a actors.
El resultado es null mientras la partida está activa. Al completarse, incluye motivo y un resultado por participante: win, loss, draw o neutral, con puntuación, puesto, equipo y métricas opcionales. En el estado privado figuran todos los participantes, incluidos los retirados. La proyección puede omitir métricas o equipos secretos, conservando la coherencia del resultado público.
{
"kind": "completed",
"reasonCode": "objective.failed",
"participants": {
"p0": { "outcome": "loss", "metrics": {} },
"p1": { "outcome": "loss", "metrics": {} }
}
}
Ese resultado representa una derrota cooperativa. Un Solitario perdido tiene únicamente p0: loss. Se permiten ganadores compartidos, empates y clasificaciones. La plataforma no declara ganador al último participante por una regla universal.
Una cancelación usa { kind: cancelled, reasonCode } y no inventa ganadores. La transición a terminal se confirma una sola vez, cancela plazos pendientes y deja de ofrecer acciones.
10. Concurrencia sin conflictos innecesarios
Todo cambio aceptado incrementa revision exactamente una vez. La creación produce revisión 0; rechazos y lecturas no aumentan esa revisión. Incluso una actualización privada genera una nueva revisión observable, aunque algunas vistas no cambien.
| Política | Precondición | Uso |
|---|---|---|
match |
{ kind: match, revision } |
Jugadas que dependen del estado compartido: disparar, tirar, mover o robar. |
actor |
{ kind: actor, phaseEpoch, actorRevision } |
Operaciones independientes sobre la preparación privada del actor. |
Con actor, un cambio privado de p1 no invalida automáticamente la colocación pendiente de p0. El servidor vuelve a validar la acción contra el estado actual bajo el bloqueo; no aplica ciegamente un parche construido sobre datos antiguos.
Las reglas de actualización de contadores son obligatorias:
- Una acción
actorincrementa la revisión de ese actor. - Una acción
match, un evento de ciclo de vida o un cambio de fase, ronda, turno o resultado incrementanphaseEpochy todas las revisiones de actor. - Dentro de una fase de preparación, cambiar la etapa o disponibilidad del propio actor mantiene la independencia. Cambiar la habilitación de otro actor exige una barrera común y el incremento de época.
- El paso de colocación a batalla de Hundido es una barrera. Un comando tardío de colocación queda obsoleto aunque su revisión privada coincidiera.
El módulo debe justificar actor mediante pruebas de independencia y confluencia: ejecutar acciones válidas de jugadores distintos en ambos órdenes da el mismo resultado lógico, salvo metadatos de orden. La última preparación puede desencadenar la transición común una vez cumplida la condición global.
El núcleo no puede deducir qué campos lee una función JavaScript arbitraria. Por eso actor es una capacidad explícita probada por el juego, no una optimización automática. Las acciones sin esa garantía usan match. Las revisiones no ocultan el hecho de que hubo actividad; los juegos que necesiten ocultar incluso tiempos o frecuencia exigirán otro perfil de transporte.
11. Comandos, recibos y transacción
Un comando lleva protocolo, partida, UUID de comando, precondición y acción. El cliente no envía actor, identidad autenticada, resultado, azar ni nueva revisión. La plataforma deriva el actor de la sesión y del asiento autorizado.
La identidad idempotente es (matchId, principal autenticado, commandId). Su huella incluye el sobre validado completo, también la precondición, serializado como JSON canónico. Cambiar un campo con el mismo UUID produce COMMAND_ID_REUSED y no reemplaza el recibo original.
La canonicalización de comandos usa también JCS. Los decodificadores de JSON de entrada deben rechazar claves duplicadas y Unicode inválido antes de perder esa información durante el parseo; los validadores de objetos adjuntos no detectan por sí solos claves duplicadas del texto original.
El procesamiento es:
- Validar tamaño y sobre; autenticar y autorizar la pertenencia.
- Bloquear la partida y resolver su módulo exacto.
- Buscar recibo previo antes de volver a evaluar fase o final de partida.
- Validar precondiciones, etapa, tipo y parámetros; ejecutar reglas y validar la posición resultante.
- Construir y validar las proyecciones, eventos visibles y acciones ofrecidas para los destinatarios autorizados.
- Confirmar en una transacción estado, revisión, recibo, eventos internos, traza de azar, plazos y publicaciones pendientes.
- Enviar el recibo y despachar las publicaciones tras el commit.
La publicación pendiente es una outbox duradera y ordenada por partida. Contiene la proyección necesaria para publicar cada revisión, no una lectura posterior del estado que mezcle revisiones. El envío revalida que el destinatario conserve acceso. Un fallo de envío se reintenta; no revierte una jugada ya confirmada.
Se guardan recibos de aceptación y rechazos de negocio definitivos: revisión o época obsoletas, acción ilegal o partida terminada. Errores transitorios de infraestructura y límites de tráfico usan game.error; no consumen el comando con un rechazo permanente.
Una repetición idéntica devuelve el mismo resultado con duplicate: true, aunque la partida ya haya terminado. Una intención corregida usa un UUID nuevo. Los recibos se conservan durante toda la retención de la partida; no caducan antes que sus datos.
El cliente conserva la intención pendiente por cuenta y partida hasta aclarar el resultado. Reintentar transporte significa reenviar exactamente el mismo sobre. game.command-status consulta un recibo; game.command-unknown no prueba que un envío retrasado nunca llegará. Ante incertidumbre se reintenta el mismo UUID.
PostgreSQL proporciona el bloqueo y la transacción de producción. Un almacén de archivos solo puede ofrecer el perfil de desarrollo de un proceso, escribiendo atómicamente el conjunto completo. Varias instancias requieren además coordinación del despachador y las suscripciones; no se habilitan solo añadiendo réplicas de Node.
12. Protocolo y reconexión
Los sobres completos están en protocol.schema.json y wire.json.
| Mensaje | Dirección y finalidad |
|---|---|
game.sync |
Cliente → servidor: negociar contrato y establecer una suscripción nueva. |
game.snapshot |
Servidor → cliente: vista completa autorizada, contexto, acciones, resultado y eventos visibles. |
game.command |
Cliente → servidor: intención con precondición. |
game.ack |
Servidor → cliente: recibo aplicado o rechazado. |
game.command-status |
Cliente → servidor: consultar una intención pendiente. |
game.command-unknown |
Servidor → cliente: no hay recibo registrado en ese momento. |
game.cursor |
Servidor → cliente: última revisión confirmada para comprobar continuidad. |
game.unsubscribe |
Cliente → servidor: cerrar esa suscripción. |
game.error |
Servidor → cliente: error y recuperación prevista. |
Se envían snapshots completos en v1. Los deltas quedan fuera del primer refactor: primero se mide el coste real. El contexto es una proyección explícita, nunca el flujo o la configuración privados copiados automáticamente.
Barrera de sincronización
Leer el estado y suscribirse después deja una ventana en la que puede perderse una jugada. La implementación debe:
- Autorizar, negociar y crear un
subscriptionId; instalar primero la recepción de publicaciones y almacenarlas temporalmente. - Leer una posición confirmada de revisión R.
- Enviar el snapshot de sincronización con R, el
requestIdcorrespondiente yevents: []. - Descartar publicaciones almacenadas hasta R y entregar las posteriores en orden.
- Si hay un hueco o se supera el límite del buffer, generar una nueva sincronización, sin presentar una secuencia incompleta.
El cliente solo acepta la respuesta de su solicitud de sincronización más reciente y mensajes de esa suscripción. El token es propio de esa conexión; el servidor autoriza también la baja para que otra conexión no pueda cancelarla.
Un snapshot vivo declara fromRevision y contiene exactamente la revisión siguiente. Los eventos visibles tienen UUID estable por proyección, revisión y orden local consecutivo. Un evento interno puede desaparecer o producir varios eventos visibles; no se filtra su índice privado original.
Duplicados se descartan. Un salto solicita sincronización. La sincronización inicial, una reconexión y una recuperación muestran el estado actual sin reproducir eventos históricos ni sonidos.
Para detectar la pérdida de la última publicación, cuando no llega otra que revele el hueco, el servidor envía un cursor de revisión periódicamente. La frecuencia inicial propuesta es 15 segundos. Un cursor superior a la revisión confirmada, volver al primer plano o un recibo aplicado cuya vista no llega activan recuperación acotada. El cursor es una lectura de progreso, no un sustituto de la vista.
Los recibos pueden llegar antes o después de la vista. Un recibo no cambia el tablero por sí solo. La vista confirmada es la autoridad visual; el cliente mantiene separadamente el estado del comando pendiente.
Errores
game.error incluye código, clave traducible y una recuperación entre none, sync, retry-same-command, reload-client y reauthenticate. Un error de mensaje que aún no identifica una partida válida puede omitir matchId.
Los errores no incluyen estado privado, trazas internas ni mensajes de excepción. Ofrecer una acción no garantiza que siga siendo legal al ejecutarla: siempre se valida en el servidor.
13. Presentación antes, durante y después
El cliente mantiene tres elementos diferentes: última proyección confirmada, proyección mostrada y plan de transición local. Una vista recién recibida actualiza inmediatamente la confirmada; la mostrada puede seguir en el estado anterior mientras se representa el cambio.
La presentación implementa mount, un renderer con update/dispose, buildTransitionPlan y un registro de efectos. Recibe solo proyecciones autorizadas. Las acciones disponibles se presentan junto con la vista correspondiente; el coordinador impide enviar una intención cuando el cliente está desactualizado o bloqueado.
buildTransitionPlan es puro. Construye un grafo acíclico de pasos con efectos registrados, parámetros validados, dependencias, marcadores, indicación de bloqueo y timeout. Un disparador after con lista vacía inicia un paso; con dependencias espera a que terminen. Un disparador marker comienza al producirse ese marcador de un paso declarado.
El plan de caída de ficha demuestra movimiento, sonido en el impacto y resaltado posterior de una victoria. El renderer puede mantener una representación de la ficha en tránsito sin modificar el resultado confirmado.
Un solo responsable del cierre
| Momento | Coordinador | Efecto |
|---|---|---|
| Antes | Congela las proyecciones de origen y destino, valida el plan y prepara recursos. | prepare(params, context) crea un handle cancelable. |
| Durante | Programa dependencias, marcadores y pistas paralelas. | play() representa el efecto y resuelve al terminar. |
| Después | Ejecuta settle una vez cuando terminan los pasos bloqueantes; instala la proyección de destino. |
dispose(reason) libera recursos y listeners. |
| Interrupción | Aborta la generación, invalida callbacks y recupera una vista coherente. | Detiene actividad y libera el handle. |
settle pertenece exclusivamente al coordinador. No aparece en la interfaz de cada efecto: terminar un sonido o una pista no termina toda la transición.
Cada handle recibe una única llamada de limpieza por el coordinador y su limpieza debe tolerar repetición defensiva. Si una preparación termina después de cancelarse, su handle se libera inmediatamente. Todos los callbacks comprueban AbortSignal y el identificador de generación antes de tocar el frame, emitir marcadores o reproducir audio.
Un marcador no emitido no deja dependientes esperando indefinidamente: al terminar su productor se omiten esos dependientes y se liberan sus bloqueos. Un fallo o timeout de un paso bloqueante cancela el plan, limpia su cola y muestra la última proyección confirmada. El finally del coordinador siempre restablece un estado utilizable.
Audio, colas y accesibilidad
El audio se sincroniza con marcadores visuales, no con retrasos calculados a partir de relojes entre máquinas. Cada reproducción declara canal, política mix | replace | skip-if-busy y retraso máximo tolerado. Un recurso que llega tarde se omite cuando ya perdió su relación con el movimiento.
Los pasos no bloqueantes no retrasan settle. Pueden continuar entre planes dentro de su timeout y política de canal, pero conservan su propietario y se limpian al salir, sincronizar o cancelar esa generación. Un navegador que no permite sonido no impide jugar.
Solo se encolan transiciones contiguas. Si la cola se desborda, se limpia y se muestra la última vista confirmada. Mientras haya un comando pendiente, una vista mostrada antigua, desconexión o animación bloqueante, no se admiten nuevos comandos del tablero. Chat, preferencias, omitir animación y salir permanecen accesibles.
Movimiento reducido y animaciones desactivadas resuelven los efectos sin esperar una duración visual ficticia. Los controles deben admitir teclado, nombres accesibles y una descripción textual del resultado; nunca dependen exclusivamente de color o audio. Una pausa para leer un dado es local, omitible y acotada; no exige un comando adicional al servidor.
14. Límites operativos iniciales
Estos valores son límites iniciales configurables para la implementación y deben medirse antes de ampliarlos:
| Recurso | Límite inicial |
|---|---|
| Comando JSON | 64 KiB y profundidad máxima 32. |
| Datos privados por partida y proyección individual | 1 MiB por documento serializado. |
| Plan | 128 pasos; 12 segundos máximos por paso y por plan bloqueante. |
| Cola de presentación | 16 transiciones. |
| Buffer de sincronización | 128 revisiones y 8 MiB, lo que se alcance primero. |
| Comandos pendientes del tablero | Uno por cliente y partida; duplicados son reintentos. |
| Cursor de revisión | Cada 15 segundos mientras exista suscripción. |
Los esquemas comprueban algunos límites estructurales; el transporte y el runtime deberán imponer bytes, profundidad, tiempo y memoria. Los recursos gráficos grandes viven en el bundle, no en los datos JSON de la partida.
V1 carga módulos de confianza incluidos en el despliegue. No ofrece un sandbox para ejecutar JavaScript arbitrario de usuarios. Las funciones puras y sus límites se comprueban con análisis y pruebas; un plugin no confiable necesitaría aislamiento de proceso y otro modelo de permisos.
15. Privacidad y persistencia verificables
La revisión de privacidad abarca vista, contexto, ofertas, resultado, eventos, errores, logs y publicaciones persistidas. No basta con borrar un campo del tablero. Un espectador solo existe si la plataforma lo autoriza y el juego define su proyección; la variante actual puede seguir sin espectadores.
Antes de confirmar, el núcleo valida esquema de datos, invariantes, flujo, resultado y esquemas de todas las salidas públicas. Se conservan datos internos para recuperar la partida y auditar comandos; nunca se entregan como snapshot al navegador.
La reproducción usa configuración, participantes, módulo fijado, comandos y eventos de ciclo de vida, instante capturado y traza de azar. Debe obtener los mismos datos, flujo y resultado. Los UUID y tiempos de transporte no forman parte de esa comparación lógica.
La observabilidad registra identificadores de partida, comando, revisión, duración, tipo de rechazo, tamaño de cola y fallos de entrega. No registra por defecto manos privadas, contraseñas ni payloads completos. Las métricas permiten distinguir un fallo de proxy de una suscripción perdida, un error de reglas o un efecto visual atascado.
16. Compilación y aceptación de un juego
El futuro comando de publicación deberá ejecutar en orden:
- Validación estructural del manifiesto y resolución exacta y acíclica de dependencias.
- Validación de archivos, exportaciones, tipos de componentes, extensiones y asociaciones visuales.
- Cobertura de ilustraciones, regiones de atlas, recursos de catálogo y traducciones.
- Compilación separada de servidor y cliente; correspondencia de identificadores, versiones, fases y acciones con el manifiesto.
- Validación de los esquemas específicos del juego y pruebas de reglas, invariantes, azar y privacidad.
- Pruebas de conformidad del protocolo y la presentación.
- Cálculo del inventario y lock, publicación inmutable y comprobación de carga.
Las licencias y atribuciones de recursos deben quedar en los paquetes que los distribuyen. El valor UNLICENSED de los ejemplos es metadato de la especificación, no una afirmación de licencia sobre ilustraciones de terceros.
No se considera conforme un juego que solo tenga un manifiesto válido: debe superar también sus pruebas de comportamiento y poder cargarse desde su distribución final.
17. Migración y criterios de aceptación
Primero se implementan núcleo y adaptador de compatibilidad; después se migra Conecta 4, que valida el flujo público sencillo. Hundido valida preparación privada y simultánea. Brisca valida cartas, zonas y eventos ocultos. Oca y Serpientes validan azar y secuencias visuales. Los demás juegos conservan sus reglas actuales durante la migración gradual.
Solitario es aquí un caso de diseño y conformidad, no un nuevo juego disponible en el catálogo. Un juego de rol es un perfil futuro que deberá concretar sus reglas y pruebas antes de incorporarse.
Cada migración conserva fixtures de partidas actuales, compara reglas y proyecciones y permite mantener partidas antiguas con su módulo fijado. El protocolo anterior necesita un adaptador explícito durante la transición; no se cambian nombres de eventos en caliente suponiendo que todos los navegadores se actualizan a la vez.
| Caso obligatorio | Resultado esperado |
|---|---|
| Victoria, empate, derrota individual y derrota cooperativa | Resultado correcto por participante. |
| Retirada en preparación, turno y fase cooperativa | Política del módulo; sin victoria universal inventada. |
| Dos colocaciones privadas simultáneas | Ambas válidas si son independientes; sin conflicto global innecesario. |
| Colocación tardía al empezar batalla | Rechazo por época o fase obsoleta. |
| Comando repetido antes y después del final | Un solo cambio; mismo recibo. |
| Mismo UUID con otro payload o precondición | Error sin modificar el recibo original. |
| Caída después del commit y antes del envío | Recuperación desde recibo y outbox, sin repetir reglas. |
| Cambio durante sincronización | Estado leído o publicación posterior, sin ventana perdida. |
| Pérdida de la última publicación | Cursor provoca resincronización. |
| Evento oculto | Ninguna filtración en vista, contexto, ofertas, eventos o resultado. |
| Cancelación mientras carga un efecto | Handle tardío liberado; sin sonido o frame antiguo. |
| Animaciones o sonido desactivados | Transición finaliza y controles recuperados. |
| Falta temporal de un recurso | Fallback declarado, sin bloqueo de reglas. |
| Dos copias de una baraja | Identidad distinta y pertenencia única a zonas. |
| Reinicio con plazos pendientes | Una ejecución por generación válida. |
| Reproducción de partida | Mismo estado lógico con el módulo y la traza fijados. |
Estos casos son requisitos de la futura implementación. Las comprobaciones actuales no simulan todavía PostgreSQL, sockets ni un navegador.
18. Verificación disponible ahora
Desde la raíz del repositorio, con las dependencias actuales:
node docs/game-engine/verify.ts
.\node_modules\.bin\tsc.cmd --project docs/game-engine/tsconfig.json
Para regenerar los esquemas después de cambiar su fuente:
node docs/game-engine/verify.ts --write-schemas
El verificador valida los ejemplos, rechaza casos incoherentes y detecta diferencias entre los esquemas generados y su fuente. El proyecto TypeScript aislado comprueba contratos y errores esperados sin importar estos archivos en la aplicación.
Quedan como trabajo de implementación el compilador de paquetes, el SDK ejecutable, el núcleo transaccional, el coordinador visual, las pruebas de integración y la migración de los juegos. Esta separación permite revisar y verificar el modelo antes de comprometer datos o producción.