Four float constants in ComputeBudget have a width a C++ port gets wrong by default, and the corpus exercises the boundary of exactly one. That one is now measured against the running game, with a control build that must fail and does. savings interest (double)0.01f boundary: treasury a multiple of 100 IN CORPUS debt interest (double)0.15f boundary: a negative treasury not in corpus research yield (double)0.85f boundary: research money mult of 40k not in corpus the three ResMod summed in f32 boundary: two of three non-zero not in corpus Three constants sitting beside those, in the same expressions, are EXACT doubles and must not be "corrected" -- so "widen every literal" is its own defect and the only safe procedure is to read each operand. Same pattern elsewhere in the chain: ComputeOutputFromRates multiplies by an exact 1.5 then a widened 1.2f two instructions later, and NormaliseOutputRates holds 1e-4 at BOTH widths. Live result (VM146, turn1-state.sav, three runs): the game pays 499 interest on a treasury of 50,000 and 380 on 38,100; the exact decimals pay 500 and 381. Every divergence in the control build lands on a multiple of 100 and nothing else diverges at all. Coverage stated as distinct states, not calls: 5 distinct treasuries, 2 on the boundary -- against the earlier green run's 20 distinct states, none on a boundary. BANKRUPTCY_PROTECTION_LIMIT_FACTOR read live: 3.29999995 = (float)3.3. Its file image is zero because the loader fills it at run time, so lane PL-3 could read the width and had to assume the value. The assumption was right and is now measured. Falsified: the difficulty-mods record does not sit inline at ServerPlayer+0x36c; that field is a heap pointer on all eight players. Recorded as a hypothesis with the measurement, not as a fact. Second finding, from the rule-19 control: turn1-state -> turn2 is NOT a deterministic pair. Three runs gave three post-turn autosaves differing in exactly four leaves -- one Singularity shadow empire's research pick and the derived checksum. Two of the three runs carried identical hooks, and the un-instrumented run was a third value, so this is the game and not the instrument. The determinism oracle stands for ref-turn2 -> turn3 and does not generalise to its neighbour; no lane should use this pair as a byte-match oracle.
2.9 KiB
turn1-state → turn2 is not a deterministic pair
Lane L5, 2026-09-08, VM146. A by-product of a rule-19 control, and a caution for any lane that reaches for this pair as an oracle.
What was measured
Three End Turns from the same input save, through the identical UI path, in three freshly launched processes:
| run | shim | (Autosave).sav sha256[0:16] |
Player[512 "Singularity"]/ResTNm |
|---|---|---|---|
| control | hooks=off (no MinHook at all) |
62061918176e3441 |
XNC_TrnsLir2 |
| A | ComputeBudget=compare |
7fc9e6ab46b47794 |
XNC_TrnsHvr2 |
| B | same hooks, different arithmetic on OUR side only | ab4ac2d7e2977260 |
BIO_GnMod |
Every pairwise diff is exactly four leaves: that research-target name, two of that player's
tech-tree status words, and the derived /Summary/Checksum. Nothing else in the state moves.
Why this is not the instrument
- Runs A and B carry identical hooks. They differ only in a constant on our side of the
compare, and compare mode runs
ourson scratch copies and never lets it touch game memory. A hook-induced perturbation cannot produce A ≠ B. - The
hooks=offcontrol is a third value, not one of the other two. - Run B reproduced
ab4ac2d7e2977260…, which is the hashrunning-the-game.mdrecords for the original live session'sAutosave - turn2.sav. So the un-instrumented behaviour is inside the observed spread. - All three runs wrote a byte-identical pre-turn autosave (
a3f9dc4b49fc669c…). The loader and the writer are deterministic; only the turn's AI decision is not.
Why it matters
determinism-oracle.md established byte-reproducibility on ref-turn2 → turn3 — five runs,
two processes, one pair of hashes. That result is sound and is not contradicted here. What is new
is that it does not generalise: the neighbouring pair in the same game is unstable, in one
shadow empire's AI research pick.
Consequences:
- Do not use
turn1-state → turn2as a byte-match oracle. Useref-turn2 → turn3. - A standalone comparison against a
turn1-state → turn2post-save is comparing against one draw from a distribution. The four unstable leaves areResTNmand twoSt[n]words onPlayer[512], plusChecksum. - This corroborates lane PL's placement of
ResTNm/ResErrRollon Rung B for the Singularity shadow empires: those leaves are not merely AI-dependent, they are not reproducible, so no amount of modelling the AI's rules will close them without also reproducing whatever the pick is actually keyed on.
What was not established
The cause. The candidates are the shadow empires' ResErrRoll (a research error roll that lane PL
saw flip False → True, i.e. an RNG consumer on exactly this player class) and anything in the AI
path keyed on wall-clock time or frame count rather than on the saved generator. Distinguishing
them is a probe on the pick site, not another End Turn.