docs: EV prediction committed before the build -- P11 posts one event, closed 4/3, regressed 0

This commit is contained in:
alex 2026-09-08 15:23:16 -04:00
parent eafbc5f03a
commit 4065808a05

128
docs/EV-events.md Normal file
View file

@ -0,0 +1,128 @@
# EV — posting events into the save's turn bucket (P11)
Roadmap item 5. The engine already **models** the event log (`game/events`: turn-bucketed
storage, per-bucket dedup, the `FLT_MAX` position sentinel, the `act == 0 && no subject && no
position -> 2` rule). What was missing is that the standalone never wrote any of it into the
save it produces. This lane wires that up for the one event a turn on the reference pair
actually posts that the standalone can compute, and reports the rest as blocked with the
blocker named.
Nothing here carries the game's displayed text. The engine holds `EVENTSUM_*` / `EVENTMSG_*`
keys only; the text is resolved at run time through a lookup the caller supplies, which the
standalone builds from the operator's own installed string table (`--data`).
---
## 1. What a turn actually posts (measured, whole corpus)
Not derived — read out of the eleven saves in `verify/results/saves` with the state-checksum
tree. Only two players in any corpus save ever hold an event at all: the human (`PlyrIdx 0`)
and the one AI empire that owns colonies (`PlyrIdx 1`). Every other player's `EvNxID` is 0 on
every save, including the two AI shadow empires that *do* pick research targets.
**Reference pair `turn1-state.sav -> turn2-state.sav` — the whole turn posts two events:**
| player | bucket | id | image | act | loc |
|---|---|---|---|---|---|
| 0 (human) | `EvTurn=2` | 1 | `EVENT_NO_RESEARCH` | 1 | 0 |
| 1 (AI) | `EvTurn=2` | 1 | `EVENT_SHIPS_BUILT` | 0 | 288 |
**Pair 2 `turn2-state.sav -> turn3-state.sav` — three:**
| player | bucket | id | image | act | loc |
|---|---|---|---|---|---|
| 0 | `EvTurn=3` | 2 | `EVENT_NO_RESEARCH` | 1 | 0 |
| 1 | `EvTurn=3` | 2 | `EVENT_SHIPS_BUILT` | 0 | 288 |
| 1 | `EvTurn=3` | 3 | `EVENT_RESEARCH_OVERBUDGET` | 1 | 0 |
**Order matters and is visible in the ids**: within player 1's turn-3 bucket the construction
event is id 2 and the research event is id 3, so the build pass posts **before** the research
pass. Ids are per player (`EvNxID` lives on the player's own storage), so no cross-player
order is observable and none is assumed.
The bucket's `EvTurn` is the **post-increment** turn: a turn run from a save at turn *N*
posts into bucket *N+1*. In the standalone that is `game.sim.frame` after `H00`.
Across the whole corpus the only other images seen are `EVENT_SHIPS_BUILT`,
`EVENT_RESEARCH_COMPLETE`, `EVENT_TECHS_UNLOCKED`, `EVENT_RESEARCH_OVERBUDGET`,
`EVENT_FLEET_ARRIVED`, `EVENT_FLEET_EXPLORED`, `EVENT_FLEET_MULTIPOINT_NONODE`,
`EVENT_LABACCIDENT_SMALL`, `EVENT_COLONY_NEWSETTLERS`, `EVENT_ENEMY_INCOMING_Human` and
`EVENT_NO_RESEARCH`. None of the tail's event phases (`T18`, `T21`, `T32`) fires on either
reference pair — measured, not assumed; on other corpus saves they clearly do.
## 2. The no-research condition, corrected
The standalone's condition was `no research target && at least one available tech`. That is
two thirds of the original's and it **over-fires by three players** on the reference pair.
The original's is three tests, all of which now have a name in `ghidra/addresses.json`:
1. `ResT == NULL` — no research target;
2. `TechTree::CollectResearchedTechs(out, turn, INT_MAX, sort=1)` came back **empty**, i.e.
**no tech finished on this turn or later**;
3. `TechTree::FindFirstAvailableTech() != NULL`, i.e. at least one node is in state 2.
Test 2 was missing here and it is not decoration. `zuul-turn23-fleet23.sav` exercises it: the
human posts `EVENT_NO_RESEARCH` on turns 2..15, posts `EVENT_RESEARCH_COMPLETE` on turn 22
and **no** no-research event that turn, then posts `EVENT_NO_RESEARCH` again on turn 23. That
is exactly test 2 suppressing the event on the turn a tech landed.
Tests 2 and 3 are computable from the wire: a `TechState` row's `St` is the node state
(0 hidden, 1 parent researched, 2 available, 3 available and selected, 4 researched) and
`TAcq` is the turn it was researched.
Test 1 is **not** fully computable. `ResTNm` gives the target the save was written with, but
three of the four real players on the reference pair acquire a target *during* the turn, and
that is AI research selection (`game/ai`). The stand-in is the operator-supplied AI roster,
`--ai-player N`, which the standalone already carries for `T31`:
> **HYPOTHESIS (workload: the whole 11-save corpus).** An AI-controlled player that begins its
> turn with no research target acquires one before the no-research check, so it never posts
> `EVENT_NO_RESEARCH`. Support: across the corpus no AI player posts that event on any of
> roughly ninety player-turns, while the human posts it on every turn it has no target.
> Falsified by a single AI player-turn carrying the event.
So `P11` stays **blocked**, commits only under `--commit-blocked=P11`, and even then posts
nothing for a player the operator named as AI and nothing at all without a text table.
## 3. Prediction (written before the build; rule 2)
Baseline moved under this lane: `main` is now `eafbc5f` (lane T2). Re-measured with this
lane's own binary before any change: pair 1 **209 -> 131** (closed 78, regressed 0), pair 2
**108 -> 72** (closed 36, regressed 0). The brief's 209 -> 157 / 108 -> 86 predate T2. The
numbers below are against the re-measured baseline, so the lane's own contribution is
pair 1 **131 -> 127** and pair 2 **72 -> 69**.
**Run A — `--data <root> --ai-player 1 --ai-player 2 --ai-player 3 --commit-blocked=P11`.**
`P11` posts exactly one event, for player 0, into bucket `EvTurn = frame`:
`EvEID` 1 (pair 1) / 2 (pair 2), `EvDsc` and `EvMsg` from `EVENTSUM_NO_RESEARCH` /
`EVENTMSG_NO_RESEARCH`, `EvImg = "EVENT_NO_RESEARCH"`, `EvLoc = 0`,
`EvPos = {FLT_MAX, FLT_MAX, FLT_MAX}`, `EvAct = 1`, `EvCID = 0`; `EvNxID` 0 -> 2 (pair 1),
2 -> 3 (pair 2).
* pair 1: **closed 4, regressed 0** — `Events/EvNxID`, the empty-collection leaf
`Events/Events/.`, the new count leaf `Events/Events/.[0]`, the new bucket
`Events/Events/.[EvTurn=2]`.
* pair 2: **closed 3, regressed 0** — `Events/EvNxID`, `Events/Events/.[0]` (1 -> 2), the new
bucket `Events/Events/.[EvTurn=3]`.
Player 1's four (pair 1) / three (pair 2) event leaves stay: they need `EVENT_SHIPS_BUILT`,
which needs a ship the turn builds, which needs an AI-generated build order — the same
blocker `B6` and `T36` already name.
**Run B — the control, no `--data`, no `--ai-player`, no `--commit-blocked`.** closed 0,
regressed 0; the phase reports the condition and posts nothing.
### Falsification
| # | if the model is wrong this way | symptom |
|---|---|---|
| F1 | the AI-picks-a-target hypothesis is wrong | with the roster supplied the event is missing for a player the oracle has one for; without it, four posts and roughly fifteen regressed leaves on pair 1 |
| F2 | the bucket turn is the input turn, not the incremented frame | the bucket lands at `EvTurn=1`; closed 1, regressed 2 |
| F3 | the position sentinel is written as `+inf` rather than `FLT_MAX` | bucket appears, `EvPos/.[0..2]` diverge; closed 3, regressed 3 |
| F4 | the `act == 0 -> 2` rule is applied where it should not be (`act` here is 1) | `EvAct` diverges; regressed 1 |
| F5 | the writer emits a trailing node on a record the engine synthesised rather than parsed | the post-turn save's coverage drops below the input's, visible in the report's `coverage` line |
| F6 | the "researched this turn" gate is wrong | no visible effect on either reference pair (nothing is researched on either turn); it would show as a spurious post on the turn a corpus save completes a tech |
Run B is the control for F1: it is the same code with the input withheld, and its over-fire
count is a direct measurement of how much of the phase the AI roster is carrying.