sots-engine/docs/L4-predictions.md

246 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# L4 — what the live AI will emit, written before the module was built
Lane L4, 2026-09-08. Guest **VM145** (`sots-re-win10-145`, 192.168.10.145). Worktree `wip/l4` off
`main` `0117495`.
Rule 2: this file is committed **before** the shim module is written, and it names the falsifier for
every claim. Nothing below is a summary of a run; there has been no run.
The question this lane exists to answer: **does our emission model produce the same commands the
game does?** `src/game/ai` models the *shape* of a `TurnCommands` block and the *cost* of each
element, and decides nothing. Nobody has ever looked at a real AI block.
---
## 0. The instrument, and why it is one detour
`Game::StrategySim::ApplyTurnCommandBatch` **0x0088f9b0** receives `(blocks, n)` as two stack
arguments with `ecx = sim` — read straight off the prologue at `0x0088f9db`–`0x0088f9e7`, where
`n` is multiplied by `0x1b4` and added to `blocks` to make the end pointer. **At that instant every
player's submitted block is complete and in memory**, so one register-transparent entry stub dumps
the entire turn's command traffic: six gates and twenty-seven list lengths per player, plus the
element bytes.
That is a strictly better instrument than counting `ModCount` bumps, which is what every previous
lane has had. A bump count says *how many* commands; the block says *which lists, how many
elements, and what values*.
Second instrument: sixteen register-transparent entry counters (`probe_entry.cpp`'s pattern, its
own table so lane H's set is untouched), on the task bodies and the emission gates. One of them —
`StrategyAIAgent::RunTaskList` **0x006b3320** — additionally records its `pass` stack argument into
a global, so every later probe hit is **attributed to a pass**. That is what turns AI3's P2 from a
count into a measurement.
**Rule 20 is the whole design.** `AITRaid`'s list-16 emit is the named open item, and a zero at
`0x007635f0` means nothing unless `AITRaid::Execute 0x0068e670` is also probed: "never ran" and
"ran and emitted nothing" are opposite answers.
**Rule 19 control.** Both workloads have published oracle bytes. Every configuration is run against
them and any configuration that moves a byte has invalidated its own numbers.
---
## 1. Workload W1 — `turn1-state.sav`, one End Turn (the turn 1→2 transition)
Facts already in the corpus, from `turn1-state.sav` → `turn2-state.sav` (read this session with
`verify/save-reader`, not taken from a report):
| | |
|---|---|
| players | 8: ids 16, 32, 496, 512 (`Species` 0/2/0/2) and 528/544/560/576 (`Species == 4`) |
| player 32 | `HomeSys` **288** = Ke'Dolarra, `PlyrIdx` 1 |
| `ModCount` | 0 → 12 |
| `ResRate` | 0.25 → **0.8** on 32, 496, 512; unchanged (0.25) on 16 |
| `ResTNm` | `''` → `IND_Waldo` (32), `DRV_PlsFiss` (496), `BIO_GnMod` (512); `''` on 16 |
| `Status` | 0 → 4 on 16/32/496/512; **0 on all four `Species == 4`** |
| player 32 designs | 5 → 6; the new one is `DesID` **18**, `"Honor Lance"` |
| `designIds` | gains **18** *and* **1712** — the second belongs to player 528, whose design count also goes 18 → 19 |
| `NMnx` | 106 → 109 (ship **1728**, fleet **1744**, and one more) |
| new fleet | **1744** `"Alpha Fleet"` at system 288, holding ship 1728 of design 18 |
### P1 — the block set
**Prediction: `n == 4`, and the four `block+0x04` player ids are 16, 32, 496, 512 in that order.**
*Falsified if* `n == 8` — then the `Species == 4` factions **do** submit, they just submit empty
blocks, and AI4 §2's "no `StrategyClient` at all" is too strong (it would become "a client that
never sets the always-set gate", which is a different and stranger claim). *Falsified differently
if* the four ids are 0,1,2,3 — then `block+0x04` is a player **index**, not the save id, and every
published reading of that field needs the same correction.
### P2 — the four blocks, list by list
| block | gates set | lists non-empty |
|---|---|---|
| 16 (human) | rate only, value **0.25** | **none** |
| 32 (AI) | rate **0.8**, target (`IND_Waldo`) | **1, 3, 5 — one element each** |
| 496 (AI) | rate **0.8**, target (`DRV_PlsFiss`) | **none** |
| 512 (AI) | rate **0.8**, target (`BIO_GnMod`) | **none** |
Boost, group-4, the three-float Hiver gate and `CivilianRatios` **clear on all four**.
That sums to AI4's twelve: 4 rate + 3 target + 3 (player 32's lists) + 2 drivers.
*Falsified if:* any list outside {1, 3, 5} is non-empty on any block; or a list in {1, 3, 5} carries
more than one element; or the human's block carries a list at all; or 496/512 carry a list (they own
nothing, so a list element from them would mean a task fired against objects they do not have).
**The sharpest falsifier is list 8/10/14 non-empty.** Player 32 gets a brand-new fleet (1744) this
turn, and a fleet is exactly the thing lists 8/10/14 order. If the AI issues a fleet order on the
same turn it creates the fleet, AI4's P2 attribution is wrong by exactly the fleet-order group and
the twelve has to be re-derived.
### P3 — the element values
* **list 3** (`{i32 ordinal, i32 designId, i32 systemId, i32 w}`): `designId == 18`,
`systemId == 288`. The `w` word has never been observed and is not predicted.
* **list 5** (`{i32 systemId, OutputRates}`): `systemId == 288`, i.e. **the same system the build
order names**.
* **list 1** (`ShipDesignDef` frame + `i32`): the element carries the id **18** somewhere in its
first 48 bytes.
*Why 18 matters.* 18 is not a multiple of 16 and does not come from the master `NMnx` counter — the
*other* new design that turn, player 528's, got **1712** from that counter, and 528 submits no
block. So there are two id spaces, and the prediction is that **a client-issued design carries a
small client-local id in the command itself**, while a server-created object gets a master id.
*Falsified if* the list-1 element's ids are all ≥ 512 / multiples of 16 — then 18 was assigned
server-side when the command was applied and the design command carries no id at all, which is a
materially different thing for a reimplementation to emit.
### P4 — the probes on W1
| probe | prediction | what a miss means |
|---|---|---|
| `RunTaskList 0x006b3320` | **6** = 3 AI players × 2 passes | 2 ⇒ only one player's task list runs and 496/512 reach `ResTNm` by some other path; 0 ⇒ the instrument is broken, not the game |
| `BuildTurnCommands 0x00783780` | **4**, one per submitting block | ≠ 4 ⇒ P1 is wrong from the other side |
| `RequestBuildForTask 0x006cea50` | **> 0 and even-ish** — entered on both passes, emits only on pass 1 | entries only in pass 1 ⇒ the caller gates before it, and AI3 §2.3's "the hub returns an empty list on pass 0" is not the whole mechanism |
| `AssignFleetsAndIssueOrders 0x006c16c0`, `IssueRouteForFleets 0x006bbd50` | **entered, both passes, zero list-8/10/14 elements** | entered + elements ⇒ P2 falsified |
| `AITRaid::Execute 0x0068e670` | **0** | — |
| `ClientOrder 0x007635f0` (list 16) | **0** | — |
| the six shared planner bodies (`AITColonize` 0x0068b400, `AITEscortGateInvade` 0x0068c7c0, `AITInvade` 0x0068d7a0, `AITNodeBore` 0x0068e590, `AITBuildPoliceShips` 0x00690380, `AITBuildDeepScanShips` 0x006901a0) | **at least one build-shaped body entered in both passes; at most one produces the single list-3 element** | none entered ⇒ the list-3 element comes from a task nobody has named, and AI3 §5's path table is not the emission path |
| `AITAdvanceIdleShips::Execute 0x0068f230` | entered (priority 0, always last) | — |
| `IsClaimedByAnotherTask 0x006a8d20` | **> 0** | — |
**Stated in advance so it cannot be claimed afterwards: W1 almost certainly cannot settle AI3 §2.4.**
A turn-1 AI with one colony and no fleet of its own has nothing to raid, so `AITRaid` is predicted
not to run at all, and a zero at `0x007635f0` will then be a *non-answer* about pass 0 — exactly the
"did not fire" vs "fired and found nothing" distinction rule 20 is about. The AITRaid question needs
`AITRaid::Execute` to be entered, and W2 is the better chance.
**Also stated in advance: this probe set cannot settle AI3's P3.** An entry counter on
`IsClaimedByAnotherTask` measures the "called often" half only; the steal branch at `0x006a8ded` is
*inside* the function and an entry probe cannot see it. P3 stays open.
---
## 2. Workload W2 — `ref-turn2.sav`, one End Turn (the turn 2→3 transition)
`ref-turn2.sav` **is** `turn2-state.sav` (same sha256). Corpus facts for 2→3:
* `ModCount` 12 → 24; `NMnx` 109 → 111 (ship **1760**, fleet **1776** `"Gamma Fleet"`);
* fleet **1744** `"Alpha Fleet"` is **gone**, replaced by fleet **34** `"Beta Fleet"`, in transit
(`FtTrans 1`), flight plan one waypoint `Wpt 272`, `pnd 288`, ETA 3, origin = Ke'Dolarra's
position;
* no `ResTNm` change on any player; `ResRate` already 0.8.
AI4 §2.1 attributes the twelve as 4 rate + list 5 + list 3 + list 10 + **2 × list 14** + list 8 + 2
drivers.
### P5 — the block
**Prediction:** `n == 4`; blocks 16, 496, 512 carry the rate gate and **nothing else** (no target
gate this turn); block 32 carries the rate gate and lists **3 = 1, 5 = 1, 8 = 1, 10 = 1, 14 = 2**,
everything else empty.
### P6 — list 14 is two elements against one fleet, and the second field is the discriminator
Lane Q read the element as `{i32, i32, bool}`; `human-turn2-orders.sav` (a UI fleet move) carries
exactly one, `{1456, 0, true}`. AI2's P1 says the AI's bridge calls the adder twice with mode 0 and
mode 1 and the adder keys on `(fleet, mode)`.
**Prediction: the two list-14 elements are `{F, 0, b}` and `{F, 1, b}` with the same `F`, and `F` is
also the fleet id list 8's element names.** That converts AI2's P1 from an inference about bump
counts into a read of the values.
*Falsified if:* both elements carry the same second word (then the adder does not key on mode and
the doubling has another cause); or the two fleet ids differ (then it is two orders against two
fleets and the "one order costs two elements" rule is wrong); or list 14 has one element (then
AI4's two list-14 bumps came from two different fleets and the reference turn moved two).
### P7 — which fleet id the order names
**Prediction: `F == 1744`** — the fleet that exists at submit time — and **34 does not appear
anywhere in the block.** 34 is created when the order is applied, from the same small-id space as
design 18.
*Falsified if* `F == 34`: then the client creates the fleet object *before* submitting and ships the
new id in the command, which would make the small-id space **client-allocated and part of the wire
protocol** — a much bigger constraint on a reimplementation than the alternative, because our engine
would have to reproduce that counter exactly to get byte-identical saves.
This is the one prediction I would most like to be wrong, because the falsifier is the more
interesting world.
### P8 — list 10's first word
Lane AI4 §4.5 refused to name list 10 (`{i32, i32, counted i32}`) on positional evidence alone.
**Prediction: its first `i32` is `F`, and its counted vector holds ship ids** — on W2 that means the
single ship **1728**. If so, "assign these ships to this fleet" is supported by values rather than
by adjacency, and the list can be named.
*Falsified if* the counted vector is empty or holds system ids (W2's route is a single hop to 272,
so a `{…, 272}` tail would make it a route-shaped command instead).
### P9 — AITRaid, the second attempt
Player 32 now owns a fleet, so `AITRaid` has a candidate. **Prediction: `AITRaid::Execute` is
entered on both passes and `ClientOrder 0x007635f0` is entered zero times**, i.e. list 16 stays
empty and AI3 §2.4's inference holds — but *for the reason the probe can see*, which is the point.
*Falsified if* `0x007635f0` is entered with `pass == 0` recorded: then pass 0 **does** emit, AI3's P2
is falsified, and every `ModCount` arithmetic in `ai-order-emission.md` that assumes one emitting
sweep is off by a factor.
---
## 3. Rule 19 — the controls, and what they are compared against
Published oracle bytes (sha256 prefixes, `findings/subsystems/running-the-game.md`):
| workload | `(Autosave EndTurn).sav` | `(Autosave).sav` |
|---|---|---|
| W1 from `turn1-state.sav` | `a3f9dc4b…` | `ab4ac2d7…` |
| W2 from `ref-turn2.sav` | `bb4fd9ac…` | `978041ac…` |
Three configurations per workload, each a separate launch:
| run | `shim.cfg` | purpose |
|---|---|---|
| **C** | `hooks=off` | proves **this clone** reproduces the oracle at all. VM145 is a ZFS clone; nothing has ever checked that it is byte-faithful. If C fails, the lane reports that and stops — every later number would be measured against an unknown baseline. |
| **A** | `aiorders=on`, `aiprobes=off` | the block dump alone: one detour |
| **B** | `aiorders=on`, `aiprobes=all` | the dump plus sixteen entry probes |
**Prediction:** C, A and B all reproduce both oracle hashes. **Falsified if** A or B moves a byte —
in which case that configuration's numbers are reported as unmeasured, not as results, and the
sixteen-probe set is bisected with `aiprobes=N`. Lane H's precedent is explicit that a MinHook
detour has changed this game's behaviour once already, and that the probe module is the one it
happened to.
---
## 4. What this lane will not do, said now
1. **It does not close AI3's P3** (§1, P4). An entry probe cannot see a branch inside the callee.
2. **It does not test the eleven "free" lists** (AI4 P1's untested half) unless one of them turns up
populated, which P2/P5 predict will not happen. If one does, that is the result.
3. **It does not settle the Hiver gate** (AI4 P3). The corpus has no Hiver player and this lane is
not manufacturing one.
4. **It reads element bytes, not element types.** The dump emits a fixed 48-byte window per element
plus the same bytes as ints and floats; decoding to lane Q's records happens offline, in the
report, where a wrong record is visible as a wrong value rather than being baked into the
instrument. Where a list's in-memory element is larger than 48 bytes the dump says so rather than
silently truncating.
5. **The dump's own coverage check**: for every list it walks the node chain *and* reads `_Mysize`,
and logs a `MISMATCH` line if they disagree. A wrong list layout would otherwise print a
confident, wrong zero (rule 1).