RB: predictions for the recorded-command replay, before the module exists
The headline is one number: our turn writes ModCount 14 where the oracle writes 24, and the missing ten are the turn's command stream. P1 decomposes the ten and names what each near miss would mean; P5 predicts the secondary pair closes five leaves and must NOT close the sixth, because the recording is from a run that diverged there.
This commit is contained in:
parent
4f25f1e8c0
commit
1e6474b31c
1 changed files with 182 additions and 0 deletions
182
docs/RB-predictions.md
Normal file
182
docs/RB-predictions.md
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
# Rung B — predictions, written before the replay module exists
|
||||
|
||||
Lane RB, 2026-09-08. Committed **before** `src/game/ai/command_capture.*`,
|
||||
`src/game/ai/apply_order.*` or `src/app/command_replay.*` were written, and before any
|
||||
`--turn-commands` run. Rule 2.
|
||||
|
||||
Baseline taken on a **fresh** `build-host` in this worktree (rule 24), `main` at `4f25f1e`,
|
||||
ctest 54/54, header regenerated from `~/sots-re/ghidra` (1,217 entries, no diff):
|
||||
|
||||
```
|
||||
turn2-state.sav -> turn3-state.sav : baseline 108, after our turn 63, closed 45, regressed 0
|
||||
our /Sim/ModCount = 14 oracle = 24 input = 12
|
||||
```
|
||||
|
||||
The whole lane is aimed at that one arithmetic gap: our turn contributes the **two driver
|
||||
bumps** and nothing else, because a command stream is the only other writer and a save does
|
||||
not carry the AI's.
|
||||
|
||||
---
|
||||
|
||||
## The inputs this lane replays
|
||||
|
||||
Lane L4's live dumps at `StrategySim::ApplyTurnCommandBatch`, already in the RE repo:
|
||||
`verify/results/shim/aiorders/l4-turn{1to2,2to3}-aiorders.txt`. Each holds two batches; the
|
||||
**second** (`seq=2`, `n=8`) is the End-Turn submission. The first (`seq=1`, `n=1`) is the
|
||||
load-time batch and is deliberately **excluded** — our standalone loads from disk and never
|
||||
runs it, and the save's on-disk `ModCount` is the value before it.
|
||||
|
||||
---
|
||||
|
||||
## P1 — `ModCount` lands on exactly 24 on the canonical pair
|
||||
|
||||
Replaying the `turn2to3` block set against `turn2-state.sav` makes `/Sim/ModCount` read
|
||||
**24**, closing that leaf. The decomposition, item by item, from AI4's cost table:
|
||||
|
||||
| term | count |
|
||||
|---|---:|
|
||||
| turn drivers (`S00`, `T00`) — already ours | 2 |
|
||||
| research-rate gate, blocks 0–3 (pids 16, 32, 496, 512) | 4 |
|
||||
| list 5 system rates ×1 | 1 |
|
||||
| list 3 build ×1 | 1 |
|
||||
| list 10 ×1 | 1 |
|
||||
| list 14 fleet task ×2 | 2 |
|
||||
| list 8 fleet move ×1 | 1 |
|
||||
| list 23 population ×1 — **free half** | 0 |
|
||||
| **total** | **24** |
|
||||
|
||||
**Falsified if** the run reports anything but 24, and each near miss names its own defect:
|
||||
|
||||
* **25** — list 23 charged; the 17..27 free half is wrong and AI4 §1 must be reopened.
|
||||
* **23** — the AI's fleet order counted as the interface's single list-14 element; AI2's P1
|
||||
and the `(fleetId, mode)` dedup key are wrong.
|
||||
* **28** — the four uninitialised monster slots' garbage rate payloads read as *set* gates.
|
||||
This is the parser failing, not the model: those slots carry `pid == 0` with the gate bit
|
||||
clear and a garbage float behind it.
|
||||
* **22** — the human's block (pid 16, every list empty) charged 0 instead of 1. The
|
||||
research-rate gate is set unconditionally by the send-buffer build; a block that ordered
|
||||
nothing still costs one. This is the boundary case rule 23 asks for and it carries four of
|
||||
the ten.
|
||||
|
||||
## P2 — the apply order is this permutation, and it is neither list order nor offset order
|
||||
|
||||
The batch is a flat run of 27 per-list loops with three per-player gate loops spliced in:
|
||||
|
||||
```
|
||||
lists 6 11 20 19 17 18 5 23 24
|
||||
gate loop A { group5(+0x3c, free), target(+0x14, bump), rate(+0x0c, bump) }
|
||||
lists 1 4 3 21 2 22 9 10 12 13 14 15 16 7 8 25 27 26
|
||||
gate loop B { boost(+0x20, bump) }
|
||||
gate loop C { group4(+0x2c, bump) }
|
||||
```
|
||||
|
||||
Each list step is `for each player block: for each element: apply` — so **every player's
|
||||
list-6 elements are applied before any player's list-11 element**. Order is per list, not
|
||||
per player.
|
||||
|
||||
**How I will verify it, three ways, and what each cannot show.**
|
||||
|
||||
1. **Address monotonicity of the six inlined bump sites.** `ApplyTurnCommandBatch` inlines
|
||||
six appliers and each writes `ModCount` in place: `0x0088fe0a` (gate A target),
|
||||
`0x008902fe` (list 12), `0x008903b9` (list 13), `0x0089046c` (list 14), `0x008905c8`
|
||||
(list 7), `0x008907bc` (gate B boost). Their positions in the permutation above are
|
||||
10th, 18th, 19th, 20th, 23rd and 28th. Adding the three gate-loop heads (A `0x0088fdb0`,
|
||||
B `0x008907b1`, C `0x0089080a`) gives a nine-point chain that must be strictly increasing
|
||||
in both address and position. **This is a check on 9 of 30 positions and no more** — the
|
||||
other 21 lists' handlers are out-of-line and this lane has no record of their call-site
|
||||
addresses inside the batch, so their relative order rests on AI4's direct read of the
|
||||
`add edi, imm` sequence and is inherited, not re-derived.
|
||||
*Falsified if* any of those nine is out of order.
|
||||
2. **The permutation is a bijection of 1..27**, asserted at compile time. Catches a
|
||||
transcription slip, catches nothing about the order being right.
|
||||
3. **Neither sorted.** Asserted: the list sequence is not ascending, and the member-offset
|
||||
sequence implied by it is not ascending either. A naive implementation that loops
|
||||
`for (list = 1..27)` or walks the block's members in memory order cannot pass this — which
|
||||
is the only reason the test earns its place, since on every workload the corpus holds the
|
||||
*outcome* is order-independent (no two commands in either capture touch the same object
|
||||
through a modelled handler).
|
||||
|
||||
**Stated plainly: apply order is unfalsifiable on this workload.** Both captures put every
|
||||
non-empty list on one player and every command on one system, so any permutation produces the
|
||||
same save and the same count. The order is implemented because a later workload will need it,
|
||||
and it is tested against the instruction stream rather than against an outcome.
|
||||
|
||||
## P3 — a replay closes `ModCount` and nothing else, and regresses nothing
|
||||
|
||||
Predicted canonical-pair result: **closed 46 (45 + `/Sim/ModCount`), regressed 0, remaining 62.**
|
||||
|
||||
Of the ten commands in the `turn2to3` block set, only three have a handler this lane can
|
||||
write, and all three are **no-ops on this workload**:
|
||||
|
||||
* the research-rate gate — `ServerPlayer.ResRate` already reads 0.25/0.8 in `turn2-state`;
|
||||
* list 5 system rates — Ke'Dolarra's rates already read `{0, 1.0, 0, 0, 0, 0, 0}`;
|
||||
* the research-target gate — not set on any block this turn.
|
||||
|
||||
The other seven need subsystems we do not have (P4). So the replay's entire contribution to
|
||||
the byte-match distance is the counter.
|
||||
|
||||
**Falsified if:** any further leaf closes — which would mean one of the seven is more
|
||||
modellable than I claimed, or that a "no-op" write is not one; or any leaf regresses — which
|
||||
would mean a modelled handler writes the wrong field, and is the failure this prediction
|
||||
exists to catch.
|
||||
|
||||
## P4 — the no-op control
|
||||
|
||||
Running `--turn-commands` with the three modelled handlers enabled must change **zero** leaves
|
||||
relative to the same run with them suppressed. That is the control on "these are no-ops here":
|
||||
a rate written to the wrong player, or rates written into the wrong system's frame, shows up
|
||||
as a regression against a run that wrote nothing, even though both agree with the oracle by
|
||||
luck. Rule 1 — the outcome agreeing is not evidence the write went to the right place.
|
||||
|
||||
## P5 — the secondary pair, where the stream actually does something
|
||||
|
||||
`turn1-state.sav` carries `ModCount 0`; the oracle `turn2-state.sav` carries 12. The
|
||||
`turn1to2` capture holds 4 rate gates, **3 research-target gates** (ids 144, 90, 288 on
|
||||
players 32, 496, 512), one list-1 design, one list-3 build, one list-5 rates and one list-23
|
||||
population.
|
||||
|
||||
**Prediction:** `ModCount` 0 → **12** (2 + 4 + 3 + 1 + 1 + 1 + 0), and — if and only if the
|
||||
capture carries the resolved tech *names* — the replay closes `ResRate` on players 32, 496 and
|
||||
512 and `ResTNm` on 32 (`IND_Waldo`) and 496 (`DRV_PlsFiss`).
|
||||
|
||||
**And it must NOT close `ResTNm` on player 512.** The capture records target id **288**, which
|
||||
that run resolved to `XNC_TrnsMorr2`; `turn2-state.sav` holds `BIO_GnMod`. The recording is
|
||||
from a *different process* than the oracle, and 512's pick is the one leaf lanes L4 and L5
|
||||
both showed is not reproducible. So the recorded stream and the reference save genuinely
|
||||
disagree there, and a replay that closed it would mean I had fitted something.
|
||||
|
||||
*This is the sharpest prediction in the set*, because it is the one where the replay's output
|
||||
is determined by the recording and the recording is known to be from a divergent run. If 512
|
||||
closes, something is copying the oracle rather than the capture.
|
||||
|
||||
*Falsified if:* `ModCount` ≠ 12; or `ResRate` fails to close on all three; or 512's `ResTNm`
|
||||
closes; or 32/496 fail to close while the capture does carry names.
|
||||
|
||||
## P6 — the wire carries an id, the save carries a name, and we cannot bridge them
|
||||
|
||||
The research-target gate's payload is an `int techId` (`block+0x10`). `ResTNm` on the wire is
|
||||
a string. Process Turn phase 18 resolves the chosen object's `std::string` at `+0x4` and
|
||||
passes a `char*` to `cl_SetResearchTarget`, so the resolution happens **client-side, off the
|
||||
command**, and the applier receives an id whose mapping to a name this campaign has not read.
|
||||
`144`, `90` and `288` are not `index * 16` and are not indices into anything we hold.
|
||||
|
||||
**Prediction:** the target-gate handler cannot be modelled from the id alone, and the capture
|
||||
format must therefore be able to carry the *observed* name alongside the id. When it does, the
|
||||
leaf is closed **by recorded payload, not by model**, and the report must say so in a separate
|
||||
column — otherwise a future reader will mistake a transcription for a reimplementation.
|
||||
|
||||
*Falsified if:* someone finds the id→name map, at which point this becomes a modelled handler
|
||||
and the column empties.
|
||||
|
||||
---
|
||||
|
||||
## What this lane is not predicting
|
||||
|
||||
* **Nothing about `Summary.Checksum`.** Its inputs are unread; it moves whenever anything else
|
||||
does.
|
||||
* **Nothing about the RNG frame.** No command in either capture draws.
|
||||
* **Nothing about the load-time batch.** `seq=1` is excluded by construction (above), and
|
||||
whether the original charges `ModCount` for it is untested — a save loaded and immediately
|
||||
re-saved would settle it in one run and this lane does not do it.
|
||||
* **Nothing about ordering effects between two commands on the same object.** No workload has
|
||||
one.
|
||||
Loading…
Add table
Reference in a new issue