Un servidor que orquesta salas, transporte y reconexión para juegos de cartas
multijugador — pero que no conoce las reglas de ningún juego concreto. Cada
juego es un plugin que implementa la interfaz engine.GameEngine;
el servidor solo sabe:
- crear/listar/unir salas por código (
internal/lobby), - correr cada partida como un nodo aislado — un goroutine con su propio
estado y canal de comandos (
internal/room), - hablar WebSocket con el cliente y serializar todo a JSON (
internal/transport).
La implementación de referencia es un motor de Exploding Kittens
(games/explodingkittens) — un puerto fiel de las reglas de un cliente
Flutter existente, pensado para que ese cliente pueda pasar de jugar por LAN a
jugar online cambiando solo la URL de conexión, sin tocar su código.
cliente WebSocket (Flutter, u otro)
│
▼
internal/transport ← HTTP/WS, formato de wire, no conoce reglas de juego
│
▼
internal/lobby ← código de sala → *room.Room
│
▼
internal/room ← un goroutine por partida: estado, timers, broadcast
│
▼
pkg/engine ← contrato GameEngine (genérico)
▲
│ implementa
games/explodingkittens (o cualquier otro juego)
Cada juego concreto es una función pura: recibe un estado inmutable y una
acción, devuelve el estado siguiente más los eventos que produjo. Sin I/O, sin
goroutines propias — eso lo hace trivial de testear y es lo que le permite a
room correr cualquier juego sin conocer sus reglas.
El detalle de cómo se llegó a este diseño (por qué lobby es tan chico,
por qué las ventanas de reacción con timer viven en room y no en el
motor, qué información oculta un juego con manos privadas como Exploding
Kittens) está en docs/ARCHITECTURE.md.
Requiere Go 1.24+.
go run ./cmd/server
# escuchando en :8080 (o $PORT si está seteada)Crear una sala y jugar:
# 1. Crear una sala de Exploding Kittens
curl -X POST localhost:8080/rooms \
-H 'Content-Type: application/json' \
-d '{"gameType":"exploding_kittens","hostId":"p1","hostName":"Ana"}'
# -> {"code":"AB12CD"}
# 2. Conectar por WebSocket a ws://localhost:8080/ws/AB12CD
# y mandar {"type":"join_room","playerId":"p1","name":"Ana"}El formato completo de mensajes (join_room, action, game_state,
game_event, etc.) está documentado en
internal/transport/protocol.go.
go test ./...Incluye tests unitarios del motor de Exploding Kittens con RNG determinista
(mismo seed → misma partida, sin mocks) y un test de integración que levanta
un servidor HTTP real y juega una partida completa con clientes WebSocket
reales (test/integration/).
- Crear un paquete en
games/<nombre-del-juego>/que implementeengine.GameEngine(firma completa). - Registrarlo en un
init():engine.Register("nombre-del-juego", NewEngine). - Importarlo con blank import en
cmd/server/main.go(_ "github.com/ZenXLK/cards_game_service/games/nombre-del-juego").
Nada más — room, lobby y transport no necesitan saber qué juegos
existen.
Pensado para correr como una sola instancia siempre viva (el estado de
cada sala vive en memoria de un único proceso, y los timers de
reconexión/lobby necesitan que el proceso no se suspenda por inactividad —
la plataforma que elijas tiene que poder desactivar eso). El Dockerfile
en deploy/ arma un binario estático sobre una
imagen distroless. Cada Release publicado en GitHub se construye y sube
automáticamente a ghcr.io/zenxlk/cards_game_service (ver
.github/workflows/publish-image.yml).
Guía paso a paso para configurar Supabase (identidad + historial,
opcional) y desplegar en docs/DEPLOYMENT.md.
Detalle de la decisión de arquitectura (por qué un solo proceso, cómo
escalar más adelante) en
docs/ARCHITECTURE.md.
Ver CONTRIBUTING.md.
MIT — ver el archivo LICENSE para el texto completo.