AP: predictions for the spy program's remaining draws, committed before the build

P's three sites are two mutually exclusive branches on the spy owner's Species;
only 0x00840a3c is reachable from a Human empire. Species table decoded from the
corpus. Predicted trajectory for the 20-turn wait, including the counter-mission
roll 0x0088dc43 firing at Frame 19, and the deploy NextFloat 0x0078c97f.
This commit is contained in:
alex 2026-09-09 00:25:11 -04:00
parent 5e4c27772d
commit f8ff1b6fa9

View file

@ -0,0 +1,301 @@
# The spy program's remaining draws — `P`'s branch split, the counter-mission roll, and the deploy's own `NextFloat`
- **Type:** subsystem (static decode + live measurement)
- **Owner / date:** lane AP · 2026-09-09 · guest **VM144** (`sots-re-win10-144`, `re@192.168.10.144`)
- **Instrument:** lane Z's `draw_sites` return-address ledger at **`probes=8`** (the byte-neutral set;
never `probes=11` — lane H bisected 11 as not byte-neutral)
- **Continues:** lane AS's `findings/subsystems/spy-detection-roll.md`, whose §6 named this workload
from its own failed conjunct; lane AG's `findings/control-flow/gate-indexed-rng-audit.md` §3.2
- **Inputs:** `verify/results/saves/as-turn15-spydeployed.sav` (target 1),
`verify/results/saves/as-turn14-predeploy.sav` (target 2)
*(Sections 0–3 were written and committed **before** the build and before any measurement, in their
own commit. Sections 4 onward are the measurement. Rule 2.)*
---
## 0. Headline of the predictions
> **The brief that created this lane asks for "`P`'s three draws". There are three sites but they are
> two mutually exclusive branches, and the branch selector is the spy owner's *species*.** From
> `as-turn15-spydeployed.sav` **exactly one** of the three can ever fire — `0x00840a3c` — because the
> spy's owner is player 0 (`re`), `Species = 0` (Human). `0x00840929` and `0x008409c7` are gated on
> `spyOwner->Species == 6`, i.e. **Morrigi**, and are unreachable from this save, from every save in
> the 22-save corpus, and from any workload that does not start a *Morrigi* empire.
>
> Predicted, before the run: `0x00840a3c` fires once, for one word, on the End Turn that produces
> `Frame == sdet + 3`; and two sites nobody expected fire on the way — the spy **counter-mission**
> roll `0x0088dc43` (lane AG §3.2, corpus 0 of 22, never fired) starting at **Frame 19**, and the
> deploy's own `NextFloat` `0x0078c97f` on a separate one-turn run.
---
## 1. `P` decoded — `SpyManager::Slot13RngCallee` `0x008408e0`, complete
Program `sots` / "Sword of the Stars.exe", ImageBase `0x00400000`, all addresses VAs. Body
disassembled from its start **to the next function start** (rule 17): `0x008408e0 .. 0x00840a5a`,
followed by `int3` padding. The ReVa MCP server is down (`CONNECTION_CLOSED`) for this lane too, so
this is instruction stream only.
### 1.1 How `P` is reached, and that there is no further gate
The chain in `ServerSpyManager::vslot13` `0x008877b0` from lane AS's §1, extended past the point AS
stopped at. **Between the last gate and the call to `P` there is no branch at all** — the whole span
`0x00887897 .. 0x00887af2` is two `EVENT_SPY_DESTROYED_OWNER`-shaped string builds, a `push_back`,
and an event post:
```
0088781d je 0x887e92 G1 spy.deat (+0x10) == 0 -> next spy
00887856 je 0x887c5b G2 spy.sdet (+0x40) == -1 -> BRANCH D (the detection roll)
00887867 jl 0x887e92 G3 (server.Frame - sdet) < 3 -> next spy
0088787a je 0x887c4f G4 ServerSystem_GetOwner(sysA) == NULL -> sdet := -1, next spy
00887891 je 0x887c4f G5 ServerSystem_GetOwner(sysA) == ownerO -> sdet := -1, next spy
008878a1 call 0x59f1a0 push_back spy.sid into a LOCAL vector<int> at [ebp-0xdc]
008879dd call 0x8862b0 PostEvent "EVENT_SPY_DESTROYED_OWNER"
00887ae3 call 0x7437e0 eax = ServerSystem_GetOwner(sysA)
00887aee push eax ; arg2 = the TARGET system's owner
00887aef push edx ; arg1 = ownerO, the SPY's owner ([ebp-0xc0])
00887af0 mov ecx,edi ; this = the spy manager
00887af2 call 0x8408e0 P <-- the false-flag draw
00887af7 ...
00887b07 cmp eax,ebx
00887b09 je 0x887b5b ; P returned NULL -> the "unknown empire" event string
```
so **once `sdet` is stamped and three turns have passed, `P` is called unconditionally** (given a
target system that is owned and not owned by the spy's own empire). `P`'s arguments are two
`Game::ServerPlayer*`, not ids — `P` compares them by pointer against entries of the player vector.
### 1.2 The species branch — and why "three draws" is one draw
```
00840909 mov esi,[ebp+8] ; esi = arg1 = the SPY'S OWNER (a ServerPlayer*)
0084090c cmp dword ptr [esi+0x5c], 0x6 ; +0x5c = ServerPlayer::Species
00840910 jne 0x840a29 ; NOT species 6 -> BRANCH B
; ---- BRANCH A, species == 6 only --------------------------------------------------------------
00840919 fld dword ptr ds:0x9e5ac4 ; 0.75f (bytes 00 00 40 3f)
0084091f mov ecx,[eax+0x16c] ; THE STRATEGIC GENERATOR (the object, not +4)
00840929 call 0x8e6dd0 A1 ; Mars::RNG::Chance(strategic, 0.75f) ret 0x0084092e
00840930 je 0x840a45 ; roll failed -> return NULL
; build a candidate list over StrategyServer +0x54..+0x58 (the player vector):
; skip p if p->+0xfb != 0, p->+0xf8 != 0, p == arg1, or p == arg2
008409ac je 0x840a07 ; list EMPTY -> free it, return NULL (no NextInt)
008409ae eax = count - 1
008409b0 mov ecx,[...+0x16c]; add ecx,4 ; the +4 SUB-OBJECT (the NextInt convention)
008409c7 call 0x4271c0 A2 ; Mars::RNG::NextInt(&(count-1)) ret 0x008409cc
008409cf esi = list[eax] ; the blamed empire
return esi
; ---- BRANCH B, every other species ------------------------------------------------------------
00840a2c fld dword ptr ds:0x9e5ac0 ; 0.25f (bytes 00 00 80 3e)
00840a32 mov ecx,[edx+0x16c] ; the strategic generator
00840a3c call 0x8e6dd0 B1 ; Mars::RNG::Chance(strategic, 0.25f) ret 0x00840a41
00840a43 jne 0x8409f2 ; roll SUCCEEDED -> return esi == arg1 (the true owner)
00840a45 xor eax,eax ; else return NULL
```
Both float constants were read as the **four bytes in the image** (rule 23): `0x009e5ac0` is
`00 00 80 3e` = `0.25f` and `0x009e5ac4` is `00 00 40 3f` = `0.75f`. `0x009e5ac4` is the *same* word
lane AS read as the species-5 multiplier in `SpyCraft_AccumulateDetectionOdds` — one constant, two
uses.
**So `P` is the attribution roll for a destroyed spy, and it has two completely different shapes:**
| spy owner | draw sites reached | semantics |
|---|---|---|
| `Species == 6` | `0x00840929` (`Chance` 0.75f), then `0x008409c7` (`NextInt`) **only if the roll succeeds and the candidate list is non-empty** | 75 % of the time a **random third empire** is blamed — a false flag. The true owner is *never* returned. |
| every other species | `0x00840a3c` (`Chance` 0.25f) | 25 % of the time the **true owner** is returned; otherwise nobody is blamed. |
### 1.3 The species table, decoded from the corpus
`Game::ServerPlayer +0x5c` is the field `objects/layouts.md` names **`Species`** (Write
`0x008563e0`). The `sim/species` node lists seven `ISsp` names in index order, identical in every
save read:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Human | Hiver | Tarkas | Liir | **_NPC** | Zuul | **Morrigi** |
Cross-checked against lane V's `zuul-turn5-species5.sav`, whose three real players all carry
`Species = 5` — and lane V's hook independently reported `species=5` on the Zuul double-roll. The
four NPC empires (`Alien Menace`, `Peacekeeper Enforcer`, `Von Neumann`, `Independent Colony`) carry
`Species = 4` in every save.
That also **names lane AS's two unexplained multipliers**: `SpyCraft_AccumulateDetectionOdds`
multiplies the per-turn detection odds by `0.75f` for **Zuul** and `0.5f` for **Morrigi**, and by
`1.0f` for everyone else. Morrigi are the stealth species on both sites — half the detection odds,
and a false flag when they are caught.
### 1.4 What happens to the spy on that turn
The local `vector<int>` filled at `0x008878a1` is drained after the spy loop:
```
00887eb0..00887ee8 for each collected sid, back to front:
[[manager]+0x28](sid) ; vtable slot 10 = 0x00838480
```
and `0x00838480` looks the `SpyCraft` up by `sid`, calls **`ServerSystem::RemoveSpy 0x0074f550`** on
the `deat` system (which clears `spy.deat` and erases `sid` from `spies2`), clears the tender's
`atto` back-pointer (`spy+0xc`, `ship+0xa8`), erases the craft from the manager's vector at `+0x10`,
and invokes the scalar deleting destructor. **The spy is gone at the end of `vslot13`**, which is
phase 23 — *before* `vslot14` runs at phase 33.
### 1.5 The deploy's `NextFloat` — `0x0078c97f`, re-read
`SHIPACTION_DEPLOYSPY` handler `0x0078c930`, body to the next function start `0x0078c9e0`. One gate,
then the draw, with no branch between them:
```
0078c93d..0078c964 build a 12-byte stack query object (vtable 0x00a09648) and ask it about the ship
0078c969 test al,al
0078c96b jne 0x78c975 ; FALSE -> return 0 with NO DRAW
0078c975 mov ecx,[esi+0x16c]; add ecx,4 ; the strategic generator's +4 sub-object
0078c97f call 0x47d830 ; Mars::RNG::NextFloat ret 0x0078c984 <-- THE DRAW
0078c984 fld qword [0x009e21b0] ; 6.2831854820251465 == 2*pi
0078c98a fldz ; LO = 0.0
angle = LO + (HI - LO) * r ; the float-range idiom, stored through a float32 temp
0078c9ac call 0x0080c860 ; (ship, angle) -> eax
0078c9bb call edx ; [[esi+0x15c]+0x1c] = ServerSpyManager::DeploySpy slot 7
```
Lane AS read this as `cbh = NextFloat() * 2pi`; that is right in value (`LO` is literally `fldz`) and
the expanded form is worth recording because it is the campaign's standard `lerp(LO, HI, r)` shape,
not a bare multiply. **The draw is unconditional once the validator passes, so it is exactly one
strategic word per applied Deploy Spy order**, in the `ProcessTurn` bracket
(`ApplyAllTurnCommands`), not the tail's.
**Its predicate is on the command stream**, not on save fields: *"a `SHIPACTION_DEPLOYSPY` command in
this turn's command stream"*. Lane AG's gate-indexed audit has two kinds of cell — a measured firing,
or a predicate over the save — and this is neither.
### 1.6 A complete RNG scan of all four bodies
Every `call`/`jmp` in each body decoded to a real instruction boundary and matched against the seven
RNG entry points (`NextFloat 0x0047d830`, `NextInt 0x004271c0`, `Chance 0x008e6dd0`,
`Twist 0x00426e00`, `Seed 0x0049fdf0`, `0x005876c0`, `0x0057da00`):
| body | range | RNG sites |
|---|---|---|
| `ServerSpyManager::vslot13` | `0x008877b0 .. 0x00887f30` | `0x00887c8a` only |
| `SpyManager::Slot13RngCallee` (`P`) | `0x008408e0 .. 0x00840a5a` | `0x00840929`, `0x008409c7`, `0x00840a3c` |
| `SHIPACTION_DEPLOYSPY` handler | `0x0078c930 .. 0x0078c9e0` | `0x0078c97f` only |
| `ServerSpyManager::vslot14` | `0x0088db80 .. 0x0088dd2c` | `0x0088dc43` only |
No inlined draw is present in any of them (rule 16's tempering immediates do not appear).
---
## 2. The corpus and the state, counted before building (rule 28 practice 5)
`as-turn15-spydeployed.sav`, read with `verify/save-reader/save_reader.py`:
| field | value | consequence |
|---|---|---|
| `sim/Frame` | 15 | |
| spy `sid` | 1 | |
| spy `sown` | 16 | player 0, `re` |
| **`re`'s `Species`** | **0 (Human)** | **`P` takes branch B; `0x00840929` and `0x008409c7` are unreachable** |
| spy `deat` | 400 (Kepler) | G1 satisfied — lane AS's contribution |
| spy `sdet` | **−1** | **the failed conjunct; G2 sends us to branch D every turn** |
| spy `sdo` | 0.00839999970048666 | |
| spy `tdep` | **15** | `vslot14`'s `Frame − tdep ≥ 3` first holds at **Frame 18** |
| spy `cm`, `cmo`, `spyon`, `ncp` | all 0 | `vslot14` G4 fails today; it bootstraps itself at Frame 18 |
| spy `cbh` | 5.033599376678467 | `= 2π × 0.8011207…`, i.e. `0x0078c97f` already fired once, unmeasured |
| players | 7 — `re`(0), `Revenge Fleet`(0), `Spengler`(0), 4 × `_NPC`(4) | branch A's candidate list would be non-empty, if it were reachable |
**Corpus count for `P`, on the predicate `∃ spy with deat != 0 && sdet != -1 && Frame − sdet ≥ 3`:
0 of 22 before this lane, and 0 of 23 including `as-turn15-spydeployed.sav`.** The failed conjunct is
`sdet == -1`, exactly as lane AS handed it over — and it is not a conjunct any amount of clicking can
satisfy directly. It is satisfied by *waiting*, at a probability the code fixes.
**Corpus count for `0x0078c97f`:** not expressible over save fields at all (§1.5). The nearest
save-side witness is `cbh != 0`, which holds in **1 of 23** saves — `as-turn15-spydeployed.sav`,
because the site fired on the turn that produced it.
**No save needs to be manufactured for either target.** Lane AS built both states. This lane
generates no map and plays no new game.
---
## 3. Predictions
Committed before the build, before the deploy of any binary to VM144, and before any End Turn.
### 3.1 The static claims
| | prediction | how it is wrong |
|---|---|---|
| **PA1** | `P` reaches **at most one** draw site per call, selected by `spyOwner->Species (+0x5c) == 6`. From this save, only `0x00840a3c`. | a `draw_sites` row at `ret_rva 0x0044092e` or `0x004409cc` on any turn of this workload falsifies it outright |
| **PA2** | Species indices are `0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi`; therefore `AccumulateDetectionOdds`'s `0.75f` is **Zuul** and its `0.5f` is **Morrigi** | a save whose `sim/species` list is ordered differently, or a Zuul save with `Species != 5` |
| **PA3** | `0x00840a3c` is `Chance(0.25f)` — a widened `float`, not the decimal 0.25 read from a decompiler | the four bytes at `0x009e5ac0` are not `00 00 80 3e` |
| **PA4** | the spy is **destroyed** at the end of `vslot13` on the turn `P` runs: `nspy` 1 → 0, `spies2` at Kepler 1 → 0, and no spy record survives in the autosave | the post-`P` autosave still carries a `spy` record |
### 3.2 Target 1 — the wait, and the per-turn word trajectory
Let `T` be the `Frame` on which `sdet` is stamped. `sdo` grows by **0.0084 per turn** and the roll on
the End Turn producing `Frame f` uses `sdo = 0.0084 × (f − 14)`, because `AccumulateDetectionOdds`
runs *before* the gate. `Chance` costs one word for `0 < p < 1` (board row 367).
| | prediction |
|---|---|
| **PB1** | `sdo` in the autosave after the End Turn producing `Frame f` is exactly `0.0084 × (f − 14)` as a float32, for every `f` until detection: 0.0168, 0.0252, 0.0336, 0.0420, 0.0504, … |
| **PB2** | `OnAllCombatDone_Tail` costs **1** word on Frames 16, 17, 18 (`vslot13`'s detection roll alone) |
| **PB3** | **`0x0088dc43` fires for the first time in this campaign on Frame 19**, one call, one word, `ret_rva 0x0048dc48`, `strategic = true` — because `vslot14` bootstraps its counter-mission at Frame 18 (`Frame − tdep = 3`, `ncp` empty), setting `spyon`, `cm := 1`, `cmo := 0.2f`, **without drawing on Frame 18 itself** |
| **PB4** | from Frame 19 the tail costs **2** words per turn (`vslot13` 1 + `vslot14` 1), and drops to 1 on any turn where `cmo` has accumulated to ≥ 1.0 or where `cm` has just cycled back to 0 |
| **PB5** | save-side: `cm` moves off its corpus-constant 0 on **Frame 18**, `spyon` moves to the Kepler owner's id on Frame 18, and `cmo` starts stepping by `0.2f` per failed counter-mission roll from Frame 19 |
| **PB6** | on the detection turn `T`: `vslot13` still costs 1 word (the roll that *succeeds*), `sdet := T` |
| **PB7** | on `T+1` and `T+2`: **`vslot13` costs ZERO words** — `sdet != -1` skips branch D, and `Frame − sdet < 3` skips the destruction path. A zero at `0x00487c8f` on those two turns is a *positive* result about a third arm and must not be read as "the roll did not fire" (rule 28 practice 6) |
| **PB8** | on `T+3`: one row at `ret_rva 0x00440a41`, `entry = Chance`, `calls = 1`, `words = 1`, `no_draw_calls = 0`, `strategic = true`; `vslot13`'s own `0x00487c8f` row **absent**; `vslot14` contributes **0** because the spy was destroyed at the end of phase 23 |
| **PB9** | the entry probe on **`Game::SpyManager::Slot13RngCallee 0x008408e0` reads 1** on Frame `T+3` — its first non-zero in the campaign, and the complement of lane AS's demonstration that its zero said nothing about the subtree |
| **PB10** | cumulative detection probability: **11 % by +4 turns, 43 % by +10, 70 % by +15, 87 % by +20** — computed from `p_k = 0.0084(k+1)`. *(Lane AS's brief quoted 38/65/85; that is the same model evaluated one turn late, using `p_k = 0.0084k`. Minor, and stated so the difference is on the record before the run.)* |
| **PB11** | the tail bracket total over the whole run equals the per-site sum: **residual 0** on every turn |
**PB12 — the tail case, stated in advance.** At +20 turns there is a 13 % chance `sdet` is still −1.
If that happens, the result reported is the **`sdo` trajectory against PB1** plus the `0x0088dc43`
series, and `P` stays a decoded-not-measured site. That is a real measurement of the accumulator and
it will be reported as such rather than ground out.
### 3.3 Target 2 — the deploy order
From `as-turn14-predeploy.sav`, one Deploy Spy order, one End Turn, `probes=8`:
| | prediction |
|---|---|
| **PC1** | one `draw_sites` row at `ret_rva 0x0038c984`, `entry = NextFloat`, `calls = 1`, `words = 1`, `no_draw_calls = 0`, `strategic = true` |
| **PC2** | it lands in the **`ProcessTurn`** bracket, not `OnAllCombatDone_Tail`'s, because `ApplyAllTurnCommands` runs first |
| **PC3** | the same turn's `OnAllCombatDone_Tail` costs **exactly 1** — the first detection roll at `sdo = 0.0084` |
| **PC4** | the resulting `spy.cbh` reproduces lane AS's value **exactly**: `5.033599376678467`, i.e. `2π × 0.80112…`. The strategic generator is seeded from the save and the per-process AI client seed does not feed it, so the same input plus the same order must draw the same word. **This is the sharpest single prediction in the document**: it says the deploy draw is reproducible across processes, sessions and lanes |
| **PC5** | `spies2` at Kepler goes 0 → `[1]`, `deat` 0 → 400, `tdep := 15`, `sdo := 0` then `0.0084` — reproducing lane AS's §4.2 from a different process |
### 3.4 The control
| | prediction |
|---|---|
| **PD1** | two fresh `hooks=off` processes on the **same** input will produce a byte-identical *pre*-turn `(Autosave EndTurn).sav` and **differing** post-turn autosaves |
| **PD2** | the differences localise to **one AI player's research pick plus `Summary/Checksum` and the `turnstats` `tch` cell** — the per-process `StrategyClient` seed of `2026-09-08-ai-seed-per-process.md`. Nothing of my empire, the spy, `deat`, `sdet`, `sdo`, `cm`, `cmo` or `spies2` moves |
| **PD3** | therefore **this is not a calibration pair and must not enter `determinism-hashes.txt`** — stated in advance so it cannot be a post-hoc excuse (rule 26) |
| **PD4** | the leaves this lane's numbers depend on — the `spymgr` sub-tree and the `systems` sub-tree — **do** reproduce across the control pair, and that is the claim that will be checked by sub-tree rather than by whole-file hash |
### 3.5 How the whole model could be wrong
* **`countC` is not stable.** `sdo`'s per-turn increment is `0.7 × (0.01 + 0.001 × countC)` with
`countC = 2` inferred, not observed. If a fleet moves in or out of Kepler the increment changes and
PB1 breaks — **and that would be the most interesting failure available**, because it would
identify `countC` from the delta. Symptom: an `sdo` step that is not 0.0084.
* **`vslot14` might not bootstrap.** AG's `ncp` check is on a container this lane has only seen as
the integer 0. If `ncp` is a count and non-empty means something else, PB3 fires late or not at
all. Symptom: Frame 19 tail costs 1, not 2, and `cm` stays 0.
* **The stage machine consults `owner->+0xff` (`CnTrd`, FTL Economics) at 1 → 2/3.** This game has no
FTL Economics, so the 1 → 3 arm is the one that runs; that changes which stage sequence is
exercised but not any word count.
* **PB7's two zero turns are the polarity trap in a new place.** `vslot13` will be *entered* on
`T+1`/`T+2` (the probe reads 1) and cost 0. If this lane reported that as "the roll stopped
working", it would repeat exactly the mistake rule 28 exists for.
* **PC4 can fail benignly.** If AI orders applied before mine consume strategic words, `cbh` differs
and the deploy draw is still 1 word. That falsifies *reproducibility*, not the site.
---
*(End of the pre-registered section. Everything below is measurement.)*