7 KiB
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-rngonz2-endturn.sav, consumes exactly 16 generator words, all of them attributed to the trade-raid phase, and the post-turnleftis395 − 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 underStrategyServer::DetectEncounters— one word each per turn on 3 of 3 measured turns; TechTree::ProcessResearch+0x1c8 andServerPlayer::RollResearchEvent+0x2f — 0–1 words each, both downstream of the research allocation, which is downstream ofComputeBudget, 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 trnthe frame counter colthe number of owned-system ids savthe player's savings incsavings minus previous-turn savings tchthe number of tech-tree states in the completed state popsummed 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,tdincand class census are all wrong, turning one container-shaped divergence per player into several leaf-shaped ones. The divergence report would show a largeregressedcount and a smallclosedone. 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)andftol(maxIncome / -0.15000000596046448)differ for somemaxIncomereachable 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.