sots-engine/docs/mars-rng.md

60 lines
3.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.