204 lines
11 KiB
Markdown
204 lines
11 KiB
Markdown
# 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.
|
||
|
||
---
|
||
|
||
# Outcomes
|
||
|
||
Written after the run, against the predictions above. Nothing above was edited.
|
||
|
||
## P-Y1 — held exactly
|
||
|
||
16 words on both calibrated pairs, all attributed to the trade-raid phase, `left` 379 and 359. The
|
||
player vector read 8 on both, as the wire says. The falsification table's other branches did not
|
||
fire: not 8 (the NPC gate was open), not 24 (the refugee gate was not).
|
||
|
||
## P-Y2 — held exactly
|
||
|
||
Residual 4 on the turn-4→5 pair and 2 on the turn-5→6 pair, matching the per-call-site split
|
||
(2 detection + 2 research, and 2 detection + 0 research). The state block is byte-identical in both
|
||
cases and only `left` differs, so the two instruments disagree about nothing.
|
||
|
||
**The answer to the milestone question is therefore "no", by 4 words and by 2.**
|
||
|
||
One thing this pair of numbers does *not* establish, and the prediction should have said so: the
|
||
residual is a subtraction, so it confirms the **sum** of the modelled and unmodelled terms, not each
|
||
term separately. The static bound is what carries the 16; this arithmetic corroborates it.
|
||
|
||
## P-Y3 — held
|
||
|
||
480 fields over 80 player-records across 11 saves, 0 mismatches, 0 dangling owned-system ids. The
|
||
same check runs inside every turn and is printed as a phase note.
|
||
|
||
## P-Y4 — held, and the shape of the result is more useful than the claim
|
||
|
||
`--commit-blocked` closes 29 and regresses 17 on the first reference pair, and closes 13 and
|
||
regresses 19 on the second. Every regressed leaf is in the unmodelled set: `almem` x8, `inc` x3,
|
||
`sav` x3, three of the ship census. Nothing outside it moved.
|
||
|
||
So "committing makes it worse" is **not** what the measurement says: on the first pair the turnstats
|
||
subsystem is net +7 and on the second it is net -9. The record is right in six fields and wrong in
|
||
five, and which way the net falls depends on the save. The phase stays blocked, and it becomes a
|
||
clean +24 the moment the alliance mask and the budget land.
|
||
|
||
## P-Y5 — WRONG in its framing, and the correction is the result
|
||
|
||
The prediction said the two constants differ "for some maxIncome reachable in a real game" and that
|
||
the smallest such value could be found by a linear scan. The scan found it immediately, and the
|
||
framing -- inherited from the phrase "one ulp out on large empires" -- was the wrong picture
|
||
entirely.
|
||
|
||
`trunc(m / -0.15)` and `trunc(m / -0.15000000596046448)` differ for **every m divisible by 3**: 1000
|
||
of the first 3000 integers, smallest `m = 3` giving -20 against -19. It is not precision loss at
|
||
scale. `m / 0.15` is an exact integer whenever 3 divides m, and the two constants straddle it.
|
||
|
||
And the multiples of 3 are only the floor: the disagreement rate is 33.3% up to about 1.2 million,
|
||
66.7% by 1.36 million and 100% at and above 3 million.
|
||
|
||
Measured on the corpus by inverting the stored limit: **6 of 25 player-records would get a different
|
||
stored value from the engine's current constant than the game wrote.** The defect is live, it is a
|
||
whole unit rather than an ulp, and it is a third of all inputs at small values and all of them at
|
||
large ones -- the opposite shape from "one ulp on large empires", which would have been harmless.
|
||
|
||
The file belongs to another lane in flight, so it is reported and not fixed here.
|
||
|
||
## An unpredicted result: the blocked input is recoverable from every save
|
||
|
||
The elimination limit is injective in the maximum-income sum it is computed from (slope about 6.67),
|
||
so it inverts. Every save in the corpus therefore states, for every player that owns anything, the
|
||
exact sum of its systems' maximum money outputs -- which is the term that blocks `ComputeBudget`,
|
||
both savings phases, the bankruptcy limits and five fields of the turn record.
|
||
|
||
That was not predicted and is the most useful thing this lane found. The same inversion recovers the
|
||
protection-limit factor as 3.3 from the saves, independently of the game data files.
|