Y: predictions for the RNG model and the tail turn record, written before the build

This commit is contained in:
lane Y 2026-09-08 11:21:00 -04:00
parent b48d860f8a
commit 20e5112398

View file

@ -0,0 +1,133 @@
# Lane Y — the predictions, written before the build
Committed before `src/app` was touched, per earned rule 2. Falsification sections included: how each
prediction could be wrong, and the symptom of each way.
Two things are being predicted: (a) what the standalone's generator does on lane Z's two calibrated
oracle pairs, and (b) whether a turn-record model built from the wire reproduces the record the game
already wrote into every save it made.
---
## P-Y1 — the trade-raid model costs exactly 16 words on an 8-player save
Lane Z measured `ServerTradeManager::GenerateTradeRaidEncounters` firing two `Mars::RNG::Chance`
rolls for every entry of the server's player vector, and showed that no back-edge contains either
site, so one word per player per site is a **bound**, not an observation. Both probabilities are
strictly inside (0, 1), so `Chance` takes neither early-out and spends exactly one word.
The reference save's player vector holds **8** entries (`Sim.NumPlrs`; the wire's `players` array is
that same vector).
> **The standalone, run with `--commit-rng` on `z2-endturn.sav`, consumes exactly 16 generator
> words**, all of them attributed to the trade-raid phase, and the post-turn `left` is
> `395 − 16 = 379`. `mt[624]` is unchanged (no block boundary is crossed).
Same claim on `z-t6-endturn.sav`: 16 words, `left` `375 − 16 = 359`.
**Falsification.**
| how it could be wrong | symptom |
|---|---|
| the loop is not over the server's player vector | a word count that is not 2 × 8 |
| `Chance` is `p >= r` or narrows differently | word count still 16 (the count does not depend on the comparison) — this prediction cannot test it, and says so |
| the NPC-raid gate is not open on this save | 8 words, not 16 |
| the refugee-raid gate *is* open | 24 words |
| the engine's `next_float` disagrees with the original | invisible here; only the count is being tested |
---
## P-Y2 — the residual against the oracle is 4 and 2, not 0
The oracle pairs carry a known cost: `z2-endturn` → `z2-autosave` is **20** words, `z-t6-endturn` →
`z-t6-autosave` is **18**. Lane Z's per-site ledger attributes the difference to four sites the
standalone does not model:
* 0x0050329d (`NextFloat`) and 0x007929a4 (`NextInt`), both under `StrategyServer::DetectEncounters`
— one word each per turn on 3 of 3 measured turns;
* `TechTree::ProcessResearch`+0x1c8 and `ServerPlayer::RollResearchEvent`+0x2f — 0–1 words each, both
downstream of the research allocation, which is downstream of `ComputeBudget`, which is blocked on
the population → base-output term.
> **The standalone lands 4 words short on the turn-5 pair and 2 words short on the turn-6 pair**, and
> the shortfall is exactly the four sites above: 2 detection + 2 research on turn 5, 2 detection + 0
> research on turn 6. The generator state therefore does **not** match the oracle on either pair, and
> the answer to "does it land on the oracle's `mt[624]`/`left`" is **no**, by a stated amount.
**Falsification.** A residual other than 4 and 2 means the site ledger and the file oracle disagree
about this turn, which would be a real contradiction between two of lane Z's three instruments and
would have to be reported as one. A residual of 0 would mean the trade-raid count is coincidentally
absorbing the other sites.
**Why the shortfall is not closed by guessing.** Modelling "2 detection words per turn" would fit
three observations with no derived rule behind it, and lane I bounds that site's cost at
`0 .. |contacts| × |detectors|`. It is left unmodelled and reported.
---
## P-Y3 — six fields of the turn record are recoverable from the wire
`StrategyServer::FinalizeTurnRecords` (tail phase 36) fills a per-player record and archives it by
turn. The archive is on the wire: it is `/Sim/turnstats/nply[]/hist/stats[]`, one `stats` element per
turn per player, and every save carries an element for its own `Frame`.
> **A record built from a save's own state reproduces that save's archive entry for its own turn, on
> every player of every save in the corpus, for these six fields:**
>
> | record field | rule |
> |---|---|
> | `trn` | the frame counter |
> | `col` | the number of owned-system ids |
> | `sav` | the player's savings |
> | `inc` | savings minus previous-turn savings |
> | `tch` | the number of tech-tree states in the completed state |
> | `pop` | summed over owned systems: population plus the pending population bonus |
>
> and it does **not** reproduce `almem`, `tdinc`, `bat`, the acquired/lost system lists, or the
> per-hull-class ship census, because each of those is written by a phase or an input the standalone
> does not hold.
**Provenance, stated plainly.** The six rules were found by search over the corpus in Python before
this was written, so this is not a blind prediction of the rules. What it *is* a prediction of is
that a C++ transcription over the engine's own typed shapes agrees with them on all 112
player-records — a test of the transcription and of the shape typing, not of the rules. Two of the
six (`pop`, `inc`) were ambiguous in the Python search and were separated by the corpus:
`pop` = Σ(`Pop` + `pbon`) matched 112/112 while Σ(`Pop` + `dcs`) matched 101/112.
**Falsification.** Any player-record where the six computed fields differ from the stored ones.
Particularly: a save where a player's owned-system list references a system id the save does not
carry (the sum would silently drop a term).
---
## P-Y4 — committing phase 36 would regress, so it is not committed
The record the standalone can build is the record for the **input** save's turn, not for the turn it
is about to produce: `sav` after the turn needs `ApplyNetToSavings`, which is blocked, and `almem`
needs the alliance-mask rebuild, which is a stub.
> **Committing phase 36 would add a new archive element whose `sav`, `inc`, `almem`, `tdinc` and
> class census are all wrong, turning one container-shaped divergence per player into several
> leaf-shaped ones. The divergence report would show a large `regressed` count and a small `closed`
> one.** It is therefore evaluated, reported, and not written. The divergence baseline stays
> 5 closed / 0 regressed on both pairs.
**Falsification.** If committing it closed more than it broke, the block was too conservative. That
is measurable with `--commit-blocked` and is reported rather than argued.
---
## P-Y5 — the bankruptcy divisor in the engine is one ulp wrong
`ComputeBankruptcyLimits` divides by `-0.15`. The constant the original loads is the double
`-0.15000000596046448`, which is `(double)(float)-0.15f`. The two differ, and the difference is
visible on a large enough maximum income.
> **`ftol(maxIncome / -0.15)` and `ftol(maxIncome / -0.15000000596046448)` differ for some
> `maxIncome` reachable in a real game**, and the smallest such value is small enough to be found by
> a linear scan.
This lane does not fix it: the file is another lane's. It is measured and reported.
**Falsification.** If no `maxIncome` in the int32 range separates the two constants, the correction
is real but inert and should be recorded as such.