Commit graph

2 commits

Author SHA1 Message Date
alex
98257b9e82 RB: sots_turn --turn-commands replays a recorded command stream, and ModCount closes
The milestone needs a second input. The AI is a client, not part of the sim: it decides once,
on one machine, and its decisions reach the server as commands. A save carries the board and
half the input, which is why our turn wrote ModCount 14 where the original writes 24 -- the
missing ten ARE the turn's command stream.

  * `game/ai/apply_order` -- the thirty-step schedule the original drains a batch in: twenty-
    seven per-LIST steps (every player's elements of one list before the next list starts) with
    three per-PLAYER gate loops spliced in at step 10, 29 and 30. Neither list order nor member
    order, and both facts are asserted so a port that sorted cannot pass.
  * `game/ai/command_capture` -- a `.tcb` recorded turn: gates, list lengths, elements in wire
    order, per-client seeds, and `?` for a field the instrument could not read. An element count
    that disagrees with its declaration is REJECTED, because a counter quietly one short is
    indistinguishable from a turn that issued one fewer command.
  * `app/command_replay` -- applies it before the drivers, where the End-Turn dispatcher does.
    Every command is CHARGED; only the ones whose subsystem we hold are APPLIED; the rest are
    declined with the named gap, or marked incomplete when the capture itself lacks the payload.
  * `--turn-commands`, `--replay-count-only`, `--replay-recorded-names`, `--ai-seed`.

Measured on a fresh build directory, canonical pair turn2-state -> turn3-state:
  108 -> 62, closed 46, regressed 0  (was 108 -> 63, closed 45) -- /Sim/ModCount now reads the
  original's 24, decomposed as 2 drivers + 4 research-rate gates + build + rates + list 10 +
  two list-14 + fleet move, with the list-23 population element free.

turn1-state replayed against the SAME run's autosave closes 7 (ModCount and all six research
leaves); against the historical turn2-state it closes 6 and leaves player 512's research pick
diverging -- which is correct, because that recording is from a process that picked differently.

One prediction was falsified and it paid for itself: the first run regressed two leaves because
the rates element's MEMORY field order is not its wire order. The converter no longer claims a
mapping it cannot support.

Two new addresses (the second and third gate-loop heads) via ghidra/addresses.d/lane-rb.json;
header regenerated, never hand-resolved.
2026-09-08 18:57:46 -04:00
alex
39c01422f7 src/app: the standalone -- load a save, run a turn, write a save
`sots_turn` loads a save through the engine's own reader, walks the published
phase order of all three turn drivers, runs what we hold, prints what we do
not, and writes the result back through the engine's own writer.

The phase catalog carries all 32 + 12 + 37 phases whether or not they are
implemented, so an unimplemented phase is a named no-op that appears in the run
log rather than a silent absence. 14 of the 44 turn-driver phases are modelled,
7 commit anything, 2 of the 37 tail phases are modelled.

Modelled but NOT committed is a first-class state. A phase whose formula we hold
and whose inputs we do not is evaluated, reported, and left unwritten unless
--commit-blocked is passed. That distinction was earned: committing phase 31's
player-status restore regressed two leaves that had agreed with the oracle
before the turn, because the phase writes 1 and the file carries 4.

Measured against the game's own post-turn saves, leaves localised by
state_checksum.py with coverage proved by re-serialisation:

  turn1-state -> turn2-state   209 -> 204 diverging, closed 5, regressed 0
  turn2-state -> turn3-state   108 -> 103 diverging, closed 5, regressed 0

Two tests: app_catalog (the tables stay complete and nothing claims to be
verified against a live game) and app_turn (11 saves driven; an untouched load
re-serialises byte-identically, a turn leaves the file re-readable, and no
blocked or stub phase writes anything). Skips cleanly without SOTS_SAVES_DIR.

ctest 38/38, clean-room OK. src/shim untouched. docs/S-standalone.md has the
full gap list.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
2026-09-08 10:35:45 -04:00