# Helix Empire — 10 000 jugadores en un mundo en tiempo real

Una estrategia espacial en tiempo real, basada en navegador, cuyo núcleo es la genética: los jugadores crían castas de criaturas editando ADN. Lo difícil es el motor: 10 000 jugadores en un único mundo compartido en un solo servidor, actualizándose en vivo. Diseñé la arquitectura en torno al verdadero cuello de botella (el tráfico saliente, no la CPU) y lo demostré con una prueba de carga medida: 237.6 Mbit/s con 10k jugadores, el 24% del presupuesto. Hecho en solitario, ahora mismo en alfa.

- **Author:** Fedor Chishchin
- **Role:** Arquitecto-ingeniero (en solitario)
- **Period:** 2026 — presente
- **Stack:** Rust · WebAssembly · WebTransport / QUIC · Event sourcing · Area of Interest · Hetzner bare-metal
- **Source:** https://fedorthinks.com/es/work/helix-empire

---

## El problema

**Helix Empire** es una estrategia espacial en tiempo real, en el navegador. Su núcleo no es construir,
sino la **genética**: un jugador cría castas de criaturas editando su ADN (de ahí «Helix», la doble
espiral). Los genes fijan los rasgos, los rasgos deciden cómo rinden las criaturas en distintos entornos,
eso mueve la economía y el ejército, lo que mueve la ciencia, que abre nuevas ediciones del genoma.
Un bucle cerrado: **genes → rasgos → economía → ciencia → laboratorio → de nuevo genes.** Fuerza del
imperio = genes × planetas × tecnología.

Técnicamente, eso significa **miles de jugadores en un mundo compartido**, vivo en tiempo real — la
economía hace tic, las criaturas se reproducen, las flotas se mueven, los recursos cambian cada segundo — y
**todo eso tiene que llegar al navegador de cada jugador sin lag.**

La mayoría de los juegos online esconden su escala: dividen a los jugadores en salas pequeñas de 20–50, o
en shards. Yo me puse un objetivo más difícil a propósito — **10 000 jugadores simultáneos en un único
mundo en un solo servidor** — porque justo en esa restricción es donde vive la arquitectura interesante.

## Los cuatro muros

Di «10 000 en un mundo» en voz alta y aparecen cuatro muros contra los que se estrella un diseño ingenuo.

1. **Difundir todos-con-todos es O(N²).** Si cada jugador debe saber de cada otro, un tic son
   `10 000 × 10 000 = 100 000 000` pares — varias veces por segundo. Físicamente imposible. Duplica los
   jugadores y cuadruplicas el trabajo.
2. **El coste dominante es el tráfico, no la CPU.** El hecho poco intuitivo del hosting: **el tráfico
   saliente (egress) es la parte cara** — decenas de veces más caro en los hyperscalers que en bare metal.
   Si cada tic envía muchos bytes a cada jugador, la factura de ancho de banda mata el proyecto mucho antes
   que la CPU. Así que la arquitectura tiene que diseñarse en torno a minimizar el tráfico.
3. **Un mundo quiere un servidor.** Repartir un mundo entre máquinas obliga a un consenso distribuido
   (¿qué servidor tiene el estado verdadero?) — lento y complejo. Elegí **single-writer**: exactamente un
   proceso puede mutar un mundo. Sin carreras, sin consenso. (Tiene una trampa desagradable — más abajo.)
4. **Un cliente fino no aguanta.** Si el servidor calcula todo y el navegador solo dibuja, el servidor es
   el cuello de botella. El cliente tiene que ser **grueso** — capaz de reconstruir la imagen a partir de
   un mínimo de datos.

## El stack — elegido contra los muros, no contra las modas

El sentido de la arquitectura es que **la tecnología sirve a las restricciones, no a la moda.** Cada
elección aquí responde a un muro concreto.

## Lo que construí, paso a paso — midiendo en cada paso

No lo construí todo de golpe. Fui por capas, **midiendo en cada una** — porque no puedes optimizar lo que no
has medido.

**Paso 0 — línea base.** Antes de arreglar nada, medí lo mal que estaba: lecturas de estado disparándose a
**4–10 segundos**, push al cliente a aproximadamente **un frame cada 20 segundos**. Eso me dio un número a
batir.

**Pasos 1–3 — matar la contención de single-writer.** Aquí está la trampa. Si un proceso muta el mundo,
entonces mientras aplica un tic (escribiendo en almacenamiento — lento), **todos los lectores esperan en el
mismo lock**. Abres la pantalla de defensa y te cuelgas 10 segundos porque el servidor está a mitad de
guardado. La solución: **cachés de read-model lock-free en RAM**, actualizadas de forma incremental y leídas
sin el lock de escritura.

El movimiento arquitectónico: **separar lecturas de escrituras.** Un único escritor sigue siendo dueño de la
consistencia; los lectores leen proyecciones en RAM y dejan de esperar. La contención desapareció, cada
endpoint pasó establemente a sub-segundo.

**Pasos 4–5 — enviar la diferencia, no el estado entero.** Incluso con lecturas rápidas, cada tic seguía
enviando el estado _completo_ — reenviando montañas de cosas que no habían cambiado. La solución: **deltas**.
Un `WireSessionStateDelta` comprobado matemáticamente donde `between(prev, next)` construye la diferencia y
`apply(prev, delta)` la reproduce. Un test de convergencia demuestra que una cadena de deltas plegada por el
cliente reproduce _exactamente_ el snapshot del servidor — y como cliente y servidor comparten el mismo
código Rust, no pueden divergir. ¿Se perdió un frame? El cliente lo nota (`ResyncRequired`) y pide un
snapshot completo.

**Paso 6 — Area of Interest recorta O(N²) a lineal.** Un jugador no necesita los 10 000 — solo sus vecinos
espaciales y diplomáticos. `AreaOfInterest` + `filter_frame` recortan cada delta a lo que un suscriptor de
verdad le importa, mientras se preserva el contexto a nivel de colonia (chat, comercios). La difusión
cuadrática se vuelve lineal: el tráfico crece como N × (tamaño del área de interés), no como N².

**Paso 7 — la última milla honesta.** Esta es la parte más importante de la historia. Una prueba de carga
reveló que el bonito número de «320 Mbit/s» descansaba sobre una **suposición fija** de 32 bytes por delta de
jugador. Conecté el codec binario real y medí: un delta real era de **104 bytes**, porque enviaba el perfil
_entero_ de un jugador (11 campos de recursos) aunque cambiara un solo campo. 104 frente a 32 es **3.25× de
más** — una proyección de ~1.04 Gbit/s con 10k. La respuesta honesta en ese momento era: **«no, con tráfico
real no aguanto 10k».**

La solución fue un **delta por campo**, `WirePlayerDelta`:

```
player_id (4 bytes) + máscara de bits de campos cambiados (2 bytes) + SOLO los valores cambiados
```

Si cambiaron population, food y science, el cable lleva exactamente esos tres más la máscara —
`4 + 2 + 4 + 8 + 8 = 26 bytes` en lugar de 104. La máscara de 16 bits dice qué campos siguen; un campo sin
cambios no cuesta nada.

## Cómo está demostrado — y por qué el número es de fiar

Arquitectura sin prueba es una promesa. Lo demostré con números: tests unitarios de round-trip y convergencia,
tests de contrato en cada puerto, y **42 tests e2e en Chromium real sobre WebTransport** (incluyendo «recibe
tics del servidor por un socket en tiempo real sin polling» y «dos jugadores en un mundo se sincronizan en
tiempo real»).

La prueba estrella es un test de enjambre de 10 000 bots que mide el egress con el **codec binario real** —
sin constantes fijas. Construye 10 000 clientes bot en un mundo, ejecuta un tic autoritativo real, aplica los
deltas visibles de cada bot a través del filtrado AoI real, y suma los bytes medidos.

La cifra está defendida por un gate `check:load`: se verifica un informe en cada ejecución, y la proyección se
deriva **del valor medido, no de una suposición**. Si alguien hincha por accidente el formato del delta, el
gate falla.

## La frontera honesta

La madurez arquitectónica no es solo conseguir un buen número — es ser honesto sobre sus límites.

**Demostrado:** por cómputo y latencia, el fan-out a 10 000 bots se mantiene dentro de 200 ms con mucho
margen; por egress, 237.6 Mbit/s (24% de un presupuesto de 1 Gbit/s), medido con el codec real, no supuesto.

**Aún no demostrado:** esto es una proyección a partir de un tic medido con 10k (bots en un proceso), no un
clúster en vivo de 10 000 sockets QUIC reales bajo carga durante horas. El comportamiento sostenido de CPU/RAM
bajo un flujo de varios minutos, y el canal AoI por suscriptor para el cliente WASM grueso, son la siguiente
capa de trabajo. Pero el **mayor riesgo — el egress, que yo mismo marqué como por encima del presupuesto en el
formato real — está cerrado y medido.**

## Qué es de arquitecto aquí, no solo de desarrollador

1. **Nombré el verdadero cuello de botella:** egress, no CPU. Todo el stack sirve a esa única conclusión. Falla
   ahí y optimizas lo que no debes.
2. **Separé lecturas de escrituras:** single-writer para la consistencia + proyecciones lock-free en RAM para
   las lecturas. Maté la contención sin sacrificar la corrección.
3. **Reduje O(N²) a lineal** mediante Area of Interest — sin ello, 10k es imposible en principio.
4. **Deltas en vez de snapshots,** con un solo código base en servidor y cliente, así que la convergencia está
   garantizada por la arquitectura, no por suerte.
5. **No confié en el número bonito hasta medirlo.** Descubrí que «32 bytes» era una suposición, medí los 104
   reales, diseñé el delta por campo, lo llevé a 26, y **lo demostré midiendo.**
6. **Blindé el resultado tras un gate** para que una regresión no se cuele sin que nadie lo note.

Un motor que **de forma medida** sirve a 10 000 jugadores en un mundo al 24% de su presupuesto de tráfico no es
suerte ni un solo truco. Es una secuencia de decisiones arquitectónicas, cada una respondiendo a un muro
concreto, cada una respaldada por un número.

## Snapshot del stack

| Capa                 | Elección                                      | Muro que resuelve                                     |
| -------------------- | --------------------------------------------- | ----------------------------------------------------- |
| Núcleo de simulación | Rust, determinista, compilado a nativo + WASM | cliente grueso, más jugadores/servidor (n.º 2, n.º 4) |
| Transporte           | WebTransport / QUIC, fallback WebSocket       | flujo binario de baja latencia (n.º 2)                |
| Modelo de mundo      | Single-writer + event sourcing                | sin consenso, recuperable (n.º 3)                     |
| Lecturas             | Proyecciones read-model lock-free en RAM      | lecturas sub-segundo sin contención                   |
| Actualizaciones      | Deltas binarios por campo + Area of Interest  | O(N²) → lineal, egress mínimo (n.º 1, n.º 2)          |
| Hosting              | Hetzner bare-metal                            | egress 20–40× más barato que los hyperscalers (n.º 2) |

Helix Empire está en alfa, hecho en solitario — diseño de producto, núcleo de simulación en Rust, cliente WASM,
transporte, la separación lectura/escritura, el pipeline de deltas + AoI, el harness de carga y la prueba
medida. Pronto se podrá jugar en **helixempire.com**.
