El juego es 100% estático (JS vanilla + HTML5 Canvas, sin build) servido por GitHub Pages.
Lo único que sale a la red es el chat con los linyeras: un fetch a nuestro proxy. Y ese
pedido atraviesa una infra Kubernetes self-hosted entera. Esto es el viaje completo de un mensaje,
de punta a punta. Nada es mágico ni una nube ajena: es fierro propio, y todo se declara por API.
Hay dos cosas distintas viajando: los estáticos del juego (HTML/JS/CSS) y el chat con IA. Los estáticos pueden servirse desde GitHub Pages (hoy) o desde tu propia infra (nginx en el cluster); el chat siempre sale por la infra self-hosted. Este es el mismo navegador, dos orígenes:
[ Navegador / GitHub Pages ] el juego (estático)
│ HTTPS fetch(llm-tormenta-solar.cybercirujas.club)
▼
[ DNS público + IP WAN ] A → IP de casa
│ :443
▼
[ HAProxy (borde, Mac mini) ] modo TCP · SNI passthrough
│ reenvía TCP por hostname (no termina TLS)
▼
[ Cilium Gateway API 192.168.178.200 ] ← acá TERMINA el TLS (Let's Encrypt)
│ cilium-envoy + HTTPRoute (hostname → Service)
▼
[ Service → Pod: tormenta-ai-proxy ] Node · CORS · personas · guardrails
│ POST /v1/chat/completions
▼
[ LiteLLM (el centralizador) ] pool de keys · fallback · ruteo
├──────────────┬──────────────┐
▼ ▼ ▼
[ OpenRouter ] [ GPU NVIDIA ] [ NPUs RK1 ]
nube · free HAMi + Ollama 4× inferencia local
Vanilla JS + Canvas, sin framework ni build, hosteado en GitHub Pages. El chat hace un
fetch POST con el NPC, tu mensaje y un poco de contexto. Tu API key es opcional:
por defecto pega contra nuestro proxy (gratis); si ponés una key de OpenRouter, queda solo en tu
navegador como override. La key "buena" del servidor nunca toca el cliente.
El dominio llm-tormenta-solar.cybercirujas.club resuelve a la IP pública de casa.
El certificado es de Let's Encrypt, emitido por cert-manager con desafío DNS-01 vía
acme-dns — así no hace falta exponer el :80 ni validar por HTTP. El cert se renueva solo.
El borde es lo más cirujeado de todo: una Mac mini G4 (PowerPC) corriendo OpenBSD —
sí, una máquina de ~2005 reciclada como router TLS. Ahí vive HAProxy en modo TCP con
SNI passthrough: mira el
req.ssl_sni del hello TLS y, según el hostname, manda el TCP crudo al backend que
corresponde — sin desencriptar nada (el TLS lo termina el gateway, más adentro). Varios dominios
comparten el mismo backend que apunta a la VIP del cluster. Acá ajustamos maxconn y
timeout para que las respuestas largas del LLM no se corten.
El tráfico entra al cluster Kubernetes por el cluster-gateway
(GatewayClass cilium), una VIP fija servida por LB-IPAM de Cilium. Acá termina el
TLS: un listener HTTPS por hostname presenta el certificado (el Secret que llenó cert-manager).
Es Gateway API, no Ingress: el routing es un recurso declarativo y estándar.
Un HTTPRoute matchea el hostname y enruta al Service del proxy. El plano de datos es cilium-envoy, que hace de reverse-proxy HTTP dentro del cluster. Sumar un dominio nuevo es, literalmente, agregar un HTTPRoute y un listener — sin tocar HAProxy más que la regla de SNI.
tormenta-ai-proxyUn servicio chico en Node (imagen propia, arm64). Hace tres cosas: pone los CORS para que GitHub Pages pueda llamarlo; guarda las personas (los system-prompts de cada linyera) del lado del servidor; aplica los guardrails (si el modelo tarda, devuelve el "la tormenta solar interfiere con el modelo" en vez de dejarte colgado). Después reenvía el pedido a LiteLLM con la key real, que jamás sale al navegador.
Un único endpoint OpenAI-compatible (/v1/chat/completions) que es el cerebro del
ruteo. Mantiene un pool de API keys, hace fallback entre modelos si uno falla o satura, y
decide a dónde mandar cada request según el model_name. Cambiar de "nube" a "fierro propio"
es cambiar un nombre de modelo — el juego no se entera de nada. Hoy el chat usa un modelo
free (familia Gemma) por defecto.
Detrás de LiteLLM hay tres destinos, intercambiables:
La idea: empezar gratis en la nube y, cuando convenga, mover el chat al hardware propio sin tocar ni el juego ni el proxy.
Hubble (de Cilium) muestra cada flujo de red L3/L4/L7 del cluster: se puede ver, en vivo, el pedido saliendo del proxy hacia LiteLLM y de ahí a la inferencia. Prometheus junta las métricas (LiteLLM expone requests, latencia, gasto, fallbacks) y Grafana las grafica. Si algo va lento o falla, se ve en un tablero, no a ciegas.
La imagen del proxy se buildea dentro del cluster con Kaniko orquestado por Argo Workflows (no hace falta un Docker daemon ni una CI externa), en un nodo arm64, y se empuja a un registry interno. El deploy es un Helm chart que crea todo lo declarativo: el HTTPRoute, el Certificate, y hasta un hook idempotente que suma el listener HTTPS al gateway compartido. Volver a levantarlo en otro cluster es correr un comando.
El deploy también es un Argo Workflow (tormenta-deploy): buildea → hace el rollout →
corre un smoke test contra la URL real y, si algo falla, hace rollback automático a la versión
anterior y te avisa por Telegram. La misma alerta cazó una vez un cron de carteles roto. Y todas las
noches un autoplay de QA juega el nivel solo (headless) y reporta si algo se rompió — todo in-cluster,
sin humanos en el loop.
Nada de esto se hizo "a mano y a ver si anda": Gateway y HTTPRoute (Gateway API), Certificate y ClusterIssuer (CRDs de cert-manager), el build (CRD de Argo), el deploy (Helm values). Todo es un objeto versionado que se reaplica igual siempre. Eso es lo que hace que una infra casera sea seria: es reproducible.
El juego no está "hardcodeado": corre desde un modelo de datos (un JSON del nivel) que un loader
convierte en salas. Las cosas son entidades + componentes (interactuar, combatir, ser cartel, tener
memoria, tener grafo social). Un nivel nuevo = cambiar datos, no código. Hay un test de paridad
que garantiza que el motor data-driven da exactamente lo mismo que la versión vieja. El despacho es por
registros (verbo→handler), sin if/else gigantes; hasta el balance es dato.
Te colás a la trastienda del chino y se GENERA un nivel que corre en el motor de verdad (saltás, hay enemigos, cámara, art real). La parte interesante: la IA puede proponer mil layouts, pero solo los jugables llegan. Hay un validador formal (la "red") que chequea que el nivel sea transitable (puertas no tapadas, salida alcanzable) antes de cargarlo; si no pasa, se re-pide o se auto-repara. Anécdota: la red caza hasta un bug que metió un humano (y queda como test de regresión).
Los linyeras-oráculo chatean con IA y tienen memoria: se acuerdan de lo que hablaste. Con eso, el oráculo puede inventarte un nivel a medida de lo que parecés querer (te junta tus charlas y la IA arma el tema, las frases y hasta elige la estructura). Tu memoria → el mundo generado. Además los NPC saben del ecosistema (tu progreso, los carteles, el Mundial) y chusmean entre ellos por un grafo social.
El cine tiene un piso "EN VIVO": una pantalla que muestra el mundo vivo (cuántos juegan ahora, en qué zona, un ticker de hitos anónimos). Y arriba un bodegón vista de arriba donde te cruzás con otros jugadores de verdad, sentados a las mesas, con sus emotes y frases preset. Lo interesante del diseño: el server es un relay sin autoridad (cada cliente postea su estado por SSE y recibe el de los demás; nadie "manda"), vive en el mismo proxy (Node sin dependencias, in-memory), y como el chat público es preset + emotes (no texto libre) no hace falta moderación. Si se cae la red, el bodegón es un bar jugable solo y nadie nota que faltaba el online.
Dos features donde los jugadores construyen algo compartido de verdad, persistido en el server (disco PVC): El Tablón — dejás un cartel corto que vive en el server hasta que OTRO jugador lo lee, y ahí se borra (consumo-en-lectura, à la Death Stranding); y el Datacenter comunitario — un estado GLOBAL único al que todos aportan partes hasta completarlo, con temporadas y una cinemática de endgame. Backend en el mismo proxy (banco en disco, cupos + rate-limit + censura) y un cron con IA que también aporta contenido. Todo aditivo: sin red, el juego anda igual.
El truco no es un mini-juego cualquiera: es un motor puro (sin estado global, 100% funciones) con envido, flor, truco/retruco y las voces cantadas, cubierto por tests (cientos de partidas sin sesgo). Y se juega 1v1 y de a 6 (3v3) contra otros humanos: el matchmaking lo hace el server (mesas reales, no P2P frágil), la partida corre host-autoritativa sobre el mismo transporte del salón, y las vistas de cada jugador no revelan la mano del rival. Con la regla de la casa del 3v3 y bots de IA que completan los asientos que falten.
Las misiones no son un montón de if: son un grafo de pasos (data) con un runtime genérico
que avanza solo según con qué NPC hablás (por rol, no por nombre fijo). Así una quest entera —por ejemplo
sacarte un chip que te puso un robot: cambiás de personaje, conseguís un troyano, comprás una consola
retro y la usás— se describe como datos y se engancha al motor sin tocar el código del loop. Los
linyeras saben todo y te van dando pistas del paso en el que estás (no te lo tiran masticado), y
los hitos se separan en principales (la cadena) y secundarias (las sueltas).
Toda feature de IA degrada a estático cuando el fierro (la GPU/los modelos) no está. Hay un circuit breaker: si una llamada falla o tarda, se abre el circuito un rato y todo cae a contenido estático al instante, sin colgarse esperando timeouts. Cuando la GPU vuelve, se cierra solo. Lo mismo con los bancos de noticias/propaganda y el chat: siempre hay un plan B.
Se va a agregar un bot de Telegram conectado a Hermes (un agente que ya corre en el mismo cluster) para manejar el juego desde el chat de Telegram: administrarlo, generar contenido nuevo y orquestar el mundo desde el celular. El juego pasa a tener un "panel de control" conversacional, sobre la misma infra.