#1 Un noyau déterministe, garanti par le compilateur
· Nick
Il n’y a encore rien à regarder, alors cette première entrée parle de la partie que personne ne verra : le noyau de simulation. Le jeu doit faire tourner jusqu’à 1 000 détenus et membres du personnel à 20 ticks par seconde, et expliquer pourquoi chacun a fait ce qu’il a fait. Cela ne fonctionne que si les mêmes entrées donnent toujours le même monde. J’ai décidé d’en faire dès le premier jour une propriété vérifiée par la machine.
Où en sont les choses
Je suis dans le premier jalon, le squelette du noyau. La simulation est une simple bibliothèque .NET sans aucun code Godot, et le client Godot 4 ouvre pour l’instant une scène 3D vide. Il n’y a ni modèle du monde, ni cases, ni personnages, ni sauvegardes. Le budget de 1 000 personnages est un objectif ; je n’ai rien mesuré à cette échelle.
Le tick
Le temps dans le noyau est un nombre entier de ticks, 20 par seconde. Il n’y a pas de temps en virgule flottante. Un tick comporte trois phases : appliquer les commandes venues de l’extérieur, exécuter les systèmes dans un ordre écrit à un seul endroit, puis valider les changements différés et avancer le compteur. Si une erreur s’échappe d’un tick, le monde est marqué comme défaillant et s’arrête. Aujourd’hui la liste des systèmes est vide ; la boucle existe pour que le premier système ait une place.
Les commandes et un journal
Le monde ne change que par des commandes. Une commande est un petit enregistrement avec un identifiant de type stable et un encodage binaire écrit à la main. Pour chacune, le monde l’encode, l’estampille avec le tick et un numéro de séquence, la valide (en lecture seule), l’écrit dans le journal, et seulement ensuite l’applique. Les commandes rejetées sont aussi journalisées, ce qui devrait aider pour les rapports de bugs. Graine plus journal doivent permettre de reproduire exactement une partie. Le format de fichier de replay n’est pas encore écrit.
Les nombres aléatoires
J’ai écrit moi-même les générateurs de nombres aléatoires (SplitMix64 et xoshiro256**) et banni Random de la bibliothèque standard dans le noyau. Le monde a quatre flux nommés (commandes, besoins, IA, événements), dérivés de la graine et stockés dans l’état du monde, donc sauvegardés et inclus dans le hash. Pour les traitements où l’ordre ne doit pas compter, il existe un générateur sans état à clé, fondé sur le domaine, l’entité et le tick. Les sorties pour la graine 42 sont figées, car c’est en pratique le format des replays : changez le générateur et tous les anciens replays cassent.
Un hash du monde
Un petit hasheur sensible à l’ordre replie la graine, le tick, les flux aléatoires et un condensé du journal de commandes en 64 bits. Deux parties censées être identiques se comparent avec un seul nombre. Pour l’instant il ne couvre que ces éléments, parce que le monde n’en contient pas d’autres. L’étape suivante est de comparer les hashs tous les N ticks pendant une partie.
Faire du non-déterminisme une erreur de build
Je ne voulais pas compter sur ma mémoire des règles. Le noyau est compilé avec des analyseurs d’API interdites : l’heure système, les nombres aléatoires sans graine, les GUID aléatoires, les hashs de chaînes randomisés, les threads et les aides au parallélisme, le SIMD et les API de chaînes dépendantes de la culture font tous échouer le build. D’autres contrôles vérifient l’absence d’état statique modifiable et analysent le code compilé à la recherche de tout ce qui aurait glissé. Ma machine utilise une locale russe avec la virgule décimale, ce qui est une bonne raison de bannir les API dépendantes de la culture.
Puis je suis allé plus loin et j’ai banni du noyau les collections de hash standard (Dictionary, HashSet et leurs semblables), puisque leur ordre d’itération est une source classique de dérive. Le noyau reçoit des tableaux, des listes et deux petites collections maison à l’ordre documenté. Ce n’est pas étanche : une API qui renvoie une collection de hash derrière une interface générique peut encore passer, et une partie de la protection ne fonctionne que dans les tests.
Comment c’est construit
L’outillage autour du code est lui aussi automatisé, et les mêmes scripts tournent sous Windows et macOS. L’approche générale est décrite sur Comment c’est fait.
La suite
Restent à faire dans ce jalon : le modèle du monde avec des étages, les fichiers de sauvegarde et de replay, le test de déterminisme, et un lanceur de scénarios sans interface avec une référence de banc d’essai. Viendra ensuite la vue de construction Godot. Voir la feuille de route.