From 3c2757600a35efe3ba32589f56826e443d057ce3 Mon Sep 17 00:00:00 2001 From: Daniel McCoy Stephenson Date: Fri, 30 Oct 2026 21:29:37 -0600 Subject: [PATCH] Pin mix64, Rng and valueNoise in the determinism test Every generated world and every tick draws from rng.h, but nothing tested it directly: a change there only showed up as a moved fingerprint. The new section pins mix64 against the published SplitMix64 reference outputs, pins the first Rng steps (output and saved state), checks that restoring the state resumes the sequence, that range() is inclusive and below() stays in bounds, and pins valueNoise on the integer lattice (including negative cells) with its midpoint blend, continuity and [0, 1) range. Co-Authored-By: Claude Opus 5.5 --- tests/test_determinism.cpp | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_determinism.cpp b/tests/test_determinism.cpp index 896abc2..81d2040 100644 --- a/tests/test_determinism.cpp +++ b/tests/test_determinism.cpp @@ -3,6 +3,7 @@ // its derived caches rebuilt from scratch (as a load does), must keep // simulating identically to the original. #include +#include #include #include #include @@ -10,6 +11,7 @@ #include "check.h" #include "colony/generate.h" +#include "colony/rng.h" #include "colony/schema.h" #include "colony/systems.h" @@ -156,6 +158,54 @@ int main() { for (const Edit& e : globals) expect(e); for (const Edit& e : other) expect(e); } + SECTION("rng: mix64, Rng and valueNoise are pinned and in range"); + { + // Every world and every tick draws from these, so a change here moves + // all saves' futures. mix64 is the SplitMix64 finaliser: these are the + // published reference outputs for SplitMix64 seeded with 0. + CHECK(mix64(0) == UINT64_C(0xe220a8397b1dcdaf)); + CHECK(mix64(UINT64_C(0x9e3779b97f4a7c15)) == UINT64_C(0x6e789e6aa1b965f4)); + + // The whole Rng state is one uint64_t (saved as Globals::rng): an LCG + // step, with mix64 applied to the output only. + Rng r{12345}; + CHECK(r.next() == UINT64_C(0xe28de178a35c2c42) && r.state == UINT64_C(0x1c0d57f10c894254)); + CHECK(r.next() == UINT64_C(0x68c46d70c61c1092) && r.state == UINT64_C(0x43f04a6ece53d613)); + CHECK(r.next() == UINT64_C(0x566ad4001df2b13e) && r.state == UINT64_C(0xe2b8410510668fa6)); + Rng copy{r.state}; + CHECK(copy.next() == r.next()); // restoring the state resumes the same sequence + + bool lo = false, hi = false, inRange = true, belowOk = true; + for (int i = 0; i < 2000; ++i) { + const int v = r.range(-3, 3); + inRange = inRange && v >= -3 && v <= 3; + lo = lo || v == -3; + hi = hi || v == 3; + const int b = r.below(7); + belowOk = belowOk && b >= 0 && b < 7; + belowOk = belowOk && r.below(1) == 0; + } + CHECK_MSG(inRange && lo && hi, "range() is inclusive at both ends"); + CHECK(belowOk); + + // On the integer lattice valueNoise is exactly the corner hash (53 bits + // scaled into [0, 1)), including negative cells; in between it blends. + const double scale = 9007199254740992.0; // 2^53 + CHECK(valueNoise(42, 3.0, 5.0) * scale == 8412913687174903.0); + CHECK(valueNoise(42, 4.0, 5.0) * scale == 5182047289156008.0); + CHECK(valueNoise(42, -2.0, -7.0) * scale == 6492508512279940.0); + CHECK(valueNoise(43, 3.0, 5.0) * scale == 2608104317372823.0); + const double c0 = valueNoise(42, 3.0, 5.0), c1 = valueNoise(42, 4.0, 5.0); + CHECK(valueNoise(42, 3.5, 5.0) == c0 + (c1 - c0) * 0.5); // smoothstep(0.5) == 0.5 + CHECK(std::fabs(valueNoise(42, 4.0 - 1e-9, 5.0) - c1) < 1e-6); // continuous across a cell edge + bool unit = true; + for (int y = -20; y < 20; ++y) + for (int x = -20; x < 20; ++x) { + const double v = valueNoise(7, x * 0.37, y * 0.53); + unit = unit && v >= 0.0 && v < 1.0; + } + CHECK_MSG(unit, "valueNoise stays in [0, 1)"); + } SECTION("pinned fingerprint: seed 42, 64x64, 30 settlers, 1000 ticks"); { // Pins the rules' exact output on every compiler and platform CI runs.