hrxdev
Español

#1 Un núcleo determinista, garantizado por el compilador

· Nick

Todavía no hay nada que ver, así que esta primera entrada trata de la parte que nadie va a ver: el núcleo de la simulación. El juego pretende mover hasta 1.000 reclusos y empleados a 20 ticks por segundo y explicar por qué cada uno hizo lo que hizo. Eso solo funciona si las mismas entradas dan siempre el mismo mundo. Decidí que fuera una propiedad que la máquina comprueba, desde el primer día.

Dónde estamos

Estoy en el primer hito, el esqueleto del núcleo. La simulación es una biblioteca .NET sin código de Godot, y el cliente de Godot 4 por ahora abre una escena 3D vacía. No hay modelo de mundo, ni baldosas, ni personajes, ni partidas guardadas. Los 1.000 personajes son un objetivo; no he medido nada a esa escala.

El tick

El tiempo en el núcleo es un número entero de ticks, 20 por segundo. No hay tiempo en coma flotante. Un tick tiene tres fases: aplicar los comandos externos, ejecutar los sistemas en un orden escrito en un único lugar, y confirmar los cambios diferidos y avanzar el contador. Si se escapa un error de un tick, el mundo se marca como defectuoso y se detiene. Hoy la lista de sistemas está vacía; el bucle existe para que el primer sistema tenga dónde ir.

Comandos y un registro

El mundo cambia solo mediante comandos. Un comando es un registro pequeño con un identificador de tipo estable y una codificación binaria escrita a mano. Con cada uno, el mundo lo codifica, le pone el tick y un número de secuencia, lo valida (solo lectura), lo escribe en el registro y solo entonces lo aplica. Los comandos rechazados también se registran, lo que debería ayudar con los informes de errores. La semilla más el registro deben reproducir una partida exactamente. El formato del archivo de repetición aún no está escrito.

Números aleatorios

Escribí yo mismo los generadores de números aleatorios (SplitMix64 y xoshiro256**) y prohibí el Random de la biblioteca estándar en el núcleo. El mundo tiene cuatro flujos con nombre (comandos, necesidades, IA, eventos), derivados de la semilla y guardados en el estado del mundo, así que se guardan y entran en el hash. Para el trabajo en el que el orden no debe importar hay un generador sin estado con clave, basado en dominio, entidad y tick. Las salidas para la semilla 42 están fijadas, porque eso es en la práctica el formato de repetición: si cambias el generador, todas las repeticiones antiguas dejan de funcionar.

Un hash del mundo

Un pequeño hasher sensible al orden combina la semilla, el tick, los flujos aleatorios y un resumen del registro de comandos en 64 bits. Dos ejecuciones que deberían ser idénticas se pueden comparar con un solo número. Ahora mismo solo cubre esas partes, porque el mundo no tiene nada más. Comparar hashes cada N ticks durante una partida es el siguiente paso.

Convertir el no determinismo en un error de compilación

No quise depender de acordarme de las reglas. El núcleo se compila con analizadores de API prohibidas: la hora del reloj, los números aleatorios sin semilla, los GUID aleatorios, los hashes de cadenas aleatorizados, los hilos y las utilidades paralelas, SIMD y las API de cadenas que dependen de la cultura hacen fallar la compilación. Otras comprobaciones verifican que no hay estado estático mutable y examinan el código compilado en busca de cualquier cosa que se haya colado. Mi equipo usa la configuración regional rusa, con coma decimal, y eso ya es motivo suficiente para prohibir las API que dependen de la cultura.

Luego fui más allá y prohibí en el núcleo las colecciones hash estándar (Dictionary, HashSet y compañía), porque su orden de iteración es una fuente clásica de desviaciones. El núcleo dispone de arrays, listas y dos pequeñas colecciones propias con un orden documentado. No es hermético: una API que devuelva una colección hash a través de una interfaz genérica aún puede colarse, y parte de la protección solo funciona en las pruebas.

Cómo se construye

Las herramientas alrededor del código también están automatizadas, y los mismos scripts funcionan en Windows y macOS. El enfoque general está en Cómo se construye.

Lo siguiente

Quedan pendientes en este hito: el modelo de mundo con plantas, los archivos de guardado y de repetición, la prueba de determinismo y un ejecutor de escenarios sin interfaz con una línea base de benchmark. Después viene la vista de construcción en Godot. Consulta la hoja de ruta.

← Todas las entradas del devlog