60 lines
3.2 KiB
Markdown
60 lines
3.2 KiB
Markdown
# 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
|
||
|
||
```cpp
|
||
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_state` round trip, `left` positioning, malformed-blob rejection.
|
||
|
||
## Choices that still need binary confirmation
|
||
|
||
1. **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 rounds `y >= 0xFFFFFF80` up to exactly `1.0f`, so the range is `[0, 1]`
|
||
in practice. Confirm against a captured sequence before relying on the exact bits.
|
||
2. **Twist timing at the block boundary.** We twist lazily when `left` reaches 0 (so a saved
|
||
state may carry `left == 0`). If the original twists eagerly after the last word of a block
|
||
(`left` then 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.
|
||
3. **`next_int(n)`** — mask = smallest `2^k - 1 >= n - 1`, reject while `r >= n`. The rejection
|
||
scheme matches the RE description; the exact mask computation is unconfirmed.
|