[Bugfix] EntityBehavior RNG is unseeded and non-injectable — no deterministic replay, no A/B control #394
Labels
No labels
Alpha Release Requirement
Bugfix
Demo Target
Documentation
Major Feature
Minor Feature
priority:tier1-active
priority:tier2-foundation
priority:tier3-future
priority:tier4-deferred
Refactoring & Cleanup
system:animation
system:documentation
system:grid
system:input
system:performance
system:procgen
system:python-binding
system:rendering
system:ui-hierarchy
Tiny Feature
workflow:blocked
workflow:needs-benchmark
workflow:needs-documentation
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
john/McRogueFace#394
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
src/EntityBehavior.cpp:22seeds the behavior RNG from the OS entropy pool, at namespace scope, with no way to observe or set the seed:Single call site today —
src/EntityBehavior.cpp:69,auto target = candidates[dist(rng)];— the random choice among candidate targets.Consequence: two runs of the same scenario, from the same script, with the same map, produce different entity behavior. There is no Python-visible knob and no record of what was used.
Why it matters
Two separate consumers, and the second is the harder requirement:
Deterministic replay. A headless script driven by
mcrfpy.step()is otherwise fully deterministic —simulation_timeis an integer millisecond counter (GameEngine.h:229), timers run off it (GameEngine.cpp:565), and rendering advances zero simulation time. Behavior RNG is the one remaining source of run-to-run divergence, so a trace cannot be replayed and a disagreement cannot be bisected.Experimental control in a live (windowed, realtime) simulation. This is the sharper case. When McRogueFace is used as an agent simulation environment (#55, #154, #156), the experiment is "same world, changed agent." If NPC choices differ run to run, an A/B against a changed agent stack has changed two variables, and the comparison is void. Unseeded RNG is not a replay nicety here — it makes the measurement unattributable.
The pattern to follow
PyNoiseSourcealready models the right shape and should be copied rather than reinvented:Nonegenerates one (src/PyNoiseSource.cpp:174-186)src/PyNoiseSource.cpp:20-21)That second half is the important one: a run that did not pin a seed should still be able to report which seed it got, so a surprising run can be reproduced after the fact.
Proposed surface
Something in the shape of:
Open to it living on
GridDatainstead if per-grid streams turn out to be the better granularity — worth a moment's thought, since one global stream means adding an NPC to a scenario perturbs every other NPC's sequence, which is its own reproducibility hazard.Also in scope (or a deliberate note that it isn't)
src/PySound.cpp:225has a second unseededstd::mt19937. It is behind--audio-offin headless and therefore harmless today, but it is the same defect and will bite the moment audio is enabled in a measured run. Either fix it here or leave a comment saying why not.Acceptance
tests/unit/test: two runs at the same seed produce an identical entity position sequence over Ngrid.step()calls; two runs at different seeds divergethread_localmeans a background thread callinggrid.step()gets its own stream, which is either correct or a bug depending on the intended model (relates to #219 / themcrfpy.lock()path)PySound.cpp:225Blocks
Prerequisite for using McRogueFace as a measured simulation environment (#55, #154). Should land before that work starts, not during it.
runtimeis wall clock #396last_ran = nowdiscards skipped intervals AND the residual #397