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.
55 lines
2.9 KiB
Markdown
55 lines
2.9 KiB
Markdown
# `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 `ours` on scratch copies and never lets it touch game memory. A
|
|
hook-induced perturbation cannot produce A ≠ B.
|
|
* The `hooks=off` control is a **third** value, not one of the other two.
|
|
* Run B reproduced `ab4ac2d7e2977260…`, which is the hash `running-the-game.md` records for the
|
|
original live session's `Autosave - 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 → turn2` as a byte-match oracle. Use `ref-turn2 → turn3`.
|
|
* A standalone comparison against a `turn1-state → turn2` post-save is comparing against one draw
|
|
from a distribution. The four unstable leaves are `ResTNm` and two `St[n]` words on
|
|
`Player[512]`, plus `Checksum`.
|
|
* This corroborates lane PL's placement of `ResTNm` / `ResErrRoll` on 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.
|