From ec8d841dbad62c725c14c9912436c3c89533c558 Mon Sep 17 00:00:00 2001 From: lane-l4 Date: Tue, 8 Sep 2026 18:06:31 -0400 Subject: [PATCH] L4: predictions for the research-selection tie set, before the instrument --- docs/L4-predictions.md | 92 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/docs/L4-predictions.md b/docs/L4-predictions.md index 19390e1..7b782e1 100644 --- a/docs/L4-predictions.md +++ b/docs/L4-predictions.md @@ -244,3 +244,95 @@ happened to. 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). + +--- + +# Addendum — the research-selection tie set + +Added after the first two runs, before the research instrument was written. The coordinator's +question: if the one varying AI decision is a **tie** broken by something per-process, the +original's possible outcomes form a small enumerable set, and a deterministic `game/ai` can pick +canonically and claim *"our choice is one of exactly k the original can produce, and here are all +k"*. That is a stronger claim than behavioural equivalence, and it needs the candidate set. + +## What the selection site actually is (read before predicting) + +Process Turn phase 18 is `0x006caf70`. It tries three producers in order and takes the first +non-null: + +``` +eax = 0x006a84f0(agent) ; producer A +if (!eax) eax = 0x006c27c0(agent) ; producer B +if (!eax) eax = 0x006c8890(agent, &agent+0x13c); producer C -- the candidate walk +if (eax && eax != player->+0x294) cl_SetResearchTarget() +``` + +`player->+0x294` is the current target, and `cl_SetResearchTarget 0x00578f60` takes the tech's +**name string**, not an id — the object's `+0x4` is a `std::string` and the caller resolves the +short-string union before pushing it. + +Producer C, `0x006c8890`, has two halves: + +**C1, the candidate walk.** `if (player->+0x294 != 0) return 0`. Otherwise build a `std::vector` +of 0x0c-stride candidates with `0x006c2490` — which constructs a working object, fills it +(`0x006bcca0`), emits the vector (`0x006bc500`, a nested walk over groups, **no `std::sort` at that +level**) and destroys it — then walk the vector **front to back** calling +`0x006c8580(ecx = the target slot, edx = cand[1], stack: agent, cand[0])` and **return the first +one that answers non-zero**. First-acceptable in arrival order. No score, no comparator, no tie +resolution: **whatever the vector's order is, is the answer.** + +**C2, the fallback**, reached only when the walk accepts nothing: `0x006b36e0(agent) & 0x80000007` +indexes one of two eight-entry `.data` tables, and the value there seeds a three-arm rotation +`(i + seed) % 3` over `0x006c8670`. Both tables are in the image and both hold values in {0,1,2}: + +``` +0x00a1a544 = 2 0 2 0 2 1 2 0 +0x00a1a564 = 0 2 0 2 1 0 2 0 +``` + +`0x006b36e0` is not an RNG call — it reads `player->+0xf4`, calls `0x0080da80` and computes. So C2's +"roll" is a **hash of player state**, and its outcome space is at most **three arms**. + +## Predictions + +### P10 — where the variation lives + +**Prediction: `0x006c8890` (producer C) is entered once per AI player on turn 1 and the variation is +in C1's candidate vector, not in C2.** Specifically, across two runs of the same workload: + +* players 32 and 496 (stable across five runs between lanes L4 and L5) produce **identical candidate + streams in identical order**, and the accepted candidate sits at the same position; +* player 512 produces a candidate stream that is **the same set in a different order**, and the + first-acceptable one is therefore a different tech. + +*Falsified if:* the streams are identical for 512 and the accepted position still differs ⇒ the +decision is inside `0x006c8580`, i.e. state or a seed, and arrival order is not the mechanism. +*Also falsified if:* `0x006b36e0` fires for 512 ⇒ the pick came from **C2**, which is not a tie at +all but a three-arm rotation seeded by a state hash, and `k <= 3` by construction. + +### P11 — k is enumerable, and small + +**Prediction: the tie set for player 512 is the set of candidates in its stream that +`0x006c8580` would accept, and the stream is short — tens, not thousands.** A player with no +colonies and no research history has few reachable techs. + +*Falsified if:* the stream is hundreds of entries ⇒ "name all k" is not a practical verification +claim for this decision and the honest move is to mask the leaf, as lane L5 already does. + +### P12 — the two stable players are the control + +**Prediction: 32 and 496 accept a candidate at position 0 or very near it**, because a player whose +choice never varies is one whose first candidate is always acceptable. If instead they accept deep +in the stream and are still stable, then order is stable for them and unstable for 512 — which +would point at 512's *container*, not at the walk. + +### P13 — what our engine should do + +If P10 holds, a deterministic `game/ai` sorts the candidate vector by a canonical key (the tech +name, which is what the original ships in the command anyway) before the walk. The verification +claim becomes: **our pick is the canonical member of the tie set; every observed original run picks +some member of the same set; here is the set.** If P10 is falsified in the C2 direction instead, the +claim is different and weaker in kind but stronger in size: three arms, both tables in the image, +so k <= 3 and all three are nameable without any capture at all. + +*Either way the capture names k.* That is the point of running it.