commit
0982ba0e52
@ -0,0 +1,2 @@
|
|||||||
|
# Backend development address. The frontend development proxy targets this port.
|
||||||
|
GAMES2_API_ADDR=127.0.0.1:8082
|
||||||
@ -0,0 +1,18 @@
|
|||||||
|
# Frontend and Go build outputs
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.svelte-kit/
|
||||||
|
node_modules/
|
||||||
|
bin/
|
||||||
|
*.exe
|
||||||
|
.cache/
|
||||||
|
|
||||||
|
# Local configuration and temporary files
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
.data/
|
||||||
|
coverage/
|
||||||
|
*.log
|
||||||
|
.npm-cache/
|
||||||
|
frontend/.npm-cache/
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
# Juegoland — games2
|
||||||
|
|
||||||
|
Proyecto independiente para la nueva plataforma de juegos. El frontend está en Svelte 5 y TypeScript; la API y el servidor de dominio están en Go. La carpeta no importa archivos de `games`, usa puertos propios y no modifica su despliegue.
|
||||||
|
|
||||||
|
**Estado actual:** base ejecutable. El catálogo está conectado desde el navegador a la API Go y empieza vacío porque ningún juego está instalado/publicado en este proyecto. Salas, cuentas, chat, motor, PostgreSQL y tiempo real aún no están habilitados. `/ready` describe expresamente esta fase como `foundation`; no acredita preparación para producción.
|
||||||
|
|
||||||
|
## Requisitos
|
||||||
|
|
||||||
|
- Go 1.26 o superior.
|
||||||
|
- Node.js 24 o superior y npm.
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
|
||||||
|
En una terminal, desde la raíz de `games2`:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go run ./cmd/games2
|
||||||
|
```
|
||||||
|
|
||||||
|
La API escucha por defecto en `127.0.0.1:8082`. Se puede elegir otro puerto con `GAMES2_API_ADDR`. Para usar otro puerto hay que actualizar también el destino del proxy de desarrollo en `frontend/vite.config.ts`.
|
||||||
|
|
||||||
|
En otra terminal:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Abre `http://127.0.0.1:4174`. El frontend consulta `/api/v2/catalog` a través del proxy local hacia Go. En producción, un proxy HTTPS servirá el frontend estático y enviará `/api` al backend; esta configuración aún no está desplegada.
|
||||||
|
|
||||||
|
Las herramientas para validar los contratos del motor tienen dependencias propias en la raíz del proyecto:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm ci
|
||||||
|
npm run contracts:check
|
||||||
|
```
|
||||||
|
|
||||||
|
Comprobar:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
go test ./...
|
||||||
|
npm run contracts:check
|
||||||
|
cd frontend
|
||||||
|
npm run check
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Rutas de la primera fase: `GET /live`, `GET /ready`, `GET /api/v2/bootstrap` y `GET /api/v2/catalog`. Las dos últimas devuelven `requestId` y `data`; el catálogo devuelve `items: []` y `nextCursor: null` hasta la primera publicación. No hay rutas de creación o publicación de juegos para usuarios.
|
||||||
|
|
||||||
|
## Estructura
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/games2/ arranque y apagado del proceso Go
|
||||||
|
internal/api/httpapi/ entrega HTTP y contratos de respuesta
|
||||||
|
internal/server/ casos de uso y capacidades habilitadas
|
||||||
|
internal/domain/catalog/ lectura de juegos publicados
|
||||||
|
internal/domain/ identidad, seguridad, salas, chat y moderación
|
||||||
|
internal/engine/ núcleo del motor propio
|
||||||
|
internal/games/ reglas Go propias publicadas internamente
|
||||||
|
internal/infra/ PostgreSQL y proveedores externos
|
||||||
|
internal/jobs/ plazos y outbox
|
||||||
|
frontend/ aplicación Svelte 5 y Vite
|
||||||
|
contracts/ índice de contratos ejecutables
|
||||||
|
db/migrations/ futuras migraciones PostgreSQL
|
||||||
|
deploy/ futuro despliegue independiente
|
||||||
|
docs/platform/ políticas y arquitectura de producto
|
||||||
|
docs/game-engine/ formato, esquemas y ejemplos del motor propio
|
||||||
|
docs/ROADMAP.md próximos hitos y criterios de aceptación
|
||||||
|
```
|
||||||
|
|
||||||
|
El directorio `docs/` contiene una copia de los contratos y propuestas de v2 para trabajar sin depender de archivos de la aplicación anterior. Documentan el destino; las funciones no disponibles siguen marcadas como pendientes. La [arquitectura](docs/platform/architecture.md), las [políticas](docs/platform/product-policies.md) y el [plan de implementación](docs/ROADMAP.md) indican los límites de esta primera fase.
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"juegoland/games2/internal/api/httpapi"
|
||||||
|
"juegoland/games2/internal/domain/catalog"
|
||||||
|
"juegoland/games2/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
|
||||||
|
address := os.Getenv("GAMES2_API_ADDR")
|
||||||
|
if address == "" {
|
||||||
|
address = "127.0.0.1:8082"
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first release includes no games. A game appears only when its rules,
|
||||||
|
// assets and publication record are installed and validated together.
|
||||||
|
registry := catalog.NewRegistry(nil)
|
||||||
|
service := server.NewService(registry, time.Now)
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: address,
|
||||||
|
Handler: httpapi.NewHandler(service),
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
MaxHeaderBytes: 1 << 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := httpServer.Shutdown(shutdownCtx); err != nil {
|
||||||
|
logger.Error("graceful shutdown failed", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Info("games2 API listening", "address", address)
|
||||||
|
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
logger.Error("API stopped", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
# Contratos ejecutables
|
||||||
|
|
||||||
|
El formato y los esquemas de juegos copiados están en [`../docs/game-engine/`](../docs/game-engine/). Su fuente Zod/TypeScript puede validarse con `npm run contracts:check` desde la raíz de `games2`.
|
||||||
|
|
||||||
|
El contrato OpenAPI de plataforma, los esquemas de eventos y las interfaces Go equivalentes al SDK de reglas son el siguiente entregable. Los tipos del frontend y las estructuras Go de esta base se limitan al catálogo y al bootstrap ya implementados. No se presentarán como contrato completo de cuentas, salas o chat.
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Migraciones de games2
|
||||||
|
|
||||||
|
La base inicial no abre PostgreSQL y no declara un esquema vacío como migración de producción. La primera migración llegará junto con una unidad de trabajo y pruebas de integración para cuentas, catálogo y recibos. Se versionará aquí de forma independiente de las migraciones de `games`.
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
# Despliegue de games2
|
||||||
|
|
||||||
|
La base solo arranca en `127.0.0.1:8082` para desarrollo. No hay unidad systemd, proxy público ni configuración de producción para games2. Cuando se habiliten PostgreSQL, migraciones, disponibilidad de reglas y observabilidad, esta carpeta contendrá artefactos propios y un despliegue reversible separado de la aplicación anterior.
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
# Plan de implementación de games2
|
||||||
|
|
||||||
|
## Base ejecutable actual
|
||||||
|
|
||||||
|
- Proyecto Go con API, capa de servidor y catálogo de publicación interna; sin juegos fingidos.
|
||||||
|
- Frontend Svelte independiente que consulta el catálogo real y representa carga, fallo y lista vacía.
|
||||||
|
- Bootstrap de capacidades: únicamente catálogo. Salud de proceso y readiness acotado a esta fase.
|
||||||
|
- Contratos y políticas de v2 copiados bajo `docs/`; su implementación permanece pendiente.
|
||||||
|
|
||||||
|
## Próximos cortes verticales
|
||||||
|
|
||||||
|
1. **Persistencia y contratos:** PostgreSQL, migraciones nuevas, repositorios dentro de una unidad de trabajo, API/esquemas verificables y pruebas de integración. `/ready` pasará a exigir base y esquema compatibles cuando se active esa dependencia.
|
||||||
|
2. **Motor propio en Go:** interfaz de reglas equivalente al SDK documentado, perfil de paquete Go versionado, resultados, azar determinista, proyecciones por jugador, recibos y outbox. Conecta 4 como primer juego propio real. No publicarlo solo por existir una ficha.
|
||||||
|
3. **Información privada y azar:** Hundido y Brisca como pruebas de vistas ocultas, acciones independientes, barajas y reproducibilidad. Conservar fixtures de resultados y privacidad.
|
||||||
|
4. **Identidad y protección:** credenciales recuperables, sesiones, edad, tutela, permisos de sala, invitaciones, bloqueo/reporte y administración. Abrir cada capacidad solo con sus proveedores, procedimientos y pruebas.
|
||||||
|
5. **Tiempo real y clientes:** binding WebSocket versionado, negociación, auth, reconexión, cursores y recibos; frontend de sala/partida con móvil y accesibilidad probados.
|
||||||
|
6. **Operación:** backups/restauración con supresiones, observabilidad externa del proxy y del backend, pruebas de carga, despliegue reversible y migración desde v1 con una sola autoridad por sala.
|
||||||
|
|
||||||
|
## Criterio para cada hito
|
||||||
|
|
||||||
|
Cada entrega incluye una ruta visible, su comportamiento de error y permisos, pruebas que cubran el riesgo propio, instrucciones de operación y una comprobación contra los documentos de contrato. Las capacidades no implementadas permanecen desactivadas en `bootstrap` y sin controles de interfaz que sugieran que funcionan.
|
||||||
|
|
||||||
|
La primera fase no requiere conexión a la base de datos ni proveedor externo. El trabajo posterior debe mantener los valores de producto de [políticas](platform/product-policies.md), la [API/eventos](platform/api-and-events.md) y el [formato de juegos](game-engine/format-and-protocol.md), actualizando esos documentos cuando una decisión de implementación cambie un contrato.
|
||||||
@ -0,0 +1,451 @@
|
|||||||
|
# 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](platform/architecture.md) fija API Go y recomienda servidor/motor Go; 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.
|
||||||
|
|
||||||
|
## 1. Qué constituye la especificación
|
||||||
|
|
||||||
|
| Artefacto | Responsabilidad |
|
||||||
|
| ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- |
|
||||||
|
| [schema.ts](game-engine/schema.ts) | Fuente de los esquemas estructurales y validadores semánticos comunes. |
|
||||||
|
| [contracts.ts](game-engine/contracts.ts) | SDK de funciones de reglas, componentes, persistencia y presentación. |
|
||||||
|
| [package.schema.json](game-engine/package.schema.json) | Manifiestos de juegos y bibliotecas. |
|
||||||
|
| [component.schema.json](game-engine/component.schema.json) | Barajas, dados, ruletas, tableros, piezas y extensiones. |
|
||||||
|
| [resource-set.schema.json](game-engine/resource-set.schema.json) | Recursos visuales asociados a las definiciones de un componente. |
|
||||||
|
| [lock.schema.json](game-engine/lock.schema.json) | Versiones resueltas y hashes de archivos del paquete publicado. |
|
||||||
|
| [protocol.schema.json](game-engine/protocol.schema.json) | Mensajes del protocolo de partidas. |
|
||||||
|
| [Formato y protocolo](game-engine/format-and-protocol.md) | Documento independiente del paquete y de los mensajes de red. |
|
||||||
|
| [presentation-plan.schema.json](game-engine/presentation-plan.schema.json) | Planes de animación y sonido. |
|
||||||
|
| [Ejemplos y perfiles](game-engine/examples/README.md) | Manifiestos completos como registros de datos y escenarios de conformidad. |
|
||||||
|
| [verify.ts](game-engine/verify.ts) y [type-check.ts](game-engine/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](https://foundryvtt.com/article/system-development/) | Manifiesto de sistema, compatibilidad, dependencias, módulos, recursos y evolución de modelos. |
|
||||||
|
| [Tabletop Playground: Packages](https://tabletop-playground.com/knowledge-base/packages/) | Paquetes con manifiesto, miniatura, objetos, texturas y scripts; reutilización de componentes. |
|
||||||
|
| [boardgame.io: contratos de tipos](https://github.com/boardgameio/boardgame.io/blob/main/src/types.ts) | Separación entre configuración inicial, movimientos, fases, etapas y vistas por jugador. |
|
||||||
|
| [Board Game Arena: estados](https://en.doc.boardgamearena.com/State_classes:_State_directory) | Participación de un jugador, varios jugadores y estados automáticos como conceptos diferentes. |
|
||||||
|
| [Board Game Arena: interfaz y notificaciones](https://en.doc.boardgamearena.com/Game_interface_logic:_yourgamename.js#Notifications) | Secuenciar efectos mediante promesas y resolverlos también cuando se desactivan las animaciones. |
|
||||||
|
| [Tabletop Simulator: Save File Format](https://kb.tabletopsimulator.com/custom-content/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](https://ludii.games/downloads/LudiiLanguageReference.pdf) | 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](https://socket.io/docs/v4/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](https://json-schema.org/draft/2020-12) y [Zod: JSON Schema](https://zod.dev/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](platform-spec-v2.md) y su [API/eventos](platform/api-and-events.md) 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.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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.
|
||||||
|
|
||||||
|
Las bibliotecas tienen `kind: components` o `kind: assets` y exportaciones con nombre. Una referencia a dependencia es un objeto inequívoco:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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](https://www.rfc-editor.org/rfc/rfc8785). 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.
|
||||||
|
|
||||||
|
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](game-engine/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](https://www.typescriptlang.org/docs/handbook/2/objects.html#readonly-properties).
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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:
|
||||||
|
|
||||||
|
1. Una acción `actor` incrementa la revisión de ese actor.
|
||||||
|
2. Una acción `match`, un evento de ciclo de vida o un cambio de fase, ronda, turno o resultado incrementan `phaseEpoch` y todas las revisiones de actor.
|
||||||
|
3. 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.
|
||||||
|
4. 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:
|
||||||
|
|
||||||
|
1. Validar tamaño y sobre; autenticar y autorizar la pertenencia.
|
||||||
|
2. Bloquear la partida y resolver su módulo exacto.
|
||||||
|
3. Buscar recibo previo antes de volver a evaluar fase o final de partida.
|
||||||
|
4. Validar precondiciones, etapa, tipo y parámetros; ejecutar reglas y validar la posición resultante.
|
||||||
|
5. Construir y validar las proyecciones, eventos visibles y acciones ofrecidas para los destinatarios autorizados.
|
||||||
|
6. Confirmar en una transacción estado, revisión, recibo, eventos internos, traza de azar, plazos y publicaciones pendientes.
|
||||||
|
7. 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](game-engine/protocol.schema.json) y [wire.json](game-engine/examples/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:
|
||||||
|
|
||||||
|
1. Autorizar, negociar y crear un `subscriptionId`; instalar primero la recepción de publicaciones y almacenarlas temporalmente.
|
||||||
|
2. Leer una posición confirmada de revisión R.
|
||||||
|
3. Enviar el snapshot de sincronización con R, el `requestId` correspondiente y `events: []`.
|
||||||
|
4. Descartar publicaciones almacenadas hasta R y entregar las posteriores en orden.
|
||||||
|
5. 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](game-engine/examples/drop.plan.json) 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:
|
||||||
|
|
||||||
|
1. Validación estructural del manifiesto y resolución exacta y acíclica de dependencias.
|
||||||
|
2. Validación de archivos, exportaciones, tipos de componentes, extensiones y asociaciones visuales.
|
||||||
|
3. Cobertura de ilustraciones, regiones de atlas, recursos de catálogo y traducciones.
|
||||||
|
4. Compilación separada de servidor y cliente; correspondencia de identificadores, versiones, fases y acciones con el manifiesto.
|
||||||
|
5. Validación de los esquemas específicos del juego y pruebas de reglas, invariantes, azar y privacidad.
|
||||||
|
6. Pruebas de conformidad del protocolo y la presentación.
|
||||||
|
7. 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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
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:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
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.
|
||||||
@ -0,0 +1,324 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "deck"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"cards": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "schemaVersion", "cards"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "dice"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"dice": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "schemaVersion", "dice"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "wheel"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"sectors": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "schemaVersion", "sectors"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "board"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"layout": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "grid"
|
||||||
|
},
|
||||||
|
"rows": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "rows", "columns"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "graph"
|
||||||
|
},
|
||||||
|
"cells": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "cells"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "schemaVersion", "layout"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "pieces"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"definitions": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "schemaVersion", "definitions"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "custom"
|
||||||
|
},
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"typeId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "schemaVersion", "typeId", "data"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"__schema0": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"suit": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"rank": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": -9007199254740991,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "suit", "rank", "attributes"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9.-]{0,79}$"
|
||||||
|
},
|
||||||
|
"__schema2": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"__schema3": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"__schema4": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"faces": {
|
||||||
|
"minItems": 2,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "faces"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema5": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 1000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "value", "weight"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema6": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"weight": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 1000000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "weight"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema7": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"neighbors": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "neighbors"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema8": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "count", "attributes"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": "urn:juegoland:component:1"
|
||||||
|
}
|
||||||
@ -0,0 +1,310 @@
|
|||||||
|
// SDK contract for the proposed engine. Types only; no production runtime.
|
||||||
|
import type { z } from 'zod';
|
||||||
|
import type {
|
||||||
|
action,
|
||||||
|
json,
|
||||||
|
lockSchema,
|
||||||
|
presentationPlanSchema,
|
||||||
|
MatchOutcome,
|
||||||
|
GamePackage,
|
||||||
|
publicEvent,
|
||||||
|
ActionOffer,
|
||||||
|
} from './schema.ts';
|
||||||
|
|
||||||
|
export type Json = z.infer<typeof json>;
|
||||||
|
export type JsonObject = { [key: string]: Json };
|
||||||
|
export type SeatId = `p${number}`;
|
||||||
|
export type DeepReadonly<T> = T extends (...args: never[]) => unknown
|
||||||
|
? T
|
||||||
|
: T extends readonly (infer V)[]
|
||||||
|
? readonly DeepReadonly<V>[]
|
||||||
|
: T extends object
|
||||||
|
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
|
||||||
|
: T;
|
||||||
|
export interface RuntimeSchema<T> {
|
||||||
|
parse(input: unknown): T;
|
||||||
|
}
|
||||||
|
export type GameAction = z.infer<typeof action>;
|
||||||
|
export type DomainEvent = { type: string; payload: JsonObject };
|
||||||
|
export type VisibleEvent = DomainEvent;
|
||||||
|
export type DeliveredEvent = z.infer<typeof publicEvent>;
|
||||||
|
|
||||||
|
export interface Participant {
|
||||||
|
seat: SeatId;
|
||||||
|
teamId: string | null;
|
||||||
|
role: string;
|
||||||
|
status: 'active' | 'withdrawn';
|
||||||
|
}
|
||||||
|
export interface Flow {
|
||||||
|
phase: string;
|
||||||
|
round: number;
|
||||||
|
turn: number;
|
||||||
|
actors: Partial<Record<SeatId, string>>; // seat -> stage
|
||||||
|
}
|
||||||
|
export interface PhaseDefinition {
|
||||||
|
participation: 'single' | 'multiple' | 'automatic';
|
||||||
|
stages: Readonly<Record<string, readonly string[]>>; // stage -> action types
|
||||||
|
}
|
||||||
|
export interface Position<Config, Data> {
|
||||||
|
matchId: string;
|
||||||
|
config: Config;
|
||||||
|
participants: Participant[];
|
||||||
|
data: Data;
|
||||||
|
flow: Flow;
|
||||||
|
outcome: MatchOutcome | null;
|
||||||
|
}
|
||||||
|
export type Recipient =
|
||||||
|
{ kind: 'participant'; seat: SeatId; role: string } | { kind: 'observer' }; // Only when the platform explicitly authorizes it.
|
||||||
|
|
||||||
|
export interface CardDefinition {
|
||||||
|
id: string;
|
||||||
|
suit: string;
|
||||||
|
rank: number;
|
||||||
|
attributes: JsonObject;
|
||||||
|
}
|
||||||
|
export interface CardInstance {
|
||||||
|
id: string;
|
||||||
|
definitionId: string;
|
||||||
|
copy: number;
|
||||||
|
componentId: string;
|
||||||
|
}
|
||||||
|
export interface Zone {
|
||||||
|
id: string;
|
||||||
|
owner: SeatId | null;
|
||||||
|
ordered: boolean;
|
||||||
|
visibility: 'public' | 'owner' | 'hidden';
|
||||||
|
items: string[];
|
||||||
|
}
|
||||||
|
export interface DeckState {
|
||||||
|
instances: CardInstance[];
|
||||||
|
zones: Zone[];
|
||||||
|
}
|
||||||
|
export interface DieFace {
|
||||||
|
id: string;
|
||||||
|
value: Json;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
export interface WheelSector {
|
||||||
|
id: string;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
export interface RuleServices {
|
||||||
|
readonly nowMs: number; // Captured once and recorded with this command.
|
||||||
|
readonly definitions: DeepReadonly<Record<string, Json>>;
|
||||||
|
readonly random: {
|
||||||
|
int(tag: string, minInclusive: number, maxExclusive: number): number;
|
||||||
|
shuffle<T extends Json>(tag: string, items: readonly T[]): T[];
|
||||||
|
weighted(
|
||||||
|
tag: string,
|
||||||
|
options: readonly { id: string; weight: number }[],
|
||||||
|
): string;
|
||||||
|
};
|
||||||
|
readonly cards: {
|
||||||
|
instantiate(
|
||||||
|
componentId: string,
|
||||||
|
definitions: readonly CardDefinition[],
|
||||||
|
copies: number,
|
||||||
|
): DeckState;
|
||||||
|
move(
|
||||||
|
state: DeepReadonly<DeckState>,
|
||||||
|
ids: readonly string[],
|
||||||
|
source: string,
|
||||||
|
destination: string,
|
||||||
|
index: number,
|
||||||
|
): DeckState;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
export type DeadlineIntent =
|
||||||
|
| { type: 'schedule'; id: string; dueAtMs: number; payload: JsonObject }
|
||||||
|
| { type: 'cancel'; id: string };
|
||||||
|
export interface Transition<Data, Event> {
|
||||||
|
data: Data;
|
||||||
|
flow: Flow;
|
||||||
|
outcome: MatchOutcome | null;
|
||||||
|
events: Event[];
|
||||||
|
deadlines: DeadlineIntent[];
|
||||||
|
}
|
||||||
|
export type RuleResult<Data, Event> =
|
||||||
|
| { ok: true; transition: Transition<Data, Event> }
|
||||||
|
| { ok: false; code: 'ILLEGAL_ACTION' };
|
||||||
|
export interface ActionHandler<Config, Data, Action, Event> {
|
||||||
|
schema: RuntimeSchema<Action>;
|
||||||
|
conflictPolicy: 'match' | 'actor';
|
||||||
|
apply(
|
||||||
|
position: DeepReadonly<Position<Config, Data>>,
|
||||||
|
command: DeepReadonly<{ actor: SeatId; action: Action }>,
|
||||||
|
services: RuleServices,
|
||||||
|
): RuleResult<Data, Event>;
|
||||||
|
}
|
||||||
|
export type OfferDraft<Action extends GameAction> =
|
||||||
|
| { kind: 'concrete'; action: Action }
|
||||||
|
| {
|
||||||
|
kind: 'parameterized';
|
||||||
|
actionType: Action['type'];
|
||||||
|
inputSchemaId: string;
|
||||||
|
constraints: JsonObject;
|
||||||
|
};
|
||||||
|
export interface Projection<View> {
|
||||||
|
view: View;
|
||||||
|
context: JsonObject; // Explicit projection; never the raw Flow or config.
|
||||||
|
outcome: MatchOutcome | null;
|
||||||
|
}
|
||||||
|
export type LifecycleEvent =
|
||||||
|
| { type: 'participant.withdrawn'; seat: SeatId }
|
||||||
|
| { type: 'deadline.reached'; deadlineId: string; payload: JsonObject };
|
||||||
|
export interface GameDefinition<
|
||||||
|
Config,
|
||||||
|
Data,
|
||||||
|
View,
|
||||||
|
Action extends GameAction,
|
||||||
|
Event extends DomainEvent,
|
||||||
|
PublicEvent extends VisibleEvent,
|
||||||
|
> {
|
||||||
|
id: string;
|
||||||
|
rulesVersion: string;
|
||||||
|
schemas: {
|
||||||
|
config: RuntimeSchema<Config>;
|
||||||
|
data: RuntimeSchema<Data>;
|
||||||
|
view: RuntimeSchema<View>;
|
||||||
|
context: RuntimeSchema<JsonObject>;
|
||||||
|
event: RuntimeSchema<Event>;
|
||||||
|
visibleEvent: RuntimeSchema<PublicEvent>;
|
||||||
|
};
|
||||||
|
phases: Readonly<Record<string, PhaseDefinition>>;
|
||||||
|
setup(
|
||||||
|
input: DeepReadonly<{
|
||||||
|
matchId: string;
|
||||||
|
config: Config;
|
||||||
|
participants: Participant[];
|
||||||
|
}>,
|
||||||
|
services: RuleServices,
|
||||||
|
): Transition<Data, Event>;
|
||||||
|
validatePosition(position: DeepReadonly<Position<Config, Data>>): void;
|
||||||
|
actions: {
|
||||||
|
[K in Action['type']]: ActionHandler<
|
||||||
|
Config,
|
||||||
|
Data,
|
||||||
|
Extract<Action, { type: K }>,
|
||||||
|
Event
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
availableActions(
|
||||||
|
position: DeepReadonly<Position<Config, Data>>,
|
||||||
|
recipient: DeepReadonly<Recipient>,
|
||||||
|
): OfferDraft<Action>[];
|
||||||
|
project(
|
||||||
|
position: DeepReadonly<Position<Config, Data>>,
|
||||||
|
recipient: DeepReadonly<Recipient>,
|
||||||
|
): Projection<View>;
|
||||||
|
projectEvent(
|
||||||
|
input: DeepReadonly<{
|
||||||
|
before: Position<Config, Data> | null;
|
||||||
|
after: Position<Config, Data>;
|
||||||
|
event: Event;
|
||||||
|
recipient: Recipient;
|
||||||
|
}>,
|
||||||
|
): PublicEvent[];
|
||||||
|
onLifecycle(
|
||||||
|
position: DeepReadonly<Position<Config, Data>>,
|
||||||
|
event: DeepReadonly<LifecycleEvent>,
|
||||||
|
services: RuleServices,
|
||||||
|
): Transition<Data, Event>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PersistedEnvelope<Config, Data> {
|
||||||
|
schemaVersion: 1;
|
||||||
|
game: {
|
||||||
|
id: string;
|
||||||
|
rulesVersion: string;
|
||||||
|
stateSchemaVersion: number;
|
||||||
|
packageDigest: string;
|
||||||
|
dependencyLockDigest: string;
|
||||||
|
};
|
||||||
|
revision: number;
|
||||||
|
phaseEpoch: number;
|
||||||
|
actorRevisions: Partial<Record<SeatId, number>>;
|
||||||
|
position: Position<Config, Data>;
|
||||||
|
}
|
||||||
|
export type PackageLock = z.infer<typeof lockSchema>;
|
||||||
|
export type PresentationPlan = z.infer<typeof presentationPlanSchema>;
|
||||||
|
export type PresentationStep = PresentationPlan['steps'][number];
|
||||||
|
export type StepTrigger = PresentationStep['trigger'];
|
||||||
|
export interface Preferences {
|
||||||
|
reducedMotion: boolean;
|
||||||
|
sound: boolean;
|
||||||
|
volume: number;
|
||||||
|
announcements: boolean;
|
||||||
|
}
|
||||||
|
// The module at a `graphic` resource path exports this definition as default.
|
||||||
|
// Renderers own insertion and removal; a graphic creates no timers or commands.
|
||||||
|
export interface GraphicDefinition {
|
||||||
|
render(
|
||||||
|
input: DeepReadonly<{
|
||||||
|
resourceId: string;
|
||||||
|
slotId: string | null;
|
||||||
|
definition: Json | null;
|
||||||
|
locale: string;
|
||||||
|
}>,
|
||||||
|
): SVGElement;
|
||||||
|
}
|
||||||
|
export type FinishReason =
|
||||||
|
'completed' | 'skipped' | 'cancelled' | 'failed' | 'timeout';
|
||||||
|
export interface EffectContext {
|
||||||
|
signal: AbortSignal;
|
||||||
|
preferences: DeepReadonly<Preferences>;
|
||||||
|
resource(id: string): Promise<unknown>;
|
||||||
|
frame(key: string, value: Json): void;
|
||||||
|
marker(name: string): void;
|
||||||
|
audio(input: {
|
||||||
|
resource: string;
|
||||||
|
channel: string;
|
||||||
|
policy: 'mix' | 'replace' | 'skip-if-busy';
|
||||||
|
maxLateMs: number;
|
||||||
|
}): void;
|
||||||
|
}
|
||||||
|
export interface EffectHandle {
|
||||||
|
play(): Promise<void>;
|
||||||
|
dispose(reason: FinishReason): void;
|
||||||
|
}
|
||||||
|
export interface EffectDefinition {
|
||||||
|
params: RuntimeSchema<JsonObject>;
|
||||||
|
prepare(
|
||||||
|
input: DeepReadonly<JsonObject>,
|
||||||
|
context: EffectContext,
|
||||||
|
): Promise<EffectHandle>;
|
||||||
|
}
|
||||||
|
export interface RenderFrame<View> {
|
||||||
|
presented: Projection<View>;
|
||||||
|
availableActions: ActionOffer[];
|
||||||
|
presentation: JsonObject;
|
||||||
|
busy: boolean;
|
||||||
|
connected: boolean;
|
||||||
|
}
|
||||||
|
export interface Renderer<View> {
|
||||||
|
update(frame: DeepReadonly<RenderFrame<View>>): void;
|
||||||
|
dispose(): void;
|
||||||
|
}
|
||||||
|
export interface PresentationDefinition<
|
||||||
|
View,
|
||||||
|
Action extends GameAction,
|
||||||
|
PublicEvent extends VisibleEvent,
|
||||||
|
> {
|
||||||
|
apiVersion: 1;
|
||||||
|
mount(
|
||||||
|
host: HTMLElement,
|
||||||
|
frame: DeepReadonly<RenderFrame<View>>,
|
||||||
|
intent: (action: Action) => void,
|
||||||
|
): Renderer<View>;
|
||||||
|
buildTransitionPlan(
|
||||||
|
input: DeepReadonly<{
|
||||||
|
from: Projection<View>;
|
||||||
|
to: Projection<View>;
|
||||||
|
fromRevision: number;
|
||||||
|
toRevision: number;
|
||||||
|
events: (PublicEvent & { eventId: string })[];
|
||||||
|
manifest: GamePackage;
|
||||||
|
preferences: Preferences;
|
||||||
|
}>,
|
||||||
|
): PresentationPlan;
|
||||||
|
effects: Readonly<Record<string, EffectDefinition>>;
|
||||||
|
}
|
||||||
@ -0,0 +1,499 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"kind": "board",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"layout": {
|
||||||
|
"kind": "grid",
|
||||||
|
"rows": 6,
|
||||||
|
"columns": 7
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "deck",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"cards": [
|
||||||
|
{
|
||||||
|
"id": "picas-1",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 1,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-2",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 2,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-3",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 3,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-4",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 4,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-5",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 5,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-6",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 6,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-7",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 7,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-8",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 8,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-9",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 9,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-10",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 10,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-11",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 11,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-12",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 12,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "picas-13",
|
||||||
|
"suit": "picas",
|
||||||
|
"rank": 13,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-1",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 1,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-2",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 2,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-3",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 3,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-4",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 4,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-5",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 5,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-6",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 6,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-7",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 7,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-8",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 8,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-9",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 9,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-10",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 10,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-11",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 11,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-12",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 12,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "corazones-13",
|
||||||
|
"suit": "corazones",
|
||||||
|
"rank": 13,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-1",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 1,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-2",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 2,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-3",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 3,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-4",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 4,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-5",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 5,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-6",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 6,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-7",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 7,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-8",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 8,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-9",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 9,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-10",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 10,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-11",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 11,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-12",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 12,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "diamantes-13",
|
||||||
|
"suit": "diamantes",
|
||||||
|
"rank": 13,
|
||||||
|
"attributes": {
|
||||||
|
"color": "red"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-1",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 1,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-2",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 2,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-3",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 3,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-4",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 4,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-5",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 5,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-6",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 6,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-7",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 7,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-8",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 8,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-9",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 9,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-10",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 10,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-11",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 11,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-12",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 12,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "treboles-13",
|
||||||
|
"suit": "treboles",
|
||||||
|
"rank": 13,
|
||||||
|
"attributes": {
|
||||||
|
"color": "black"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "dice",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"dice": [
|
||||||
|
{
|
||||||
|
"id": "d6",
|
||||||
|
"faces": [
|
||||||
|
{
|
||||||
|
"id": "face-1",
|
||||||
|
"value": 1,
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "face-2",
|
||||||
|
"value": 2,
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "face-3",
|
||||||
|
"value": 3,
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "face-4",
|
||||||
|
"value": 4,
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "face-5",
|
||||||
|
"value": 5,
|
||||||
|
"weight": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "face-6",
|
||||||
|
"value": 6,
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "wheel",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"sectors": [
|
||||||
|
{
|
||||||
|
"id": "advance",
|
||||||
|
"weight": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "wait",
|
||||||
|
"weight": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"kind": "pieces",
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"definitions": [
|
||||||
|
{
|
||||||
|
"id": "disc",
|
||||||
|
"count": 42,
|
||||||
|
"attributes": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
{
|
||||||
|
"format": "juegoland.package",
|
||||||
|
"formatVersion": 1,
|
||||||
|
"id": "conecta4",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"engineApiVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Conecta 4",
|
||||||
|
"description": "Paquete de referencia de Conecta 4; especificación previa a su implementación.",
|
||||||
|
"authors": ["Juegoland"],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"defaultLocale": "es",
|
||||||
|
"locales": {
|
||||||
|
"es": "locales/es.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"kind": "game",
|
||||||
|
"classification": {
|
||||||
|
"category": "board",
|
||||||
|
"tags": ["conecta4"],
|
||||||
|
"competition": "individual"
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"min": 2,
|
||||||
|
"max": 2
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"hiddenInformation": false,
|
||||||
|
"simultaneousActions": false,
|
||||||
|
"deadlines": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"board": {
|
||||||
|
"kind": "board",
|
||||||
|
"definition": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "components/board.json"
|
||||||
|
},
|
||||||
|
"copies": 1
|
||||||
|
},
|
||||||
|
"discs": {
|
||||||
|
"kind": "pieces",
|
||||||
|
"definition": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "components/discs.json"
|
||||||
|
},
|
||||||
|
"copies": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"entry": "rules/index.ts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stateSchemaVersion": 1,
|
||||||
|
"viewSchemaVersion": 1,
|
||||||
|
"eventSchemaVersion": 1,
|
||||||
|
"schemas": {
|
||||||
|
"config": "schemas/config.json",
|
||||||
|
"data": "schemas/data.json",
|
||||||
|
"view": "schemas/view.json",
|
||||||
|
"context": "schemas/context.json",
|
||||||
|
"event": "schemas/event.json",
|
||||||
|
"visibleEvent": "schemas/visible-event.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"entry": "presentation/index.ts",
|
||||||
|
"apiVersion": 1,
|
||||||
|
"bundleVersion": "1.0.0",
|
||||||
|
"resources": {
|
||||||
|
"catalog.cover": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog.thumbnail": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog.icon": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sound.drop": {
|
||||||
|
"kind": "audio",
|
||||||
|
"path": "assets/connect-four-drop.wav",
|
||||||
|
"load": "board",
|
||||||
|
"required": false,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "omit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bindings": {},
|
||||||
|
"catalog": {
|
||||||
|
"cover": "catalog.cover",
|
||||||
|
"thumbnail": "catalog.thumbnail",
|
||||||
|
"icon": "catalog.icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
{
|
||||||
|
"id": "drop-13",
|
||||||
|
"fromRevision": 12,
|
||||||
|
"toRevision": 13,
|
||||||
|
"steps": [
|
||||||
|
{
|
||||||
|
"id": "fall",
|
||||||
|
"effect": "disc.drop",
|
||||||
|
"trigger": {
|
||||||
|
"kind": "after",
|
||||||
|
"steps": []
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"column": 3,
|
||||||
|
"row": 5
|
||||||
|
},
|
||||||
|
"emits": ["impact"],
|
||||||
|
"blocking": true,
|
||||||
|
"timeoutMs": 1500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "impact",
|
||||||
|
"effect": "sound.play",
|
||||||
|
"trigger": {
|
||||||
|
"kind": "marker",
|
||||||
|
"step": "fall",
|
||||||
|
"marker": "impact"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"resource": "sound.drop",
|
||||||
|
"channel": "board",
|
||||||
|
"policy": "mix",
|
||||||
|
"maxLateMs": 150
|
||||||
|
},
|
||||||
|
"emits": [],
|
||||||
|
"blocking": false,
|
||||||
|
"timeoutMs": 800
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "finish",
|
||||||
|
"effect": "line.highlight",
|
||||||
|
"trigger": {
|
||||||
|
"kind": "after",
|
||||||
|
"steps": ["fall"]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"cells": [35, 36, 37, 38]
|
||||||
|
},
|
||||||
|
"emits": [],
|
||||||
|
"blocking": true,
|
||||||
|
"timeoutMs": 1000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -0,0 +1,194 @@
|
|||||||
|
{
|
||||||
|
"format": "juegoland.package",
|
||||||
|
"formatVersion": 1,
|
||||||
|
"id": "hundido",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"engineApiVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Hundido",
|
||||||
|
"description": "Paquete de referencia de Hundido; especificación previa a su implementación.",
|
||||||
|
"authors": ["Juegoland"],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"defaultLocale": "es",
|
||||||
|
"locales": {
|
||||||
|
"es": "locales/es.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"kind": "game",
|
||||||
|
"classification": {
|
||||||
|
"category": "board",
|
||||||
|
"tags": ["hundido"],
|
||||||
|
"competition": "individual"
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"min": 2,
|
||||||
|
"max": 2
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"hiddenInformation": true,
|
||||||
|
"simultaneousActions": true,
|
||||||
|
"deadlines": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"board": {
|
||||||
|
"kind": "board",
|
||||||
|
"definition": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "components/board.json"
|
||||||
|
},
|
||||||
|
"copies": 2
|
||||||
|
},
|
||||||
|
"fleet": {
|
||||||
|
"kind": "pieces",
|
||||||
|
"definition": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "components/fleet.json"
|
||||||
|
},
|
||||||
|
"copies": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"entry": "rules/index.ts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stateSchemaVersion": 1,
|
||||||
|
"viewSchemaVersion": 1,
|
||||||
|
"eventSchemaVersion": 1,
|
||||||
|
"schemas": {
|
||||||
|
"config": "schemas/config.json",
|
||||||
|
"data": "schemas/data.json",
|
||||||
|
"view": "schemas/view.json",
|
||||||
|
"context": "schemas/context.json",
|
||||||
|
"event": "schemas/event.json",
|
||||||
|
"visibleEvent": "schemas/visible-event.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"entry": "presentation/index.ts",
|
||||||
|
"apiVersion": 1,
|
||||||
|
"bundleVersion": "1.0.0",
|
||||||
|
"resources": {
|
||||||
|
"catalog.cover": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog.thumbnail": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog.icon": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fleet.ships": {
|
||||||
|
"kind": "atlas",
|
||||||
|
"path": "assets/fleet.png",
|
||||||
|
"load": "board",
|
||||||
|
"required": true,
|
||||||
|
"width": 1024,
|
||||||
|
"height": 1536,
|
||||||
|
"regions": {
|
||||||
|
"portaaviones": {
|
||||||
|
"x": 14,
|
||||||
|
"y": 48,
|
||||||
|
"width": 999,
|
||||||
|
"height": 322
|
||||||
|
},
|
||||||
|
"acorazado": {
|
||||||
|
"x": 13,
|
||||||
|
"y": 395,
|
||||||
|
"width": 1000,
|
||||||
|
"height": 270
|
||||||
|
},
|
||||||
|
"crucero": {
|
||||||
|
"x": 72,
|
||||||
|
"y": 694,
|
||||||
|
"width": 870,
|
||||||
|
"height": 229
|
||||||
|
},
|
||||||
|
"submarino": {
|
||||||
|
"x": 110,
|
||||||
|
"y": 956,
|
||||||
|
"width": 804,
|
||||||
|
"height": 184
|
||||||
|
},
|
||||||
|
"destructor": {
|
||||||
|
"x": 219,
|
||||||
|
"y": 1155,
|
||||||
|
"width": 590,
|
||||||
|
"height": 195
|
||||||
|
},
|
||||||
|
"lancha": {
|
||||||
|
"x": 371,
|
||||||
|
"y": 1350,
|
||||||
|
"width": 287,
|
||||||
|
"height": 164
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "fleet.label"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sound.hit": {
|
||||||
|
"kind": "audio",
|
||||||
|
"path": "assets/hit.wav",
|
||||||
|
"load": "phase",
|
||||||
|
"phase": "battle",
|
||||||
|
"required": false,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "omit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sound.miss": {
|
||||||
|
"kind": "audio",
|
||||||
|
"path": "assets/miss.wav",
|
||||||
|
"load": "phase",
|
||||||
|
"phase": "battle",
|
||||||
|
"required": false,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "omit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sound.sunk": {
|
||||||
|
"kind": "audio",
|
||||||
|
"path": "assets/sunk.wav",
|
||||||
|
"load": "phase",
|
||||||
|
"phase": "battle",
|
||||||
|
"required": false,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "omit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"fleet": {
|
||||||
|
"source": "local",
|
||||||
|
"path": "presentation/fleet-artwork.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"cover": "catalog.cover",
|
||||||
|
"thumbnail": "catalog.thumbnail",
|
||||||
|
"icon": "catalog.icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"format": "juegoland.package",
|
||||||
|
"formatVersion": 1,
|
||||||
|
"id": "playing-cards",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"engineApiVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "playing-cards",
|
||||||
|
"description": "Biblioteca de referencia del contrato Juegoland.",
|
||||||
|
"authors": ["Juegoland"],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"defaultLocale": "es",
|
||||||
|
"locales": {
|
||||||
|
"es": "locales/es.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"kind": "components",
|
||||||
|
"exports": {
|
||||||
|
"poker52": "definitions/poker-52.json",
|
||||||
|
"espanola40": "definitions/espanola-40.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"format": "juegoland.package",
|
||||||
|
"formatVersion": 1,
|
||||||
|
"id": "cards-poker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"engineApiVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "cards-poker",
|
||||||
|
"description": "Biblioteca de referencia del contrato Juegoland.",
|
||||||
|
"authors": ["Juegoland"],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"defaultLocale": "es",
|
||||||
|
"locales": {
|
||||||
|
"es": "locales/es.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": [],
|
||||||
|
"kind": "assets",
|
||||||
|
"exports": {
|
||||||
|
"standard": "artwork/standard.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -0,0 +1,187 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"resources": {
|
||||||
|
"cards.faces": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "graphics/poker-faces.ts",
|
||||||
|
"load": "board",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "cards.face"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cards.back": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "graphics/poker-back.ts",
|
||||||
|
"load": "board",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "cards.hidden"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slots": {
|
||||||
|
"picas-1": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-2": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-3": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-4": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-5": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-6": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-7": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-8": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-9": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-10": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-11": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-12": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"picas-13": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-1": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-2": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-3": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-4": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-5": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-6": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-7": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-8": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-9": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-10": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-11": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-12": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"corazones-13": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-1": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-2": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-3": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-4": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-5": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-6": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-7": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-8": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-9": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-10": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-11": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-12": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"diamantes-13": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-1": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-2": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-3": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-4": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-5": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-6": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-7": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-8": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-9": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-10": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-11": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-12": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
},
|
||||||
|
"treboles-13": {
|
||||||
|
"resourceId": "cards.faces"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"resourceId": "cards.back"
|
||||||
|
},
|
||||||
|
"aspectRatio": 0.7142857142857143
|
||||||
|
}
|
||||||
@ -0,0 +1,119 @@
|
|||||||
|
{
|
||||||
|
"format": "juegoland.package",
|
||||||
|
"formatVersion": 1,
|
||||||
|
"id": "solitario",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"engineApiVersion": 1,
|
||||||
|
"metadata": {
|
||||||
|
"name": "Solitario Klondike",
|
||||||
|
"description": "Paquete de referencia de Solitario Klondike; especificación previa a su implementación.",
|
||||||
|
"authors": ["Juegoland"],
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"defaultLocale": "es",
|
||||||
|
"locales": {
|
||||||
|
"es": "locales/es.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": [
|
||||||
|
{
|
||||||
|
"id": "playing-cards",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"kind": "components"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "cards-poker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"kind": "assets"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"kind": "game",
|
||||||
|
"classification": {
|
||||||
|
"category": "solitaire",
|
||||||
|
"tags": ["solitario"],
|
||||||
|
"competition": "solo"
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"min": 1,
|
||||||
|
"max": 1
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"hiddenInformation": true,
|
||||||
|
"simultaneousActions": false,
|
||||||
|
"deadlines": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"mainDeck": {
|
||||||
|
"kind": "deck",
|
||||||
|
"definition": {
|
||||||
|
"source": "dependency",
|
||||||
|
"packageId": "playing-cards",
|
||||||
|
"export": "poker52"
|
||||||
|
},
|
||||||
|
"copies": 1
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"entry": "rules/index.ts",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"stateSchemaVersion": 1,
|
||||||
|
"viewSchemaVersion": 1,
|
||||||
|
"eventSchemaVersion": 1,
|
||||||
|
"schemas": {
|
||||||
|
"config": "schemas/config.json",
|
||||||
|
"data": "schemas/data.json",
|
||||||
|
"view": "schemas/view.json",
|
||||||
|
"context": "schemas/context.json",
|
||||||
|
"event": "schemas/event.json",
|
||||||
|
"visibleEvent": "schemas/visible-event.json"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"entry": "presentation/index.ts",
|
||||||
|
"apiVersion": 1,
|
||||||
|
"bundleVersion": "1.0.0",
|
||||||
|
"resources": {
|
||||||
|
"catalog.cover": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog.thumbnail": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog.icon": {
|
||||||
|
"kind": "graphic",
|
||||||
|
"path": "presentation/catalog.ts",
|
||||||
|
"load": "catalog",
|
||||||
|
"required": true,
|
||||||
|
"fallback": {
|
||||||
|
"kind": "text",
|
||||||
|
"key": "catalog.name"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"mainDeck": {
|
||||||
|
"source": "dependency",
|
||||||
|
"packageId": "cards-poker",
|
||||||
|
"export": "standard"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"cover": "catalog.cover",
|
||||||
|
"thumbnail": "catalog.thumbnail",
|
||||||
|
"icon": "catalog.icon"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,202 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.sync",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"requestId": "439221f0-6f92-4c94-8fcf-a420f0dddf31",
|
||||||
|
"knownRevision": null,
|
||||||
|
"client": {
|
||||||
|
"engineApiVersion": 1,
|
||||||
|
"presentationApiVersion": 1,
|
||||||
|
"presentations": [
|
||||||
|
{
|
||||||
|
"id": "solitario",
|
||||||
|
"rulesVersion": "1.0.0",
|
||||||
|
"viewSchemaVersion": 1,
|
||||||
|
"eventSchemaVersion": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.command",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"commandId": "19a772c9-8569-43b8-a4be-294402f58043",
|
||||||
|
"expected": {
|
||||||
|
"kind": "actor",
|
||||||
|
"phaseEpoch": 0,
|
||||||
|
"actorRevision": 2
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "place",
|
||||||
|
"payload": {
|
||||||
|
"shipId": "fleet:p0:lancha",
|
||||||
|
"row": 2,
|
||||||
|
"column": 3,
|
||||||
|
"vertical": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.ack",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9",
|
||||||
|
"duplicate": false,
|
||||||
|
"receipt": {
|
||||||
|
"status": "applied",
|
||||||
|
"appliedRevision": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.ack",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9",
|
||||||
|
"duplicate": true,
|
||||||
|
"receipt": {
|
||||||
|
"status": "applied",
|
||||||
|
"appliedRevision": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.ack",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"commandId": "e1d20e1b-5aa4-4468-8e78-23440d5f76c0",
|
||||||
|
"duplicate": false,
|
||||||
|
"receipt": {
|
||||||
|
"status": "rejected",
|
||||||
|
"code": "STALE_REVISION",
|
||||||
|
"observedRevision": 13
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.snapshot",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"subscriptionId": "d0dd4fae-b908-4ab2-a12c-32c87d1cf1a2",
|
||||||
|
"delivery": {
|
||||||
|
"kind": "sync",
|
||||||
|
"requestId": "439221f0-6f92-4c94-8fcf-a420f0dddf31"
|
||||||
|
},
|
||||||
|
"revision": 0,
|
||||||
|
"game": {
|
||||||
|
"id": "solitario",
|
||||||
|
"rulesVersion": "1.0.0",
|
||||||
|
"viewSchemaVersion": 1,
|
||||||
|
"eventSchemaVersion": 1
|
||||||
|
},
|
||||||
|
"status": "active",
|
||||||
|
"view": {
|
||||||
|
"example": "wire-envelope-only"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"phase": "playing",
|
||||||
|
"actors": ["p0"]
|
||||||
|
},
|
||||||
|
"availableActions": [],
|
||||||
|
"outcome": null,
|
||||||
|
"events": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.snapshot",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"subscriptionId": "d0dd4fae-b908-4ab2-a12c-32c87d1cf1a2",
|
||||||
|
"delivery": {
|
||||||
|
"kind": "live",
|
||||||
|
"fromRevision": 0
|
||||||
|
},
|
||||||
|
"revision": 1,
|
||||||
|
"game": {
|
||||||
|
"id": "solitario",
|
||||||
|
"rulesVersion": "1.0.0",
|
||||||
|
"viewSchemaVersion": 1,
|
||||||
|
"eventSchemaVersion": 1
|
||||||
|
},
|
||||||
|
"status": "completed",
|
||||||
|
"view": {
|
||||||
|
"example": "wire-envelope-only"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"phase": "playing",
|
||||||
|
"actors": ["p0"]
|
||||||
|
},
|
||||||
|
"availableActions": [],
|
||||||
|
"outcome": {
|
||||||
|
"kind": "completed",
|
||||||
|
"reasonCode": "resigned",
|
||||||
|
"participants": {
|
||||||
|
"p0": {
|
||||||
|
"outcome": "loss",
|
||||||
|
"metrics": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"events": [
|
||||||
|
{
|
||||||
|
"eventId": "598c0e3a-72e0-48a8-a0b0-a11a8b70584e",
|
||||||
|
"revision": 1,
|
||||||
|
"order": 0,
|
||||||
|
"type": "match.completed",
|
||||||
|
"payload": {
|
||||||
|
"reasonCode": "resigned"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.command-status",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"requestId": "439221f0-6f92-4c94-8fcf-a420f0dddf31",
|
||||||
|
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.command-unknown",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"requestId": "439221f0-6f92-4c94-8fcf-a420f0dddf31",
|
||||||
|
"commandId": "30c474dd-030a-4e0e-883f-c8a6d636c8c9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.cursor",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"subscriptionId": "d0dd4fae-b908-4ab2-a12c-32c87d1cf1a2",
|
||||||
|
"revision": 13
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.unsubscribe",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"subscriptionId": "d0dd4fae-b908-4ab2-a12c-32c87d1cf1a2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocolVersion": 1,
|
||||||
|
"type": "game.error",
|
||||||
|
"matchId": "cc749a64-c8e4-4d74-a913-54b39f4c9089",
|
||||||
|
"requestId": "439221f0-6f92-4c94-8fcf-a420f0dddf31",
|
||||||
|
"code": "INCOMPATIBLE_CLIENT",
|
||||||
|
"recovery": "reload-client",
|
||||||
|
"messageKey": "game.reloadRequired"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"formatVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "version", "sha256"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"resolved": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "version", "sha256"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"packageId": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?(?:\\/[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?)*$"
|
||||||
|
},
|
||||||
|
"sha256": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"bytes": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"mediaType": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"url": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"format": "uri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"packageId",
|
||||||
|
"path",
|
||||||
|
"sha256",
|
||||||
|
"bytes",
|
||||||
|
"mediaType",
|
||||||
|
"url"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["formatVersion", "root", "resolved", "files"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"__schema0": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9.-]{0,79}$"
|
||||||
|
},
|
||||||
|
"__schema1": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$"
|
||||||
|
},
|
||||||
|
"__schema2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-f0-9]{64}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": "urn:juegoland:lock:1"
|
||||||
|
}
|
||||||
@ -0,0 +1,550 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"formatVersion": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"engineApiVersion": {
|
||||||
|
"$ref": "#/$defs/__schema4"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"$ref": "#/$defs/__schema8"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game"
|
||||||
|
},
|
||||||
|
"classification": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"category": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"competition": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["individual", "teams", "cooperative", "solo", "open"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["category", "tags", "competition"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"players": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"min": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 64
|
||||||
|
},
|
||||||
|
"max": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 64
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["min", "max"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"hiddenInformation": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"simultaneousActions": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"deadlines": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["hiddenInformation", "simultaneousActions", "deadlines"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"components": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["deck", "dice", "wheel", "board", "pieces", "custom"]
|
||||||
|
},
|
||||||
|
"definition": {
|
||||||
|
"$ref": "#/$defs/__schema9"
|
||||||
|
},
|
||||||
|
"copies": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "definition", "copies"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entry": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"stateSchemaVersion": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"viewSchemaVersion": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"eventSchemaVersion": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"schemas": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"config": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"event": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"visibleEvent": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"config",
|
||||||
|
"data",
|
||||||
|
"view",
|
||||||
|
"context",
|
||||||
|
"event",
|
||||||
|
"visibleEvent"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"entry",
|
||||||
|
"version",
|
||||||
|
"stateSchemaVersion",
|
||||||
|
"viewSchemaVersion",
|
||||||
|
"eventSchemaVersion",
|
||||||
|
"schemas"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"presentation": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"entry": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"apiVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"bundleVersion": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["image", "atlas", "audio", "font", "graphic"]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["catalog", "board", "phase", "lazy"]
|
||||||
|
},
|
||||||
|
"phase": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"altKey": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"regions": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["x", "y", "width", "height"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "text"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "key"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "resource"
|
||||||
|
},
|
||||||
|
"resourceId": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "resourceId"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "omit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "path", "load", "required", "fallback"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bindings": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"catalog": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"cover": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"icon": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["cover", "thumbnail", "icon"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"entry",
|
||||||
|
"apiVersion",
|
||||||
|
"bundleVersion",
|
||||||
|
"resources",
|
||||||
|
"bindings",
|
||||||
|
"catalog"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"format",
|
||||||
|
"formatVersion",
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"engineApiVersion",
|
||||||
|
"metadata",
|
||||||
|
"dependencies",
|
||||||
|
"kind",
|
||||||
|
"classification",
|
||||||
|
"players",
|
||||||
|
"capabilities",
|
||||||
|
"components",
|
||||||
|
"rules",
|
||||||
|
"presentation"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"format": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"formatVersion": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"engineApiVersion": {
|
||||||
|
"$ref": "#/$defs/__schema4"
|
||||||
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"$ref": "#/$defs/__schema8"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["components", "assets"]
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"format",
|
||||||
|
"formatVersion",
|
||||||
|
"id",
|
||||||
|
"version",
|
||||||
|
"engineApiVersion",
|
||||||
|
"metadata",
|
||||||
|
"dependencies",
|
||||||
|
"kind",
|
||||||
|
"exports"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"__schema0": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "juegoland.package"
|
||||||
|
},
|
||||||
|
"__schema1": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"__schema2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9.-]{0,79}$"
|
||||||
|
},
|
||||||
|
"__schema3": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$"
|
||||||
|
},
|
||||||
|
"__schema4": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"__schema5": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 120
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 2000
|
||||||
|
},
|
||||||
|
"authors": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"defaultLocale": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z]{2}(?:-[A-Z]{2})?$"
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"description",
|
||||||
|
"authors",
|
||||||
|
"license",
|
||||||
|
"defaultLocale",
|
||||||
|
"locales"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema6": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"__schema7": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?(?:\\/[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?)*$"
|
||||||
|
},
|
||||||
|
"__schema8": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"version": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["components", "assets"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "version", "kind"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"__schema9": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"source": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "local"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["source", "path"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"source": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "dependency"
|
||||||
|
},
|
||||||
|
"packageId": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"export": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["source", "packageId", "export"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": "urn:juegoland:package:1"
|
||||||
|
}
|
||||||
@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"fromRevision": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"toRevision": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"maxItems": 128,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["id", "fromRevision", "toRevision", "steps"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"__schema0": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"__schema1": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"effect": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"trigger": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "after"
|
||||||
|
},
|
||||||
|
"steps": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "steps"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "marker"
|
||||||
|
},
|
||||||
|
"step": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"marker": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "step", "marker"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"emits": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"blocking": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"timeoutMs": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 12000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"effect",
|
||||||
|
"trigger",
|
||||||
|
"params",
|
||||||
|
"emits",
|
||||||
|
"blocking",
|
||||||
|
"timeoutMs"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema2": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9.-]{0,79}$"
|
||||||
|
},
|
||||||
|
"__schema3": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": "urn:juegoland:presentation-plan:1"
|
||||||
|
}
|
||||||
@ -0,0 +1,734 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.command"
|
||||||
|
},
|
||||||
|
"commandId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"$ref": "#/$defs/__schema4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"type",
|
||||||
|
"commandId",
|
||||||
|
"expected",
|
||||||
|
"action"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.snapshot"
|
||||||
|
},
|
||||||
|
"subscriptionId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "sync"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "requestId"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "live"
|
||||||
|
},
|
||||||
|
"fromRevision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "fromRevision"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"game": {
|
||||||
|
"$ref": "#/$defs/__schema8"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["active", "completed", "cancelled"]
|
||||||
|
},
|
||||||
|
"view": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
},
|
||||||
|
"context": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
},
|
||||||
|
"availableActions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "concrete"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"$ref": "#/$defs/__schema4"
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "action", "expected"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "parameterized"
|
||||||
|
},
|
||||||
|
"actionType": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"inputSchemaId": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"constraints": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
},
|
||||||
|
"expected": {
|
||||||
|
"$ref": "#/$defs/__schema2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"kind",
|
||||||
|
"actionType",
|
||||||
|
"inputSchemaId",
|
||||||
|
"constraints",
|
||||||
|
"expected"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outcome": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "completed"
|
||||||
|
},
|
||||||
|
"reasonCode": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"participants": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^p(0|[1-9]\\d*)$"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"outcome": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["win", "loss", "draw", "neutral"]
|
||||||
|
},
|
||||||
|
"score": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"rank": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"teamId": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["outcome", "metrics"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "reasonCode", "participants"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "cancelled"
|
||||||
|
},
|
||||||
|
"reasonCode": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "reasonCode"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"events": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"eventId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["eventId", "revision", "order", "type", "payload"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"type",
|
||||||
|
"subscriptionId",
|
||||||
|
"delivery",
|
||||||
|
"revision",
|
||||||
|
"game",
|
||||||
|
"status",
|
||||||
|
"view",
|
||||||
|
"context",
|
||||||
|
"availableActions",
|
||||||
|
"outcome",
|
||||||
|
"events"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.ack"
|
||||||
|
},
|
||||||
|
"commandId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"duplicate": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"receipt": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "applied"
|
||||||
|
},
|
||||||
|
"appliedRevision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "appliedRevision"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"status": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "rejected"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"STALE_REVISION",
|
||||||
|
"STALE_PHASE",
|
||||||
|
"STALE_ACTOR",
|
||||||
|
"ILLEGAL_ACTION",
|
||||||
|
"MATCH_FINISHED"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"observedRevision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["status", "code", "observedRevision"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"type",
|
||||||
|
"commandId",
|
||||||
|
"duplicate",
|
||||||
|
"receipt"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"$ref": "#/$defs/__schema9"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.sync"
|
||||||
|
},
|
||||||
|
"knownRevision": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"client": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"engineApiVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"presentationApiVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"presentations": {
|
||||||
|
"minItems": 1,
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema8"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"engineApiVersion",
|
||||||
|
"presentationApiVersion",
|
||||||
|
"presentations"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"requestId",
|
||||||
|
"type",
|
||||||
|
"knownRevision",
|
||||||
|
"client"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.unsubscribe"
|
||||||
|
},
|
||||||
|
"subscriptionId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["protocolVersion", "matchId", "type", "subscriptionId"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"$ref": "#/$defs/__schema9"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.command-status"
|
||||||
|
},
|
||||||
|
"commandId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"requestId",
|
||||||
|
"type",
|
||||||
|
"commandId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"$ref": "#/$defs/__schema9"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.command-unknown"
|
||||||
|
},
|
||||||
|
"commandId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"requestId",
|
||||||
|
"type",
|
||||||
|
"commandId"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"$ref": "#/$defs/__schema1"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.cursor"
|
||||||
|
},
|
||||||
|
"subscriptionId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"protocolVersion",
|
||||||
|
"matchId",
|
||||||
|
"type",
|
||||||
|
"subscriptionId",
|
||||||
|
"revision"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"protocolVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"matchId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "game.error"
|
||||||
|
},
|
||||||
|
"requestId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"commandId": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"code": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"INVALID_MESSAGE",
|
||||||
|
"UNSUPPORTED_PROTOCOL",
|
||||||
|
"UNAUTHORIZED",
|
||||||
|
"NOT_PARTICIPANT",
|
||||||
|
"STALE_REVISION",
|
||||||
|
"STALE_PHASE",
|
||||||
|
"STALE_ACTOR",
|
||||||
|
"ILLEGAL_ACTION",
|
||||||
|
"MATCH_FINISHED",
|
||||||
|
"COMMAND_ID_REUSED",
|
||||||
|
"PACKAGE_UNAVAILABLE",
|
||||||
|
"INCOMPATIBLE_CLIENT",
|
||||||
|
"RATE_LIMITED",
|
||||||
|
"INTERNAL_ERROR"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"recovery": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"sync",
|
||||||
|
"retry-same-command",
|
||||||
|
"reload-client",
|
||||||
|
"reauthenticate"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"messageKey": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["protocolVersion", "type", "code", "recovery", "messageKey"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"__schema0": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"__schema1": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
},
|
||||||
|
"__schema2": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "match"
|
||||||
|
},
|
||||||
|
"revision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "revision"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "actor"
|
||||||
|
},
|
||||||
|
"phaseEpoch": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
},
|
||||||
|
"actorRevision": {
|
||||||
|
"$ref": "#/$defs/__schema3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "phaseEpoch", "actorRevision"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"__schema3": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"__schema4": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"payload": {
|
||||||
|
"$ref": "#/$defs/__schema6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["type", "payload"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema5": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9.-]{0,79}$"
|
||||||
|
},
|
||||||
|
"__schema6": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"__schema7": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/__schema7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"__schema8": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"$ref": "#/$defs/__schema5"
|
||||||
|
},
|
||||||
|
"rulesVersion": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$"
|
||||||
|
},
|
||||||
|
"viewSchemaVersion": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"eventSchemaVersion": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"id",
|
||||||
|
"rulesVersion",
|
||||||
|
"viewSchemaVersion",
|
||||||
|
"eventSchemaVersion"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"__schema9": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": "urn:juegoland:protocol:1"
|
||||||
|
}
|
||||||
@ -0,0 +1,175 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"schemaVersion": {
|
||||||
|
"type": "number",
|
||||||
|
"const": 1
|
||||||
|
},
|
||||||
|
"resources": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["image", "atlas", "audio", "font", "graphic"]
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?(?:\\/[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?)*$"
|
||||||
|
},
|
||||||
|
"load": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["catalog", "board", "phase", "lazy"]
|
||||||
|
},
|
||||||
|
"phase": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"required": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"altKey": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"regions": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"x": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"y": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"width": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
},
|
||||||
|
"height": {
|
||||||
|
"type": "integer",
|
||||||
|
"exclusiveMinimum": 0,
|
||||||
|
"maximum": 9007199254740991
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["x", "y", "width", "height"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"fallback": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "text"
|
||||||
|
},
|
||||||
|
"key": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "key"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "resource"
|
||||||
|
},
|
||||||
|
"resourceId": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "resourceId"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"kind": {
|
||||||
|
"type": "string",
|
||||||
|
"const": "omit"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["kind", "path", "load", "required", "fallback"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"slots": {
|
||||||
|
"type": "object",
|
||||||
|
"propertyNames": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"resourceId": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["resourceId"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"back": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"resourceId": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
},
|
||||||
|
"region": {
|
||||||
|
"$ref": "#/$defs/__schema0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["resourceId"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"aspectRatio": {
|
||||||
|
"type": "number",
|
||||||
|
"exclusiveMinimum": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["schemaVersion", "resources", "slots"],
|
||||||
|
"additionalProperties": false,
|
||||||
|
"$defs": {
|
||||||
|
"__schema0": {
|
||||||
|
"type": "string",
|
||||||
|
"pattern": "^[a-z][a-zA-Z0-9.-]{0,79}$"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$id": "urn:juegoland:resource-set:1"
|
||||||
|
}
|
||||||
@ -0,0 +1,720 @@
|
|||||||
|
// Executable specification. Not imported by the production application.
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const key = z.string().regex(/^[a-z][a-zA-Z0-9.-]{0,79}$/);
|
||||||
|
export const version = z
|
||||||
|
.string()
|
||||||
|
.regex(/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/);
|
||||||
|
export const relativePath = z
|
||||||
|
.string()
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?(?:\/[a-zA-Z0-9_-](?:[a-zA-Z0-9_.-]*[a-zA-Z0-9_-])?)*$/,
|
||||||
|
);
|
||||||
|
export const revision = z.number().int().min(0).max(Number.MAX_SAFE_INTEGER);
|
||||||
|
export const json = z.json();
|
||||||
|
export const jsonObject = z.record(z.string(), json);
|
||||||
|
export const seat = z.string().regex(/^p(0|[1-9]\d*)$/);
|
||||||
|
export const reference = z.discriminatedUnion('source', [
|
||||||
|
z.strictObject({ source: z.literal('local'), path: relativePath }),
|
||||||
|
z.strictObject({
|
||||||
|
source: z.literal('dependency'),
|
||||||
|
packageId: key,
|
||||||
|
export: key,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const dependency = z.strictObject({
|
||||||
|
id: key,
|
||||||
|
version,
|
||||||
|
kind: z.enum(['components', 'assets']),
|
||||||
|
});
|
||||||
|
const resource = z.strictObject({
|
||||||
|
kind: z.enum(['image', 'atlas', 'audio', 'font', 'graphic']),
|
||||||
|
path: relativePath,
|
||||||
|
load: z.enum(['catalog', 'board', 'phase', 'lazy']),
|
||||||
|
phase: key.optional(),
|
||||||
|
required: z.boolean(),
|
||||||
|
altKey: key.optional(),
|
||||||
|
width: z.number().int().positive().optional(),
|
||||||
|
height: z.number().int().positive().optional(),
|
||||||
|
regions: z
|
||||||
|
.record(
|
||||||
|
key,
|
||||||
|
z.strictObject({
|
||||||
|
x: z.number().int().nonnegative(),
|
||||||
|
y: z.number().int().nonnegative(),
|
||||||
|
width: z.number().int().positive(),
|
||||||
|
height: z.number().int().positive(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
fallback: z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({ kind: z.literal('text'), key }),
|
||||||
|
z.strictObject({ kind: z.literal('resource'), resourceId: key }),
|
||||||
|
z.strictObject({ kind: z.literal('omit') }),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
const metadata = z.strictObject({
|
||||||
|
name: z.string().min(1).max(120),
|
||||||
|
description: z.string().min(1).max(2000),
|
||||||
|
authors: z.array(z.string().min(1)).min(1),
|
||||||
|
license: z.string().min(1),
|
||||||
|
defaultLocale: z.string().regex(/^[a-z]{2}(?:-[A-Z]{2})?$/),
|
||||||
|
locales: z.record(z.string(), relativePath),
|
||||||
|
});
|
||||||
|
const common = {
|
||||||
|
format: z.literal('juegoland.package'),
|
||||||
|
formatVersion: z.literal(1),
|
||||||
|
id: key,
|
||||||
|
version,
|
||||||
|
engineApiVersion: z.literal(1),
|
||||||
|
metadata,
|
||||||
|
dependencies: z.array(dependency),
|
||||||
|
};
|
||||||
|
export const gamePackage = z.strictObject({
|
||||||
|
...common,
|
||||||
|
kind: z.literal('game'),
|
||||||
|
classification: z.strictObject({
|
||||||
|
category: key,
|
||||||
|
tags: z.array(key),
|
||||||
|
competition: z.enum(['individual', 'teams', 'cooperative', 'solo', 'open']),
|
||||||
|
}),
|
||||||
|
players: z.strictObject({
|
||||||
|
min: z.number().int().min(1).max(64),
|
||||||
|
max: z.number().int().min(1).max(64),
|
||||||
|
}),
|
||||||
|
capabilities: z.strictObject({
|
||||||
|
hiddenInformation: z.boolean(),
|
||||||
|
simultaneousActions: z.boolean(),
|
||||||
|
deadlines: z.boolean(),
|
||||||
|
}),
|
||||||
|
components: z.record(
|
||||||
|
key,
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.enum(['deck', 'dice', 'wheel', 'board', 'pieces', 'custom']),
|
||||||
|
definition: reference,
|
||||||
|
copies: z.number().int().min(1).max(100),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
rules: z.strictObject({
|
||||||
|
entry: relativePath,
|
||||||
|
version,
|
||||||
|
stateSchemaVersion: z.number().int().positive(),
|
||||||
|
viewSchemaVersion: z.number().int().positive(),
|
||||||
|
eventSchemaVersion: z.number().int().positive(),
|
||||||
|
schemas: z.strictObject({
|
||||||
|
config: relativePath,
|
||||||
|
data: relativePath,
|
||||||
|
view: relativePath,
|
||||||
|
context: relativePath,
|
||||||
|
event: relativePath,
|
||||||
|
visibleEvent: relativePath,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
presentation: z.strictObject({
|
||||||
|
entry: relativePath,
|
||||||
|
apiVersion: z.literal(1),
|
||||||
|
bundleVersion: version,
|
||||||
|
resources: z.record(key, resource),
|
||||||
|
bindings: z.record(key, reference),
|
||||||
|
catalog: z.strictObject({ cover: key, thumbnail: key, icon: key }),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
export const libraryPackage = z.strictObject({
|
||||||
|
...common,
|
||||||
|
kind: z.enum(['components', 'assets']),
|
||||||
|
exports: z.record(key, relativePath),
|
||||||
|
});
|
||||||
|
export const packageSchema = z.union([gamePackage, libraryPackage]);
|
||||||
|
export type GamePackage = z.infer<typeof gamePackage>;
|
||||||
|
export type Package = z.infer<typeof packageSchema>;
|
||||||
|
|
||||||
|
const cardDefinition = z.strictObject({
|
||||||
|
id: key,
|
||||||
|
suit: key,
|
||||||
|
rank: z.number().int(),
|
||||||
|
attributes: jsonObject,
|
||||||
|
});
|
||||||
|
export const componentSchema = z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('deck'),
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
cards: z.array(cardDefinition).min(1),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('dice'),
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
dice: z
|
||||||
|
.array(
|
||||||
|
z.strictObject({
|
||||||
|
id: key,
|
||||||
|
faces: z
|
||||||
|
.array(
|
||||||
|
z.strictObject({
|
||||||
|
id: key,
|
||||||
|
value: json,
|
||||||
|
weight: z.number().int().positive().max(1000000),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.min(2),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('wheel'),
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
sectors: z
|
||||||
|
.array(
|
||||||
|
z.strictObject({
|
||||||
|
id: key,
|
||||||
|
weight: z.number().int().positive().max(1000000),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('board'),
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
layout: z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('grid'),
|
||||||
|
rows: z.number().int().positive(),
|
||||||
|
columns: z.number().int().positive(),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('graph'),
|
||||||
|
cells: z
|
||||||
|
.array(z.strictObject({ id: key, neighbors: z.array(key) }))
|
||||||
|
.min(1),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('pieces'),
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
definitions: z
|
||||||
|
.array(
|
||||||
|
z.strictObject({
|
||||||
|
id: key,
|
||||||
|
count: z.number().int().positive(),
|
||||||
|
attributes: jsonObject,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.min(1),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('custom'),
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
typeId: key,
|
||||||
|
data: jsonObject,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
export type ComponentDefinition = z.infer<typeof componentSchema>;
|
||||||
|
export const resourceSetSchema = z.strictObject({
|
||||||
|
schemaVersion: z.literal(1),
|
||||||
|
resources: z.record(key, resource),
|
||||||
|
slots: z.record(
|
||||||
|
key,
|
||||||
|
z.strictObject({ resourceId: key, region: key.optional() }),
|
||||||
|
),
|
||||||
|
back: z.strictObject({ resourceId: key, region: key.optional() }).optional(),
|
||||||
|
aspectRatio: z.number().positive().optional(),
|
||||||
|
});
|
||||||
|
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
||||||
|
export const lockSchema = z.strictObject({
|
||||||
|
formatVersion: z.literal(1),
|
||||||
|
root: z.strictObject({ id: key, version, sha256: digest }),
|
||||||
|
resolved: z.array(z.strictObject({ id: key, version, sha256: digest })),
|
||||||
|
files: z.array(
|
||||||
|
z.strictObject({
|
||||||
|
packageId: key,
|
||||||
|
path: relativePath,
|
||||||
|
sha256: digest,
|
||||||
|
bytes: revision,
|
||||||
|
mediaType: z.string().min(1),
|
||||||
|
url: z.string().url().nullable(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export const presentationPlanSchema = z.strictObject({
|
||||||
|
id: z.string().min(1),
|
||||||
|
fromRevision: revision,
|
||||||
|
toRevision: revision,
|
||||||
|
steps: z
|
||||||
|
.array(
|
||||||
|
z.strictObject({
|
||||||
|
id: key,
|
||||||
|
effect: key,
|
||||||
|
trigger: z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({ kind: z.literal('after'), steps: z.array(key) }),
|
||||||
|
z.strictObject({ kind: z.literal('marker'), step: key, marker: key }),
|
||||||
|
]),
|
||||||
|
params: jsonObject,
|
||||||
|
emits: z.array(key),
|
||||||
|
blocking: z.boolean(),
|
||||||
|
timeoutMs: z.number().int().min(1).max(12000),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.max(128),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const result = z.strictObject({
|
||||||
|
kind: z.literal('completed'),
|
||||||
|
reasonCode: key,
|
||||||
|
participants: z.record(
|
||||||
|
seat,
|
||||||
|
z.strictObject({
|
||||||
|
outcome: z.enum(['win', 'loss', 'draw', 'neutral']),
|
||||||
|
score: z.number().finite().optional(),
|
||||||
|
rank: z.number().int().positive().optional(),
|
||||||
|
teamId: key.optional(),
|
||||||
|
metrics: jsonObject,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
export const matchOutcome = z.union([
|
||||||
|
result,
|
||||||
|
z.strictObject({ kind: z.literal('cancelled'), reasonCode: key }),
|
||||||
|
]);
|
||||||
|
export type MatchOutcome = z.infer<typeof matchOutcome>;
|
||||||
|
|
||||||
|
export const action = z.strictObject({ type: key, payload: jsonObject });
|
||||||
|
export const precondition = z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({ kind: z.literal('match'), revision }),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('actor'),
|
||||||
|
phaseEpoch: revision,
|
||||||
|
actorRevision: revision,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
const base = { protocolVersion: z.literal(1), matchId: z.uuid() };
|
||||||
|
const request = { ...base, requestId: z.uuid() };
|
||||||
|
const gameViewContract = z.strictObject({
|
||||||
|
id: key,
|
||||||
|
rulesVersion: version,
|
||||||
|
viewSchemaVersion: z.number().int().positive(),
|
||||||
|
eventSchemaVersion: z.number().int().positive(),
|
||||||
|
});
|
||||||
|
export const command = z.strictObject({
|
||||||
|
...base,
|
||||||
|
type: z.literal('game.command'),
|
||||||
|
commandId: z.uuid(),
|
||||||
|
expected: precondition,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
export const errorCode = z.enum([
|
||||||
|
'INVALID_MESSAGE',
|
||||||
|
'UNSUPPORTED_PROTOCOL',
|
||||||
|
'UNAUTHORIZED',
|
||||||
|
'NOT_PARTICIPANT',
|
||||||
|
'STALE_REVISION',
|
||||||
|
'STALE_PHASE',
|
||||||
|
'STALE_ACTOR',
|
||||||
|
'ILLEGAL_ACTION',
|
||||||
|
'MATCH_FINISHED',
|
||||||
|
'COMMAND_ID_REUSED',
|
||||||
|
'PACKAGE_UNAVAILABLE',
|
||||||
|
'INCOMPATIBLE_CLIENT',
|
||||||
|
'RATE_LIMITED',
|
||||||
|
'INTERNAL_ERROR',
|
||||||
|
]);
|
||||||
|
export const publicEvent = z.strictObject({
|
||||||
|
eventId: z.uuid(),
|
||||||
|
revision,
|
||||||
|
order: revision,
|
||||||
|
type: key,
|
||||||
|
payload: jsonObject,
|
||||||
|
});
|
||||||
|
export const offer = z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('concrete'),
|
||||||
|
action,
|
||||||
|
expected: precondition,
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
kind: z.literal('parameterized'),
|
||||||
|
actionType: key,
|
||||||
|
inputSchemaId: key,
|
||||||
|
constraints: jsonObject,
|
||||||
|
expected: precondition,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
export const snapshot = z.strictObject({
|
||||||
|
...base,
|
||||||
|
type: z.literal('game.snapshot'),
|
||||||
|
subscriptionId: z.uuid(),
|
||||||
|
delivery: z.discriminatedUnion('kind', [
|
||||||
|
z.strictObject({ kind: z.literal('sync'), requestId: z.uuid() }),
|
||||||
|
z.strictObject({ kind: z.literal('live'), fromRevision: revision }),
|
||||||
|
]),
|
||||||
|
revision,
|
||||||
|
game: gameViewContract,
|
||||||
|
status: z.enum(['active', 'completed', 'cancelled']),
|
||||||
|
view: jsonObject,
|
||||||
|
context: jsonObject,
|
||||||
|
availableActions: z.array(offer),
|
||||||
|
outcome: matchOutcome.nullable(),
|
||||||
|
events: z.array(publicEvent),
|
||||||
|
});
|
||||||
|
export const ack = z.strictObject({
|
||||||
|
...base,
|
||||||
|
type: z.literal('game.ack'),
|
||||||
|
commandId: z.uuid(),
|
||||||
|
duplicate: z.boolean(),
|
||||||
|
receipt: z.discriminatedUnion('status', [
|
||||||
|
z.strictObject({ status: z.literal('applied'), appliedRevision: revision }),
|
||||||
|
z.strictObject({
|
||||||
|
status: z.literal('rejected'),
|
||||||
|
code: z.enum([
|
||||||
|
'STALE_REVISION',
|
||||||
|
'STALE_PHASE',
|
||||||
|
'STALE_ACTOR',
|
||||||
|
'ILLEGAL_ACTION',
|
||||||
|
'MATCH_FINISHED',
|
||||||
|
]),
|
||||||
|
observedRevision: revision,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
export const wireSchema = z.union([
|
||||||
|
command,
|
||||||
|
snapshot,
|
||||||
|
ack,
|
||||||
|
z.strictObject({
|
||||||
|
...request,
|
||||||
|
type: z.literal('game.sync'),
|
||||||
|
knownRevision: revision.nullable(),
|
||||||
|
client: z.strictObject({
|
||||||
|
engineApiVersion: z.literal(1),
|
||||||
|
presentationApiVersion: z.literal(1),
|
||||||
|
presentations: z.array(gameViewContract).min(1),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
...base,
|
||||||
|
type: z.literal('game.unsubscribe'),
|
||||||
|
subscriptionId: z.uuid(),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
...request,
|
||||||
|
type: z.literal('game.command-status'),
|
||||||
|
commandId: z.uuid(),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
...request,
|
||||||
|
type: z.literal('game.command-unknown'),
|
||||||
|
commandId: z.uuid(),
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
...base,
|
||||||
|
type: z.literal('game.cursor'),
|
||||||
|
subscriptionId: z.uuid(),
|
||||||
|
revision,
|
||||||
|
}),
|
||||||
|
z.strictObject({
|
||||||
|
protocolVersion: z.literal(1),
|
||||||
|
matchId: z.uuid().optional(),
|
||||||
|
type: z.literal('game.error'),
|
||||||
|
requestId: z.uuid().optional(),
|
||||||
|
commandId: z.uuid().optional(),
|
||||||
|
code: errorCode,
|
||||||
|
recovery: z.enum([
|
||||||
|
'none',
|
||||||
|
'sync',
|
||||||
|
'retry-same-command',
|
||||||
|
'reload-client',
|
||||||
|
'reauthenticate',
|
||||||
|
]),
|
||||||
|
messageKey: key,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
export type WireMessage = z.infer<typeof wireSchema>;
|
||||||
|
export type Command = z.infer<typeof command>;
|
||||||
|
export type Snapshot = z.infer<typeof snapshot>;
|
||||||
|
export type ActionOffer = z.infer<typeof offer>;
|
||||||
|
|
||||||
|
// Cross-field and cross-package rules are a second validation layer, documented
|
||||||
|
// in the specification. JSON Schema describes the structural layer only.
|
||||||
|
export function validatePackage(
|
||||||
|
value: unknown,
|
||||||
|
registry: ReadonlyMap<string, Package> = new Map(),
|
||||||
|
): Package {
|
||||||
|
const parsed = packageSchema.parse(value);
|
||||||
|
const fail = (message: string): never => {
|
||||||
|
throw new Error(`${parsed.id}: ${message}`);
|
||||||
|
};
|
||||||
|
const dependencies = new Map<string, Package>();
|
||||||
|
for (const dep of parsed.dependencies) {
|
||||||
|
if (dependencies.has(dep.id) || dep.id === parsed.id)
|
||||||
|
fail('duplicate or self dependency');
|
||||||
|
const resolved = registry.get(`${dep.id}@${dep.version}`);
|
||||||
|
if (!resolved || resolved.kind !== dep.kind)
|
||||||
|
fail(`unresolved dependency ${dep.id}@${dep.version}`);
|
||||||
|
dependencies.set(dep.id, resolved!);
|
||||||
|
}
|
||||||
|
if (!Object.hasOwn(parsed.metadata.locales, parsed.metadata.defaultLocale))
|
||||||
|
fail('missing default locale');
|
||||||
|
if (parsed.kind !== 'game') {
|
||||||
|
if (!Object.keys(parsed.exports).length) fail('library has no exports');
|
||||||
|
if (parsed.dependencies.some((dep) => dep.kind !== parsed.kind))
|
||||||
|
fail('library dependency crosses server/client boundary');
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
if (parsed.players.min > parsed.players.max) fail('invalid player bounds');
|
||||||
|
if (parsed.classification.competition === 'solo' && parsed.players.max !== 1)
|
||||||
|
fail('solo requires one player');
|
||||||
|
const resolve = (
|
||||||
|
ref: z.infer<typeof reference>,
|
||||||
|
expected: 'components' | 'assets',
|
||||||
|
) => {
|
||||||
|
if (ref.source === 'local') return;
|
||||||
|
const dep = dependencies.get(ref.packageId);
|
||||||
|
if (
|
||||||
|
!dep ||
|
||||||
|
dep.kind === 'game' ||
|
||||||
|
dep.kind !== expected ||
|
||||||
|
!Object.hasOwn(dep.exports, ref.export)
|
||||||
|
)
|
||||||
|
fail('invalid dependency export');
|
||||||
|
};
|
||||||
|
for (const component of Object.values(parsed.components))
|
||||||
|
resolve(component.definition, 'components');
|
||||||
|
for (const [id, binding] of Object.entries(parsed.presentation.bindings)) {
|
||||||
|
if (!Object.hasOwn(parsed.components, id))
|
||||||
|
fail(`binding without component ${id}`);
|
||||||
|
resolve(binding, 'assets');
|
||||||
|
}
|
||||||
|
const resources = parsed.presentation.resources;
|
||||||
|
for (const id of Object.values(parsed.presentation.catalog)) {
|
||||||
|
const asset = resources[id];
|
||||||
|
if (
|
||||||
|
!asset ||
|
||||||
|
!['image', 'graphic'].includes(asset.kind) ||
|
||||||
|
asset.load !== 'catalog'
|
||||||
|
)
|
||||||
|
fail(`invalid catalog resource ${id}`);
|
||||||
|
}
|
||||||
|
validateResources(resources);
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateResources(
|
||||||
|
resources: z.infer<typeof resourceSetSchema>['resources'],
|
||||||
|
) {
|
||||||
|
const fail = (message: string): never => {
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
for (const [id, asset] of Object.entries(resources)) {
|
||||||
|
if ((asset.load === 'phase') !== (asset.phase !== undefined))
|
||||||
|
fail(`invalid phase loading ${id}`);
|
||||||
|
if (
|
||||||
|
['image', 'atlas'].includes(asset.kind) &&
|
||||||
|
(!asset.width || !asset.height)
|
||||||
|
)
|
||||||
|
fail(`missing image dimensions ${id}`);
|
||||||
|
if (asset.kind === 'atlas' && !Object.keys(asset.regions ?? {}).length)
|
||||||
|
fail(`empty atlas ${id}`);
|
||||||
|
if (asset.regions && asset.kind !== 'atlas')
|
||||||
|
fail(`regions outside atlas ${id}`);
|
||||||
|
for (const region of Object.values(asset.regions ?? {})) {
|
||||||
|
if (
|
||||||
|
region.x + region.width > asset.width! ||
|
||||||
|
region.y + region.height > asset.height!
|
||||||
|
)
|
||||||
|
fail(`atlas bounds ${id}`);
|
||||||
|
}
|
||||||
|
if (asset.required && asset.fallback.kind === 'omit')
|
||||||
|
fail(`required resource cannot be omitted ${id}`);
|
||||||
|
const visited = new Set([id]);
|
||||||
|
let cursor = asset;
|
||||||
|
while (cursor.fallback.kind === 'resource') {
|
||||||
|
const next = cursor.fallback.resourceId;
|
||||||
|
if (visited.has(next) || !Object.hasOwn(resources, next))
|
||||||
|
fail(`fallback cycle or missing resource ${id}`);
|
||||||
|
visited.add(next);
|
||||||
|
cursor = resources[next];
|
||||||
|
}
|
||||||
|
if (asset.required && cursor.fallback.kind === 'omit')
|
||||||
|
fail(`required fallback chain ends in omission ${id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateRegistry(
|
||||||
|
values: unknown[],
|
||||||
|
): ReadonlyMap<string, Package> {
|
||||||
|
const registry = new Map<string, Package>();
|
||||||
|
for (const value of values) {
|
||||||
|
const parsed = packageSchema.parse(value);
|
||||||
|
const identity = `${parsed.id}@${parsed.version}`;
|
||||||
|
if (registry.has(identity))
|
||||||
|
throw new Error(`duplicate package ${identity}`);
|
||||||
|
registry.set(identity, parsed);
|
||||||
|
}
|
||||||
|
const visiting = new Set<string>(),
|
||||||
|
complete = new Set<string>();
|
||||||
|
const visit = (identity: string) => {
|
||||||
|
if (visiting.has(identity))
|
||||||
|
throw new Error(`dependency cycle at ${identity}`);
|
||||||
|
if (complete.has(identity)) return;
|
||||||
|
const value = registry.get(identity);
|
||||||
|
if (!value) throw new Error(`unresolved package ${identity}`);
|
||||||
|
visiting.add(identity);
|
||||||
|
validatePackage(value, registry);
|
||||||
|
for (const dep of value.dependencies) visit(`${dep.id}@${dep.version}`);
|
||||||
|
visiting.delete(identity);
|
||||||
|
complete.add(identity);
|
||||||
|
};
|
||||||
|
for (const identity of registry.keys()) visit(identity);
|
||||||
|
// A registry can host several releases, but one package closure must resolve
|
||||||
|
// exactly one release per package id (the lock uses packageId for its files).
|
||||||
|
for (const root of registry.values()) {
|
||||||
|
const selected = new Map<string, string>();
|
||||||
|
const resolve = (value: Package) => {
|
||||||
|
const previous = selected.get(value.id);
|
||||||
|
if (previous && previous !== value.version)
|
||||||
|
throw new Error(`conflicting dependency versions for ${value.id}`);
|
||||||
|
if (previous) return;
|
||||||
|
selected.set(value.id, value.version);
|
||||||
|
for (const dep of value.dependencies)
|
||||||
|
resolve(registry.get(`${dep.id}@${dep.version}`)!);
|
||||||
|
};
|
||||||
|
resolve(root);
|
||||||
|
}
|
||||||
|
return registry;
|
||||||
|
}
|
||||||
|
|
||||||
|
function unique(ids: string[], label: string) {
|
||||||
|
if (new Set(ids).size !== ids.length) throw new Error(`duplicate ${label}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateComponent(value: unknown): ComponentDefinition {
|
||||||
|
const component = componentSchema.parse(value);
|
||||||
|
if (component.kind === 'deck')
|
||||||
|
unique(
|
||||||
|
component.cards.map((card) => card.id),
|
||||||
|
'card definition',
|
||||||
|
);
|
||||||
|
if (component.kind === 'dice') {
|
||||||
|
unique(
|
||||||
|
component.dice.map((die) => die.id),
|
||||||
|
'die',
|
||||||
|
);
|
||||||
|
for (const die of component.dice)
|
||||||
|
unique(
|
||||||
|
die.faces.map((face) => face.id),
|
||||||
|
'die face',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (component.kind === 'wheel')
|
||||||
|
unique(
|
||||||
|
component.sectors.map((sector) => sector.id),
|
||||||
|
'wheel sector',
|
||||||
|
);
|
||||||
|
if (component.kind === 'pieces')
|
||||||
|
unique(
|
||||||
|
component.definitions.map((piece) => piece.id),
|
||||||
|
'piece definition',
|
||||||
|
);
|
||||||
|
if (component.kind === 'board' && component.layout.kind === 'graph') {
|
||||||
|
const ids = component.layout.cells.map((cell) => cell.id);
|
||||||
|
unique(ids, 'board cell');
|
||||||
|
const known = new Set(ids);
|
||||||
|
for (const cell of component.layout.cells) {
|
||||||
|
unique(cell.neighbors, 'neighbor');
|
||||||
|
if (cell.neighbors.some((id) => !known.has(id)))
|
||||||
|
throw new Error('unknown board neighbor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return component;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateResourceSet(
|
||||||
|
value: unknown,
|
||||||
|
requiredSlots: readonly string[] = [],
|
||||||
|
) {
|
||||||
|
const set = resourceSetSchema.parse(value);
|
||||||
|
validateResources(set.resources);
|
||||||
|
for (const slot of [
|
||||||
|
...Object.values(set.slots),
|
||||||
|
...(set.back ? [set.back] : []),
|
||||||
|
]) {
|
||||||
|
if (!Object.hasOwn(set.resources, slot.resourceId))
|
||||||
|
throw new Error('unknown slot resource');
|
||||||
|
const resource = set.resources[slot.resourceId];
|
||||||
|
if (!['image', 'atlas', 'graphic'].includes(resource.kind))
|
||||||
|
throw new Error('nonvisual slot resource');
|
||||||
|
if (
|
||||||
|
slot.region &&
|
||||||
|
(resource.kind !== 'atlas' ||
|
||||||
|
!Object.hasOwn(resource.regions ?? {}, slot.region))
|
||||||
|
)
|
||||||
|
throw new Error('unknown slot region');
|
||||||
|
}
|
||||||
|
if (requiredSlots.some((id) => !Object.hasOwn(set.slots, id)))
|
||||||
|
throw new Error('missing component artwork slot');
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMessage(value: unknown): WireMessage {
|
||||||
|
const parsed = wireSchema.parse(value);
|
||||||
|
if (parsed.type !== 'game.snapshot') return parsed;
|
||||||
|
const fail = (message: string): never => {
|
||||||
|
throw new Error(message);
|
||||||
|
};
|
||||||
|
if (parsed.delivery.kind === 'sync' && parsed.events.length)
|
||||||
|
fail('sync must not replay events');
|
||||||
|
if (
|
||||||
|
parsed.delivery.kind === 'live' &&
|
||||||
|
parsed.revision !== parsed.delivery.fromRevision + 1
|
||||||
|
)
|
||||||
|
fail('noncontiguous live revision');
|
||||||
|
const ids = new Set<string>();
|
||||||
|
parsed.events.forEach((event, index) => {
|
||||||
|
if (
|
||||||
|
ids.has(event.eventId) ||
|
||||||
|
event.revision !== parsed.revision ||
|
||||||
|
event.order !== index
|
||||||
|
)
|
||||||
|
fail('invalid visible event order');
|
||||||
|
ids.add(event.eventId);
|
||||||
|
});
|
||||||
|
if ((parsed.status === 'active') !== (parsed.outcome === null))
|
||||||
|
fail('status/outcome mismatch');
|
||||||
|
if (parsed.outcome && parsed.outcome.kind !== parsed.status)
|
||||||
|
fail('terminal outcome mismatch');
|
||||||
|
if (parsed.status !== 'active' && parsed.availableActions.length)
|
||||||
|
fail('finished match offers actions');
|
||||||
|
if (
|
||||||
|
parsed.outcome?.kind === 'completed' &&
|
||||||
|
!Object.keys(parsed.outcome.participants).length
|
||||||
|
)
|
||||||
|
fail('empty result');
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validatePlan(value: unknown) {
|
||||||
|
const plan = presentationPlanSchema.parse(value);
|
||||||
|
if (plan.toRevision !== plan.fromRevision + 1)
|
||||||
|
throw new Error('noncontiguous plan');
|
||||||
|
const steps = new Map(plan.steps.map((step) => [step.id, step]));
|
||||||
|
if (steps.size !== plan.steps.length) throw new Error('duplicate step');
|
||||||
|
const visiting = new Set<string>(),
|
||||||
|
done = new Set<string>();
|
||||||
|
const visit = (id: string) => {
|
||||||
|
if (visiting.has(id)) throw new Error('plan cycle');
|
||||||
|
if (done.has(id)) return;
|
||||||
|
const step = steps.get(id);
|
||||||
|
if (!step) throw new Error('unknown step');
|
||||||
|
visiting.add(id);
|
||||||
|
if (step.trigger.kind === 'marker') {
|
||||||
|
const source = steps.get(step.trigger.step);
|
||||||
|
if (!source?.emits.includes(step.trigger.marker))
|
||||||
|
throw new Error('unknown marker');
|
||||||
|
visit(step.trigger.step);
|
||||||
|
} else for (const dependency of step.trigger.steps) visit(dependency);
|
||||||
|
visiting.delete(id);
|
||||||
|
done.add(id);
|
||||||
|
};
|
||||||
|
for (const id of steps.keys()) visit(id);
|
||||||
|
return plan;
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2023",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"types": ["node"],
|
||||||
|
"lib": ["ES2023", "DOM"]
|
||||||
|
},
|
||||||
|
"include": ["*.ts"]
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import type {
|
||||||
|
ActionHandler,
|
||||||
|
DeepReadonly,
|
||||||
|
GameDefinition,
|
||||||
|
Position,
|
||||||
|
} from './contracts.ts';
|
||||||
|
|
||||||
|
type Config = { columns: number };
|
||||||
|
type Data = { grid: number[] };
|
||||||
|
type View = { grid: number[] };
|
||||||
|
type Drop = { type: 'drop'; payload: { column: number } };
|
||||||
|
type Event = { type: 'disc.dropped'; payload: { row: number; column: number } };
|
||||||
|
|
||||||
|
function readonlyContract(position: DeepReadonly<Position<Config, Data>>) {
|
||||||
|
// @ts-expect-error Nested arrays must be immutable at the module boundary.
|
||||||
|
position.data.grid.push(1);
|
||||||
|
// @ts-expect-error Nested configuration must also be immutable.
|
||||||
|
position.config.columns = 9;
|
||||||
|
}
|
||||||
|
function actionContract(handler: ActionHandler<Config, Data, Drop, Event>) {
|
||||||
|
handler.schema.parse({ type: 'drop', payload: { column: 3 } });
|
||||||
|
// @ts-expect-error apply is synchronous; a Promise is not a rule transition.
|
||||||
|
handler.apply = async () => ({ ok: true });
|
||||||
|
}
|
||||||
|
type Definition = GameDefinition<Config, Data, View, Drop, Event, Event>;
|
||||||
|
function definitionContract(definition: Definition) {
|
||||||
|
// @ts-expect-error Only declared action discriminants can be registered.
|
||||||
|
definition.actions.fire;
|
||||||
|
// @ts-expect-error The canonical action payload is checked by TypeScript.
|
||||||
|
const invalid: Drop = { type: 'drop', payload: { cell: 1 } };
|
||||||
|
return invalid;
|
||||||
|
}
|
||||||
|
void readonlyContract;
|
||||||
|
void actionContract;
|
||||||
|
void definitionContract;
|
||||||
@ -0,0 +1,330 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import {
|
||||||
|
componentSchema,
|
||||||
|
gamePackage,
|
||||||
|
lockSchema,
|
||||||
|
packageSchema,
|
||||||
|
presentationPlanSchema,
|
||||||
|
resourceSetSchema,
|
||||||
|
validateComponent,
|
||||||
|
validateResourceSet,
|
||||||
|
validatePackage,
|
||||||
|
validatePlan,
|
||||||
|
validateRegistry,
|
||||||
|
validateMessage,
|
||||||
|
wireSchema,
|
||||||
|
} from './schema.ts';
|
||||||
|
|
||||||
|
const read = async (file: string) =>
|
||||||
|
JSON.parse(await readFile(new URL(file, import.meta.url), 'utf8'));
|
||||||
|
const libraries: unknown[] = await read('examples/libraries.json');
|
||||||
|
const games = await Promise.all(
|
||||||
|
['conecta4', 'hundido', 'solitario'].map((id) =>
|
||||||
|
read(`examples/${id}.game.json`),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const registry = validateRegistry([...libraries, ...games]);
|
||||||
|
const messages: unknown[] = await read('examples/wire.json');
|
||||||
|
for (const message of messages) validateMessage(message);
|
||||||
|
let negativeCases = 0;
|
||||||
|
function rejects(label: string, run: () => unknown) {
|
||||||
|
assert.throws(run, `Must reject: ${label}`);
|
||||||
|
negativeCases++;
|
||||||
|
}
|
||||||
|
function changeGame(
|
||||||
|
index: number,
|
||||||
|
edit: (game: z.infer<typeof gamePackage>) => void,
|
||||||
|
) {
|
||||||
|
const game = structuredClone(games[index]);
|
||||||
|
edit(game);
|
||||||
|
return () => validatePackage(game, registry);
|
||||||
|
}
|
||||||
|
rejects('unknown manifest field', () =>
|
||||||
|
validatePackage({ ...games[0], runtimeMagic: true }, registry),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'missing rules entry',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.rules.entry = '';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'path traversal',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.rules.entry = '../server/secrets.ts';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'absolute path',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.rules.entry = '/server/rules.ts';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'empty path segment',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.rules.entry = 'rules//index.ts';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'directory used as entry',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.rules.entry = 'rules/';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'floating dependency',
|
||||||
|
changeGame(2, (game) => {
|
||||||
|
game.dependencies[0].version = '^1.0.0';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'missing dependency',
|
||||||
|
changeGame(2, (game) => {
|
||||||
|
game.dependencies[0].id = 'missing';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'missing export',
|
||||||
|
changeGame(2, (game) => {
|
||||||
|
game.presentation.bindings.mainDeck = {
|
||||||
|
source: 'dependency',
|
||||||
|
packageId: 'cards-poker',
|
||||||
|
export: 'missing',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'inherited export',
|
||||||
|
changeGame(2, (game) => {
|
||||||
|
game.presentation.bindings.mainDeck = {
|
||||||
|
source: 'dependency',
|
||||||
|
packageId: 'cards-poker',
|
||||||
|
export: 'constructor',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'wrong dependency kind',
|
||||||
|
changeGame(2, (game) => {
|
||||||
|
game.presentation.bindings.mainDeck = {
|
||||||
|
source: 'dependency',
|
||||||
|
packageId: 'playing-cards',
|
||||||
|
export: 'poker52',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'invalid player interval',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.players.min = 4;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'solo with multiple players',
|
||||||
|
changeGame(2, (game) => {
|
||||||
|
game.players.max = 2;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'missing cover',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.presentation.catalog.cover = 'missing';
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'required asset omitted',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.presentation.resources['catalog.cover'].fallback = { kind: 'omit' };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'fallback cycle',
|
||||||
|
changeGame(0, (game) => {
|
||||||
|
game.presentation.resources['catalog.cover'].fallback = {
|
||||||
|
kind: 'resource',
|
||||||
|
resourceId: 'catalog.cover',
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'atlas bounds',
|
||||||
|
changeGame(1, (game) => {
|
||||||
|
game.presentation.resources['fleet.ships'].regions!.lancha.x = 1024;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects(
|
||||||
|
'phase load without phase',
|
||||||
|
changeGame(1, (game) => {
|
||||||
|
delete game.presentation.resources['sound.hit'].phase;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
rejects('duplicate package', () =>
|
||||||
|
validateRegistry([...libraries, ...games, games[0]]),
|
||||||
|
);
|
||||||
|
const cycleA = structuredClone(libraries[0]) as Record<string, unknown>;
|
||||||
|
const cycleB = structuredClone(libraries[0]) as Record<string, unknown>;
|
||||||
|
cycleA.id = 'cycle-a';
|
||||||
|
cycleB.id = 'cycle-b';
|
||||||
|
cycleA.dependencies = [{ id: 'cycle-b', version: '1.0.0', kind: 'components' }];
|
||||||
|
cycleB.dependencies = [{ id: 'cycle-a', version: '1.0.0', kind: 'components' }];
|
||||||
|
rejects('dependency cycle', () => validateRegistry([cycleA, cycleB]));
|
||||||
|
const firstVersion = structuredClone(libraries[0]) as Record<string, any>;
|
||||||
|
const secondVersion = structuredClone(firstVersion);
|
||||||
|
secondVersion.version = '2.0.0';
|
||||||
|
const parentA = structuredClone(firstVersion);
|
||||||
|
parentA.id = 'parent-a';
|
||||||
|
parentA.dependencies = [
|
||||||
|
{ id: firstVersion.id, version: '1.0.0', kind: 'components' },
|
||||||
|
];
|
||||||
|
const parentB = structuredClone(firstVersion);
|
||||||
|
parentB.id = 'parent-b';
|
||||||
|
parentB.dependencies = [
|
||||||
|
{ id: firstVersion.id, version: '2.0.0', kind: 'components' },
|
||||||
|
];
|
||||||
|
const root = structuredClone(firstVersion);
|
||||||
|
root.id = 'root';
|
||||||
|
root.dependencies = [
|
||||||
|
{ id: 'parent-a', version: '1.0.0', kind: 'components' },
|
||||||
|
{ id: 'parent-b', version: '1.0.0', kind: 'components' },
|
||||||
|
];
|
||||||
|
rejects('conflicting transitive versions', () =>
|
||||||
|
validateRegistry([root, parentA, parentB, firstVersion, secondVersion]),
|
||||||
|
);
|
||||||
|
validateRegistry([parentA, parentB, firstVersion, secondVersion]);
|
||||||
|
const message = (index: number) =>
|
||||||
|
structuredClone(messages[index]) as Record<string, any>;
|
||||||
|
const badActor = message(1);
|
||||||
|
badActor.actor = 'p1';
|
||||||
|
rejects('client cannot supply actor', () => validateMessage(badActor));
|
||||||
|
const badPolicy = message(2);
|
||||||
|
badPolicy.expected.actorRevision = -1;
|
||||||
|
rejects('negative revision', () => validateMessage(badPolicy));
|
||||||
|
const live = message(7);
|
||||||
|
const gap = structuredClone(live);
|
||||||
|
gap.revision = 3;
|
||||||
|
rejects('live revision gap', () => validateMessage(gap));
|
||||||
|
const historyOnSync = structuredClone(live);
|
||||||
|
historyOnSync.delivery = message(6).delivery;
|
||||||
|
rejects('sync replays event history', () => validateMessage(historyOnSync));
|
||||||
|
const repeated = structuredClone(live);
|
||||||
|
repeated.events.push({ ...repeated.events[0], order: 1 });
|
||||||
|
rejects('duplicate event', () => validateMessage(repeated));
|
||||||
|
const activeWithOutcome = structuredClone(live);
|
||||||
|
activeWithOutcome.status = 'active';
|
||||||
|
rejects('active match with terminal outcome', () =>
|
||||||
|
validateMessage(activeWithOutcome),
|
||||||
|
);
|
||||||
|
const terminalOffer = structuredClone(live);
|
||||||
|
terminalOffer.availableActions = [
|
||||||
|
{
|
||||||
|
kind: 'concrete',
|
||||||
|
action: { type: 'draw', payload: {} },
|
||||||
|
expected: { kind: 'match', revision: 1 },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
rejects('finished match offering actions', () =>
|
||||||
|
validateMessage(terminalOffer),
|
||||||
|
);
|
||||||
|
const emptyOutcome = structuredClone(live);
|
||||||
|
emptyOutcome.outcome.participants = {};
|
||||||
|
rejects('empty completed outcome', () => validateMessage(emptyOutcome));
|
||||||
|
const transientRejection = message(5);
|
||||||
|
transientRejection.receipt.code = 'INTERNAL_ERROR';
|
||||||
|
rejects('transient failure must not become terminal receipt', () =>
|
||||||
|
validateMessage(transientRejection),
|
||||||
|
);
|
||||||
|
const cooperative = structuredClone(live);
|
||||||
|
cooperative.outcome = {
|
||||||
|
kind: 'completed',
|
||||||
|
reasonCode: 'objective.failed',
|
||||||
|
participants: {
|
||||||
|
p0: { outcome: 'loss', metrics: {} },
|
||||||
|
p1: { outcome: 'loss', metrics: {} },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
validateMessage(cooperative);
|
||||||
|
assert.equal(
|
||||||
|
(live.outcome.participants.p0 as { outcome: string }).outcome,
|
||||||
|
'loss',
|
||||||
|
);
|
||||||
|
|
||||||
|
const plan = await read('examples/drop.plan.json');
|
||||||
|
validatePlan(plan);
|
||||||
|
const cyclic = structuredClone(plan);
|
||||||
|
cyclic.steps[0].trigger = { kind: 'after', steps: ['finish'] };
|
||||||
|
rejects('animation dependency cycle', () => validatePlan(cyclic));
|
||||||
|
const marker = structuredClone(plan);
|
||||||
|
marker.steps[1].trigger.marker = 'nonexistent';
|
||||||
|
rejects('animation marker missing', () => validatePlan(marker));
|
||||||
|
const components = ((await read('examples/components.json')) as unknown[]).map(
|
||||||
|
validateComponent,
|
||||||
|
);
|
||||||
|
const poker = components.find((component) => component.kind === 'deck')!;
|
||||||
|
assert.equal(poker.kind, 'deck');
|
||||||
|
if (poker.kind !== 'deck') throw new Error('missing poker fixture');
|
||||||
|
assert.equal(poker.cards.length, 52);
|
||||||
|
const repeatedCard = structuredClone(poker);
|
||||||
|
repeatedCard.cards.push(repeatedCard.cards[0]);
|
||||||
|
rejects('duplicate card definition', () => validateComponent(repeatedCard));
|
||||||
|
rejects('unknown graph neighbor', () =>
|
||||||
|
validateComponent({
|
||||||
|
kind: 'board',
|
||||||
|
schemaVersion: 1,
|
||||||
|
layout: { kind: 'graph', cells: [{ id: 'a', neighbors: ['b'] }] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const artwork = await read('examples/poker.resource-set.json');
|
||||||
|
validateResourceSet(
|
||||||
|
artwork,
|
||||||
|
poker.cards.map((card) => card.id),
|
||||||
|
);
|
||||||
|
const incompleteArtwork = structuredClone(artwork);
|
||||||
|
delete incompleteArtwork.slots[poker.cards[0].id];
|
||||||
|
rejects('missing card artwork', () =>
|
||||||
|
validateResourceSet(
|
||||||
|
incompleteArtwork,
|
||||||
|
poker.cards.map((card) => card.id),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const missingRegion = structuredClone(artwork);
|
||||||
|
missingRegion.slots[poker.cards[0].id].region = 'missing';
|
||||||
|
rejects('unknown artwork region', () => validateResourceSet(missingRegion));
|
||||||
|
const inheritedResource = structuredClone(artwork);
|
||||||
|
inheritedResource.back.resourceId = 'constructor';
|
||||||
|
rejects('inherited artwork resource', () =>
|
||||||
|
validateResourceSet(inheritedResource),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const [name, schema] of [
|
||||||
|
['package', packageSchema],
|
||||||
|
['protocol', wireSchema],
|
||||||
|
['component', componentSchema],
|
||||||
|
['resource-set', resourceSetSchema],
|
||||||
|
['lock', lockSchema],
|
||||||
|
['presentation-plan', presentationPlanSchema],
|
||||||
|
] as const) {
|
||||||
|
const generated = {
|
||||||
|
...z.toJSONSchema(schema, { target: 'draft-2020-12', reused: 'ref' }),
|
||||||
|
$id: `urn:juegoland:${name}:1`,
|
||||||
|
};
|
||||||
|
const file = `${name}.schema.json`;
|
||||||
|
if (process.argv.includes('--write-schemas'))
|
||||||
|
await writeFile(
|
||||||
|
new URL(file, import.meta.url),
|
||||||
|
JSON.stringify(generated, null, 2) + '\n',
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
await read(file),
|
||||||
|
generated,
|
||||||
|
`generated schema drift: ${file}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
console.log(
|
||||||
|
`${registry.size} manifests, ${messages.length + 1} messages, ${negativeCases} rejection cases, ${components.length} components, 52 artwork slots, presentation plan and 6 generated schemas verified.`,
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'Scope: specification structure, references and invariants. Game payloads, package files and the engine runtime are separate implementation checks.',
|
||||||
|
);
|
||||||
@ -0,0 +1,152 @@
|
|||||||
|
# Arquitectura de tres capas y evaluación de Go — Juegoland v2
|
||||||
|
|
||||||
|
**Estado:** propuesta técnica del 5 de octubre de 2026. El usuario fija tres capas —API, servidor y frontend— y sitúa la API en Go; solicita valorar también Go para servidor. La recomendación es Go para ambas capas de backend en un monolito modular, con frontend Svelte/TypeScript. La elección de Go para el servidor y el nuevo transporte se documentan como recomendación, no como migración ya aprobada o ejecutada.
|
||||||
|
|
||||||
|
El [modelo de plataforma](../platform-spec-v2.md), sus [políticas](product-policies.md) y la [API](api-and-events.md) definen comportamiento independiente del lenguaje. El [SDK actual de diseño](../game-engine/contracts.ts) sí tiene firmas TypeScript y el formato v1 describe entradas de módulos: necesita una adaptación explícita para reglas nativas Go. La producción sigue siendo Node/Socket.IO/PostgreSQL.
|
||||||
|
|
||||||
|
## 1. Las tres capas
|
||||||
|
|
||||||
|
| Capa | Responsabilidad | Tecnología propuesta | Límite |
|
||||||
|
| -------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||||
|
| Frontend | Pantallas, interacción, accesibilidad, vistas autorizadas, animación/audio y recuperación de conexión. | Svelte/TypeScript; HTML/CSS y recursos propios. | No conoce secretos, no decide edad/permiso, resultado o azar; no accede a DB. |
|
||||||
|
| API | HTTP/tiempo real, negociación, decodificación y límites, sesión/CSRF/origen, correlación y traducción de errores. | Go. | No implementa reglas de juego ni duplica políticas en cada controlador. |
|
||||||
|
| Servidor | Casos de uso, autorización de dominio, cuentas, salas, protección, motor, transacciones, plazos, moderación y publicación. | Go recomendado. | Es la única autoridad que confirma cambios. No depende de componentes UI ni llama a su propia API por HTTP. |
|
||||||
|
|
||||||
|
API y servidor son separaciones de código/responsabilidad. En la primera topología se compilan y despliegan juntos; se llaman mediante funciones e interfaces Go. No hacen falta dos servicios de red ni bases de datos distintas para conservar tres capas. PostgreSQL, correo/SMS, almacenamiento de recursos y métricas son infraestructura de soporte.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
USER[Navegador] --> PROXY[Proxy HTTPS]
|
||||||
|
PROXY --> FE[Frontend Svelte]
|
||||||
|
PROXY --> API[API Go: HTTP y tiempo real]
|
||||||
|
subgraph BACKEND[Un backend modular Go]
|
||||||
|
API --> APP[Casos de uso y autorización]
|
||||||
|
APP --> DOMAIN[Identidad, salas y protección]
|
||||||
|
APP --> ENGINE[Núcleo y reglas propias]
|
||||||
|
JOBS[Plazos y tareas] --> APP
|
||||||
|
end
|
||||||
|
APP --> DB[(PostgreSQL)]
|
||||||
|
DB --> OUT[Outbox y entregas autorizadas]
|
||||||
|
OUT --> API
|
||||||
|
APP --> PROVIDERS[Adaptadores de email, edad y SMS]
|
||||||
|
FE --> ASSETS[Recursos públicos por digest]
|
||||||
|
```
|
||||||
|
|
||||||
|
El frontend puede servirse estático si se adopta una compilación compatible, o conservar SSR de SvelteKit con su proceso Node. Usar Go en el backend no elimina por sí solo Node del build o del SSR. Esta decisión de entrega se toma después de inventariar rutas/SSR, SEO y autenticación; no se exige rehacer Svelte.
|
||||||
|
|
||||||
|
## 2. Por qué Go encaja y qué no resuelve
|
||||||
|
|
||||||
|
Go proporciona compilación nativa y mecanismos de concurrencia adecuados para servicios de red; su documentación describe estos objetivos en la [FAQ oficial](https://go.dev/doc/faq). Para este producto, las ventajas esperadas son un backend tipado, distribución del ejecutable, herramientas de análisis/prueba y control explícito de cancelaciones, recursos y errores. Son razones de ingeniería, no una medición de que vaya a superar al servicio actual.
|
||||||
|
|
||||||
|
Los juegos por turnos no justifican por sí solos una reescritura por rendimiento. Persistencia, proyecciones por usuario, fanout, bloqueos y recuperación pueden dominar la latencia. Hay que medir el perfil de carga de las políticas en hardware documentado. Goroutines no sustituyen límites de concurrencia, colas acotadas ni bloqueos de base de datos.
|
||||||
|
|
||||||
|
Go tampoco resuelve errores del proxy, rutas/NAT, políticas incompletas, autorización o pérdida de mensajes. Mantener las invariantes importa más que el lenguaje. La API en Go será útil si centraliza entrega y llama a una autoridad de dominio clara; añadirla solo como segundo CRUD que escribe sobre los mismos datos que Node introduciría carreras.
|
||||||
|
|
||||||
|
## 3. Alternativas comparadas
|
||||||
|
|
||||||
|
| Alternativa | Ventaja | Coste/riesgo | Uso recomendado |
|
||||||
|
| -------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||||
|
| API y servidor Node | Menor trabajo inicial, reutiliza reglas y SDK TS. | No satisface la dirección de API Go elegida. | Línea base para comparar resultados y mantener producción durante el cambio. |
|
||||||
|
| API Go → servidor Node autoritativo | Conserva motor y una sola transacción en Node. | Dos procesos, salto de red, identidad interna, despliegue y trazas duplicados. | Puente temporal si necesitamos API Go antes de portar el motor. |
|
||||||
|
| API y servidor Go, reglas Go incluidas | Una autoridad, misma transacción, un runtime de backend y separación de capas por paquetes. | Portar SDK, reglas, validadores y comprobar equivalencia. | Objetivo recomendado al publicar solo juegos propios. |
|
||||||
|
| Go + evaluador JS/WASM | Podría conservar módulos de reglas o abrir varios runtimes. | ABI, límites, compatibilidad JS, aislamiento, determinismo y coordinación añadidos. | Fuera del primer alcance; exige una necesidad y un prototipo específico. |
|
||||||
|
|
||||||
|
No se incluye un intérprete JS o un sistema de plugins dinámicos solo para simular que el SDK TS funciona en Go. Tampoco se cargan `.go` de un paquete en ejecución. Los juegos propios permiten registrar implementaciones compiladas y versionadas junto al servidor. Publicar nuevos gráficos/metadatos y publicar nuevas reglas tienen ciclos distintos.
|
||||||
|
|
||||||
|
## 4. Autoridad y transacciones
|
||||||
|
|
||||||
|
Una operación autorizada entra por un caso de uso común, tanto desde HTTP/tiempo real como desde tareas internas:
|
||||||
|
|
||||||
|
1. Validar estructura, identidad, presupuesto técnico y contexto de autorización.
|
||||||
|
2. Abrir la unidad de trabajo. Serializar autorización/revocaciones aplicables y bloquear agregados en orden global: contexto de permisos, sala, partida, registros dependientes. Toda ruta y tarea usa el mismo orden; relaciones de varias personas se ordenan por ID y se revalidan bajo bloqueo.
|
||||||
|
3. Buscar recibo propio y comprobar idempotencia; si el comando es nuevo, comprobar permisos, revisiones, admisión y versión instalada.
|
||||||
|
4. Ejecutar reglas locales, puras y acotadas con servicios deterministas. Validar transición y proyecciones.
|
||||||
|
5. Confirmar sala, partida, resultado, tareas, recibo y outbox en **una transacción PostgreSQL**.
|
||||||
|
6. Fuera de la transacción, entregar acuse/publicaciones con permisos actuales y ejecutar efectos externos idempotentes.
|
||||||
|
|
||||||
|
La documentación de [transacciones Go](https://go.dev/doc/database/execute-transactions) distingue operaciones sobre `sql.Tx` de operaciones sobre el pool: mezclarlas puede ejecutar escrituras fuera de la transacción. Los repositorios participantes reciben la misma unidad de trabajo; no hacen commit interno ni abren otra conexión para «guardar el resultado».
|
||||||
|
|
||||||
|
Sin llamadas a SMS, verificador, correo, HTTP interno ni espera de UI dentro de un bloqueo. Los efectos externos usan tarea/outbox y conciliación. Un timeout no demuestra que un commit falló: se consulta/reintenta el mismo comando y se recupera su recibo. No se promete exactamente una entrega de red; se evita repetir el efecto de dominio.
|
||||||
|
|
||||||
|
La revocación debe cerrar entregas también si no llega otro comando: invalidación directa, reautorización de outbox e historial y purga de cachés controladas. Un snapshot con permisos antiguos no se publica por haber sido calculado antes de la revocación.
|
||||||
|
|
||||||
|
## 5. Qué se conserva del protocolo y qué requiere adaptación
|
||||||
|
|
||||||
|
| Contrato actual | Tratamiento con Go |
|
||||||
|
| ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
|
| IDs, sobres JSON, revisiones, recibos, estados, vistas por destinatario | Conservar semántica y fixtures. No cambiar la versión de red solo por cambiar de lenguaje. |
|
||||||
|
| Manifiesto, recursos, barajas/dados/tableros, presentación y animaciones | Conservar separación entre reglas privadas y recursos/UI públicos. El frontend sigue TS. |
|
||||||
|
| `setup`, acciones, validación, `availableActions`, `project`, `projectEvent`, `onLifecycle` | Diseñar interfaces Go equivalentes; las firmas TS son referencia semántica, no código invocable desde Go. |
|
||||||
|
| `rules.entry` y compilación TS descrita por formato v1 | Añadir un perfil de ejecución y distribución versionado antes de publicar paquetes Go. El esquema v1 no se modifica de forma incompatible manteniendo la misma versión. |
|
||||||
|
| Zod y sus refinamientos | Zod puede seguir como herramienta de autoría/build. Go necesita validación estructural y semántica equivalente; generar tipos no reproduce refinamientos entre campos. |
|
||||||
|
| Lock/digests y reglas retenidas | Relacionar paquete y versión con implementación compilada y build; conservar todas las versiones referenciadas por partidas activas. |
|
||||||
|
|
||||||
|
El futuro perfil nativo tendrá un identificador de implementación registrado por el build, no una ruta suministrada por un jugador. Debe especificar `runtime`, versión de ABI/SDK, compatibilidad de estado y prueba de que el build contiene exactamente la implementación asociada al digest. Sus nombres/campos definitivos pertenecen a la siguiente versión del esquema de paquete, no se introducen como campos libres en v1.
|
||||||
|
|
||||||
|
No recompilar un módulo con cambios de reglas bajo la misma versión. Una build nueva puede contener varias implementaciones antiguas; readiness impide servir partidas cuyo módulo no esté disponible. El rollback conserva el conjunto completo necesario o detiene nuevas admisiones, nunca sustituye reglas sobre una partida existente.
|
||||||
|
|
||||||
|
La equivalencia entre runtimes exige vectores compartidos de estado, acciones, errores, proyecciones, resultados y eventos. En azar: mismo algoritmo/versionado y estado, consumo exacto de tiradas, rechazo de sesgo y reglas de barajado. No sustituir un RNG persistido por el generador estándar de Go. Datos dentro del rango seguro JSON, reglas claras para `null`/ausencia, Unicode, orden canónico, timestamps y mapas; no depender del orden de iteración Go ni de aritmética diferente.
|
||||||
|
|
||||||
|
Los juegos actuales que dependen de una librería JS, como ajedrez, necesitan equivalencia probada de reglas/casos extremos y revisión de licencia al portar o sustituir la dependencia. No basta con que las primeras jugadas parezcan iguales. Las vistas de Hundido deben probar además ausencia de filtraciones por jugador, eventos y errores.
|
||||||
|
|
||||||
|
## 6. HTTP, tiempo real y compatibilidad de transporte
|
||||||
|
|
||||||
|
Mantener HTTP JSON bajo `/api/v2`, contratos explícitos y errores de dominio de la API. La recomendación para el backend Go final es WebSocket estándar con un binding propio pequeño de los sobres ya definidos, evitando depender de una implementación no evaluada de Socket.IO en Go. **La API documentada actualmente usa Socket.IO; la recomendación no cambia ese contrato automáticamente.**
|
||||||
|
|
||||||
|
Socket.IO añade su propio protocolo; su documentación confirma que un cliente Socket.IO no se conecta a un servidor WebSocket simple ([fuente oficial](https://socket.io/docs/v4/#what-socketio-is-not)). Antes de adoptar el binding nuevo se debe publicar y probar:
|
||||||
|
|
||||||
|
- Ruta y subprotocolo versionados, un sobre por mensaje de texto UTF-8, separación `platform.*`/`game.*` por contrato y límites propios; rechazo de binario y compresión hasta evaluar su necesidad/riesgos.
|
||||||
|
- Cookie de sesión, `Origin` permitido y autenticación/CSRF antes de suscribir. El navegador no añade cabeceras arbitrarias a WebSocket: usar primer mensaje de autenticación con nonce/CSRF obtenido por HTTP, timeout corto y conexión todavía sin acceso; ningún secreto en query string.
|
||||||
|
- Negociación, ping/pong, timeouts del proxy, cierre/drenaje, backoff con jitter, reconexión, cursores, límites de buffers y recuperación tras segundo plano móvil.
|
||||||
|
- Recibos duraderos y barreras iguales a las del protocolo de aplicación. No confundir confirmación de transporte con commit.
|
||||||
|
- Política explícita cuando no se puede abrir WebSocket: mensaje recuperable y reintento acotado; cualquier fallback HTTP requiere rutas y pruebas de equivalencia. No prometer long-polling si no se implementa.
|
||||||
|
|
||||||
|
Durante la transición `/socket.io` sigue con Node para clientes/partidas que lo requieran. El endpoint nuevo negocia de forma distinta; no se redirige el protocolo antiguo al nuevo como si fueran intercambiables. Un adaptador de compatibilidad, si se necesita, traduce transporte hacia la misma autoridad, nunca aplica dos comandos.
|
||||||
|
|
||||||
|
## 7. Organización de código propuesta
|
||||||
|
|
||||||
|
Estructura orientativa, no directorios ya creados:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cmd/juegoland/ composición y arranque
|
||||||
|
internal/api/http/ handlers, decodificación, CSRF, errores
|
||||||
|
internal/api/realtime/ conexión, negociación, suscripciones
|
||||||
|
internal/application/ casos de uso y unidad de trabajo
|
||||||
|
internal/domain/identity/ cuentas y sesiones
|
||||||
|
internal/domain/safety/ edad, teléfono, tutela y permisos
|
||||||
|
internal/domain/rooms/ salas, miembros e invitaciones
|
||||||
|
internal/domain/chat/ comunicación y redacción
|
||||||
|
internal/domain/moderation/ casos, restricciones y recursos
|
||||||
|
internal/engine/ núcleo determinista y versiones
|
||||||
|
internal/games/ reglas propias registradas por build
|
||||||
|
internal/infra/postgres/ repositorios, migraciones y outbox
|
||||||
|
internal/infra/providers/ email, SMS y verificador
|
||||||
|
internal/jobs/ tareas que invocan casos de uso
|
||||||
|
contracts/ esquemas y fixtures compartidos
|
||||||
|
```
|
||||||
|
|
||||||
|
El dominio no importa HTTP, Socket.IO, drivers SQL o SDK de proveedor. API invoca aplicación; infraestructura implementa interfaces requeridas por esta. El punto de composición conecta implementaciones. No crear una jerarquía vacía por cada entidad ni llamar «microservicio» a un paquete.
|
||||||
|
|
||||||
|
Se adopta una versión soportada de Go al implementar, fijada en toolchain/CI; driver, router y librería WebSocket se seleccionan con mantenimiento, licencias y compatibilidad comprobados. No se instala ni contrata nada en esta fase de especificación.
|
||||||
|
|
||||||
|
## 8. Migración sin doble autoridad
|
||||||
|
|
||||||
|
1. Completar contratos de plataforma y fixtures compartidos; inventariar dependencias/estado de los juegos actuales. Mantener producción estable.
|
||||||
|
2. Prototipo vertical Go con Conecta 4: cuenta sintética, sala, comando, commit, outbox, reconexión y reinicio con PostgreSQL. Probar también revocación concurrente. Comparar con Node; aún sin tráfico real.
|
||||||
|
3. Probar Hundido para información oculta/acciones independientes y Brisca para azar/mazos. Fijar SDK Go, perfil de paquete y binding antes de migrar el resto.
|
||||||
|
4. Si se necesita API Go mientras el dominio sigue Node, Go delega la operación completa al dueño Node, sin escritura parcial. Canal interno autenticado, origen no público, identidad verificable y deadlines; Node sigue validando permisos y controla toda la transacción. Una identidad en cabecera sin autenticar no vale.
|
||||||
|
5. Preferir drenar salas activas antiguas en su propietario y enviar **nuevas** salas por cohortes al backend nuevo. No repartir la escritura de una misma sala entre Go y Node. Identidad y protección mantienen una autoridad común, y una revocación afecta a ambos antes de nuevas entregas.
|
||||||
|
6. Si un traslado de partida activa fuera imprescindible: detener nuevas escrituras, bloquear y cambiar propietario con generación/fencing duradero, migrar snapshot validado sin repetir `setup`, confirmar antes de enrutar. El servidor anterior rechaza su generación obsoleta. No activar este camino sin pruebas de caída en cada paso.
|
||||||
|
7. Retirar Node de juego solo cuando no queden partidas/compatibilidades que dependan de él. SSR/build de frontend se evalúan por separado. Rollback mantiene contratos, esquema, versiones y políticas de seguridad; no vuelve a habilitar chat a menores.
|
||||||
|
|
||||||
|
No existe una transacción `sql.Tx` compartible entre procesos Go y Node. Tampoco soluciona ese problema «usar la misma base de datos» con commits independientes. Si en un futuro Go calcula mediante un evaluador remoto sin efectos, deberá hacerlo fuera de locks y confirmar por comparación de todas las revisiones/lecturas relevantes; eso es otro diseño y no la topología inicial recomendada.
|
||||||
|
|
||||||
|
## 9. Criterios de decisión antes de implementar el cambio completo
|
||||||
|
|
||||||
|
- Go conserva equivalencia semántica de reglas, filtros privados, RNG y recibos, y puede instalar/retener versiones de juego.
|
||||||
|
- Existe un único dueño por agregado, una unidad de trabajo para sala/partida y revocación consistente entre interfaces.
|
||||||
|
- Fixtures JSON pasan en Go y TS, incluidos casos negativos, refinamientos y canonicalización; ningún generador se trata como prueba de equivalencia.
|
||||||
|
- Pruebas de integración con PostgreSQL, carreras, proceso detenido tras commit, restart, outbox y cliente lento; carrera de datos Go en pruebas pertinentes.
|
||||||
|
- Perfil de carga y restauración de las políticas medidos en el despliegue candidato; frontend móvil y accesibilidad verificados.
|
||||||
|
|
||||||
|
La recomendación queda, por tanto, en **frontend Svelte/TypeScript + API Go + servidor/motor Go**, con separación lógica entre API y dominio y un backend modular al inicio. Su principal coste es portar y probar el runtime de juegos; los contratos y recursos ya diseñados siguen siendo una inversión aprovechable.
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="es">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#eef3fb" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Juegoland: juegos para compartir en una misma mesa, estés donde estés."
|
||||||
|
/>
|
||||||
|
<title>Juegoland — Juegos para compartir</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "games2-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host 127.0.0.1 --port 4174 --strictPort",
|
||||||
|
"check": "svelte-check --tsconfig ./tsconfig.json",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview --host 127.0.0.1 --port 4175 --strictPort"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"svelte": "5.57.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "7.3.1",
|
||||||
|
"svelte-check": "4.7.6",
|
||||||
|
"typescript": "6.0.3",
|
||||||
|
"vite": "8.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
export interface GameCard {
|
||||||
|
id: string;
|
||||||
|
version: string;
|
||||||
|
title: string;
|
||||||
|
summary: string;
|
||||||
|
minPlayers: number;
|
||||||
|
maxPlayers: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGameCard(value: unknown): value is GameCard {
|
||||||
|
if (!value || typeof value !== 'object') return false;
|
||||||
|
const game = value as Record<string, unknown>;
|
||||||
|
return (
|
||||||
|
typeof game.id === 'string' &&
|
||||||
|
typeof game.version === 'string' &&
|
||||||
|
typeof game.title === 'string' &&
|
||||||
|
typeof game.summary === 'string' &&
|
||||||
|
typeof game.minPlayers === 'number' &&
|
||||||
|
typeof game.maxPlayers === 'number'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchCatalog(signal?: AbortSignal): Promise<GameCard[]> {
|
||||||
|
const response = await fetch('/api/v2/catalog', {
|
||||||
|
headers: { Accept: 'application/json' },
|
||||||
|
signal,
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('CATALOG_UNAVAILABLE');
|
||||||
|
}
|
||||||
|
|
||||||
|
const body: unknown = await response.json();
|
||||||
|
if (!body || typeof body !== 'object') {
|
||||||
|
throw new Error('CATALOG_INVALID');
|
||||||
|
}
|
||||||
|
const data = (body as Record<string, unknown>).data;
|
||||||
|
if (!data || typeof data !== 'object') {
|
||||||
|
throw new Error('CATALOG_INVALID');
|
||||||
|
}
|
||||||
|
const items = (data as Record<string, unknown>).items;
|
||||||
|
if (!Array.isArray(items) || !items.every(isGameCard)) {
|
||||||
|
throw new Error('CATALOG_INVALID');
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
|
import App from './App.svelte';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
const target = document.getElementById('app');
|
||||||
|
if (!target) {
|
||||||
|
throw new Error('No se encontró el contenedor de la aplicación');
|
||||||
|
}
|
||||||
|
|
||||||
|
mount(App, { target });
|
||||||
@ -0,0 +1,334 @@
|
|||||||
|
:root {
|
||||||
|
font-family: 'Trebuchet MS', 'Segoe UI', sans-serif;
|
||||||
|
color: #18243c;
|
||||||
|
background: #eef3fb;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
min-width: 320px;
|
||||||
|
}
|
||||||
|
button,
|
||||||
|
a {
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
:focus-visible {
|
||||||
|
outline: 3px solid #dd523e;
|
||||||
|
outline-offset: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
width: min(100%, 1440px);
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 clamp(20px, 5vw, 80px);
|
||||||
|
}
|
||||||
|
.site-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
min-height: 104px;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: clamp(22px, 2.6vw, 29px);
|
||||||
|
font-weight: 900;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
}
|
||||||
|
.brand-mark {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 9px);
|
||||||
|
gap: 3px;
|
||||||
|
transform: rotate(-8deg);
|
||||||
|
}
|
||||||
|
.brand-mark i {
|
||||||
|
display: block;
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: #2352c6;
|
||||||
|
}
|
||||||
|
.brand-mark i:nth-child(2) {
|
||||||
|
background: #e95c47;
|
||||||
|
}
|
||||||
|
.brand-mark i:nth-child(3) {
|
||||||
|
background: #efba42;
|
||||||
|
}
|
||||||
|
.brand-mark i:nth-child(4) {
|
||||||
|
background: #4bb89e;
|
||||||
|
}
|
||||||
|
.site-header nav a {
|
||||||
|
text-decoration-thickness: 2px;
|
||||||
|
text-underline-offset: 6px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.intro {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) minmax(290px, 0.82fr);
|
||||||
|
align-items: center;
|
||||||
|
gap: clamp(28px, 5vw, 80px);
|
||||||
|
min-height: 560px;
|
||||||
|
padding: 45px 0 80px;
|
||||||
|
}
|
||||||
|
.intro-copy {
|
||||||
|
max-width: 670px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.intro-note {
|
||||||
|
color: #2352c6;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 16px;
|
||||||
|
margin: 0 0 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(48px, 7vw, 94px);
|
||||||
|
line-height: 1.02;
|
||||||
|
letter-spacing: -0.075em;
|
||||||
|
margin: 0;
|
||||||
|
max-width: 10ch;
|
||||||
|
}
|
||||||
|
.intro-description {
|
||||||
|
max-width: 51ch;
|
||||||
|
margin: 28px 0 30px;
|
||||||
|
font-size: clamp(18px, 1.8vw, 21px);
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #43516b;
|
||||||
|
}
|
||||||
|
.primary-link {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 52px;
|
||||||
|
padding: 12px 24px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: #2352c6;
|
||||||
|
color: white;
|
||||||
|
font-weight: 800;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.primary-link:hover {
|
||||||
|
background: #173e9b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.game-scene {
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
.board {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
width: min(100%, 490px);
|
||||||
|
aspect-ratio: 1;
|
||||||
|
padding: 18px;
|
||||||
|
border-radius: 30px;
|
||||||
|
background: #fff;
|
||||||
|
box-shadow: 0 24px 60px #253f7024;
|
||||||
|
transform: rotate(7deg);
|
||||||
|
}
|
||||||
|
.square {
|
||||||
|
background: #dbe7fa;
|
||||||
|
}
|
||||||
|
.square.deep {
|
||||||
|
background: #b7cff5;
|
||||||
|
}
|
||||||
|
.square:first-child {
|
||||||
|
border-top-left-radius: 12px;
|
||||||
|
}
|
||||||
|
.square:nth-child(4) {
|
||||||
|
border-top-right-radius: 12px;
|
||||||
|
}
|
||||||
|
.square:nth-child(13) {
|
||||||
|
border-bottom-left-radius: 12px;
|
||||||
|
}
|
||||||
|
.square:nth-child(16) {
|
||||||
|
border-bottom-right-radius: 12px;
|
||||||
|
}
|
||||||
|
.piece {
|
||||||
|
position: absolute;
|
||||||
|
display: block;
|
||||||
|
width: 17%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 8px solid #ffffffc9;
|
||||||
|
box-shadow:
|
||||||
|
0 8px 18px #18243c38,
|
||||||
|
inset 0 -7px 0 #00000019;
|
||||||
|
}
|
||||||
|
.piece-blue {
|
||||||
|
top: 27%;
|
||||||
|
left: 28%;
|
||||||
|
background: #2455d6;
|
||||||
|
}
|
||||||
|
.piece-coral {
|
||||||
|
top: 52%;
|
||||||
|
left: 54%;
|
||||||
|
background: #ec6754;
|
||||||
|
}
|
||||||
|
.piece-gold {
|
||||||
|
top: 27%;
|
||||||
|
left: 54%;
|
||||||
|
background: #ecc04f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.games-section {
|
||||||
|
border-top: 2px solid #cbd8ed;
|
||||||
|
padding: 42px 0 100px;
|
||||||
|
min-height: 290px;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: start;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: clamp(32px, 4vw, 45px);
|
||||||
|
line-height: 1.1;
|
||||||
|
letter-spacing: -0.055em;
|
||||||
|
margin: 0 0 8px;
|
||||||
|
}
|
||||||
|
.section-header p {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #52617c;
|
||||||
|
}
|
||||||
|
.refresh-button {
|
||||||
|
cursor: pointer;
|
||||||
|
background: transparent;
|
||||||
|
border: 2px solid #2352c6;
|
||||||
|
color: #2352c6;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.refresh-button:hover {
|
||||||
|
background: #deebff;
|
||||||
|
}
|
||||||
|
.catalog-message {
|
||||||
|
max-width: 58ch;
|
||||||
|
padding: 28px;
|
||||||
|
margin: 30px 0 0;
|
||||||
|
border-left: 5px solid #e7b741;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
background: #ffffffaa;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: #43516b;
|
||||||
|
}
|
||||||
|
.games-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(min(100%, 270px), 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 28px;
|
||||||
|
}
|
||||||
|
.game-card {
|
||||||
|
padding: 26px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
.game-card h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
.game-card p {
|
||||||
|
color: #52617c;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.game-card span {
|
||||||
|
color: #2352c6;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 24px 0 32px;
|
||||||
|
border-top: 1px solid #cbd8ed;
|
||||||
|
color: #52617c;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 850px) {
|
||||||
|
.intro {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 42px;
|
||||||
|
padding-top: 36px;
|
||||||
|
}
|
||||||
|
.intro-copy {
|
||||||
|
max-width: 700px;
|
||||||
|
}
|
||||||
|
.board {
|
||||||
|
width: min(72vw, 420px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 520px) {
|
||||||
|
.site-header {
|
||||||
|
min-height: 78px;
|
||||||
|
}
|
||||||
|
.intro {
|
||||||
|
min-height: 0;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 38px 0 56px;
|
||||||
|
}
|
||||||
|
.intro-note {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(43px, 12vw, 60px);
|
||||||
|
}
|
||||||
|
.intro-description {
|
||||||
|
margin: 21px 0 24px;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
.game-scene {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
.board {
|
||||||
|
width: min(72vw, 290px);
|
||||||
|
padding: 11px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
.piece {
|
||||||
|
border-width: 5px;
|
||||||
|
}
|
||||||
|
.games-section {
|
||||||
|
padding: 32px 0 68px;
|
||||||
|
}
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.catalog-message {
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
footer {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.svelte", "vite.config.ts"]
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
server: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 4174,
|
||||||
|
strictPort: true,
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://127.0.0.1:8082',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
preview: {
|
||||||
|
host: '127.0.0.1',
|
||||||
|
port: 4175,
|
||||||
|
strictPort: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -0,0 +1,118 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"juegoland/games2/internal/domain/catalog"
|
||||||
|
"juegoland/games2/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Handler struct {
|
||||||
|
service *server.Service
|
||||||
|
}
|
||||||
|
|
||||||
|
type gameView struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
MinPlayers int `json:"minPlayers"`
|
||||||
|
MaxPlayers int `json:"maxPlayers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type response[T any] struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
Data T `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type catalogData struct {
|
||||||
|
Items []gameView `json:"items"`
|
||||||
|
NextCursor *string `json:"nextCursor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewHandler(service *server.Service) http.Handler {
|
||||||
|
h := &Handler{service: service}
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("GET /live", h.live)
|
||||||
|
mux.HandleFunc("GET /ready", h.ready)
|
||||||
|
mux.HandleFunc("GET /api/v2/bootstrap", h.bootstrap)
|
||||||
|
mux.HandleFunc("GET /api/v2/catalog", h.catalog)
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) live(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"status": "live"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) ready(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
// This only asserts that the bootstrap server can serve its current
|
||||||
|
// in-memory catalog. PostgreSQL and games are not enabled in this phase.
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ready", "phase": "foundation"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) bootstrap(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
id, ok := newRequestID(w)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, response[server.Bootstrap]{RequestID: id, Data: h.service.Bootstrap()})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) catalog(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
id, ok := newRequestID(w)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
games := h.service.PublishedGames()
|
||||||
|
items := make([]gameView, 0, len(games))
|
||||||
|
for _, game := range games {
|
||||||
|
items = append(items, toGameView(game))
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, response[catalogData]{
|
||||||
|
RequestID: id,
|
||||||
|
Data: catalogData{Items: items, NextCursor: nil},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func toGameView(game catalog.Game) gameView {
|
||||||
|
return gameView{
|
||||||
|
ID: game.ID,
|
||||||
|
Version: game.Version,
|
||||||
|
Title: game.Title,
|
||||||
|
Summary: game.Summary,
|
||||||
|
MinPlayers: game.MinPlayers,
|
||||||
|
MaxPlayers: game.MaxPlayers,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequestID(w http.ResponseWriter) (string, bool) {
|
||||||
|
var bytes [16]byte
|
||||||
|
if _, err := rand.Read(bytes[:]); err != nil {
|
||||||
|
http.Error(w, "Unable to create request ID", http.StatusInternalServerError)
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
bytes[6] = (bytes[6] & 0x0f) | 0x40
|
||||||
|
bytes[8] = (bytes[8] & 0x3f) | 0x80
|
||||||
|
var encoded [36]byte
|
||||||
|
hex.Encode(encoded[0:8], bytes[0:4])
|
||||||
|
encoded[8] = '-'
|
||||||
|
hex.Encode(encoded[9:13], bytes[4:6])
|
||||||
|
encoded[13] = '-'
|
||||||
|
hex.Encode(encoded[14:18], bytes[6:8])
|
||||||
|
encoded[18] = '-'
|
||||||
|
hex.Encode(encoded[19:23], bytes[8:10])
|
||||||
|
encoded[23] = '-'
|
||||||
|
hex.Encode(encoded[24:36], bytes[10:16])
|
||||||
|
return string(encoded[:]), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, body any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
|
w.Header().Set("Cache-Control", "no-store")
|
||||||
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(body)
|
||||||
|
}
|
||||||
@ -0,0 +1,68 @@
|
|||||||
|
package httpapi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"juegoland/games2/internal/domain/catalog"
|
||||||
|
"juegoland/games2/internal/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPublishedCatalogIsTheOnlyPublicCatalog(t *testing.T) {
|
||||||
|
registry := catalog.NewRegistry([]catalog.Game{
|
||||||
|
{ID: "draft", Title: "Borrador", Published: false},
|
||||||
|
{ID: "conecta4", Version: "1.0.0", Title: "Conecta 4", Summary: "Cuatro fichas en línea", MinPlayers: 2, MaxPlayers: 2, Published: true},
|
||||||
|
})
|
||||||
|
handler := NewHandler(server.NewService(registry, time.Now))
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/v2/catalog", nil))
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", recorder.Code)
|
||||||
|
}
|
||||||
|
if recorder.Header().Get("Cache-Control") != "no-store" {
|
||||||
|
t.Fatal("catalog response must not be cached before audience policies exist")
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
RequestID string `json:"requestId"`
|
||||||
|
Data struct {
|
||||||
|
Items []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"items"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body.RequestID == "" || len(body.Data.Items) != 1 || body.Data.Items[0].ID != "conecta4" {
|
||||||
|
t.Fatalf("unexpected catalog response: %+v", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBootstrapDoesNotAdvertiseUnavailableFeatures(t *testing.T) {
|
||||||
|
handler := NewHandler(server.NewService(catalog.NewRegistry(nil), func() time.Time {
|
||||||
|
return time.Date(2026, time.October, 5, 12, 0, 0, 0, time.UTC)
|
||||||
|
}))
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/api/v2/bootstrap", nil))
|
||||||
|
if recorder.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want 200", recorder.Code)
|
||||||
|
}
|
||||||
|
var body struct {
|
||||||
|
Data server.Bootstrap `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(recorder.Body.Bytes(), &body); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if body.Data.Features.Rooms || body.Data.Features.Chat || !body.Data.Features.Catalog {
|
||||||
|
t.Fatalf("incorrect advertised features: %+v", body.Data.Features)
|
||||||
|
}
|
||||||
|
if len(body.Data.PlatformProtocolVersions) != 0 || len(body.Data.GameProtocolVersions) != 0 {
|
||||||
|
t.Fatal("unimplemented realtime protocols must not be advertised")
|
||||||
|
}
|
||||||
|
if body.Data.ServerTime.Format(time.RFC3339) != "2026-10-05T12:00:00Z" {
|
||||||
|
t.Fatalf("wrong server time: %s", body.Data.ServerTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
package catalog
|
||||||
|
|
||||||
|
import "slices"
|
||||||
|
|
||||||
|
// Game describes an installed, internally published version. The registry
|
||||||
|
// never imports manifests from a player request or the legacy application.
|
||||||
|
type Game struct {
|
||||||
|
ID string
|
||||||
|
Version string
|
||||||
|
Title string
|
||||||
|
Summary string
|
||||||
|
MinPlayers int
|
||||||
|
MaxPlayers int
|
||||||
|
Published bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Registry struct {
|
||||||
|
games []Game
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRegistry(games []Game) *Registry {
|
||||||
|
return &Registry{games: slices.Clone(games)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Registry) Published() []Game {
|
||||||
|
result := make([]Game, 0)
|
||||||
|
for _, game := range r.games {
|
||||||
|
if game.Published {
|
||||||
|
result = append(result, game)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
slices.SortFunc(result, func(a, b Game) int {
|
||||||
|
if a.Title < b.Title {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.Title > b.Title {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if a.ID < b.ID {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.ID > b.ID {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package chat will own adult-only room communication and redactions.
|
||||||
|
// It is disabled until eligibility and moderation are operational.
|
||||||
|
package chat
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package identity will own accounts, credentials and sessions.
|
||||||
|
// No identity capability is enabled in the foundation release.
|
||||||
|
package identity
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
// Package moderation will own reports, restrictions and appeals.
|
||||||
|
package moderation
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package rooms will own rooms, memberships and invitations.
|
||||||
|
// Room commands will share a transaction with the game engine.
|
||||||
|
package rooms
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package safety will decide age, supervision and communication capabilities.
|
||||||
|
// It will be the shared authority for HTTP, realtime delivery and jobs.
|
||||||
|
package safety
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package engine will execute versioned, deterministic game rules.
|
||||||
|
// The TypeScript contract in docs is a semantic reference, not runnable Go.
|
||||||
|
package engine
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package games will register internally published Go rule modules.
|
||||||
|
// No game is installed in the foundation release.
|
||||||
|
package games
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package postgres will implement repositories using one shared transaction.
|
||||||
|
// PostgreSQL is not connected in the foundation release.
|
||||||
|
package postgres
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package providers will adapt email, age assurance and SMS providers.
|
||||||
|
// No external provider is configured in the foundation release.
|
||||||
|
package providers
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
// Package jobs will drive durable deadlines and outbox delivery through
|
||||||
|
// the same application services used by the API.
|
||||||
|
package jobs
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"juegoland/games2/internal/domain/catalog"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CatalogReader interface {
|
||||||
|
Published() []catalog.Game
|
||||||
|
}
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
catalog CatalogReader
|
||||||
|
now func() time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type Features struct {
|
||||||
|
Catalog bool `json:"catalog"`
|
||||||
|
Rooms bool `json:"rooms"`
|
||||||
|
Chat bool `json:"chat"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Bootstrap struct {
|
||||||
|
ServerTime time.Time `json:"serverTime"`
|
||||||
|
PlatformProtocolVersions []int `json:"platformProtocolVersions"`
|
||||||
|
GameProtocolVersions []int `json:"gameProtocolVersions"`
|
||||||
|
Features Features `json:"features"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewService(reader CatalogReader, now func() time.Time) *Service {
|
||||||
|
return &Service{catalog: reader, now: now}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) Bootstrap() Bootstrap {
|
||||||
|
return Bootstrap{
|
||||||
|
ServerTime: s.now().UTC(),
|
||||||
|
PlatformProtocolVersions: []int{},
|
||||||
|
GameProtocolVersions: []int{},
|
||||||
|
Features: Features{
|
||||||
|
Catalog: true,
|
||||||
|
Rooms: false,
|
||||||
|
Chat: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) PublishedGames() []catalog.Game {
|
||||||
|
return s.catalog.Published()
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"name": "games2-contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "games2-contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "24.19.1",
|
||||||
|
"typescript": "6.0.3",
|
||||||
|
"zod": "4.6.5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/node": {
|
||||||
|
"version": "24.19.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.1.tgz",
|
||||||
|
"integrity": "sha512-aS3/DG0oM05K0RIXXP+hKjinGG5IgSSVGzswZxW3O0sS3pH4/fycXundUC9XsszgKCk4gHXylTEK6hyFxVxnoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "6.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
||||||
|
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/undici-types": {
|
||||||
|
"version": "7.24.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
|
||||||
|
"integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.6.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz",
|
||||||
|
"integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "games2-contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=24.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"contracts:check": "node docs/game-engine/verify.ts && tsc --project docs/game-engine/tsconfig.json"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "24.19.1",
|
||||||
|
"typescript": "6.0.3",
|
||||||
|
"zod": "4.6.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue