Un Worker + Durable Objects que (1) sirve el juego (public/index.html, generado) y (2) reenvía mensajes WebSocket entre jugadores. El anfitrión sigue siendo quien decide la partida; el servidor solo retransmite. No guarda datos.
Requisitos: Node 18+ y tu cuenta de Cloudflare.
npm install
npx wrangler login # abre el navegador, autoriza
npm run deploy # wrangler deploy (el hook `build` minifica antes)
Al terminar muestra https://boardgames.<tu-subdominio>.workers.dev. Ábrelo: ya es la web completa con salas online (comprueba /api/ping → mesa-ok).
https://<tu-dominio>/game/<id> abre ese juego directamente (por ejemplo /game/sudoku, /game/texas, /game/ajedrez), ideal para marcadores. Los assets sirven el mismo index.html (que ya lleva window.MJ_PATHS=1 desde build.js) para / y /game/* sin pasar por el worker (not_found_handling: "single-page-application" en wrangler.jsonc; solo /ws/* y /api/* van al worker); el cliente abre el juego y mantiene la URL y el título de la pestaña. Un id desconocido lleva al inicio. Sin servidor (archivo abierto, visor de Claude) el mismo enlace se escribe como #/game/<id>.
Ids: el id de cada línea de src/app/core/13-catalog.js (el mismo nombre que su fichero en src/app/mods/).
El código del cliente vive en src/app/: shell.html (esqueleto), style.css, core/*.js (núcleo, por orden de nombre), mods/<id>.js (un fichero por juego o módulo compartido), i18n/<id>.js (diccionarios de inglés) y mods.json (lista ordenada de módulos con sus dependencias). Para actualizar:
src/app/.git push → Cloudflare (Workers Builds) ejecuta wrangler deploy, cuyo hook build (node build.js) genera public/index.html, public/m/*.js, sw.js y offline-es.html y offline-en.html. En local: npm run deploy.Solo generar: npm run build. Elimina todos los comentarios (las fuentes los conservan). npm run page escribe dev.html (la página completa sin minificar, con los dos idiomas) para abrirla como archivo o depurar. El núcleo pesa ≈120 KB (63 KB gzip); cada juego se descarga al abrirlo (/m/<id>.<hash>.js, caché inmutable) y un service worker permite jugar sin conexión tras pulsar «Guardar juegos en este dispositivo». «Descarga offline» baja offline-es.html u offline-en.html (todo en un archivo, del idioma actual).
public/index.html es un fichero generado: está en .gitignore, no se edita ni se versiona.
En Chrome/Edge/Android: menú → «Instalar aplicación» (o el icono de instalar de la barra). En iPhone/iPad (Safari): Compartir → «Añadir a pantalla de inicio». Una vez instalada, el service worker guarda en segundo plano el núcleo y todos los juegos: la app abre al instante y funciona sin conexión. En el navegador (sin instalar) cada juego se guarda al abrirlo, y «Guardar juegos en este dispositivo» los guarda todos. Cuando hay una versión nueva, la app avisa y el menú ofrece «Actualizar ahora».
Sin internet se juega solo/contra IA y en sala sin conexión: los móviles deben estar en la misma Wi-Fi (o punto de acceso); el anfitrión enseña un QR, el invitado lo escanea con la cámara dentro de la app (o lo lee con otra app de QR y pega el texto), y viceversa con la respuesta. Las salas online sí necesitan internet. La cámara requiere https (la web desplegada o localhost).
«Descarga offline» sigue entregando un único mesa-de-juegos.html sin servidor ni instalación, para enviarlo por mensajería.
Abre una vez https://TU-PAGINA/?srv=https://boardgames.<subdominio>.workers.dev; la dirección queda guardada en ese navegador. O fíjala al construir: MJ_SERVER=https://… node build.js.
Si quieres limitar quién puede conectar, edita ALLOWED_ORIGINS en wrangler.jsonc (p. ej. "https://USUARIO.github.io,null"; null es el HTML abierto desde archivo) y vuelve a desplegar.
npx wrangler dev; no en Cloudflare real.wrangler.jsonc; se consulta en el panel de Cloudflare → Workers → boardgames → Observability, o con npx wrangler tail.boardgames.*.workers.dev) y crea un worker nuevo: los Durable Objects del anterior no se migran (aquí no guardan datos).Notificaciones para quien bloquea el móvil en una sala online («Empieza la partida», «Te toca», «X quiere entrar en tu sala»). Sin la clave el resto funciona igual y la opción no aparece activa.
node -e "const k=require('crypto').generateKeyPairSync('ec',{namedCurve:'prime256v1'});console.log(JSON.stringify(k.privateKey.export({format:'jwk'})))"
npx wrangler secret put VAPID_JWK y pegar el JSON.wrangler dev): fichero .dev.vars (no se versiona) con VAPID_JWK='{"kty":"EC",…}'.VAPID_SUBJECT en wrangler.jsonc (mailto:… o https://…); vacío = el origen de la página.Las suscripciones se guardan en el Durable Object de cada sala y se borran 12 h después de que quede vacía. En iPhone los avisos solo funcionan con la app instalada (iOS 16.4+).