3.2 KiB
mars::rng — the engine PRNG
src/mars/rng/mt19937.h — a textbook 32-bit Mersenne Twister (MT19937), the generator the
strategy simulation draws every roll from (map generation, research, encounters, raids, …).
Because all lockstep peers share one seeded stream, the reimplementation has to be bit-exact
and consume words in the same order; the save file carries the generator state verbatim.
API
mars::rng::MT19937 r(seed); // seed(): Knuth initializer, then one twist (left == 624)
uint32_t y = r.next_u32(); // tempered output
float f = r.next_float(); // (float)(y * 2^-32), see below
uint32_t k = r.next_int(n); // uniform [0, n): power-of-two mask + rejection
r.load_state(mt, left); // or load_state(blob, 0x9c4) from a save's "RNG" item
r.save_state(out); // mt[624] + left, 0x9c4 bytes little-endian
r.left(); r.index(); r.state();
State model
| member | meaning |
|---|---|
mt[624] |
the untempered state block |
left |
words still unread in the current block; next output is mt[624 - left] |
next_u32() twists when left is 0, hands out mt[624 - left], decrements left, and
tempers. A freshly seeded generator has already twisted once, so left == 624 and the
first draw is mt[0].
Serialized form (the save's RNG frame)
Sim → RNG { "." raw[2503] }: 624 × uint32 (mt) followed by one int32 (left) = 0x9c4 =
2500 bytes, plus the 3 joint-padding bytes of the item. MT19937::load_state(blob, n) parses
it and rejects left outside 0..624; save_state writes the same layout.
Verified on the real saves (tests/mars_stream/test_save.cpp): the 624-word block in all
three saves equals seed(CreateParams.RSeed) followed by exactly two whole twists, and left
decreases turn over turn (454 → 432 → 413, i.e. ~20 draws per turn). That confirms the
initializer, the twist, the seed source (RSeed) and the blob layout. It does not exercise
the tempering or the float mapping (those never touch the saved state).
Reference vectors (tests/mars_stream/test_rng.cpp)
- seed 5489 → 3499211612, 581869302, 3890346734, … ; the 10000th output is 4123659995.
save_state/load_stateround trip,leftpositioning, malformed-blob rejection.
Choices that still need binary confirmation
- Float mapping.
next_float()returns(float)((double)y * 2^-32). This is the mapping recorded in the RE notes for the engine's float roll (product in double, then narrowed). Note the narrowing roundsy >= 0xFFFFFF80up to exactly1.0f, so the range is[0, 1]in practice. Confirm against a captured sequence before relying on the exact bits. - Twist timing at the block boundary. We twist lazily when
leftreaches 0 (so a saved state may carryleft == 0). If the original twists eagerly after the last word of a block (leftthen never saved as 0, block already advanced), the output sequence is identical but the saved blob at that one boundary differs. The three saves (left= 454/432/413) do not distinguish the two. next_int(n)— mask = smallest2^k - 1 >= n - 1, reject whiler >= n. The rejection scheme matches the RE description; the exact mask computation is unconfirmed.