[Minor Feature] Per-call RNG sites still generate an unrecorded seed when seed=None (procgen, sfxr) #408

Open
opened 2026-07-27 15:38:33 +00:00 by john · 0 comments
Owner

Follow-up to #394, which fixed the two RNGs that had no seed parameter at all
(EntityBehavior.cpp, PySound::py_play_varied). It deliberately did not touch the sites that
already accept an explicit seed, because those are a different shape and a different fix.

Those sites are still not replayable when the caller passes nothing, which is the common case:

site behaviour when seed=None
PySoundBuffer::sfxr std::random_device rd; rng.seed(rd()) — generated, not recorded, not readable back
PySoundBuffer::sfxr_mutate same
PyBSP::split_recursive passes nullptr to TCOD_bsp_split_recursive, i.e. libtcod's global default RNG — not recordable at all from Python
PyHeightMap (CreateTCODRandom) same nullptr → libtcod's global default RNG, used by add_hill, dig_hill, mid_point_displacement etc.

PyNoiseSource is the counter-example and the model: it generates a seed when given none, records
it
, and exposes it as a read-only seed property.

Two consequences worth separating:

  1. Reproducibility. A dungeon generated with bsp.split_recursive(depth=5) cannot be regenerated,
    even in principle, because the seed it used is not recoverable. Same for a procedural sound.
  2. Global state. The libtcod default RNG is process-global, so two independent generators
    interleave and neither is reproducible on its own even if you did know the seed.

Separately and in the same family: src/audio/SfxrSynth.cpp sfxr_synthesize() calls
std::srand(42) unconditionally and draws with std::rand(). That makes sfxr itself deterministic,
but it clobbers the process-global C rand() state as a side effect of synthesising a sound —
any other consumer of rand() in the process is silently reset.

Suggested shape, matching PyNoiseSource and #394: when seed=None, draw the seed from the engine's
seeded stream (mcrf::EngineRandom) rather than from std::random_device/libtcod's global, and
record it on the resulting object so it can be read back. That makes the whole engine replayable from
one mcrfpy.set_random_seed() without changing any call site.

Follow-up to #394, which fixed the two RNGs that had *no* seed parameter at all (`EntityBehavior.cpp`, `PySound::py_play_varied`). It deliberately did **not** touch the sites that already accept an explicit seed, because those are a different shape and a different fix. Those sites are still not replayable when the caller passes nothing, which is the common case: | site | behaviour when `seed=None` | |---|---| | `PySoundBuffer::sfxr` | `std::random_device rd; rng.seed(rd())` — generated, **not recorded**, not readable back | | `PySoundBuffer::sfxr_mutate` | same | | `PyBSP::split_recursive` | passes `nullptr` to `TCOD_bsp_split_recursive`, i.e. libtcod's **global default RNG** — not recordable at all from Python | | `PyHeightMap` (`CreateTCODRandom`) | same `nullptr` → libtcod's global default RNG, used by `add_hill`, `dig_hill`, `mid_point_displacement` etc. | `PyNoiseSource` is the counter-example and the model: it generates a seed when given none, **records it**, and exposes it as a read-only `seed` property. Two consequences worth separating: 1. **Reproducibility.** A dungeon generated with `bsp.split_recursive(depth=5)` cannot be regenerated, even in principle, because the seed it used is not recoverable. Same for a procedural sound. 2. **Global state.** The libtcod default RNG is process-global, so two independent generators interleave and neither is reproducible on its own even if you did know the seed. Separately and in the same family: `src/audio/SfxrSynth.cpp` `sfxr_synthesize()` calls `std::srand(42)` unconditionally and draws with `std::rand()`. That makes sfxr itself deterministic, but it **clobbers the process-global C `rand()` state** as a side effect of synthesising a sound — any other consumer of `rand()` in the process is silently reset. Suggested shape, matching `PyNoiseSource` and #394: when `seed=None`, draw the seed from the engine's seeded stream (`mcrf::EngineRandom`) rather than from `std::random_device`/libtcod's global, and record it on the resulting object so it can be read back. That makes the whole engine replayable from one `mcrfpy.set_random_seed()` without changing any call site.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
john/McRogueFace#408
No description provided.