Compare commits
3 commits
3a44f9178b
...
f6a914beee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a914beee | ||
|
|
55ea86f0d5 | ||
|
|
fbe3c7bd86 |
6 changed files with 455 additions and 42 deletions
|
|
@ -232,3 +232,6 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| ship design: three sections, not five | objects | verified | high | 100% | 2026-09-08 | THREE CORRECTIONS FORCED. (1) THREE sections, not five - the campaign's "slots 3-4 reserved" was save_reader.py's trailing Rest("sections") sweeping Dtc/Dwgv into the section list and stock_designs.py decoding them as empty slots. **RULE 8 EXACTLY: reader and consumer agreed and were both wrong.** Two independent enumerations say three (straight-line writer; ctor eh_vector_constructor_iterator(this+0x24, 0x28, 3) closing at 0x9c = sizeof(ShipDesignDef)). (2) DWep/Dwgv are BOOLS, byte-indistinguishable at a 4-char tag - same class as odet. (3) Offset order != write order: array [+0x24,+0x4c,+0x74] = mission, command, engine; WIRE = command, mission, engine. Oracles fixed openly (rule 12): save_reader.py 49/49, stock_designs.json regenerated with the diff ONLY raw_slots 5->3 and dWep int->bool across all 127 designs, design rules still 32/32 and 127/127 |
|
||||
| hull size and the 0x400 flag | objects | verified | high | 90% | 2026-09-08 | **0x400 = defence_platform**, read off the .shipsection parser's own bit setter at 0x005749b7 directly after the strcmp against the literal; the design's word is the OR across its <=3 sections. **NOT lane B5's 0x400** - that is a FLEET flag, on the wire as FtFlg (turn1-state fleet 0 has FtFlg 1024). Different word, different object. HULL SIZE = section_class via ParseShipClassName 0x0056e1c0, _stricmp against Destroyer/Cruiser/Dreadnought -> 0/1/2; absent or unrecognised -> **0 WITH A LOG LINE, NOT AN ERROR** (16 shipped sections have no section_class); ASSIGNMENT NOT MAX - last resolved section in MEMORY ORDER wins. Both live on ShipDesign, NEITHER ON THE WIRE, both rebuilt by UpdateDerivedStats. Independent corroboration: the same bit picks default hull health (500/3000/15000 without, 100/500/1000 with) - platforms, not ships. CENSUS VERIFIED: 11 saves, 503 designs, 480 leaves, 0 mismatched, computed TWICE independently (Python off save_reader, C++ off the engine's shapes reader - they DISAGREE about section count and still agree on all 480) |
|
||||
| census coverage: 3 of 6 leaves UNEXERCISED | verify | backlog | — | 0% | 2026-09-08 | Rule 15 again, unprompted. Only 32 of 480 census leaves are NONZERO: cls0 shpt 18, cls0 satt 3, cls1 shpt 0, cls1 satt 0, cls2 shpt 11, cls2 satt 0. **Three of the six census leaves are UNEXERCISED, NOT VERIFIED** - no cruiser of any kind and no DN platform in the corpus. ONE SAVE WITH A CRUISER CLOSES TWO OF THEM. Also 0 ships with an unresolvable design, 0 unclassifiable designs, 0 designs where first- and last-resolved section disagree on hull size |
|
||||
| T36 census wired; a NEW blocker found by a wrong prediction | verify | mapped | high | 100% | 2026-09-08 | Lane E2. Default run UNCHANGED at closed 5 / regressed 0 on both pairs, exactly as predicted (T36 does not commit by default). --commit-blocked regressions fell 9 -> **7** (pair 1) and 11 -> **9** (pair 2). PREDICTION WAS 6 AND 8, AND THE MISS IS THE FINDING: falsification hypothesis 1 fired - two census leaves closed (shpt[2]=5 dreadnoughts, satt[0]=10 platforms) but **shpt[0] did not, and the archived value is higher than ours by EXACTLY ONE DESTROYER ON BOTH PAIRS** (0 vs 1, then 1 vs 2). The self-check on the INPUT turn is exact, so this is NOT a classification error - it is a NEW NAMED BLOCKER: **no phase we run builds a ship** (S11's build-queue sub-pass is the declared input boundary). Census itself reproduced through the app's own path: 11 saves, 80 records, 1040 fields, 480 census leaves, 0 mismatches - and it visits slots in the ORIGINAL'S IN-MEMORY ORDER (mission, command, engine) rather than the wire's, so it is an independent route to lane D2's 480/480 |
|
||||
| T36 remains blocked on TWO named things | verify | backlog | — | 0% | 2026-09-08 | Neither is lane E2's: (1) sav/inc x3 = the budget, lane E1's income chain; (2) shpt[0] x1 = unmodelled ship construction. **The briefed --commit-blocked-except=<fields> knob is NOT IMPLEMENTABLE and lane E2 did not fake one**: the archived record is ONE STRUCT ON THE WIRE, so sav/inc cannot be omitted while the other eleven fields are written - the format has no hole. It implemented the same convention at PHASE granularity instead (--commit-blocked=IDS / --commit-blocked-except=IDS), which is what isolated the T36 rows and cleanly separated S31's two Player/Status regressions out of the pair-2 number |
|
||||
| standalone data root | engine | verified | high | 100% | 2026-09-08 | The standalone grew `--data DIR` / $SOTS_DATA_DIR. NO GAME DATA EMBEDDED; absence costs exactly the six census counters and SAYS SO. Note for integrators: with a data root set, `game_data_realdata` fails - and it FAILS IDENTICALLY ON UNMODIFIED MAIN, because the extracted sots.gob is a different data version than the counts that test pins. Not a regression; worth pinning the data version in that test |
|
||||
|
|
|
|||
106
findings/subsystems/system-visibility-prediction.md
Normal file
106
findings/subsystems/system-visibility-prediction.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# Prediction: the visibility phase, before it is built
|
||||
|
||||
Lane E3, 2026-09-08. Written and committed **before** the engine change exists (rule 2).
|
||||
Model: `findings/subsystems/system-visibility-record.md`.
|
||||
|
||||
Baseline, measured on `sots-engine` main `0592104` this morning:
|
||||
|
||||
```
|
||||
turn1-state -> turn2-state 204 leaf differences
|
||||
turn2-state -> turn3-state 103 leaf differences
|
||||
```
|
||||
|
||||
## The model, as three rules
|
||||
|
||||
1. **T17 `RecordObservation`.** For every player `p` and system `s` with
|
||||
`s.AFlags & (1 << p.PlyrIdx)`: set `NVE[p] = { EPid = p's handle id, ETS = frame,
|
||||
Eid = the encounter id at s }`. `frame` is the post-increment frame, i.e. the turn number
|
||||
the autosave carries. Entries are ordered by `PlyrIdx` ascending (a `std::map`). Nothing is
|
||||
ever erased.
|
||||
2. **T21 `SetExploredBy`.** `s.EFlags |= s.AFlags`.
|
||||
3. **S29 `ltis`.** If `s.AFlags != 0` then `s.ltis = frame`.
|
||||
|
||||
`Eid` is taken from an existing `NVE` entry when the system already has one (the field it
|
||||
mirrors is fixed for the life of the game); otherwise from a fleet at the system with
|
||||
`FtEnc != 0`; otherwise `-1`.
|
||||
|
||||
`AFlags` is **read from the save, not recomputed**. Justification, and it is checkable: neither
|
||||
reference pair moves an `AFlags` leaf, so the loaded value is the value the phase would see.
|
||||
|
||||
## Predicted leaves closed
|
||||
|
||||
### `turn1-state -> turn2-state` (the 32-leaf target)
|
||||
|
||||
Eight systems — 64 Hyperion, 112 Gamma Cephei, 288 Ke'Dolarra, 304 Koa'Vo, 336 Kaa'Vaalu,
|
||||
400 Markab, 448 Kea'Pono, 480 Ko'Rorkor — are exactly the systems with `AFlags != 0`.
|
||||
|
||||
| leaves | rule | count |
|
||||
|---|---|---:|
|
||||
| `Sys[*]/NVE` count `0 -> 1` | 1 | 8 |
|
||||
| `Sys[*]/EPid` only-in-B | 1 | 8 |
|
||||
| `Sys[*]/ETS` only-in-B | 1 | 8 |
|
||||
| `Sys[*]/Eid` only-in-B | 1 | 8 |
|
||||
| **the brief's target** | | **32** |
|
||||
| `Sys[*]/EFlags` `0 -> {16,128,16,16,16,16}` on 64, 304, 336, 400, 448, 480 | 2 | 6 |
|
||||
| `Sys[*]/ltis` `1 -> 2` on all eight | 3 | 8 |
|
||||
| **total predicted closed** | | **46** |
|
||||
|
||||
Predicted `EPid` values, per system, in order: 528, 16, 32, 576, 528, 528, 528, 528.
|
||||
Predicted `Eid` values: 5, -1, -1, -1, 3, 3, 4, 4. Predicted `ETS`: 2 everywhere.
|
||||
|
||||
Expected result: **204 -> 158, closed 46, regressed 0.**
|
||||
|
||||
### `turn2-state -> turn3-state`
|
||||
|
||||
The entries already exist, so only their stamps move.
|
||||
|
||||
| leaves | rule | count |
|
||||
|---|---|---:|
|
||||
| `Sys[*]/ETS` `2 -> 3` on the same eight | 1 | 8 |
|
||||
| `Sys[*]/EFlags` | 2 | 0 (already equal to `AFlags`) |
|
||||
| `Sys[*]/ltis` `2 -> 3` on the same eight | 3 | 8 |
|
||||
| **total predicted closed** | | **16** |
|
||||
|
||||
Expected result: **103 -> 87, closed 16, regressed 0.**
|
||||
|
||||
`EPid` and `Eid` must be **re-emitted unchanged**; if either moves, the model has regressed a
|
||||
leaf that agreed by construction, and that is a failure of rule 3, not a rounding detail.
|
||||
|
||||
## Falsification
|
||||
|
||||
Each of these is a way the model is wrong and the symptom that would show it.
|
||||
|
||||
* **The gate is `VFlags`, not `AFlags`.** On both reference pairs `VFlags == AFlags` on every
|
||||
system, so the pairs cannot separate them. The discriminator already in the corpus is
|
||||
`zuul-turn23-fleet23.sav` Bismol: `VFlags = 2`, `AFlags = 0`, `ETS` frozen at 22 while the
|
||||
save is turn 23. A `VFlags` model refreshes it to 23. **Symptom:** a host test that replays
|
||||
Bismol's row fails. That test is written.
|
||||
* **The gate is `EFlags`.** Same corpus discriminator: Bismol has `EFlags = 2`.
|
||||
* **`ETS` is the sighting turn, set once, not refreshed.** Then `turn2 -> turn3` closes 0 of
|
||||
its 8, and the eight `ETS 2 -> 3` leaves stay. **Symptom:** pair 2 closes 8 instead of 16.
|
||||
* **`Eid` is re-derived from the fleet every turn rather than parked on the system.** Invisible
|
||||
on this corpus — no encounter fleet dies in it. **Symptom:** none available; that is why §8
|
||||
of the model doc names the workload instead of claiming the rule.
|
||||
* **The `Eid` test is `FtFlg & 0x10`, not `FtEnc != 0`.** Invisible on this corpus; the six
|
||||
encounter fleets agree on both. **Symptom:** none available.
|
||||
* **`EPid` is the player index, not the handle id.** Then all eight `EPid` leaves regress to
|
||||
0..7 instead of closing. **Symptom:** 8 closed becomes 8 regressed, and the run's closed
|
||||
count drops to 38 / 16.
|
||||
* **The map is ordered by insertion, not by key.** Invisible: every system in the corpus has
|
||||
exactly one entry. **Symptom:** none available; the ordering is asserted from the container
|
||||
type, and §8 names the two-empire-contact workload that would exercise it.
|
||||
* **`ltis` is not the frame but something that merely equals it here.** `ltis` is `1` at
|
||||
turn 1 and `2` at turn 2 on the same systems, which is consistent with a great many
|
||||
counters. The reading is a 41-byte function body; if it is wrong the 8 `ltis` leaves regress
|
||||
rather than close.
|
||||
* **The phase runs before the frame increment.** Then every stamp is one low and 24 leaves
|
||||
regress across the two pairs.
|
||||
|
||||
## What the model deliberately does not write
|
||||
|
||||
* `NVO.TShn` (10 leaves per pair). Its gate is not `AFlags` — Spica has `AFlags == 0` and
|
||||
`TShn` still moves. Formula not held; **evaluated and reported, not written** (lane S2's
|
||||
rule).
|
||||
* `rcex` (6 leaves per pair). Unexplained.
|
||||
* `NVs` / `PlayerView`. Count 0 on both reference pairs; the `PlayerView` refresh loop is a
|
||||
separate phase and no leaf of it diverges on either pair.
|
||||
160
findings/subsystems/system-visibility-record.md
Normal file
160
findings/subsystems/system-visibility-record.md
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
# The per-system visibility record: `NVE`, `ltis`, and the explored mask
|
||||
|
||||
Lane E3, 2026-09-08. Target: the 32 diverging `nve` leaves on the reference pair.
|
||||
|
||||
Status of each claim is marked **[V] instruction-verified**, **[D] decompiler-only**, or
|
||||
**[H] hypothesis** (rule 6).
|
||||
|
||||
---
|
||||
|
||||
## 1. What the record is
|
||||
|
||||
`Sys.NVE` is a `std::map<int playerIdx, {int16 ETS, int32 Eid}>` at `ServerSystem+0x284/+0x288`.
|
||||
It is **"what this player last saw at this system"**: the turn of the sighting and the
|
||||
random-encounter type that was there. Its sibling `NVO` is the same idea for *ownership*
|
||||
(`TShn` = turn, `OID` = owner as last seen), and `NVs`/`PlayerView` is the same idea for the
|
||||
*colony's numbers*. Three maps, one concept, one key.
|
||||
|
||||
Node layout, reconciling `struct-recovery.md` §1.2 with the accessors below: MSVC `_Tree` nodes
|
||||
put `_Myval` at `+0xc`, so the key (player index) is `node+0xc` and the **8-byte value is
|
||||
`node+0x10`**:
|
||||
|
||||
| value offset | wire name | type | meaning |
|
||||
|---|---|---|---|
|
||||
| +0x0 | *(not serialised)* | int16 | the turn the record was last **touched** |
|
||||
| +0x2 | `ETS` | int16 | the turn the **sighting** was made |
|
||||
| +0x4 | `Eid` | int32 | the random-encounter type id present, `-1` for none |
|
||||
|
||||
The key is written to the wire as `EPid`, the player **handle id** (16, 32, 528 …), not the
|
||||
index. That reconciles the isnil byte at `+0x19` with an 8-byte value. **[V]** (arithmetic
|
||||
over the accessors in §2, each of which reads a named offset).
|
||||
|
||||
## 2. Every writer and reader of the map
|
||||
|
||||
| addr | what it is | body |
|
||||
|---|---|---|
|
||||
| `0x00756300` | **`ServerSystem::RecordObservation(p, encId)`** — the primary writer | `f = (int16)this->owner(+0x10)->Frame(+0x8); rec = NVE[p->PlyrIdx(+0x28)]; rec[0] = (f<<16)|f; rec[1] = encId;` **[D]** |
|
||||
| `0x007536a0` | `ServerSystem::CopyObservationTo(p, srcRec)` — intel sharing | `rec = NVE[p->PlyrIdx]; rec.touched = (int16)Frame; rec.ETS = src.ETS; rec.Eid = src.Eid;` — the *sighting* stamp is copied, only the touch stamp is refreshed **[D]** |
|
||||
| `0x00754d90` | `ShareObservation(systhis, from, to)` | `a = Find(from); b = Find(to); if (a && (!b || b->ETS < a->ETS)) CopyObservationTo(to, a);` — newer sighting wins **[D]** |
|
||||
| `0x0074d360` | `ServerSystem::FindObservation(p)` | map find on `p->PlyrIdx`, returns `node+0x10` or null **[D]** |
|
||||
| `0x0074f830` | `ServerSystem::LastSeenEncounterId(p)` | map find, returns `node+0x14` (`Eid`) or `-1` **[D]** |
|
||||
| `0x00752730` | the map's `operator[]` | 3 callers only: the two writers above and `ServerSystem::Read` **[V]** (cross-reference query) |
|
||||
|
||||
**There is no eraser.** `operator[]` has exactly three callers and none of them removes an
|
||||
entry, so once a player has an entry at a system it keeps it for the rest of the game. That is
|
||||
what makes the record a *memory* rather than a *state*.
|
||||
|
||||
## 3. Where the primary writer is called from
|
||||
|
||||
Tail phase 17, `FUN_007cf560` (the "rebuild every `PlayerView`" phase). Disassembled at
|
||||
`0x007cf7a7..0x007cf7ce` — **[V]**, byte-decoded, not from the decompiler:
|
||||
|
||||
```
|
||||
mov edx,[esi+0x44] ; systems.begin
|
||||
mov edi,[edx+ebx*4] ; edi = sys
|
||||
mov eax,[ebp-0x14] ; player
|
||||
push eax
|
||||
mov ecx,edi ; this = sys
|
||||
call 0x00743fb0 ; ServerSystem::IsVisibleTo(sys, player)
|
||||
test al,al
|
||||
jz skip
|
||||
mov eax,[edi+0x184] ; the encounter type id parked on the system
|
||||
mov ecx,[ebp-0x14] ; player
|
||||
push eax ; arg: encId
|
||||
push ecx ; arg: player
|
||||
mov ecx,edi ; this = sys
|
||||
call 0x00756300 ; RecordObservation
|
||||
```
|
||||
|
||||
Outer loop is over **players**, inner over **systems**. The gate is one instruction:
|
||||
|
||||
* `FUN_00743fb0(sys, p)` = `(sys->AFlags(+0xd4) >> p->PlyrIdx(+0x28)) & 1` **[V]**.
|
||||
|
||||
`AFlags` — not `VFlags`, not `EFlags`. `combat-retreat-pipeline.md` §5 already had `AFlags` as
|
||||
the *derived, non-sticky* union `FFlags | GFlags | isOwner`, recomputed on every fleet arrival
|
||||
and departure. So the record is refreshed exactly while the player still has something at the
|
||||
system, and freezes the moment the last fleet leaves and the player does not own it.
|
||||
|
||||
## 4. `Eid`'s source
|
||||
|
||||
`sys->+0x184` is a `StarSystem` member initialised to `-1` in the constructor
|
||||
(`FUN_00752ea0`, `param_1[0x61] = 0xffffffff`) **[D]**, and set by
|
||||
`FUN_007887c0(sys, encId)` — the encounter-placement routine, which refuses unless the system
|
||||
is unowned, planetless and still `-1` **[D]**. **It is not on the wire.** It is fixed for the
|
||||
life of the game, so the only observable it can be recovered from is the encounter fleet
|
||||
sitting at the system, whose `FtEnc` carries the same id.
|
||||
|
||||
Across all 11 saves, `Eid == FtEnc` of the fleet at the system, for every one of the six
|
||||
encounter fleets in the corpus, and `-1` everywhere else. The `Defenses` fleet at Koa'Vo has
|
||||
`FtEnc == 0` and `FtFlg == 0x400`, and its system's `Eid` is `-1`; every real encounter fleet
|
||||
has `FtEnc > 0` and `FtFlg & 0x10`. **Nothing in the corpus separates "`FtEnc > 0`" from
|
||||
"`FtFlg & 0x10`" as the test, and nothing separates either from the true rule, which reads a
|
||||
field that is not saved at all.** Labelled **[H]**; §8 names the workload.
|
||||
|
||||
## 5. `ltis` — a second writer, in the other driver
|
||||
|
||||
Driver phase 29, `FUN_00743ec0` (41 B, whole body) **[D]**:
|
||||
|
||||
```c
|
||||
if (this->AFlags(+0xd4) != 0)
|
||||
this->ltis(+0x2c8) = this->owner(+0x10)->Frame(+0x8);
|
||||
```
|
||||
|
||||
`ltis` is the system-level twin of `ETS`: same clock, same gate, but the gate is
|
||||
`AFlags != 0` for **any** player rather than for a particular one, and the value is a full
|
||||
int rather than an int16. `board.md` records `TShn`/`ltis` as *"NOTHING NAMES THEIR WRITER"* —
|
||||
this names `ltis`'s. `TShn` is still unnamed and is **not** the same rule (§7).
|
||||
|
||||
## 6. The explored mask, and the one thing `OnExploredChanged` does
|
||||
|
||||
Tail phase 21, `FUN_007a3c60`, per system × per player **[D]**:
|
||||
|
||||
```c
|
||||
if (IsVisibleTo(sys, p) && !IsExploredBy(sys, p)) {
|
||||
SetExploredBy(sys, p, true); // 0x007438b0: EFlags |= 1 << PlyrIdx
|
||||
NodeGraphRegister(...); // 0x006e4620
|
||||
PostEvent("EVENT_FLEET_EXPLORED", ...);
|
||||
}
|
||||
```
|
||||
|
||||
so **`EFlags |= AFlags`**, once per turn, in the tail.
|
||||
|
||||
`SetExploredBy`'s trailing `vft[0x1c](p, wasSet, on)` — the indirect edge `lane-b5.json` flagged
|
||||
as unresolved — resolves through `Game::ServerSystem`'s primary vftable `0x00a2044c` slot 7 to
|
||||
**`0x007480b0`** (`tools/vtable_map.py`). Its whole body **[D]**:
|
||||
|
||||
```c
|
||||
if (!wasSet && on && server->Frame(+0x8) > 1) {
|
||||
FUN_00747a20(p->PlyrIdx, 1);
|
||||
if (p->PlyrIdx < 15) this->+0x2a4 |= 1 << p->PlyrIdx;
|
||||
}
|
||||
```
|
||||
|
||||
`+0x2a4` is past `NVs` and is not in the serialised table: a runtime "newly explored this turn"
|
||||
mask, not save state. **The edge is resolved and it writes nothing that reaches the wire.**
|
||||
Note the `Frame > 1` guard — turn 1 is special-cased throughout this subsystem.
|
||||
|
||||
## 7. What this does NOT explain
|
||||
|
||||
`NVO.TShn` moves on **more** systems than `NVE.ETS` does. On `turn1 -> turn2`, Spica has
|
||||
`AFlags == 0`, no `NVE` entry and no `ltis` move — and its `TShn` still goes 1 -> 2. So `NVO`
|
||||
has a looser gate than `AFlags`, and it is **not** `VFlags`, `EFlags` or `FFlags` either (all
|
||||
zero at Spica). `TShn`'s writer remains unnamed. Conversely `zuul-turn23-fleet23.sav`'s Bismol
|
||||
has `AFlags == 0` and freezes `TShn`, `ETS` **and** `ltis` at 22 together — so on that save the
|
||||
two rules agree. Whatever `NVO`'s gate is, it admits Spica and excludes Bismol.
|
||||
|
||||
`rcex` (int64) moves `0 -> 65536` on the same six systems on `turn1 -> turn2` and back
|
||||
`65536 -> 0` on `turn2 -> turn3`. `65536 = 1 << 16`, and Koa'Vo takes `1 << 28`. It is a
|
||||
per-system word that toggles with the encounter/visibility cluster and it is **not** modelled
|
||||
here.
|
||||
|
||||
## 8. Workloads that would settle the open items
|
||||
|
||||
| open item | workload |
|
||||
|---|---|
|
||||
| whether `Eid` is `sys->+0x184` (fixed at generation) or re-read from the fleet each turn | a save where an encounter fleet is **destroyed** at a system that stays visible: `+0x184` keeps the id, a fleet-derived rule drops to `-1` |
|
||||
| whether the `Eid` source test is `FtEnc > 0` or `FtFlg & 0x10` | any save with a fleet carrying `FtEnc > 0` and `FtFlg & 0x10` clear, or the converse |
|
||||
| `NVO.TShn`'s gate | a watchpoint on `ServerSystem+0x274`'s map during one turn — rule 18; Spica vs Bismol is already the discriminating pair |
|
||||
| more than one player observing one system | every save in the corpus has **single-bit** `AFlags`. A two-empire contact save would exercise the map ordering (ascending `PlyrIdx`) and the `EPid` handle lookup for a second entry |
|
||||
| `ShareObservation` (alliance intel) | no save has two players in an alliance; the newer-sighting-wins rule has never executed |
|
||||
| `rcex` | unassigned |
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# standalone vs the oracle
|
||||
|
||||
generated 2026-09-08T15:35:00Z binary /home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn
|
||||
generated 2026-09-08T17:23:54Z binary /home/alex/sots-engine-wt-turnrecord/build-host/src/app/sots_turn
|
||||
|
||||
phases: 14/44 of the two turn drivers modelled, 8 committed (implemented 2, partial 6, blocked 6, stub 30)
|
||||
phases: 15/44 of the two turn drivers modelled, 9 committed (implemented 3, partial 6, blocked 6, stub 29)
|
||||
3/37 of the post-combat tail modelled
|
||||
|
||||
## turn1-state.sav -> turn2-state.sav (real End Turn)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": "sots-standalone-status/1",
|
||||
"generated": "2026-09-08T15:35:00Z",
|
||||
"binary": "/home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn",
|
||||
"generated": "2026-09-08T17:23:54Z",
|
||||
"binary": "/home/alex/sots-engine-wt-turnrecord/build-host/src/app/sots_turn",
|
||||
"reference": {
|
||||
"input": "turn1-state.sav",
|
||||
"oracle": "turn2-state.sav",
|
||||
|
|
@ -30,12 +30,12 @@
|
|||
"phases": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 2,
|
||||
"implemented": 3,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"stub": 30,
|
||||
"modelled": 14,
|
||||
"committed": 8
|
||||
"stub": 29,
|
||||
"modelled": 15,
|
||||
"committed": 9
|
||||
},
|
||||
"tailPhases": {
|
||||
"total": 37,
|
||||
|
|
@ -62,16 +62,17 @@
|
|||
" 591376 inflated bytes, 0 error(s), 0 warning(s)",
|
||||
" turn 1, frame 1, modCount 0, 8 player(s), 28 system(s), 6 fleet(s)",
|
||||
"roundtrip (untouched): byte-identical (591376 bytes)",
|
||||
"data: /tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/data -- 885 section(s) over 7 race(s), 46 load problem(s)",
|
||||
"",
|
||||
"phases",
|
||||
" turn drivers (the milestone's denominator): 14 of 44 modelled, 8 committed",
|
||||
" verified 0 implemented 2 partial 6 blocked 6 stub 30",
|
||||
" turn drivers (the milestone's denominator): 15 of 44 modelled, 9 committed",
|
||||
" verified 0 implemented 3 partial 6 blocked 6 stub 29",
|
||||
" post-combat tail (written to the autosave, tracked separately): 3 of 37 modelled",
|
||||
" verified 0 implemented 1 partial 0 blocked 2 stub 34",
|
||||
"",
|
||||
"this run",
|
||||
" leaves written 7",
|
||||
" leaves NOT written by a blocked phase 64",
|
||||
" leaves NOT written by a blocked phase 120",
|
||||
" generator words consumed 16 (state loaded, left untouched)",
|
||||
" generator words NOT accounted (never netted off the above):",
|
||||
" - encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
|
||||
|
|
@ -80,8 +81,8 @@
|
|||
" ! the generator advanced during this run but the save keeps its original state (--commit-rng to write it)",
|
||||
" ! the modelled words are a LOWER BOUND on the turn's cost, so a committed generator is short by the unaccounted sites below and its drawn VALUES are not the game's",
|
||||
"",
|
||||
"wrote /tmp/tmp_cz5dsbf/post-turn1-state.sav (65145 bytes gzipped, 591376 inflated)",
|
||||
"metric -> /tmp/tmp_cz5dsbf/metric-turn1-state.sav.json"
|
||||
"wrote /tmp/tmpbodkynxg/post-turn1-state.sav (65145 bytes gzipped, 591376 inflated)",
|
||||
"metric -> /tmp/tmpbodkynxg/metric-turn1-state.sav.json"
|
||||
],
|
||||
"coverage": {
|
||||
"input": true,
|
||||
|
|
@ -89,9 +90,9 @@
|
|||
"ours": true
|
||||
},
|
||||
"roots": {
|
||||
"input": "ce36f6e5a2e41ae1a90d5b7fefda8f8f",
|
||||
"oracle": "b0e2448b83ad1da254d526f4f553ff8d",
|
||||
"ours": "8836ea25910897ec7a9b7d69fcaf6c87"
|
||||
"input": "64b836b966299d9b72fc02244ed31e31",
|
||||
"oracle": "4bed514f853332b9fa35b388c3aa5558",
|
||||
"ours": "1acc372b1d410b966ab544dffbe43cb5"
|
||||
},
|
||||
"baselineDiverging": 209,
|
||||
"divergingAfterTurn": 204,
|
||||
|
|
@ -151,16 +152,16 @@
|
|||
"standalone": {
|
||||
"schema": "sots-standalone-metric/1",
|
||||
"input": "/home/alex/sots-re/verify/results/saves/turn1-state.sav",
|
||||
"output": "/tmp/tmp_cz5dsbf/post-turn1-state.sav",
|
||||
"output": "/tmp/tmpbodkynxg/post-turn1-state.sav",
|
||||
"spine": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 2,
|
||||
"implemented": 3,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"stub": 30,
|
||||
"modelled": 14,
|
||||
"committed": 8
|
||||
"stub": 29,
|
||||
"modelled": 15,
|
||||
"committed": 9
|
||||
},
|
||||
"tail": {
|
||||
"total": 37,
|
||||
|
|
@ -174,7 +175,7 @@
|
|||
},
|
||||
"run": {
|
||||
"leafWrites": 7,
|
||||
"blockedLeafWrites": 64,
|
||||
"blockedLeafWrites": 120,
|
||||
"rngWords": 16,
|
||||
"rngLoaded": true,
|
||||
"rngCommitted": false,
|
||||
|
|
@ -239,8 +240,8 @@
|
|||
"driver": "StrategyServer::ProcessTurn",
|
||||
"id": "S04",
|
||||
"name": "RebuildAllianceMasks",
|
||||
"status": "stub",
|
||||
"ran": 0,
|
||||
"status": "implemented",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
"rng": 0
|
||||
|
|
@ -1002,7 +1003,7 @@
|
|||
"status": "blocked",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 48,
|
||||
"blockedWrites": 104,
|
||||
"rng": 0
|
||||
},
|
||||
{
|
||||
|
|
@ -1028,16 +1029,17 @@
|
|||
" 603360 inflated bytes, 0 error(s), 0 warning(s)",
|
||||
" turn 2, frame 2, modCount 12, 8 player(s), 28 system(s), 7 fleet(s)",
|
||||
"roundtrip (untouched): byte-identical (603360 bytes)",
|
||||
"data: /tmp/claude-1000/-home-alex/ec8e34f8-af37-4ef2-a309-ed6a15293097/scratchpad/data -- 885 section(s) over 7 race(s), 46 load problem(s)",
|
||||
"",
|
||||
"phases",
|
||||
" turn drivers (the milestone's denominator): 14 of 44 modelled, 8 committed",
|
||||
" verified 0 implemented 2 partial 6 blocked 6 stub 30",
|
||||
" turn drivers (the milestone's denominator): 15 of 44 modelled, 9 committed",
|
||||
" verified 0 implemented 3 partial 6 blocked 6 stub 29",
|
||||
" post-combat tail (written to the autosave, tracked separately): 3 of 37 modelled",
|
||||
" verified 0 implemented 1 partial 0 blocked 2 stub 34",
|
||||
"",
|
||||
"this run",
|
||||
" leaves written 7",
|
||||
" leaves NOT written by a blocked phase 58",
|
||||
" leaves NOT written by a blocked phase 114",
|
||||
" generator words consumed 16 (state loaded, left untouched)",
|
||||
" generator words NOT accounted (never netted off the above):",
|
||||
" - encounter detection draws one unit value and one bounded integer per turn on every turn measured (2 words), with no derived rule behind the count -- its bound is the product of the contact and detector counts, so it is left unmodelled",
|
||||
|
|
@ -1046,8 +1048,8 @@
|
|||
" ! the generator advanced during this run but the save keeps its original state (--commit-rng to write it)",
|
||||
" ! the modelled words are a LOWER BOUND on the turn's cost, so a committed generator is short by the unaccounted sites below and its drawn VALUES are not the game's",
|
||||
"",
|
||||
"wrote /tmp/tmp_cz5dsbf/post-turn2-state.sav (66883 bytes gzipped, 603360 inflated)",
|
||||
"metric -> /tmp/tmp_cz5dsbf/metric-turn2-state.sav.json"
|
||||
"wrote /tmp/tmpbodkynxg/post-turn2-state.sav (66883 bytes gzipped, 603360 inflated)",
|
||||
"metric -> /tmp/tmpbodkynxg/metric-turn2-state.sav.json"
|
||||
],
|
||||
"coverage": {
|
||||
"input": true,
|
||||
|
|
@ -1055,9 +1057,9 @@
|
|||
"ours": true
|
||||
},
|
||||
"roots": {
|
||||
"input": "b0e2448b83ad1da254d526f4f553ff8d",
|
||||
"oracle": "7ea50b486c2e9f9f1a8ec5b9646165a8",
|
||||
"ours": "6523aa25786814380a26f48c046da22b"
|
||||
"input": "4bed514f853332b9fa35b388c3aa5558",
|
||||
"oracle": "e9c161e311f8ef8fad6f1aa1903dcf3f",
|
||||
"ours": "22071ae1303f308a93b805e940b626b5"
|
||||
},
|
||||
"baselineDiverging": 108,
|
||||
"divergingAfterTurn": 103,
|
||||
|
|
@ -1117,16 +1119,16 @@
|
|||
"standalone": {
|
||||
"schema": "sots-standalone-metric/1",
|
||||
"input": "/home/alex/sots-re/verify/results/saves/turn2-state.sav",
|
||||
"output": "/tmp/tmp_cz5dsbf/post-turn2-state.sav",
|
||||
"output": "/tmp/tmpbodkynxg/post-turn2-state.sav",
|
||||
"spine": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 2,
|
||||
"implemented": 3,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"stub": 30,
|
||||
"modelled": 14,
|
||||
"committed": 8
|
||||
"stub": 29,
|
||||
"modelled": 15,
|
||||
"committed": 9
|
||||
},
|
||||
"tail": {
|
||||
"total": 37,
|
||||
|
|
@ -1140,7 +1142,7 @@
|
|||
},
|
||||
"run": {
|
||||
"leafWrites": 7,
|
||||
"blockedLeafWrites": 58,
|
||||
"blockedLeafWrites": 114,
|
||||
"rngWords": 16,
|
||||
"rngLoaded": true,
|
||||
"rngCommitted": false,
|
||||
|
|
@ -1205,8 +1207,8 @@
|
|||
"driver": "StrategyServer::ProcessTurn",
|
||||
"id": "S04",
|
||||
"name": "RebuildAllianceMasks",
|
||||
"status": "stub",
|
||||
"ran": 0,
|
||||
"status": "implemented",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
"rng": 0
|
||||
|
|
@ -1968,7 +1970,7 @@
|
|||
"status": "blocked",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 48,
|
||||
"blockedWrites": 104,
|
||||
"rng": 0
|
||||
},
|
||||
{
|
||||
|
|
|
|||
142
verify/results/standalone/t36-turnrecord.md
Normal file
142
verify/results/standalone/t36-turnrecord.md
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
# T36 `FinalizeTurnRecords` — the ship census wired in, and what T36 is still blocked on
|
||||
|
||||
Lane E2, 2026-09-08. Engine branch `wip/turnrecord` off `sots-engine` main `0592104`.
|
||||
Measured with `verify/state-checksum/state_checksum.py` leaves, the same currency as
|
||||
`report.txt`. **`closed` and `regressed` are reported separately on every line; nothing here
|
||||
is netted.**
|
||||
|
||||
`status.json` and `report.txt` in this directory were regenerated from the same binary.
|
||||
|
||||
## What changed in the engine
|
||||
|
||||
`ShipCensus` (lane D2) is wired into `BuildTurnRecord`. The classification walks every
|
||||
player's designs — normal and legacy, one id space — resolves each design's sections against
|
||||
the section catalog, and walks the global fleet list keyed by `Flt.PID` against each player's
|
||||
**object id**, not its vector position. The record's modelled field count goes 7 → 13.
|
||||
|
||||
Two things the standalone did not have before:
|
||||
|
||||
* **a data root.** The census cannot come from a save: a design's hull size and its
|
||||
defence-platform flag are recomputed from the section catalog whenever the design changes
|
||||
and are never written down. `sots_turn --data DIR` (or `$SOTS_DATA_DIR`) supplies it. No
|
||||
game data enters the repo, and without a root the six counters are reported unmodelled
|
||||
rather than written as six zeros.
|
||||
* **`--commit-blocked=IDS` / `--commit-blocked-except=IDS`.** `--commit-blocked` alone is
|
||||
all-or-nothing across every blocked phase, which reports one closed count and one regressed
|
||||
count for all of them together — the netting the campaign does not do. The two lists narrow
|
||||
it to named phase ids, so a phase's own contribution can be measured. Every number below
|
||||
that names T36 was measured with `--commit-blocked=T36`.
|
||||
|
||||
## Prediction, written before the code (rule 2)
|
||||
|
||||
> Default mode unchanged at 209 → 204, closed 5 / regressed 0 and 108 → 103, closed 5 /
|
||||
> regressed 0. With `--commit-blocked`, the three census leaves close and the regressed list
|
||||
> falls from 9 to 6 on pair 1 (`inc`×3 + `sav`×3, the budget) and from 11 to 8 on pair 2.
|
||||
|
||||
Falsification hypothesis 1 was: *the census is built from the fleet list as it stands, which
|
||||
is pre-construction; if a real turn's census differs from the input turn's, the leaf does not
|
||||
close and shows a third value.*
|
||||
|
||||
**That is what happened.** Two of the three closed; the third did not, and the symptom was
|
||||
exactly the one written down.
|
||||
|
||||
## Measured — reference pair `turn1-state.sav` → `turn2-state.sav`
|
||||
|
||||
| run | closed | regressed |
|
||||
|---|---:|---:|
|
||||
| default (no commit flags) — before and after this lane | 5 | 0 |
|
||||
| `--commit-blocked`, before this lane | 29 | 9 |
|
||||
| `--commit-blocked`, after | 29 | **7** |
|
||||
| `--commit-blocked=T36` alone, with a data root | 29 | 7 |
|
||||
| `--commit-blocked=T36` alone, no data root | 29 | 9 |
|
||||
|
||||
Regressed after, named: `hist[0]/stats[1]/{sav,inc}`, `hist[1]/stats[1]/{sav,inc}`,
|
||||
`hist[7]/stats[1]/{sav,inc}` — the budget, lane E1 — and `hist[1]/stats[1]/shpt[0]`, ours 0,
|
||||
archived 1.
|
||||
|
||||
Closed by the census: `hist[4]/stats[1]/shpt[2]` (5 dreadnoughts) and
|
||||
`hist[7]/stats[1]/satt[0]` (10 defence platforms).
|
||||
|
||||
## Measured — second pair `turn2-state.sav` → `turn3-state.sav`
|
||||
|
||||
| run | closed | regressed |
|
||||
|---|---:|---:|
|
||||
| default | 5 | 0 |
|
||||
| `--commit-blocked`, before this lane | 13 | 11 |
|
||||
| `--commit-blocked`, after | 13 | **9** |
|
||||
| `--commit-blocked=T36` alone, with a data root | 13 | 7 |
|
||||
| `--commit-blocked=T36` alone, no data root | 13 | 9 |
|
||||
|
||||
The two extra regressions under a full `--commit-blocked` are `Player/Status` on two players.
|
||||
They are S31's, not T36's, and the new id filter separates them cleanly — which is the whole
|
||||
reason the filter exists.
|
||||
|
||||
## The one census leaf that did not close is not a census error
|
||||
|
||||
`app_test_turn_record`, extended this lane, now compares the six counters against the record
|
||||
the game itself archived, on every save: **11 saves, 80 player-records, 1040 fields, 480 of
|
||||
them census leaves, 0 mismatches.** That is the same 480/480 lane D2 got, reproduced through
|
||||
the app's own code path (which visits slots in the original's in-memory order rather than the
|
||||
wire's) rather than through D2's test.
|
||||
|
||||
So the model reproduces the archived census exactly for the turn the save was written at. The
|
||||
remaining leaf is `shpt[0]` for one player, and the archived value is higher than ours by
|
||||
exactly one destroyer on **both** pairs (0 vs 1, then 1 vs 2). That player's build queue
|
||||
completes one ship per turn, and no phase the standalone runs creates a ship — S11's
|
||||
build-queue sub-pass is the declared input boundary. The leaf is short by the turn's
|
||||
construction, not wrong about classification.
|
||||
|
||||
**This is a new named blocker, and it is not lane E1's and not mine.**
|
||||
|
||||
## T36's status: still `Blocked`, on two named things, neither of them in this phase
|
||||
|
||||
1. `sav` and `inc` for the new turn come from P01/P02, blocked on the per-system money
|
||||
output — **lane E1**.
|
||||
2. `shpt[0]` is short by the ships the turn builds — **ship construction, unmodelled**.
|
||||
|
||||
There is **no field-granular knob that could rescue this**, and the brief's
|
||||
`--commit-blocked-except=<fields>` idea is not implementable as stated: the archived record
|
||||
is one struct on the wire, so `sav` and `inc` cannot be left out while the other eleven fields
|
||||
are written. The format has no hole. Committing is all-or-nothing at the record, so the
|
||||
choice is 29 closed with 7 confidently-wrong leaves, or neither — and 7 wrong leaves is not
|
||||
worth 29 that a later lane closes for free. What *is* implementable, and is implemented, is
|
||||
the same convention at **phase** granularity, which is what produced the isolated rows above.
|
||||
|
||||
T36 flips the moment E1 lands, minus one leaf that waits on construction.
|
||||
|
||||
## Coverage, stated as loudly as the verdict (rules 6 and 15)
|
||||
|
||||
Only **32 of the 480** archived census leaves are nonzero anywhere in the 11-save corpus.
|
||||
Per leaf (cls0 shpt/satt, cls1 shpt/satt, cls2 shpt/satt) = **18/3, 0/0, 11/0**.
|
||||
|
||||
* **cls1 (cruisers), ships and platforms both, and `satt` for cls2, have never been observed
|
||||
nonzero.** Three of the six counters are unexercised hypotheses, not verified. A zero leaf
|
||||
agrees for free. The corpus needs a cruiser and a dreadnought-hull platform
|
||||
(roadmap §5 already lists both).
|
||||
* The other four words of each class group — `shpl`, `shpk`, `satl`, `satk`, losses and kills
|
||||
— are zero throughout the corpus, and this phase writes zeros with no model behind them.
|
||||
Listed in the phase's own unmodelled list rather than passed off as agreement.
|
||||
* Hull size is an **assignment** in slot order, so the visit order is load-bearing in
|
||||
principle. The original's stats pass walks mission, command, engine; the wire writes
|
||||
command, mission, engine. This code reproduces the memory order. **Design rule A6 makes
|
||||
every shipped design class-homogeneous, so no save in the corpus can tell the two orders
|
||||
apart** — the order is carried from the instruction stream, not confirmed by these bytes.
|
||||
* `verified` stays **0**. Nothing here was compared against a running game; this lane holds no
|
||||
VM. The comparison is against bytes the original wrote, which is the strongest check
|
||||
available without one, and it is not the same thing.
|
||||
|
||||
## Gates
|
||||
|
||||
Run as separate commands, never chained (rule 13):
|
||||
|
||||
* `tools/clean_room_check.sh` — OK
|
||||
* host `ctest`, no `SOTS_SAVES_DIR` — 45/45
|
||||
* host `ctest`, `SOTS_SAVES_DIR` set — 45/45
|
||||
* host `ctest`, `SOTS_SAVES_DIR` + `SOTS_DATA_DIR` set — 44/45. The one failure is
|
||||
`game_data_realdata`, and it **fails identically on unmodified `main`** with the same data
|
||||
root: that test pins section and bank counts against the campaign's canonical extraction and
|
||||
my extraction of `sots.gob` is a different data version (9 pinned counts differ, e.g. a
|
||||
`human/DEWAR` section the pin says should be absent). It is a data-root mismatch, not a
|
||||
regression, and the census matched all 480 archived leaves against that same root anyway.
|
||||
* **Shim cross-build NOT run**: no `i686-w64-mingw32-g++` on this host. No `src/shim` file was
|
||||
touched, but per rule 13 the integrator should run the CT111 shim build before pushing.
|
||||
Loading…
Add table
Reference in a new issue