lane A2: prediction for the alliance mask and ModCount, written before the build

This commit is contained in:
alex 2026-09-08 12:23:06 -04:00
parent b2bad30f6c
commit 49ae628220

View file

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