diff --git a/docs/Y-standalone-rng-and-tail.md b/docs/Y-standalone-rng-and-tail.md new file mode 100644 index 0000000..eb3053d --- /dev/null +++ b/docs/Y-standalone-rng-and-tail.md @@ -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.