hrxdev
Italiano

#1 Un nucleo deterministico, garantito dal compilatore

· Nick

Non c’è ancora niente da guardare, quindi questa prima voce parla della parte che nessuno vedrà: il nucleo della simulazione. Il gioco dovrà far girare fino a 1.000 detenuti e dipendenti a 20 tick al secondo e spiegare perché ciascuno ha fatto ciò che ha fatto. Questo funziona solo se gli stessi input producono sempre lo stesso mondo. Ho deciso di renderlo una proprietà verificata dalla macchina, fin dal primo giorno.

A che punto siamo

Sono nella prima tappa, lo scheletro del nucleo. La simulazione è una semplice libreria .NET senza codice Godot, e il client Godot 4 per ora apre solo una scena 3D vuota. Non ci sono un modello del mondo, né tessere, né personaggi, né salvataggi. I 1.000 personaggi sono un obiettivo; non ho misurato nulla a quella scala.

Il tick

Il tempo nel nucleo è un conteggio intero di tick, 20 al secondo. Non esiste un tempo in virgola mobile. Un tick ha tre fasi: applicare i comandi esterni, eseguire i sistemi in un ordine scritto in un unico punto, e confermare le modifiche differite e far avanzare il contatore. Se un errore sfugge da un tick, il mondo viene segnato come guasto e si ferma. Oggi l’elenco dei sistemi è vuoto; il ciclo esiste perché il primo sistema abbia un posto dove andare.

Comandi e un registro

Il mondo cambia solo tramite comandi. Un comando è un piccolo record con un identificatore di tipo stabile e una codifica binaria scritta a mano. Per ognuno, il mondo lo codifica, lo marca con il tick e un numero di sequenza, lo convalida (in sola lettura), lo scrive nel registro e solo allora lo applica. Anche i comandi rifiutati vengono registrati, il che dovrebbe aiutare con le segnalazioni di bug. Seed più registro dovrebbero riprodurre esattamente una partita. Il formato del file di replay non è ancora stato scritto.

Numeri casuali

Ho scritto io stesso i generatori di numeri casuali (SplitMix64 e xoshiro256**) e ho vietato nel nucleo il Random della libreria standard. Il mondo ha quattro flussi con nome (comandi, bisogni, IA, eventi), derivati dal seed e conservati nello stato del mondo, quindi vengono salvati e finiscono nell’hash. Per il lavoro in cui l’ordine non deve contare c’è un generatore senza stato e con chiave, basato su dominio, entità e tick. Le uscite per il seed 42 sono fissate, perché di fatto sono il formato del replay: cambia il generatore e ogni vecchio replay si rompe.

Un hash del mondo

Un piccolo hasher sensibile all’ordine fonde in 64 bit il seed, il tick, i flussi casuali e un riassunto del registro dei comandi. Due esecuzioni che dovrebbero essere identiche si possono confrontare con un solo numero. Per ora copre solo queste parti, perché il mondo non contiene altro. Confrontare gli hash ogni N tick durante una partita è il passo successivo.

Rendere il non determinismo un errore di build

Non volevo affidarmi alla memoria delle regole. Il nucleo viene compilato con analizzatori di API vietate: l’ora dell’orologio, i numeri casuali senza seed, i GUID casuali, gli hash di stringa randomizzati, i thread e le utilità parallele, la SIMD e le API di stringa dipendenti dalla cultura fanno fallire la build. Altri controlli verificano che non ci sia stato statico mutabile e scansionano il codice compilato alla ricerca di qualsiasi cosa sia sfuggita. La mia macchina usa le impostazioni regionali russe, con la virgola decimale, che è un buon motivo per vietare le API dipendenti dalla cultura.

Poi sono andato oltre e ho vietato nel nucleo le collezioni hash standard (Dictionary, HashSet e simili), perché il loro ordine di iterazione è una classica fonte di deriva. Il nucleo dispone di array, liste e due piccole collezioni proprie con un ordine documentato. Non è a tenuta stagna: un’API che restituisce una collezione hash attraverso un’interfaccia generica può ancora passare, e parte della protezione funziona solo nei test.

Come è fatto

Anche gli strumenti attorno al codice sono automatizzati, e gli stessi script girano su Windows e macOS. L’approccio generale è descritto in Come è fatto.

Prossimi passi

Ancora aperti in questa tappa: il modello del mondo con più piani, i file di salvataggio e di replay, il test di determinismo e un runner di scenari senza interfaccia con una baseline di benchmark. Dopo arriva la vista di costruzione in Godot. Vedi la roadmap.

← Tutte le voci del devlog