Same Seed, Different Output — Then a Hierarchy to Keep It That Way
Last week’s fix made individual work items deterministic by keying seeds on item position instead of thread identity. But it only solved the local problem. Each call to rng_for_index(i) still needs a base seed, and the simulation has structure above that — 20 simulated years, 12+ phases per year, multiple steps per phase, all running in parallel. Every seed needs to be a pure function of where you are in the simulation, not what’s running it. That meant building a seed hierarchy. Four levels.
The two core utilities live in Rand.gd:
| |
Every level of the hierarchy feeds through splitmix64. At the top, a session seed fans out across years:
| |
Each of the 20 years gets an independent starting point — session seed XOR’d with the year number. Different base seed, different world. Different year, different slice of the same world.
Below that, each year seed fans out to phases and steps. The actual function is _step_rng in ClassGenerator.gd:
| |
Each phase — hs_generation, college_recruiting, nfl_draft, nfl_season, eight others — gets its seed by hashing the label with FNV1a-64 and XOR-ing with the year seed. Steps within a phase use compound labels like "hs_generation:hs_player_gen". Two steps sharing a parent produce different seeds because the labels differ. No magic numbers.
FNV1a-64 instead of something fancier: it’s one loop over the UTF-8 bytes, it’s fast, and it distributes strings well enough that "nfl_draft" and "nfl_free_agency" don’t collide. That’s all it needs to do.
That covers three levels — session, year, phase/step. The fourth is the thread boundary. Every ThreadPool callable receives an explicit seed and returns an explicit output dict. No callable reads or writes shared state. Pure fan-out/fan-in. Thread count becomes a deployment parameter — the simulation produces identical output with one thread or eight.
The hierarchy also makes debugging surgical. If a player’s stats look wrong in year 14, nfl_season phase, I can re-run that phase with its exact seed and inspect in isolation. SimLogger.step_seed logs the seed at each level — year, phase, step — so the full lineage from session seed to a specific RNG call is recoverable after the fact.