sots-re/findings/subsystems/spy-program-draws.md

70 KiB
Raw Blame History

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.)


4. What was run

Shim build ap-989c692-20260909T0425Z, cross-built on CT111 in this lane's own directory /srv/re-lab/build/sots-engine-ap after rm -rf of the whole tree, from a lane-private git worktree at ~/sots-engine-ap with no local build* shipped (rules 21 and 24). binkw32.dll sha256 ae084ed4774caf24ea7927fcb012d1999633f10556c7c99f0209f66067416a17. Staged to /srv/re-lab/shim/dist-ap, deployed to C:\SOTS\shimdist-ap on VM144. This lane wrote no engine code; shim.cfg.hp8 and shim.cfg.hoff were used unmodified.

run config input End Turns purpose
W shim.cfg.hp8 (probes=8) MyGameas1spydep.sav (Frame 15) 11 (15 → 26) target 1
D shim.cfg.hp8 MyGameas1predeploy.sav (Frame 14) + one Deploy Spy order 1 target 2
OA / OB shim.cfg.hoff MyGameas1spydep.sav 1 each, two fresh processes control (§7)

Instrument armed and verified from shim.log, not assumed (rule 1). Both instrumented runs: seven drawsite: detours create=MH_OK enable=MH_OK; config: probes=8 -> 8 lane-H entry probes and all eight create=MH_OK enable=MH_OK; zero COVERAGE: lines; zero config: ignoring unknown key lines; draw_site_overflow = 0 on every turn. Probe indices 8–11 report NOT INSTALLED, never 0. The one MH_ERROR_ALREADY_CREATED line is the fpu module losing the race for BeginProcessTurn to the ledger; it is present verbatim in lanes AC, AD, AR and AS's logs, so this is the same instrument those lanes ran.

probes=11 was not used anywhere in this lane. No number here is comparable to lane AC's turn totals.

4.1 The wait cost eleven minutes, not an evening

The brief budgeted "~20 End Turns, expensive in wall-clock". On this state an End Turn under probes=8 completes in 6 seconds; the eleven turns took 11 minutes including the load. The expensive part is the launch (~2 min through the intro movies) and the load (~3 min). Turn 27 hit a Von Neumann encounter dialog at Rigel and the driver stopped on it exactly as designed; by then the question was answered and the run was ended there rather than clicked through.

The driver is worth carrying forward: a scheduled task (SOTSAP) running an in-guest loop that clicks End Turn, then polls the autosave's mtime — never a button colour, which is lane AS's lesson — snapshots (Autosave).sav per turn, and stops on the first stall with a screenshot. It is left at C:\SOTS\ui\ap\ap_loop.ps1.


5. Target 1 — the wait, measured turn by turn

Detection landed on Frame 22, seven turns in. P therefore ran on Frame 25.

5.1 The per-turn ledger

Every row is draw_sites at the post-turn autosave marker, which the shim resets at the pre-turn marker — so these are per-turn figures, not cumulative.

Frame 0x00887c8a spy 13 0x0088dc43 spy 14 0x00840a3c P B1 0x00820e1d trade raid tail bracket probe vslot13 probe Slot13RngCallee
16 1 — — — 1 1 0
17 1 — — — 1 1 0
18 1 — — — 1 1 0
19 1 1 ← first ever — — 2 1 0
20 1 1 — 2 4 1 0
21 1 1 — 4 6 1 0
22 1 ← succeeds, sdet := 22 1 — 7 9 1 0
23 — 1 — 7 8 1 0
24 — 1 — 8 9 1 0
25 — — 1 ← first ever 7 8 1 1 ← first ever
26 — — — 8 8 1 0

Every cell is calls = words, no_draw_calls = 0, strategic = true. The tail bracket equals the sum of its rows on all eleven turns: residual 0 every turn, and draw_site_overflow = 0.

Three lines of that table are the lane:

Frame 25: entry = Chance, ret_rva = 0x0044_0a41 (VA 0x00840a41, the instruction after call 0x8e6dd0 at 0x00840a3c), calls = 1, words = 1, no_draw_calls = 0, strategic = true. 0x00840a3c had never fired in this campaign.

Frames 23 and 24: no row at 0x0048_7c8f at all, while the entry probe on ServerSpyManager::vslot13 reads 1 on both. That is "entered and gated", not "did not run" — rule 28 practice 1, in a third distinct arm of the same function.

Frame 25, the probe column: Game::SpyManager::Slot13RngCallee 0x008408e0 reads 1. Lane AS demonstrated that this probe's zero said nothing about the subtree. This is the other half: the same probe, on the one turn the callee is actually entered, reads one.

5.2 0x00840929 and 0x008409c7 did not fire, and could not have

Neither site appears in any of the eleven turns, including Frame 25 — the one turn P ran. PA1 held. The reason is not a state we failed to build: P's first instruction pair is cmp [spyOwner+0x5c], 6 / jne, the spy's owner is player 16 re with Species = 0 (Human), and the launch card confirms it in the UI. Branch A is a Morrigi-only path.

The brief that created this lane, lane AS's §6, board row 207 and lane AG's §6 all list 0x00840929 / 0x008409c7 / 0x00840a3c as three sites behind one gate. They are two mutually exclusive branches selected by species, and no workload can fire more than one of them per call. To reach the other two, a lane must play a Morrigi empire, deploy a spy, and lose it.

5.3 sdo — the accumulator, exact to the float32

Read from the eleven autosaves. Prediction PB1 was sdo(f) = 0.0084 × (f − 14) as a float32.

Frame 16 17 18 19 20 21 22 23 24
sdo 0.016800 0.025200 0.033600 0.042000 0.050400 0.058800 0.067200 0.067200 0.067200
bits 3c89a027 3cce703a 3d09a027 3d2c0831 3d4e703b 3d70d845 3d89a027 frozen frozen

Seven consecutive exact steps of 0.0084, then frozen the moment sdet stamps — because branch D is the only writer of sdo and sdet != -1 skips it. The freeze is a second, save-side proof that the branch stopped running on Frames 23 and 24, independent of the ledger's zero.

Lane AS measured two points of this line (0 → 0.0084 → 0.0168) and derived the rest. Nine points now lie on it, and countC stayed at 2 for the whole window, so sdo's per-turn increment is a constant on this state rather than a fitted one.

5.4 The counter-mission machine, measured for the first time

0x0088dc43 (lane AG §3.2) had corpus count 0 of 22 and had never fired. It fires here from Frame 19, one call and one word per turn, and the save-side state machine is fully visible:

Frame 18 19 20 21 22 23 24 25
spyon 32 32 32 32 32 32 32 —
cm 1 1 2 2 3 3 4 —
cmo 0.2 0.4 0.2 0.4 0.2 0.4 0.2 —
ncp 0 0 1 1 2 2 3 —
words at 0x0088dc43 0 1 1 1 1 1 1 0

Everything AG decoded statically is confirmed, and four things are added:

  1. The bootstrap turn costs nothing. Frame 18 is Frame − tdep == 3; vslot14 sets spyon, runs the stage machine cm: 0 → 1 and cmo := 0.2f, and does not draw. The first draw is Frame 19. PB3 predicted exactly this and it held.
  2. cmo steps by 0.2f on failure and resets to 0.2f on success, as decoded. The stage advanced on Frames 20, 22 and 24 — always on the second try, at p = 0.4. Six rolls, three successes; expectation at (0.2, 0.4) alternating is 2.4. Consistent, not validated.
  3. ncp is a scalar counter of completed stages, not a container. It is a plain int item in the stream and it increments by exactly 1 on each stage completion (0,0,1,1,2,2,3), in lock-step with cm. AG's decode reads the cm == 0 branch as gated on "ncp non-empty"; that gate was never reached here, because cm never returned to 0 before the spy was destroyed. It stays a rule-6 hypothesis — and a sharper one now that ncp's values are known.
  4. The CnTrd the stage machine consults is the TARGET's, not the spy owner's. AG's caution to lane AS was that a spy program run without FTL Economics takes the 1 → 3 arm. This run took 1 → 2, and in the save my player (16) has CnTrd = False while Kepler's owner (32) has CnTrd = True. So owner->+0xff at that branch is the victim empire's FTL Economics. The caution as written would have mispredicted this run.

cm, cmo, spyon and ncp are four of the 234 fields the value-domain census lists as having only ever held one value. All four are now off zero, with a measured trajectory each.

5.5 The spy is destroyed at sdet + 3, and vslot13 does it

nspy goes 1 → 0 between the Frame-24 and Frame-25 autosaves, spies2 at Kepler returns to count = 0, and the spymgr record loses its only spy child. That is vslot13's drain loop at 0x00887eb0 calling manager vtable slot 10 0x00838480, which calls ServerSystem::RemoveSpy 0x0074f550. PA4 held.

It also settles the ordering question in §1.4 behaviourally: 0x0088dc43 costs 0 on Frame 25 although vslot14's entry probe still reads 1. Phase 23 destroys the spy before phase 33 walks the list.


6. Target 2 — 0x0078c97f, the site in no inventory, measured

Run D: MyGameas1predeploy.sav (Frame 14) in a fresh process under probes=8; Kepler selected on the map; Special → Deploy Spy → OK; one End Turn.

draw_sites, post-turn autosave marker, the row verbatim:

  entry           = NextFloat
  ret_rva         = 0x0038_c984        (VA 0x0078c984 -- after `call 0x47d830` at 0x0078c97f)
  calls           = 1
  words           = 1
  no_draw_calls   = 0
  strategic       = true

boundary ledger, same turn:

  BeginProcessTurn        326 -> 326    0
    ProcessTurn           326 -> 344   18      <-- the deploy draw is in here
    OnAllCombatDone_Tail  344 -> 345    1      <-- the first detection roll, sdo = 0.0084
  Autosave                345 -> 345    0

PC1, PC2 and PC3 all held exactly. The site fires once per applied SHIPACTION_DEPLOYSPY command, for one word, on the strategic generator, inside the ProcessTurn bracket — and the same turn's tail costs exactly 1, because ApplyAllTurnCommands runs before OnAllCombatDone_Tail and the freshly deployed spy is immediately eligible for its first detection roll.

It appears in no other turn of this lane: eleven turns of run W carry no row at 0x0038c984. That is the stream predicate showing its shape — the site's cost is not a function of the save at all, it is a function of what the player ordered.

6.1 The sharpest prediction held: the deploy draw is reproducible across processes

PC4. Lane AS's as-turn15-spydeployed.sav was written by a different process, on a different day, from a different build. This lane's Frame-15 autosave was written by run D.

lane AS   spy.cbh = 5.033599376678467      (float32 0x40A11333)
lane AP   spy.cbh = 5.033599376678467      identical
                    cbh / 2pi = 0.8011222000

The same input state plus the same order drew the same word, in two processes separated by hours and by a rebuild. Together with deat = 400, tdep = 15, sdo = 0.0084, sdet = -1 and spies2 = [1] at Kepler reproducing exactly (PC5), this says the strategic generator's consumption up to ApplyAllTurnCommands is deterministic given the save — the per-process AI client seed does not reach it. It is a much stronger statement than a whole-file hash comparison, because it isolates one word to one site.

6.2 A correction to lane AS's reading of the same bytes

AS gives the site as cbh = NextFloat() * 2*pi. The value is right; the expression is the campaign's standard range idiom with LO supplied by an fldz:

0078c984  fld qword [0x009e21b0]   ; HI = 6.2831854820251465
0078c98a  fldz                     ; LO = 0.0
0078c994  fsub st(1),st            ; HI - LO
0078c99b  fmulp st(1),st           ; (HI - LO) * r
0078c99d  fstp [ebp+8]             ; through a FLOAT32 temp
0078c9a0  fadd [ebp+8]             ; LO + that

so it is lerp(0.0, 2*pi, r) rounded to float32 mid-expression, not a double multiply. A reimplementation that computes r * 6.2831854820251465 in double and stores at the end can differ in the last bit.

The other half of the sequence is worth writing down because it reads wrong at first glance:

0078c998  push ecx                 ; reserve DeploySpy's SECOND argument slot
0078c9a9  fstp [esp]               ; slot := the angle
0078c9ac  call 0x0080c860          ; __thiscall getter, `mov eax,[ecx+0xa8]; ret` -- NO stack args,
                                   ;   so it leaves the angle slot untouched. eax = the spy's id.
0078c9ba  push eax                 ; the FIRST argument
0078c9bb  call edx                 ; ServerSpyManager::DeploySpy(spyId, angle)

0x0080c860 looks like it consumes the float and does not; the compiler built the argument list out of order and called a zero-argument getter across the half-built frame. DeploySpy takes two arguments, and 0x008874b0 fld [ebp+0xc] / 0x008874c0 fstp [esi+0x18] is where cbh is written. Lane AS's "spy->cbh (+0x18) := <the float argument>" is exactly right; this only names which argument and shows why an eye following eax misses it.


7. The control — a truthful non-pair, and the exposure predicate checked against it

Two fresh hooks=off processes (OA, OB) on the same input, one End Turn each, plus this lane's instrumented run W and lane AS's three runs from the day before. Six processes, one input, one End Turn.

7.1 The hashes

input      MyGameas1spydep.sav       854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08
run lane config pre-turn (Autosave EndTurn).sav post-turn (Autosave).sav size
A AS hoff 98e45d3745b91450… 262f8bda97c9511d… 77,685
B AS hoff 98e45d3745b91450… e34775a757e6fb3d… 77,649
M AS probes=8 98e45d3745b91450… 262f8bda97c9511d… 77,685
W AP probes=8 — 262f8bda97c9511d… 77,685
OA AP hoff 98e45d3745b91450… e296394f578b3a56… 77,677
OB AP hoff (§7.4) (§7.4)

The pre-turn save is byte-identical across four processes, two lanes and two builds — including this lane's own, which is not lane AS's binary. That is the strongest statement available about the input, and it is what makes the post-turn comparison meaningful.

The post-turn saves are not. Five processes have produced three distinct outcomes: 262f8bda (three times: AS's A, AS's M, this lane's W), e34775a7 (once: AS's B), e296394f (once: this lane's OA).

This is not a calibration pair and must not enter determinism-hashes.txt. Said in advance as PD3, and it is the honest verdict rather than a post-hoc excuse.

7.2 What varies, localised by sub-tree

state_checksum --no-audit --floats bits --mask none, OA against W:

DIVERGED: 15 leaf difference(s)
  /Summary/Checksum                                                 985948923 -> 985948837
  /Sim/turnstats/history/hist[1]/stats[15]/tch                             45 -> 46
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[60]                   3 -> 2
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[60]         22469 -> 0
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/Tbd[60]                  2 -> 1
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/St[64]                   2 -> 4
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TResDone[64]             0 -> 22469
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TAcq[64]                -1 -> 16
  /Sim/players/Player[32 "Revenge Fleet"]/TechTree/TiAcq[64]               -1 -> 45
  /Sim/players/Player[32]/Events/…/EvDsc  'Research Over Budget' -> 'Research Complete'
  /Sim/players/Player[32]/Events/…/EvMsg  'Research for Micro-Fusion Drives has gone overbudget.'
                                       -> 'Tech Overthrusting has been acquired'
  /Sim/players/Player[32]/Events/…/EvImg  'EVENT_RESEARCH_OVERBUDGET' -> 'EVENT_RESEARCH_COMPLETE'
  /Sim/players/Player[32 "Revenge Fleet"]/ResTNm             'DRV_McroFus' -> ''
  /Sim/players/Player[32 "Revenge Fleet"]/otch/.[34]                          only-in-B
  /Sim/players/Player[32 "Revenge Fleet"]/otch/.[0]                         33 -> 34

And the sub-tree digests say which halves of the save are affected, which is the form the claim should take (PD4):

sub-tree OA W
Sim/spymgr 9c3f224cf954aa54 9c3f224cf954aa54 identical
Sim/systems 3227b08124339fe2 3227b08124339fe2 identical
Sim/fleets 26a640cdfc597869 26a640cdfc597869 identical
Sim/trdmgr ed99071bdc121694 ed99071bdc121694 identical
Sim/players 2d1c0cad92679606 46ddf6778ef36e0b differs
Summary 66db5e1055c9fe37 cb8a4c8e22480af0 differs (the derived checksum)

Every leaf this lane's numbers depend on reproduces across the pair. spymgr — the spy's deat, sdet, sdo, cm, cmo, ncp, cbh — is byte-identical between an instrumented run and an un-instrumented one, as is every system's spies2. What does not reproduce is one AI empire's research pick and the two derived values downstream of it.

Three distinct picks across five processes is worth stating plainly, because it is the first time this outcome set has been sampled more than twice: A/M/W completed Overthrusting (tech 64), B was part-way through DRV_NodFoc, OA went over budget on DRV_McroFus. AS observed A and M agreeing and correctly flagged it as a 1/k coincidence rather than evidence about the instrument. With five samples the set has at least three members and the modal outcome took 3 of 5 — so the coincidence AS refused to lean on was about a 1-in-3 event, not a 1-in-2 one, and refusing to lean on it was right.

7.3 The seed-exposure predicate, checked (rule 26 corollary (c))

The coordinator's instruction is to record the exposure fields next to every control hash. Here they are for the pre-turn save of the control pair, and for every turn this lane played, so the predicate can be checked rather than asserted. Species = 4 (NPC) players are omitted; they never decide. Net ids: player 16 is re (human), 32 is Revenge Fleet (the AI), 48 is Spengler.

pre-turn save Frame Player[32] ResTNm NDes NOwn ships exposure
as-turn14-predeploy 14 '' 15 10 146 pick
as-turn15-spydeployed (the control input) 15 '' 15 10 146→148 pick + colonise + 2 ships
w-16 16 '' 15 11 157 pick + 9 ships
w-17 17 '' 16 12 161 pick + design + colonise + 4 ships
w-18 18 '' 20 12 169 pick + 4 designs + 8 ships
w-19 19 '' 20 12 181 pick + 12 ships
w-20 20 'WEP_AmWhd' 20 12 185 4 ships
w-21 21 'WEP_AmWhd' 23 12 201 3 designs + 16 ships
w-22 22 'WEP_AmWhd' 27 12 218 4 designs + 17 ships
w-23 23 'WEP_AmWhd' 27 12 236 18 ships
w-24 24 'WEP_AmWhd' 27 14 235 colonise ×2
w-25 25 '' 28 15 244 pick + design + colonise + 9 ships
w-26 26 '' 28 15 251 pick + 7 ships

The predicate held, and it held in its strong direction. The control input carries Player[32].ResTNm == '' — a pick turn — and the control varied, in exactly the research-pick leaves the predicate names and nothing else. Two independent lanes have now produced that signature on this input.

And the state says something the predicate did not anticipate: on this map exposure is saturated. Every one of the thirteen pre-turn states above carries at least one exposure signature; nine of thirteen carry a research pick, and every single turn adds ships. There is no quiet turn anywhere in this lineage to attempt an agreeing control on — so no rung-A oracle pair is obtainable from as-turn15-spydeployed.sav at any horizon by waiting, and a lane that wants one on this map must pin the client seeds instead. That is a fact about a 2-empire game where the AI runs a 146 → 251-ship war economy, and it is the opposite end of the same axis from lane AR's quiet turns 38–39.

One nuance that cuts against a naive reading of the predicate. Exposure is necessary for variation, not sufficient per process pair: W (probes=8) and AS's A and M (two different configurations, two different builds, three different processes) landed on the same post-turn bytes on this exposed pick turn. A single agreeing pair on an exposed turn therefore proves nothing — which is rule 26's original point, now with a measured outcome-set size behind it.

7.4 OB, and the verdict

OB   (Autosave EndTurn).sav   98e45d3745b914506fc4c409ad5a5ad0938a2003ab4c845421a99a8065149c3a  76,706
OB   (Autosave).sav           262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe  77,685
  • OA ≠ OB. The two hooks=off processes of this lane's own control disagree, in 15 leaves, all of them Player[32]'s research pick plus /Summary/Checksum and the turnstats tch cell. Nothing of my empire, the spy, deat, sdet, sdo, cm, cmo, ncp, spies2, the systems, the fleets or trdmgr moved between them.
  • OB is byte-identical to W, the instrumented run: state_checksum prints IDENTICAL over 44,438 leaves. Both are fc85c4228b9973fa159c1ed6aa851ae0.

So the honest three-part statement, which is all the evidence supports:

  1. Not a calibration pair. Two fresh un-instrumented processes on this input do not agree, so nothing here may enter determinism-hashes.txt as a rung-A pair. PD1 and PD3 held.
  2. The instrument is neutral on everything this lane measured. probes=8 reproduced an un-instrumented run leaf for leaf, and the sub-trees carrying the spy state are identical across all three of this lane's processes. PD4 held. That is a stronger neutrality statement than lane AS could make, because AS's instrumented run agreed with only one of two controls; here it agrees with one control exactly and completely while the two controls differ from each other in a leaf class the instrument provably does not touch.
  3. The divergence is the known one, and it was predicted. PD2 held, and it held with the pre-turn exposure fields recorded in advance (§7.3) rather than diagnosed afterwards.

A note for whoever certifies pairs. Lane AS reported A ≠ B and called the class "one AI player's research pick plus the derived checksum". Two more processes, taken independently, land in the same class and add a third member to the outcome set. That is a second lane reproducing the class while failing to reproduce the value — which is exactly what a per-process seed feeding a small discrete choice should look like, and it is now measured rather than asserted.


8. Predictions scored — including the one that was wrong

prediction verdict
PA1 P reaches at most one draw site per call, selected by Species == 6; only 0x00840a3c from this save HELD. No row at 0x0044092e or 0x004409cc on any of the twelve measured turns, including the turn P ran
PA2 species table 0 Human … 4 _NPC, 5 Zuul, 6 Morrigi; 0.75f is Zuul, 0.5f is Morrigi HELD (corpus-wide; the launch card independently shows re = Human)
PA3 0x00840a3c is Chance on a widened 0.25f HELD — 0x009e5ac0 is 00 00 80 3e
PA4 the spy is destroyed at the end of vslot13 on the P turn HELD — nspy 1 → 0, spies2 emptied, no spy record at Frame 25
PB1 sdo(f) = 0.0084 × (f − 14) exactly, until detection HELD — seven exact float32 steps, then frozen
PB2 tail costs 1 on Frames 16, 17, 18 HELD
PB3 0x0088dc43 first fires on Frame 19; Frame 18 bootstraps without drawing HELD exactly
PB4 the tail costs 2 words/turn from Frame 19 WRONG. It costs 2 on Frame 19 and then 4, 6, 9, 8, 9, 8, 8
PB5 cm, spyon move on Frame 18; cmo steps by 0.2f HELD
PB6 on the detection turn vslot13 still costs 1 and sdet := T HELD — Frame 22
PB7 vslot13 costs zero on T+1, T+2 while still being entered HELD — Frames 23, 24: no row, probe reads 1
PB8 T+3: one row at 0x0044_0a41, 1/1/0/strategic; no 0x0048_7c8f; vslot14 contributes 0 HELD in all three parts
PB9 the Slot13RngCallee probe reads 1 on T+3 HELD — its first non-zero in the campaign
PB10 11 % by +4, 43 % by +10, 70 % by +15, 87 % by +20 not tested. Detection landed at +7, where the model says 26 %. One sample is consistent with the model and does not test it
PB11 residual 0 on every turn HELD on all eleven turns of W and on run D
PB12 the +20 tail case not reached
PC1 0x0038c984, NextFloat, 1/1/0, strategic HELD exactly
PC2 in the ProcessTurn bracket HELD
PC3 the same turn's tail costs exactly 1 HELD
PC4 cbh reproduces 5.033599376678467 exactly HELD
PC5 spies2 0 → [1], deat → 400, tdep := 15, sdo := 0.0084 HELD
PD1 the control pair's pre-turn saves agree, post-turn saves differ HELD
PD2 the divergence is one AI's research pick + checksum + tch, and nothing of mine HELD — 15 leaves, localised
PD3 not a calibration pair; must not enter determinism-hashes.txt HELD, and stated in advance
PD4 the sub-trees this lane's numbers depend on reproduce HELD — spymgr, systems, fleets, trdmgr digests identical; and W is byte-identical to OB over 44,438 leaves

8.1 PB4, the one that was wrong, and why it was wrong

I predicted the tail bracket would cost 2 words per turn from Frame 19, on the reasoning that vslot13 contributes 1 and vslot14 contributes 1. Both halves of that were right on every single turn. The prediction was still wrong, because the tail has a third contributor and I treated it as a constant zero.

Frame          16  17  18  19  20  21  22  23  24  25  26
vslot13         1   1   1   1   1   1   1   0   0   0   0     <- predicted exactly
vslot14         0   0   0   1   1   1   1   1   1   0   0     <- predicted exactly
0x00820e1d      0   0   0   0   2   4   7   7   8   7   8     <- NOT PREDICTED
tail bracket    1   1   1   2   4   6   9   8   9   8   8

0x00820e1d is the trade-raid roll in TradeManager::Slot13RngCalleeA — lane AC's site, priced per fleet by lane AD. It was at zero on Frames 16–19 and entered the tail on Frame 20, growing to 8. That is the AI's raider fleets arriving on trade-sector nodes as its navy went from 157 to 251 ships: calls == words == the number of qualifying fleets, exactly lane AD's per-fleet model, now reproduced on a different map, a different lineage and an AI-owned fleet rather than a player-owned one. AD measured 2; this run measured 2, 4, 7, 7, 8, 7, 8 on seven consecutive turns.

The lesson is rule 20's, in the direction nobody watches: I checked that my sites were priced, and then quoted a bracket total as if the bracket contained only my sites. A bracket is a sum over subsystems, and predicting one requires predicting all of them. The correct form of PB4 was "the spy contribution to the tail is 2 words per turn from Frame 19", which is what was measured and what should have been written.

It is also the most useful failure available, because a wrong bracket prediction that decomposes cleanly into "my model was right, an unmodelled neighbour moved" is a free confirmation of the neighbour's model. The residual was 0 on every turn, so nothing is unaccounted for.


9. Coverage — what this did not touch, said as loudly as what it did

  • 0x00840929 and 0x008409c7 remain unfired, and are now known to need a Morrigi empire. That is a stronger negative than "not reached in the states measured": the gate is decoded, the branch was taken twelve times in the other direction, and the predicate is a single field on the spy's owner. The workload that closes them, named from the failed conjunct: a Morrigi single-player game, four spy techs, a spy deployed at a foreign colony with a belt, and ~7 turns of waiting. 0x008409c7 needs one more thing on top — the roll at 0x00840929 must succeed (75 %) and the candidate list must be non-empty, which needs a third non-eliminated empire that is neither the spy's owner nor the target's. This map has one (Spengler), so a 3-empire Morrigi game reaches both.
  • P ran exactly once. Everything about its cost is a single observation. The loop is over the spy vector, so two spies detected three turns apart should give two calls on different turns and two spies detected on the same turn should give two calls on one turn — that is a reading, not a measurement (rule 20: do not fit a constant to one observation).
  • The sdo arithmetic is still validated only in its first and third terms. countA, countB and the species multiplier were 0 / 0 / 1.0 for all eleven turns. countC held at 2 throughout, which is why the line is straight; nothing here identifies what countC counts, only that it did not change while one fleet of one ship sat at Kepler.
  • The auto-detect arm (TerrFl & 1) == 0 was never taken. Lane AS flagged it as the most load-bearing unmeasured branch in the chain and it still is. Nothing in this lane touched it.
  • The ncp-blocks-rebootstrap gate was never reached (§5.4 item 3). cm never wrapped 4 → 0 because the spy was destroyed at Frame 25 with cm = 4. Two more turns of survival would have tested it; a second spy deployed later than the first would too.
  • P's return value was not traced. The Frame-25 roll's outcome (whether re was named in Kepler's EVENT_SPY_DESTROYED event, a 25 % chance) was not read out of the event text. The ledger prices the site; the semantics in §1.2 are from the instruction stream.
  • One turn, one process for the eleven-turn series. W is a single process. The control pair covers only its first turn. Frames 17–26 have no two-process control at all, and by §7.3 every one of them is an exposed turn, so they should not be assumed reproducible.
  • Turn 27 was not played. The driver stopped on a Von Neumann encounter dialog at Rigel, which is where the series ends. Nothing after Frame 26 is measured.
  • probes=11 was not used anywhere in this lane, and no number here is comparable to lane AC's turn totals.
  • No Guard region is declared by any hook in this family — the same gap lanes Z, H, AS and AR reported. undeclared = 0 in these traces is vacuous.
  • The species table is read from the save's own ISsp list, cross-checked on one Zuul save and one hook report. No Morrigi, Hiver, Liir or Tarka save exists in the corpus, so indices 1, 2, 3 and 6 are named but not exercised.

10. Corrections to earlier findings (rule 11)

  • Lane AS §6, board row 207, lane AG §6 and this lane's own brief: "P's three draws". 0x00840929, 0x008409c7 and 0x00840a3c are not three sites behind one gate. They are two mutually exclusive branches selected by spyOwner->Species == 6, and no call can reach more than one of them. Every inventory that lists them as a group of three should say so. §1.2.
  • Lane AG §3.2's caution to lane AS: "a spy program run without FTL Economics takes a different branch of the machine". The CnTrd the stage machine consults at cm 1 → 2/3 is the target system owner's, not the spy owner's. My empire has CnTrd = False and the machine still took the 1 → 2 arm, because Kepler's owner has CnTrd = True. §5.4 item 4.
  • Lane AG §3.2's ncp reading. ncp is a scalar int in the stream, and it behaves as a counter of completed counter-mission stages (0,0,1,1,2,2,3 in lock-step with cm). "Non-empty" is therefore != 0, and the branch that consults it was never reached here. §5.4 item 3.
  • Lane AS §1.4 / §6.1: "cbh = NextFloat() * 2*pi". Correct in value; the code is lerp(0.0, 2*pi, r) with a float32 round in the middle, and the angle is DeploySpy's second argument, set up before 0x0080c860 (a zero-argument getter) is called across the half-built frame. AS's "spy->cbh := <the float argument>" is right; §6.2 only names which argument.
  • Lane AS §6's detection-probability table (38 % / 65 % / 85 % at +10 / +15 / +20) is the right model evaluated one turn late. AccumulateDetectionOdds runs before the gate, so the roll on the End Turn producing Frame f uses sdo = 0.0084 × (f − 14), not 0.0084 × (f − 15). The corrected figures are 43 % / 70 % / 87 %. Measured sdo (§5.3) settles it: the Frame-16 autosave already holds 0.0168, so the Frame-16 roll was at 0.0168.
  • Lane AG's audit and lane V2's inventory have a column they need. 0x0078c97f's predicate is "a SHIPACTION_DEPLOYSPY command in this turn's command stream". It is now measured, so the gate-indexed audit's two cell kinds (a firing, or a save predicate) become three: a firing, a save predicate, or a stream predicate. This is the first entry in that column.

11. Artifacts

what where
the instrumented eleven-turn trace (per-turn draw_sites + probe_entries + brackets) verify/traces/ap-probes8-turn16-turn26.jsonl.gz
the deploy-turn trace verify/traces/ap-probes8-deploy-turn15.jsonl.gz
shim log for the eleven-turn run verify/results/shim/ap/ap-probes8-w.shim.log
shim log for the deploy run verify/results/shim/ap/ap-probes8-d.shim.log
the detected-spy save — first sdet != -1, first non-zero cm/cmo/ncp/spyon in the corpus verify/results/saves/ap-turn22-spydetected.sav (83,188 B, --strict 0 errors / 0 warnings)
the post-P save — the spy destroyed, nspy back to 0, spies2 emptied verify/results/saves/ap-turn25-spydestroyed.sav (84,487 B, --strict clean)
the deploy run's Frame-15 autosave (cbh reproduced) verify/results/saves/ap-deploy-turn15.sav
the two control post-turn saves verify/results/saves/ap-oa-turn16.sav, ap-ob-turn16.sav
addresses this lane mints ghidra/addresses.d/ap.json
predictions, committed before the build this file §0–§3, commit f8ff1b6
instrument sots-engine src/shim/shim.cfg.hp8 and shim.cfg.hoff, both unchanged — this lane wrote no engine code

11.1 The hashes, for the record

input      MyGameas1spydep.sav        854a10fa1ea602f0f7909f9bf9154942e56ea5fa1db4cce47d6d32bf53952e08
A/B/M/OA/OB  (Autosave EndTurn).sav   98e45d3745b914506fc4c409ad5a5ad0938a2003ab4c845421a99a8065149c3a
W  (probes=8)  turn 16                262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe
OB (hooks=off) turn 16                262f8bda97c9511d8d2b41e6a8a0582d6f5364c2b01b1caf99e639d0e4f53ebe
OA (hooks=off) turn 16                e296394f578b3a5668e5934b9eec8d6c4638ca7411200078f97de1abd346f4c5
   turn 22 (detection)                91381bd7e722ac3847795a402d78ebb31d74dbb5d2909f33d243aec3ef8abaf8
   turn 25 (P ran; spy destroyed)     46380fcf4498d67507e4471eb9ee3762647c53ee5473be142fe5c34e37344bc3
deploy run, turn 15                   844f2b746060dc2c4e2099cdc6ceffd5f55ef858c9e347df28c8aca3041f7165
shim binkw32.dll  ap-989c692-…        ae084ed4774caf24ea7927fcb012d1999633f10556c7c99f0209f66067416a17

None of these is a calibration pair. §7.


12. VM144 as left

Restored and verified by screenshot at the main menu, profile re, 2026-09-09.

  • C:\SOTS\binkw32.dll restored to lane L3's build from C:\SOTS\shimdist-l3\binkw32.dll (sha256 479B8614D2417603…, byte-identical to what this lane found in place).
  • C:\SOTS\shim.cfg restored from C:\SOTS\ui\preAS-shim.cfg (L3's shim.cfg.l3probe, probes=11).
  • The three autosaves restored byte-identical from C:\SOTS\ui\preAS-SavedGames (1985E6F4… / 24B2E072… / 5EC80C1E…). This lane's eleven turns had overwritten their contents; lane AS's snapshot is what made the restore possible and it is worth keeping.
  • C:\SOTS\shim.trace.jsonl and shim.log removed before the relaunch.

SavedGames is still 15 files — this lane added none and removed none, so lane AS's row positions still hold: MyGameas1predeploy at (400, 347), MyGameas1spydep at (400, 376), OK at (682, 624). Verified by screenshot twice this session. All fifteen files hash as AS left them.

Left in place, all harmless: C:\SOTS\shimdist-ap\ (build ap-989c692-20260909T0425Z, 63 files), C:\SOTS\ui\apgo.ps1, C:\SOTS\ui\aphash.ps1, C:\SOTS\ui\ap\ (the End-Turn driver, its per-turn saves and screenshots), and the scheduled task SOTSAP. click_helper.ps1 was not touched — this lane needed no new verbs either.

12.1 Guest notes worth carrying forward

  • The End-Turn driver is the reusable part. C:\SOTS\ui\ap\ap_loop.ps1, run through the SOTSAP scheduled task (an InteractiveToken clone of SOTSUI), reads C:\SOTS\ui\ap\job.txt (turns=, tag=, first=, maxwait=), clicks End Turn, polls the autosave's mtime until it moves, snapshots (Autosave).sav to ap\saves\<tag>-<NN>.sav, and stops on the first stall with a screenshot. Eleven turns cost 11 minutes unattended. Set job.txt, schtasks /Run /TN SOTSAP, then watch ap\loop.log from outside.
  • Nine minutes of this lane went into a screenshot-lag artefact, and the lesson is general. Four consecutive clicks appeared to select the wrong Load-dialog rows, which read exactly like a coordinate offset and nearly produced a "the click helper is 87 px out on this guest" note. It was not: the click task takes ~3 s to start and ~3 s to run, and a screenshot taken 4 s after schtasks /Run shows the state before the click. Wait ≥ 9 s between triggering the click task and screenshotting. Every documented coordinate on this guest is correct.
  • The first click after fg is swallowed by the focus activation. Send fg, then move, then sleep, then click — or spend a throwaway click.
  • A wheel zoom on the star map separates systems that overlap at the default zoom. Lane AS lost a run to Kepler and Midway sharing a hit test; six notches at the cluster centre and then eight more at the target spread them to ~120 px apart, and the labels render. No affine fit was needed.
  • Selecting the system, not the fleet, is what enables Special. With Kepler selected the left panel grows a Fleets at Kepler list and Manage Fleets / Move / Special light up; Special → Deploy Spy then opens a ship picker whose ship is already selected, so OK is the only further click. The whole deploy is four clicks once the right star is under the cursor.
  • The pixel test for "is the main menu up" must sample the button, not its label. (511, 536) is inside the white text of Load Game and reads ~(195,191,191) on a bright frame; (470, 530) is the button itself and reads (184, 0, 0). A test on the first point passes only by luck on a dark frame.
  • An End Turn on this state costs 6 seconds under probes=8 with ~150–250 AI ships. The campaign's "30–45 s per End Turn" figure is from a different guest and a different config.
  • Turn 27 raised a Von Neumann encounter at Rigel ("No ships available", Done at (233, 673)). A driver that polls the autosave rather than the screen stalls on it cleanly instead of clicking through something it cannot see.

Proposed board rows

New row:

| THE SPY ATTRIBUTION ROLL FIRES - and P's "three draws" are TWO SPECIES BRANCHES, only one reachable per game | verify | verified | high | 100% | 2026-09-09 | **Lane AP, VM144, build `ap-989c692-20260909T0425Z`, `probes=8` (never 11), own build dir /srv/re-lab/build/sots-engine-ap rm -rf'd first.** Continues lane AS from `as-turn15-spydeployed.sav`: **11 End Turns, 6 SECONDS EACH** (the brief budgeted an evening; the cost is the launch and the load, not the turns). **DETECTION AT FRAME 22, `P` RAN AT FRAME 25.** `Mars::RNG::Chance` at **0x00840a3c** fired **1 call / 1 word / no_draw_calls=0 / strategic=true** at `ret_rva 0x00840a41` - a site that had never fired. **CORRECTION TO EVERY INVENTORY (row 207, lane AG 6, lane AS 6, and this lane's own brief): 0x00840929 / 0x008409c7 / 0x00840a3c ARE NOT THREE SITES BEHIND ONE GATE.** `SpyManager::Slot13RngCallee 0x008408e0` opens `cmp [spyOwner+0x5c], 6; jne` - `Species == 6` (MORRIGI) takes `Chance(0.75f)` at 0x00840929 and then `NextInt` at 0x008409c7 to blame a RANDOM THIRD EMPIRE (a false flag; the true owner is never named); every other species takes `Chance(0.25f)` at 0x00840a3c and on success returns the TRUE owner. **No call can reach more than one.** Species table decoded from the save's own ISsp list and cross-checked on lane V's Zuul save: **0 Human, 1 Hiver, 2 Tarkas, 3 Liir, 4 _NPC, 5 Zuul, 6 Morrigi** - which also NAMES lane AS's two unexplained multipliers in `AccumulateDetectionOdds` (0.75f = ZUUL, 0.5f = MORRIGI). Constants read as the four bytes (rule 23): 0x009e5ac0 = 0.25f, 0x009e5ac4 = 0.75f (the same word AS read as the Zuul multiplier). **0x008409c7 needs BOTH the 0.75f roll to succeed AND a third non-eliminated empire** that is neither the spy's owner nor the target's. **SECOND SITE CLOSED: 0x0088dc43** (the counter-mission roll, lane AG 3.2, corpus 0 of 22, never fired) fires 1 word/turn from **Frame 19**; the bootstrap turn (Frame == tdep+3) sets spyon/cm:=1/cmo:=0.2f and DRAWS NOTHING. `cm`, `cmo`, `spyon`, `ncp` - four of the value-domain census's 234 single-valued fields - are all OFF ZERO with a measured trajectory. **TWO CORRECTIONS TO AG 3.2:** the `CnTrd` the stage machine consults at cm 1->2/3 is the TARGET OWNER's not the spy owner's (my player has CnTrd=False and it still took 1->2, because Kepler's owner has CnTrd=True), and `ncp` is a SCALAR COUNTER of completed stages (0,0,1,1,2,2,3 in lock-step with cm), not a container. **THIRD SITE CLOSED: 0x0078c97f**, the deploy `NextFloat` in the SHIPACTION_DEPLOYSPY handler that is in NO inventory - 1 call / 1 word / strategic, in the **ProcessTurn** bracket, and **spy.cbh came out 5.033599376678467 BIT-IDENTICAL to lane AS's** from a different process, build and session. **Its predicate is on the COMMAND STREAM, and this is the first measured entry in a column the gate-indexed audit does not have.** THE POLARITY CASE, MEASURED: on Frames 23 and 24 (sdet+1, sdet+2) `vslot13` is ENTERED (probe = 1) and costs **ZERO** - a third arm of the same function, and `sdo` FREEZES at 0.0672 in the save, proving branch D stopped independently of the ledger. On Frame 25 the entry probe on `SpyManager::Slot13RngCallee 0x008408e0` reads **1** - ITS FIRST NON-ZERO EVER, the complement of lane AS's demonstration that its zero said nothing about the subtree. The spy is DESTROYED at the end of phase 23 on sdet+3 by manager vtable slot 10 `0x00838480` -> `ServerSystem::RemoveSpy`, so vslot14 costs 0 that turn: nspy 1->0, spies2 emptied. `sdo` measured on NINE points, exactly `0.0084*(f-14)` as float32 (`3c89a027 3cce703a 3d09a027 3d2c0831 3d4e703b 3d70d845 3d89a027`), which also corrects AS's detection-probability table by one turn: **43%/70%/87%** at +10/+15/+20, not 38/65/85. Residual **0 on all 12 measured turns**, overflow 0. **ONE PREDICTION WRONG AND NAMED: PB4** said the tail costs 2 words/turn from Frame 19; it costs 2,4,6,9,8,9,8,8 because the TRADE-RAID roll 0x00820e1d entered the tail at Frame 20 and grew 2,4,7,7,8,7,8 as the AI's navy went 157->251 ships - an independent live re-confirmation of lane AD's per-fleet model on a different map and an AI-owned fleet. The spy halves were predicted exactly; quoting a BRACKET total requires modelling every subsystem in it. Saves `ap-turn22-spydetected.sav`, `ap-turn25-spydestroyed.sav`, `ap-deploy-turn15.sav`; findings `findings/subsystems/spy-program-draws.md` |
| CONTROL ON as-turn15: THREE OUTCOMES IN SIX PROCESSES, and exposure is SATURATED on this map | verify | verified | high | 100% | 2026-09-09 | Lane AP, VM144. Two fresh `hooks=off` processes (OA, OB) on `MyGameas1spydep.sav` + 1 End Turn, added to lane AS's A/B/M and this lane's `probes=8` run W. **Pre-turn `(Autosave EndTurn).sav` is byte-identical across four processes, two lanes and TWO DIFFERENT BUILDS** (`98e45d3745b91450...`). Post-turn: **three distinct outcomes in six processes** - `262f8bda` x4 (AS's A and M, AP's W and OB), `e34775a7` x1 (AS's B), `e296394f` x1 (AP's OA). **OA != OB, so this is NOT a calibration pair and must not enter `determinism-hashes.txt`** - stated in advance as PD3. OA vs OB diverge in **15 leaves, ALL of them Player[32]'s research pick plus /Summary/Checksum and one turnstats `tch` cell**; three distinct picks were sampled (Overthrusting completed / DRV_NodFoc in progress / DRV_McroFus over budget). **`probes=8` IS BYTE-NEUTRAL, PROVEN PROPERLY THIS TIME: W is IDENTICAL to OB over 44,438 leaves** (`state_checksum --floats bits --mask none`), while the two un-instrumented controls differ from each other - so the instrument agrees exactly with a control in a leaf class it provably does not touch. Sub-tree digests: `Sim/spymgr`, `Sim/systems`, `Sim/fleets`, `Sim/trdmgr` all IDENTICAL across the pair; only `Sim/players` and the derived `Summary` move. **RULE 26(c) EXPOSURE FIELDS RECORDED NEXT TO THE HASHES** (coordinator's request): the control input carries `Player[32].ResTNm == ''` - a pick turn - and it varied in exactly the leaves the predicate names. **AND THE PREDICATE IS SATURATED ON THIS MAP: all 13 pre-turn states from Frame 14 to Frame 26 carry at least one exposure signature** (9 of 13 a research pick, every single one a batch of ship completions, four a design, four a colonisation; Player[32]'s navy goes 146 -> 251 ships in twelve turns). **There is NO quiet turn anywhere in this lineage**, so no rung-A pair is obtainable from `as-turn15-spydeployed.sav` by waiting - the opposite end of the axis from lane AR's turns 38-39, and a lane that wants a pair on this map must pin the client seeds. Counter-nuance: exposure is necessary for variation, not sufficient per pair - W, A and M (three processes, two configurations, two builds) landed on the SAME bytes on this exposed turn, so a single agreeing pair on an exposed turn proves nothing. Saves `ap-oa-turn16.sav`, `ap-ob-turn16.sav` |

Edits to existing rows:

  • Row 62 (guest holders) — VM144 = FREE (lane AP released 2026-09-09; restored to L3's build 479B8614… + L3's shim.cfg (probes=11) + the three pre-AS autosaves BYTE-IDENTICAL from ui\preAS-SavedGames, main menu verified by screenshot). SavedGames is STILL 15 FILES and lane AS's row positions STILL HOLD - MyGameas1predeploy (400,347), MyGameas1spydep (400,376), OK (682,624), verified by screenshot twice. AP added C:\SOTS\shimdist-ap, ui\ap{go,hash}.ps1, ui\ap\ (an END-TURN DRIVER worth reusing: ap_loop.ps1 + the SOTSAP scheduled task clicks End Turn, POLLS THE AUTOSAVE MTIME, snapshots (Autosave).sav per turn and stops on the first stall with a screenshot - 11 turns unattended in 11 minutes). click_helper.ps1 untouched. GOTCHA THAT COST THIS LANE NINE MINUTES: the click task takes ~6 s end to end, so a screenshot taken 4 s after schtasks /Run /TN SOTSUIshows the state BEFORE the click - four such shots read exactly like an 87 px coordinate offset and nearly became a false gotcha. WAIT >= 9 s. GOTCHA: the first click afterfgis swallowed by the focus activation. GOTCHA: the main-menu pixel test must sample the BUTTON (470,530) = (184,0,0), not the white label at (511,536). TIP:wheel zoom separates stars that share a hit test at the default zoom (Kepler vs Midway, which cost lane AS a run) - no affine fit needed. An End Turn on this state costs 6 SECONDS under probes=8.
  • Row 207 (V2's eight sites) — append: CORRECTED AND ADVANCED 2026-09-09 by lane AP. 0x00840a3c FIRED (1 word, Frame sdet+3 from as-turn15-spydeployed.sav). 0x00840929 and 0x008409c7 are NOT two more sites behind the same gate - they are the MORRIGI branch of SpyManager::Slot13RngCallee (cmp [spyOwner+0x5c],6), mutually exclusive with 0x00840a3c, and unreachable in any Human/Hiver/Tarka/Liir/Zuul game. Closing them needs a MORRIGI empire (and, for 0x008409c7, a third surviving empire). Of V2's eight, only 0x0082cdb8 (trade 15) is now both reachable and unfired.
  • Row 418 (lane AS's detection-roll row) — append: EXTENDED 2026-09-09 by lane AP: the roll fired on seven consecutive turns and SUCCEEDED on Frame 22; sdois exactly 0.0084*(f-14) as float32 over NINE points and FREEZES the moment sdet stamps. AS's probability table is one turn late - the corrected figures are 43%/70%/87% at +10/+15/+20. AS's 0x0078c97f is now MEASURED (1 word, ProcessTurn bracket) andcbh reproduced bit-identically across processes and builds.
  • The 0x0088dc43 row of lane AG's audit (§3.2) — FIRED 2026-09-09 by lane AP, 1 word/turn from Frame 19 (= tdep + 4). The bootstrap turn draws nothing. TWO CORRECTIONS: the CnTrd consulted at cm 1->2/3 is the TARGET OWNER's, and ncp is a scalar counter of completed stages, not a container.
  • The gate-indexed audit's cell taxonomy — A THIRD CELL KIND EXISTS AND NOW HAS ITS FIRST MEASURED ENTRY: a STREAM predicate. 0x0078c97f fires once per SHIPACTION_DEPLOYSPY command applied; no predicate over the save can express it, and no call-graph sweep can find it (stack-built ship-action table, lane B6's class).