determinism qualified: oracle holds only where no AI must choose; Rung B = command-stream replay, Rung C = behavioural AI; canonical pair now turn2->turn3
This commit is contained in:
parent
7e6bda3699
commit
b3299979a7
4 changed files with 75 additions and 11 deletions
|
|
@ -25,20 +25,42 @@ run the economy/research/movement/output pieces to the original's numbers, and s
|
||||||
**16 of 44** driver phases and **5 of 37** tail phases modelled (10 committed). It cannot yet
|
**16 of 44** driver phases and **5 of 37** tail phases modelled (10 committed). It cannot yet
|
||||||
reproduce a turn.
|
reproduce a turn.
|
||||||
|
|
||||||
## 2. The milestone, restated
|
## 2. The milestone, restated (again — 2026-09-08 evening)
|
||||||
|
|
||||||
> `sots_turn` loads a save, runs one strategic turn, and writes an autosave that byte-matches
|
> `sots_turn` loads a save, runs one strategic turn, and writes an autosave that byte-matches
|
||||||
> the original's.
|
> the original's.
|
||||||
|
|
||||||
Today showed the milestone as first stated is **underdetermined on the reference pair**:
|
**Lane L5 showed the original cannot always do this itself.** Three runs of `turn1-state → turn2`
|
||||||
`ModCount` = 2 + one per command applied from every player's block, and on `turn1-state.sav` the
|
gave three different autosaves, differing in exactly four leaves — one AI empire's research pick and
|
||||||
human block is empty yet 10 non-driver bumps occur — **the AI's orders are generated during the
|
the derived checksum — with the instrument exonerated (two runs had identical hooks and still
|
||||||
turn, after the autosave**. `Summary.Checksum` is fed by `ModCount`. So, two rungs:
|
differed; `hooks=off` was a third value). The oracle stands on `ref-turn2 → turn3`, where every AI
|
||||||
|
already holds a target; it fails on the neighbour, where three AI players must *choose* one.
|
||||||
|
|
||||||
|
This is not a contradiction of the lockstep multiplayer design — it is the reason for it. The AI is a
|
||||||
|
`StrategyClient`, not part of the sim: it decides **once**, on one machine, and its decisions travel
|
||||||
|
as `TurnCommands` through the same host relay as a human's. Peers reproduce the sim's response to a
|
||||||
|
command, never the decision. The sim is deterministic (lane Z's zero-residual ledger is the strategic
|
||||||
|
generator, which the AI never touches); the decision layer is not, and does not need to be.
|
||||||
|
|
||||||
|
So the rungs are:
|
||||||
|
|
||||||
- **Rung A — byte-match modulo the command stream.** Every leaf except `ModCount`,
|
- **Rung A — byte-match modulo the command stream.** Every leaf except `ModCount`,
|
||||||
`Summary.Checksum`, and what the AI's own orders touch. **No AI needed.** Near target.
|
`Summary.Checksum`, and what the AI's own orders touch. No AI needed. **Canonical reference pair
|
||||||
- **Rung B — full byte-match.** Needs AI order generation in our engine (§4.1), or recorded AI
|
is now `turn2-state → turn3-state`** — deterministic, oracle-hashed. `turn1-state → turn2` stays
|
||||||
command blocks fed as input. Rung B is where the engine becomes the game.
|
as a secondary pair with its four non-deterministic leaves masked.
|
||||||
|
- **Rung B — byte-match given a recorded command stream.** `sots_turn --turn-commands <blocks>`
|
||||||
|
replays the AI's (and human's) orders captured from a real turn, and the output must match that
|
||||||
|
turn's autosave byte-for-byte. This is what "the engine is the game" means when the game's own
|
||||||
|
decision layer is not reproducible from a save. Lane AI1's fallback becomes the primary path.
|
||||||
|
- **Rung C — a `game/ai` that is behaviourally equivalent.** Reproduces the AI's *choice class*
|
||||||
|
(same task list, same priorities, same candidates), verified against the distribution of the
|
||||||
|
original's choices across runs rather than one run's bytes. Ties may resolve differently; that is
|
||||||
|
the original's own behaviour.
|
||||||
|
|
||||||
|
Mechanism, stated as inference: only **one** of three AI players varies, which looks like a tie
|
||||||
|
broken by something per-process — most likely iteration order over a pointer-keyed container under
|
||||||
|
ASLR — rather than a time seed (which would move all three). One probe separates them: read the AI
|
||||||
|
client's generator state after construction in two processes. Routed to lane L1.
|
||||||
|
|
||||||
## 3. Engine work to Rung A (ranked by leaves closed per lane)
|
## 3. Engine work to Rung A (ranked by leaves closed per lane)
|
||||||
|
|
||||||
|
|
@ -53,7 +75,7 @@ turn, after the autosave**. `Summary.Checksum` is fed by `ModCount`. So, two run
|
||||||
| 5 | **post events into the save's turn bucket** (P11 + tail event phases) | `app` + `game/events` | model exists; wiring only | events subtree |
|
| 5 | **post events into the save's turn bucket** (P11 + tail event phases) | `app` + `game/events` | model exists; wiring only | events subtree |
|
||||||
| 6 | ~~`Player.Status` restore~~ **PREMISE REFUTED** (lane T2): there is **no writer between tail 31 and the autosave**. A whole-image scan finds three stores to `+0x164` — `ProcessTurn`'s encounter loop writes 1, `ResumePlaying` writes 0 on load, and `MarkPlayerTurnEnded` writes 4 from the End-Turn **submission** paths, *before* the turn runs. It needs **one predicate**, not a watchpoint — and only two corpus saves carry a non-zero `Status`, so an entry probe should name the set (rule 20). | `app` | lane T2 | S31 regression |
|
| 6 | ~~`Player.Status` restore~~ **PREMISE REFUTED** (lane T2): there is **no writer between tail 31 and the autosave**. A whole-image scan finds three stores to `+0x164` — `ProcessTurn`'s encounter loop writes 1, `ResumePlaying` writes 0 on load, and `MarkPlayerTurnEnded` writes 4 from the End-Turn **submission** paths, *before* the turn runs. It needs **one predicate**, not a watchpoint — and only two corpus saves carry a non-zero `Status`, so an entry probe should name the set (rule 20). | `app` | lane T2 | S31 regression |
|
||||||
| 7 | **tail phases** — bankruptcy decision half, turn results outbox swap, per-player sync | `game/sim` + `app` | lane K map; 34/37 stubs | tail subtree |
|
| 7 | **tail phases** — bankruptcy decision half, turn results outbox swap, per-player sync | `game/sim` + `app` | lane K map; 34/37 stubs | tail subtree |
|
||||||
| 8 | Rung B: **AI order generation** | new `game/ai` | §4.1 | `ModCount`, Checksum |
|
| 8 | Rung B: **`--turn-commands` replay** from a captured block (then Rung C: `game/ai` behavioural equivalence) | `app`, then `game/ai` | lanes AI1–AI4, L4 | `ModCount`, Checksum given the stream |
|
||||||
|
|
||||||
Rules that hold for every item: formula-held-inputs-missing is **evaluated and reported, not
|
Rules that hold for every item: formula-held-inputs-missing is **evaluated and reported, not
|
||||||
written**; `closed` and `regressed` reported separately, never netted; `verified` in the phase
|
written**; `closed` and `regressed` reported separately, never netted; `verified` in the phase
|
||||||
|
|
|
||||||
|
|
@ -328,3 +328,4 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
||||||
| "widen every literal" is ITSELF a defect | verify | verified | high | 100% | 2026-09-08 | Two MORE rule-23 defects found by operand sweep in ComputeBudget, both corrected and both **marked unverified because no reference turn can see them**: the research-yield factor is a widened 0.85f (needs research money a multiple of 40,000 - the run presented 9 distinct values, NONE is) and the three research modifiers are summed in SINGLE precision (needs shrm/TRM non-zero; the corpus has both at 0). Both pinned by tests that fail with decimals. **CRITICALLY: three constants BESIDE them are genuinely exact doubles** - so blanket-widening is its own bug. Read the four bytes, every time |
|
| "widen every literal" is ITSELF a defect | verify | verified | high | 100% | 2026-09-08 | Two MORE rule-23 defects found by operand sweep in ComputeBudget, both corrected and both **marked unverified because no reference turn can see them**: the research-yield factor is a widened 0.85f (needs research money a multiple of 40,000 - the run presented 9 distinct values, NONE is) and the three research modifiers are summed in SINGLE precision (needs shrm/TRM non-zero; the corpus has both at 0). Both pinned by tests that fail with decimals. **CRITICALLY: three constants BESIDE them are genuinely exact doubles** - so blanket-widening is its own bug. Read the four bytes, every time |
|
||||||
| BANKRUPTCY factor measured, not assumed | objects | verified | high | 100% | 2026-09-08 | = 3.29999995 = (float)3.3. **Its file image is ZERO - the loader fills it at run time** - so lane PL had read the operand width and been forced to ASSUME the value. Now measured on the running game: the assumption was right and the narrowing is real. **No static reading could have produced this** |
|
| BANKRUPTCY factor measured, not assumed | objects | verified | high | 100% | 2026-09-08 | = 3.29999995 = (float)3.3. **Its file image is ZERO - the loader fills it at run time** - so lane PL had read the operand width and been forced to ASSUME the value. Now measured on the running game: the assumption was right and the narrowing is real. **No static reading could have produced this** |
|
||||||
| L5 kept `verified` at 0, and defended it | meta | verified | high | 100% | 2026-09-08 | The column has read 0 all campaign and L5 held it there ON PURPOSE. Every phase its compare touches is `Partial` for reasons UPSTREAM of the compare: P01's repair demand is taken as 0, P02 needs the AI's research orders, T31's BnkPr needs an unwired tuning constant. **None is fixable by a VM.** Promoting one because part of it was checked is exactly the drift app_test_catalog asserts against - the ladder's two conditions, COMPLETE and COMPARED, are blocked by different things and the column measures the conjunction. Catalog notes now carry the live evidence and its coverage instead of the older call counts. Also two predictions of its own falsified and recorded: the difficulty record is a HEAP POINTER at ServerPlayer+0x36c not an inline record (correcting the hook's standing "not reachable" note; the fitted pair stays a hypothesis, deliberately not dereferenced), and it predicted 2 divergences and got 1,359 because it counted players where the harness counts calls |
|
| L5 kept `verified` at 0, and defended it | meta | verified | high | 100% | 2026-09-08 | The column has read 0 all campaign and L5 held it there ON PURPOSE. Every phase its compare touches is `Partial` for reasons UPSTREAM of the compare: P01's repair demand is taken as 0, P02 needs the AI's research orders, T31's BnkPr needs an unwired tuning constant. **None is fixable by a VM.** Promoting one because part of it was checked is exactly the drift app_test_catalog asserts against - the ladder's two conditions, COMPLETE and COMPARED, are blocked by different things and the column measures the conjunction. Catalog notes now carry the live evidence and its coverage instead of the older call counts. Also two predictions of its own falsified and recorded: the difficulty record is a HEAP POINTER at ServerPlayer+0x36c not an inline record (correcting the hook's standing "not reachable" note; the fitted pair stays a hypothesis, deliberately not dereferenced), and it predicted 2 divergences and got 1,359 because it counted players where the harness counts calls |
|
||||||
|
| DECISION: determinism qualified, Rung B restated, reference pair swapped | meta | verified | high | 100% | 2026-09-08 | Adopted after lane L5's finding. (1) The oracle HOLDS where no AI must choose a target and FAILS where one must - never wrong, measured on the idle turn. (2) This is NOT a contradiction of lockstep MP but the reason for its shape: the AI is a StrategyClient deciding ONCE and relaying commands; the sim is deterministic given the stream. (3) **Canonical reference pair is now turn2-state -> turn3-state** (deterministic, oracle-hashed); turn1 -> turn2 is secondary with its four non-deterministic leaves masked. (4) **Rung B = byte-match given a recorded command stream (--turn-commands)**; a behaviourally-equivalent game/ai is **Rung C**, verified against the distribution of the original's choices. (5) Mechanism inferred, not proved: one-of-three varying suggests a pointer-ordered tie under ASLR rather than a time seed (which would move all three); probe routed to L1. Documented in backlog.md 2 and determinism-oracle.md |
|
||||||
|
|
|
||||||
|
|
@ -171,3 +171,36 @@ inflated offset (1-based) ref-turn2 re-save field (save_reader --dump path
|
||||||
State left behind: the game is still running on VM140 (pid 4384, task `SOTS`, shim `hooks=trace`), sitting
|
State left behind: the game is still running on VM140 (pid 4384, task `SOTS`, shim `hooks=trace`), sitting
|
||||||
on the turn-3 strategy map after run E; `C:\SOTS\SavedGames\` holds the run-E set (`ref-turn2.sav`,
|
on the turn-3 strategy map after run E; `C:\SOTS\SavedGames\` holds the run-E set (`ref-turn2.sav`,
|
||||||
`MyGameverify1*.sav`, the three `(Autosave*)` files).
|
`MyGameverify1*.sav`, the three `(Autosave*)` files).
|
||||||
|
|
||||||
|
## Qualified 2026-09-08 (lane L5; decision by the project)
|
||||||
|
|
||||||
|
**The byte-identical-autosave property holds on turns where no AI player has to choose a research
|
||||||
|
target. It does not hold on turns where one does.**
|
||||||
|
|
||||||
|
Lane L5 ran `turn1-state.sav → End Turn` three times on VM146 and got three different post-turn
|
||||||
|
autosaves, differing in exactly four leaves: one Singularity AI empire's research pick and
|
||||||
|
`Summary/Checksum` derived from it. The instrument is exonerated — two of the three runs carried
|
||||||
|
identical hooks and still differed, and the `hooks=off` run produced a third value.
|
||||||
|
|
||||||
|
Every earlier verification of this oracle used `ref-turn2.sav` (which lane AI4 showed is
|
||||||
|
byte-identical to `turn2-state.sav`). On that save all three AI players already hold a research
|
||||||
|
target, so the decision layer has nothing to decide; on `turn1-state` they must each choose (lane
|
||||||
|
AI4: all three set `ResRate 0.25→0.8` and pick a target on turn 1). The oracle was never wrong; it
|
||||||
|
was measured on the one turn where the non-deterministic layer was idle.
|
||||||
|
|
||||||
|
Why this does not break lockstep multiplayer: the AI is a `StrategyClient` (lane AI1), not part of
|
||||||
|
the sim. It decides once, on one machine, and its decisions are relayed as `TurnCommands` like any
|
||||||
|
player's. The sim — the two turn drivers, the tail, the strategic generator (lane Z, zero residual,
|
||||||
|
which the AI never draws from) — remains deterministic given the command stream. Peers reproduce
|
||||||
|
the response to a command, not the decision.
|
||||||
|
|
||||||
|
Consequences adopted:
|
||||||
|
- Canonical reference pair for the standalone metric is now **`turn2-state → turn3-state`**.
|
||||||
|
`turn1-state → turn2` is secondary, with the four leaves masked.
|
||||||
|
- Rung B is restated as *byte-match given a recorded command stream* (`--turn-commands`); a
|
||||||
|
behaviourally equivalent `game/ai` becomes Rung C, verified against the distribution of the
|
||||||
|
original's choices, not one run.
|
||||||
|
- Mechanism (inference, one probe pending with lane L1): only one of three AI players varies, which
|
||||||
|
suggests a tie broken by per-process iteration order over a pointer-keyed container under ASLR,
|
||||||
|
rather than a time seed. Reading the AI client's generator state after construction in two
|
||||||
|
processes separates the two.
|
||||||
|
|
|
||||||
|
|
@ -47,9 +47,17 @@ import state_checksum as ck # noqa: E402
|
||||||
# state the game itself produced by ending a turn on the input. Only the first family is a
|
# state the game itself produced by ending a turn on the input. Only the first family is a
|
||||||
# true End-Turn transition of one game; the others are listed so a regression on them is
|
# true End-Turn transition of one game; the others are listed so a regression on them is
|
||||||
# still visible, with their nature stated.
|
# still visible, with their nature stated.
|
||||||
|
# Order matters: pairs[0] is the REFERENCE the dashboard quotes.
|
||||||
|
#
|
||||||
|
# 2026-09-08: turn2->turn3 is now the reference. turn1->turn2 is NOT deterministic in the original
|
||||||
|
# (lane L5: three runs, three autosaves, four leaves — one AI empire's research pick plus the
|
||||||
|
# derived checksum; hooks=off was a third value, so it is the game). turn2->turn3 is the pair the
|
||||||
|
# byte-identical oracle hashes were taken on (every AI already holds a target, so the decision
|
||||||
|
# layer is idle). turn1->turn2 stays as a secondary pair; its four non-deterministic leaves are a
|
||||||
|
# Rung-B residual by construction and should be read as such (masking them is a follow-up).
|
||||||
PAIRS = [
|
PAIRS = [
|
||||||
("turn1-state.sav", "turn2-state.sav", "real End Turn"),
|
("turn2-state.sav", "turn3-state.sav", "real End Turn (canonical reference)"),
|
||||||
("turn2-state.sav", "turn3-state.sav", "real End Turn"),
|
("turn1-state.sav", "turn2-state.sav", "real End Turn (secondary; 4 leaves non-deterministic in the original)"),
|
||||||
]
|
]
|
||||||
|
|
||||||
DEFAULT_BINARIES = [
|
DEFAULT_BINARIES = [
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue