From 49ae6282202d3024ec305302c801195584f87675 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 8 Sep 2026 12:23:06 -0400 Subject: [PATCH] lane A2: prediction for the alliance mask and ModCount, written before the build --- docs/A2-alliance-and-modcount.md | 105 +++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 docs/A2-alliance-and-modcount.md diff --git a/docs/A2-alliance-and-modcount.md b/docs/A2-alliance-and-modcount.md new file mode 100644 index 0000000..523869d --- /dev/null +++ b/docs/A2-alliance-and-modcount.md @@ -0,0 +1,105 @@ +# Lane A2 — the alliance mask (`S04`) and `ModCount` + +Lane A2, 2026-09-08. Branch `wip/alliance`, off `main` `b2bad30` (lane N's output term already +merged). Host + static only; lane A2 holds no VM. + +**This section is the prediction, written before the implementation, per earned rule 2.** +Everything below the horizontal rule at §3 is the result. + +--- + +## 1. The prediction — `S04`, the alliance / shared-vision mask + +The spine's fourth phase is a per-player pre-pass that rebuilds one word of each player's turn +record from scratch, every turn, before anything else in the turn can read it. Read from the +instruction stream (not the decompiler), the phase is: + +``` +for i in 0 .. numPlayers-1: # i is the player's POSITION IN THE SERVER VECTOR + p = players[i] + rec = p.turnRecord + rec.allianceMask = 0 + rec.allianceMask |= (1 << i) + if p.allianceId != -1: + rec.allianceMask |= p.allianceMembers +``` + +so, as one expression, + +``` +almem[i] = (1u << i) | (alid[i] != -1 ? al[i] : 0) +``` + +Both inputs are on the wire: they are the two leading ints of the player's second `Team` item +(`ALid`, `AL`). The output is on the wire too — it is the `almem` field of the player's +turn-statistics element for the turn. + +**The prediction, in the form the test will check it:** over the whole eleven-save corpus, for +every player of every save, for the archive element whose `trn` equals the save's own frame, + +> `almem == (1 << playerVectorIndex) | (ALid != -1 ? AL : 0)` + +with **zero** mismatches, and with `playerVectorIndex` being the player's ordinal position in +`/Sim/players` — **not** its `PlyrIdx`. + +Committing this closes the 8 `almem` leaves that lane Y measured as regressing `T36`, and moves +`T36` from "right in six fields, wrong in five" to "right in seven fields, wrong in four". + +### 1.1 Why the bit index is the vector position and not `PlyrIdx` + +The shift count is the loop induction variable, which is the index used to subscript the player +vector on the same iteration. Nothing loads `PlyrIdx` anywhere in the phase. Lane T recorded this +correctly and it is restated here because it is the single most likely place for a +reimplementation to be quietly wrong: every *other* per-player index in the tail (the turn-results +array, the battle tally, the archive key) uses `PlyrIdx`, and this one does not. + +### 1.2 What this phase does NOT do + +It does not read, and does not write, `NA` or `CF` — the other two ints of the same `Team` item. +It does not consult the game's alliance-enabled flag (`EnAl`). It writes exactly one word. + +## 2. Falsification + +Each row is a way the model above can be wrong, and the symptom that would show it. + +| # | how the model could be wrong | symptom | +|---|---|---| +| F1 | the bit is `1 << PlyrIdx`, not `1 << vectorIndex` | on any save where the two orders differ, `almem`'s low bit pattern is off by a permutation. **Zero mismatches on a save where they agree proves nothing** — the check must report how many saves actually separate the two | +| F2 | the guard is on something other than `ALid != -1` (e.g. on `AL != 0`, or on the game's `EnAl`) | indistinguishable while no player is in an alliance. Only a save with a live alliance can separate them | +| F3 | `AL` is a mask over `PlyrIdx` bits while the self-bit is a vector-position bit | `almem` would mix two index spaces. Same blind spot as F2: unobservable without an alliance | +| F4 | phase 36 (`FinalizeTurnRecords`) overwrites `almem` after phase 4 sets it | mismatches everywhere, and the stored value would correlate with something else entirely | +| F5 | the archived element for the save's own frame was written by the **load** path rather than by the tail (`FinalizeTurnRecords` runs in both) | the self-bit would be missing, because the load path does not run spine phase 4. Expect `almem == 0`, or a value that is stale by one turn | +| F6 | the shift wraps | `shl` masks its count to 5 bits, so a 32nd player would set bit 0. No save has more than 8 players; the model is untested above 31 and says so | +| F7 | the corpus never exercises the alliance term at all | then this lane has closed the **self-bit** and *nothing else*, and must say so under rule 6 rather than claim the rule | + +F7 is the one I expect to bite. The prior is that no save in the corpus has a player in an +alliance, in which case the measured agreement tests only `1 << i`, and the `|= AL` arm is a +**hypothesis** read from the instruction stream with no exercised evidence. That is a real result +and it is a smaller one than "the alliance mask is closed", and it will be reported as the smaller +one. + +## 3. The prediction — `ModCount` + +`ModCount` is a **modification counter**, not a turn number and not a phase counter, and the count +a turn adds is a function of the *command stream*, not of the pre-turn state. + +**The prediction:** + +1. Every writer is an **entry-block increment inside a `StrategySim` command-application method** — + the `OnCommand_*` family. The bump is unconditional and happens *before* the handler validates + its arguments, so a command that fails validation still advances the counter. +2. The two turn drivers each add exactly 1. +3. `ProcessTurn` phase 1 adds 1 per system that passes its gate; the gate is closed on all eleven + saves, so it contributes 0 to every measured turn. +4. Therefore **`ModCount` is not derivable from the pre-turn save**, and `S00`/`T00`'s two bumps + are the only part of it this standalone can ever produce without modelling the AI's order + stream. The remaining 10–42 bumps a turn are commands. + +**Falsification for §3:** if the delta were a function of the pre-turn state, then two saves with +the same state shape would show the same delta. `turn1→turn2` and `turn2→turn3` both show exactly +12, which is *consistent* with a state function and is the strongest evidence against my reading; +a third pair from the same game showing a different delta with an unchanged board would settle it. +The counter-evidence I already hold is that `human-turn2→turn3` is 28 and `zuul-turn16→turn17` is +16 on boards whose system and player counts are identical (28 systems, 7 players) — so it is not a +function of the board size. If someone finds a closed-form rule over the pre-turn save that fits +all eleven saves, this section is wrong.