From 0d3bfa11cfdbafe529a50b02196ac22e009f51c5 Mon Sep 17 00:00:00 2001 From: Jeff Quast Date: Mon, 28 Sep 2026 18:45:55 +0000 Subject: [PATCH] Seed the RNG in simInit and load a city once initWillStuff() called randomlySeedRandom(), and every path to it runs doSimInit(), whose map scans draw random numbers and write the results into map tiles, so initWillStuff() overwrote a seed the caller had set with seedRandom() before that seed could take effect. Seed in simInit(), where a fresh simulation gets a random world, and leave reproducible runs to callers. Call seedRandom() before loadCity(). loadFileData() runs doSimInit(), whose map scans draw from the same stream, so a seed set afterwards finds the loaded city on the clock-seeded stream. Measured both ways: seeding after loadCity gives a different map on every run, seeding before gives the same map every time. loadCity() also called doSimInit() after loadFileData() had already run it, so a loaded city came back one initialization step past the file. A save and reload changes about 240 map tiles, and the change accumulates over repeated round trips, because loadFileData() runs doSimInit() and that call is kept here. --- .../src/lib/cityLoadDeterminism.test.ts | 36 +++++++++++++++++++ packages/micropolis-engine/src/fileio.cpp | 2 -- packages/micropolis-engine/src/initialize.cpp | 1 - packages/micropolis-engine/src/micropolis.cpp | 1 + 4 files changed, 37 insertions(+), 3 deletions(-) create mode 100644 apps/micropolis/src/lib/cityLoadDeterminism.test.ts diff --git a/apps/micropolis/src/lib/cityLoadDeterminism.test.ts b/apps/micropolis/src/lib/cityLoadDeterminism.test.ts new file mode 100644 index 00000000..71f9c56c --- /dev/null +++ b/apps/micropolis/src/lib/cityLoadDeterminism.test.ts @@ -0,0 +1,36 @@ +import { describe, it, expect } from 'vitest'; +import { loadMicropolisMainModule } from './wasm/node'; +import { createNoopJsCallback } from './wasm/callbacks'; +import { createMapMopViews } from './wasm/views'; +import type { MainModule, Micropolis } from '../types/micropolisengine.d.js'; + +const CITY = '/cities/kobe.cty'; + +const loadWithSeed = (engine: MainModule, seed: number): Micropolis => { + const micropolis = new engine.Micropolis(); + micropolis.setCallback(createNoopJsCallback(engine), {}); + micropolis.init(); + micropolis.seedRandom(seed); + expect(micropolis.loadCity(CITY)).toBe(true); + return micropolis; +}; + +const mapHash = (engine: MainModule, micropolis: Micropolis): number => { + let hash = 0; + for (const tile of createMapMopViews(engine, micropolis)!.mapData) hash = (Math.imul(hash, 31) + tile) | 0; + return hash >>> 0; +}; + +describe('city load determinism', () => { + it('same seed gives the same map, a different seed gives another', async () => { + const engine = await loadMicropolisMainModule(); + const first = loadWithSeed(engine, 42); + const second = loadWithSeed(engine, 42); + const other = loadWithSeed(engine, 7); + + expect(mapHash(engine, second)).toBe(mapHash(engine, first)); + expect(mapHash(engine, other)).not.toBe(mapHash(engine, first)); + + for (const micropolis of [first, second, other]) micropolis.delete(); + }); +}); diff --git a/packages/micropolis-engine/src/fileio.cpp b/packages/micropolis-engine/src/fileio.cpp index 30e91a04..94cea582 100644 --- a/packages/micropolis-engine/src/fileio.cpp +++ b/packages/micropolis-engine/src/fileio.cpp @@ -592,8 +592,6 @@ bool Micropolis::loadCity(const std::string &filename) std::string newCityName = cityFileName.substr(pos, last - pos); setCityName(newCityName); - doSimInit(); - didLoadCity(filename); return true; diff --git a/packages/micropolis-engine/src/initialize.cpp b/packages/micropolis-engine/src/initialize.cpp index 9ff81d07..e7b50071 100644 --- a/packages/micropolis-engine/src/initialize.cpp +++ b/packages/micropolis-engine/src/initialize.cpp @@ -84,7 +84,6 @@ /** Reset many game state variables */ void Micropolis::initWillStuff() { - randomlySeedRandom(); initGraphMax(); destroyAllSprites(); diff --git a/packages/micropolis-engine/src/micropolis.cpp b/packages/micropolis-engine/src/micropolis.cpp index 5230c74e..ebf955b3 100644 --- a/packages/micropolis-engine/src/micropolis.cpp +++ b/packages/micropolis-engine/src/micropolis.cpp @@ -738,6 +738,7 @@ void Micropolis::simInit() resetMapState(); resetEditorState(); clearMap(); + randomlySeedRandom(); initWillStuff(); setFunds(5000); setGameLevelFunds(LEVEL_EASY);