hrxdev

#1 A deterministic core, enforced by the compiler

· Nick

There is nothing to look at yet, so this first entry is about the part nobody will see: the simulation core. The game is meant to run up to 1,000 inmates and staff at 20 ticks per second and to explain why anyone did anything. That only works if the same inputs always give the same world. I decided to make that a property the machine checks, from day one.

Where things stand

I am in the first milestone, the core skeleton. The solution is set up: a plain .NET library for the simulation with no Godot references, an xUnit test project, and a Godot 4 .NET client that so far opens an empty 3D scene. There is no world model, no tiles, no characters, no saves. The 1,000-character budget is a target; I have not measured anything at that scale. At the time of writing the full test run has 281 passing tests.

The tick

Time in the core is an integer tick count, 20 per second. There is no floating-point time. A tick runs three phases: apply outside commands, run the systems in an order written down in one place in code, then commit deferred changes and advance the counter. If an exception escapes a tick, the world is marked faulted and stops. Today the list of systems is empty; the loop exists so that the first system has a place to go.

Commands and a log

The world changes only through commands. A command is a record with a stable type id and a hand-written binary encoding. For each one the world serializes it, stamps it with the tick and a sequence number, validates it (read-only), writes it to the log, and only then applies it. Rejected commands are logged as well, which should help with bug reports. Seed plus log is meant to reproduce a run exactly. The replay file format is not written yet.

Random numbers

I wrote the generators myself (SplitMix64 and xoshiro256**) and banned System.Random in the core. The world has four named streams (commands, needs, AI, events), derived from the seed and stored in the world state, so they are saved and hashed. For work where order must not matter there is a stateless keyed generator based on domain, entity and tick. The outputs for seed 42 are pinned by a test, because that is effectively the replay format.

A hash of the world

A small order-sensitive hasher folds the seed, the tick, the random streams and a digest of the command log into 64 bits. Two runs that should be identical can be compared with one number. Right now it covers only those parts, because the world has nothing else in it. A test that compares hashes every N ticks is still on the list.

Making nondeterminism a build error

I did not want to rely on remembering the rules. The core is built with banned-API analyzers: wall-clock time, Random, random GUIDs, randomized string hashes, threads and parallel helpers, SIMD, and culture-dependent string APIs all fail the build. A test checks by reflection that there is no mutable static state, and another scans the compiled code to catch anything slipped past with a pragma. My machine uses a Russian locale with a decimal comma, which is a good reason to ban culture-dependent APIs.

Then I went further and banned the standard hash collections (Dictionary, HashSet and friends) from the core, since their iteration order is a classic source of drift. The core gets arrays, lists and two small collections of its own with a documented order. Closing the loopholes took two rounds of review, each hole confirmed with a canary test first. It is not airtight: an API that hands back a hash collection through IEnumerable can still slip through, and part of the protection works only in tests.

Process, and a slip

Two chores went into the tooling. Hooks now stop the main Claude Code session from editing code and keep the reviewer read-only; I wrote that rule after the first task, when the main session did the work itself instead of delegating. The scripts and hooks moved to PowerShell 7 so the same files run on Windows and macOS, and the fast check passed live on a Mac with 182 quick tests. Details are on How it’s built.

Next

Still open in this milestone: the world model with floors, save and replay files, the determinism test, and a headless scenario runner with a benchmark baseline. After that comes the Godot building view. See the roadmap.

← All devlog entries