☀ TORMENTA SOLAR

Cómo funciona, capa por capa

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.

¿Por dónde entra el juego? — GitHub Pages vs. infra propia

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:

🖥️ Tu navegador
▼ pide dos cosas ▼

① El juego — estáticos

HTML · JS · CSS · assets (sin build)
MODO A — HOY · GitHub Pages CDN de GitHub · villadalmine.github.io/tormenta-solar
gratis, global, cero infra propia.
MODO B — self-host · ✅ LIVE nginx en tu cluster · tormenta-solar.cybercirujas.club
por HAProxy → Cilium Gateway → pod nginx. "Todo local".

② El chat — IA

fetch al proxy (en los dos modos)
SIEMPRE · tu infra self-hosted llm-tormenta-solar.cybercirujas.club
HAProxy (SNI) → Cilium Gateway (TLS) → proxy Node → LiteLLM → OpenRouter / GPU (HAMi+Ollama) / NPU RK1.
Da igual de dónde venga el juego: la IA, la key y el fierro son siempre tuyos. Por eso es gratis para el jugador.
▮ GitHub Pages = solo archivos estáticos (modo A) · ▮ Infra propia = el chat siempre, y los estáticos también en modo B.

El viaje de un mensaje del chat

🖥️Navegador / GitHub Pages
el juego estático · hace fetch al proxy
HTTPS
🌐DNS público + IP WAN
llm-tormenta-solar… → IP de casa · :443
🧱HAProxy — Mac mini G4 · OpenBSD
TCP · SNI passthrough (no termina TLS)
TCP crudo
🚪Cilium Gateway API · VIP .200
acá TERMINA el TLS (Let's Encrypt)
HTTPRoute + cilium-envoy
⚙️Proxy Node — tormenta-ai-proxy
CORS · personas · guardrails
POST /v1/chat/completions
🔀LiteLLM — router
pool de keys · fallback · ruteo por modelo
según el modelo
☁️OpenRouter
nube · free
🎮GPU NVIDIA
HAMi + Ollama
🔌NPU RK1 ×4
inferencia local
Ver el mismo recorrido en ASCII
   [ 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
    
0

El cliente — el juego en tu navegador

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.

JS vanillaHTML5 CanvasGitHub Pagesi18n ES/EN
1

DNS + TLS — entrar sin abrir puertos de más

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.

cert-managerLet's EncryptDNS-01 / acme-dns
2

HAProxy — el borde (fuera del cluster)

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.

HAProxyTCP / L4SNI routing
3

Cilium Gateway API — la puerta del cluster

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.

Cilium 1.19Gateway APIeBPFLB-IPAM
4

HTTPRoute + Envoy — routing L7

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.

HTTPRoutecilium-envoyL7
5

El proxy del juego — tormenta-ai-proxy

Un 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.

NodeCORSpersonas server-sideService ClusterIP
6

LiteLLM — el centralizador de modelos

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.

LiteLLMOpenAI APIpool de keysfallbacks
7

La inferencia — nube y/o fierro propio

Detrás de LiteLLM hay tres destinos, intercambiables:

  • OpenRouter (nube): modelos free, costo $0 — el default actual del chat.
  • GPU NVIDIA self-hosted: un nodo con GPU, compartida por HAMi (vGPU slicing, varias cargas sobre la misma placa), corriendo Ollama.
  • NPUs RK1 self-hosted: 4 placas RK1 (una por nodo arm64) haciendo inferencia local, en round-robin.

La idea: empezar gratis en la nube y, cuando convenga, mover el chat al hardware propio sin tocar ni el juego ni el proxy.

OpenRouterNVIDIA + HAMiOllamaRK1 NPU ×4
8

Observabilidad — ver todo lo que pasa

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.

HubblePrometheusGrafana
9

Build & deploy — sin Docker daemon, todo reproducible

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.

KanikoArgo Workflowsregistry internoHelmrollback + alertas Telegramautoplay QA nocturno

Todo es API / declarativo

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 motor del juego — data-driven (todo es un objeto)

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.

entidades + componentesschema validadoparidad v1≡v2
🏭

La máquina de niveles por IA + la RED de jugabilidad

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).

generación procedural+ IA+ validación formal= sin niveles rotos
🔮

Niveles a tu medida — la IA usa lo que sabe de vos

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.

memoria por NPCgrounding del mundonivel personalizado
📡

Multijugador en tiempo real — sin servidor de autoridad

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.

SSE relaysin autoridadco-presenciapreset = sin moderacióndegrada solo
🧱

Construcción colaborativa — el barrio construye junto (estado global)

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.

estado globalPVC persistenteconsumo-en-lecturacron + IAtemporadas
🃏

Truco criollo real — motor puro, testeado, y PvP humano

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.

motor puro + tests1v1 y 3v3 humanomesas server-sidehost-autoritativo
🤖

Historias y quests como DATA — un grafo, no un guion hardcodeado

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).

quest = grafodata-drivenpistas, no spoilerstodo es un objeto
🛡️

Si la GPU se cae, no se cae el juego — modo estático al toque

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.

circuit breakerfallback estáticocero "se tildó"
🔜

Lo que viene — Telegram + Hermes

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.

Telegram BotHermes (agente)control conversacional