diff --git a/docs/B6-ship-construction.md b/docs/B6-ship-construction.md index 28f1932..e672d0d 100644 --- a/docs/B6-ship-construction.md +++ b/docs/B6-ship-construction.md @@ -39,14 +39,14 @@ What this lane does deliver, and what it is worth: * the build-queue advance read byte for byte from `Game::BuildQueue::ProcessTurn` (`0x00890d50`, real end `0x00891240` — Ghidra says 1230 bytes, the body runs 1264), which **independently confirms** lane B4's `sim::ProcessBuildQueue` and finds **one divergence** - in it (§4); + in it (§4, "What changed in the engine"); * the `ShipRecords` update the pass performs, whose per-class counter has **exactly one writer in the whole image**; * the pass wired into the standalone's S11 as its own reported sub-pass; * a real oracle for the pass that the campaign already owns and had not noticed: `zuul-turn16-noderoute.sav` → `zuul-turn17-rollpending.sav` is a genuine consecutive-turn pair (frames 16 and 17) in which **six orders complete and one is partially advanced** - across two systems (§5). + across two systems (§3, "The corpus oracle"). ## 2. How this prediction could be wrong, and the symptom of each way @@ -65,7 +65,98 @@ What this lane does deliver, and what it is worth: test: a per-order point budget, a non-FIFO order, or a "skip and continue" rule instead of "stop" each fail it. 4. **The `points <= 0` gate on the removal sweep is wrong.** Unobservable in this corpus - (§4) and therefore carried as a labelled hypothesis, not a result. + and therefore carried as a labelled hypothesis, not a result. 5. **`shpt` is not the fleet walk.** If the census leaf were fed by `ShipRecs` rather than by walking fleets, the fix would be different. Lane E2 already falsifies this: the fleet walk reproduces 480/480 archived census leaves. + +--- + +## 3. Measured + +Built on CT111 (`/srv/re-lab/build/sots-engine-b6`), report run there with +`tools/standalone_report.py --binary …`. Host gates run separately, never `&&`-chained. + +| pair | baseline | after | **closed** | **regressed** | +|---|---:|---:|---:|---:| +| `turn1-state.sav` → `turn2-state.sav` | 209 | 158 | **51** | **0** | +| `turn2-state.sav` → `turn3-state.sav` | 108 | 87 | **21** | **0** | + +Identical to the pre-lane baseline, which is the prediction holding: **this lane closed 0 and +regressed 0.** `regressedPaths` is empty on both pairs. + +The prediction's own numbers, checked one by one: + +* three `BQ` frames per reference save (systems owned by players 16, 32 and 576), **0 orders in + all three**, on both sides of both pairs — the standalone now prints this on its own line; +* every `hbq` in `turn1/2/3-state.sav` is `false` (15, 16 and 17 of them), so **0 ship-borne + orders**; +* the four `CDT` ids are `Player.00000016.TurnCommands_v5` plus three `AIAgent` blobs, and the + one command block is the 36-node empty one. + +Falsification hypotheses 1 and 2 are therefore both refuted by measurement rather than by +argument, and hypothesis 3 is refuted by the corpus oracle below. Hypothesis 4 stands as a +labelled hypothesis; hypothesis 5 was already refuted by lane E2. + +### What the pass does on the saves that are not the reference pair + +`sots_turn --phases` now prints a `build queue:` line under S11 on every save: + +| save | queues | pending orders | demand (points) | +|---|---:|---:|---:| +| `turn1/2/3-state.sav` | 3 | 0 | 0 | +| `human-turn2-orders.sav` | 20 | 0 | 0 | +| `zuul-turn5-species5.sav` | 4 | 4 | 7,120 | +| `zuul-turn16-noderoute.sav` | 4 | 7 | 12,099 | +| `zuul-turn17-orders2.sav` | 4 | 2 | 5,109 | +| `zuul-turn23-fleet23.sav` | 4 | 1 | 370 | + +Five of eleven saves exercise the pass the moment points exist. Nothing is committed: an order +that advanced with no ship behind it leaves a state the game never produces. + +### The corpus oracle, which the campaign already owned + +`zuul-turn16-noderoute.sav` (frame 16) → `zuul-turn17-rollpending.sav` (frame 17) is a real +consecutive-turn pair. Two systems, six completions, one partial advance: + +| system | owner | before (`desID`:`conleft`) | after | solved points | +|---|---|---|---|---:| +| 384 | 16 | 608:1753, 576:1860, 576:1860 | 576:**1291** | **4182** | +| 80 | 32 | 114:959, 114:1889 ×3 (+816:6974 appended in-turn) | 816:**3818** | **9782** | + +`test_construction.cpp` does not assume those totals — it **searches** 0..200,000 and asserts a +solution exists and is **unique**. A per-order budget, a non-FIFO drain, or skip-instead-of-stop +each make the search come back empty. The completions then reproduce the `ShipRecs` deltas the +same two files carry (class-0 `srb` 2→4 and 53→57; design 608 2→3; a new record for 576; +design 114 18→22; design 816 unchanged because its order did not finish). + +## 4. What changed in the engine + +* **`game/sim/construction.{h,cpp}`** — `ShipRecords` and the completion bookkeeping. The + per-class `built` counter's indexed increment has **exactly one writer in the whole image** + (an image-wide byte scan for the form at that displacement returns one site, inside this + pass), so a hull on the wire with that counter bumped came through the build queue and + nothing else — the encounter spawner that also creates ships touches no `ShipRecords`. + `RunSystemConstruction` wraps the point pass and keeps each completion's design id. +* **`game/sim/colony.{h,cpp}`** — one corrected rule: with `points <= 0` the original skips the + **removal sweep** as well, because the entry test branches to the epilogue. Carried as a + labelled hypothesis; no corpus save can reach the state that shows it, and the workload that + would is named in the header. +* **`app/construction_phase.{h,cpp}`** — S11's build-queue sub-pass, reported on its own line. +* **`app/phase_catalog.cpp`** — S11's blurb no longer calls the build queue the input boundary + for the census leaf. + +## 5. What this lane did NOT do, said plainly + +* **It did not close `shpt[0]`, and no build-queue work can.** The blocker moves to `game/ai`. +* **It did not model the ship or the fleet at birth in code.** The chain is read end to end and + written up in the RE repo (`findings/subsystems/ship-construction.md` §4): `StarShip` is 0xb0 + bytes, its id comes from the object-id allocator and lands in `+0x04`, `FltID` is born NULL; + the hull joins its system's **cached home fleet** (`ServerSystem+0x238`) and a fleet is created + only when that slot is empty; every fleet born that way gets `FtFlg |= 0x400` — which + **corrects lane B5**, that bit is not a retreat marker. It is not implemented because the + newborn hull's stats are copied from design words the engine does not yet compute, and because + no save exercises the path with an oracle behind it. +* **The money charge has never fired.** No design in the corpus costs money, so the slot-9 + refusal path is read from the bytes and never observed. +* **`srl`, `srk`, `sri` carry no model.** Losses and kills are zero in all eleven saves. diff --git a/src/game/sim/construction.h b/src/game/sim/construction.h index e79a3b4..0f44201 100644 --- a/src/game/sim/construction.h +++ b/src/game/sim/construction.h @@ -106,4 +106,41 @@ struct SystemConstructionResult { // CONFIDENCE: high -- see colony.h's ProcessBuildQueue for the rules and their evidence. SystemConstructionResult RunSystemConstruction(std::vector& queue, int points); +// --------------------------------------------------------------------------------------- +// The hull and the fleet at birth -- READ, NOT YET IMPLEMENTED +// --------------------------------------------------------------------------------------- +// +// This is written down here rather than coded because the newborn hull copies four cached +// stat words out of its design, and those words are recomputed by the design's own stats +// pass, which this engine models only far enough to get a hull class. Coding it now would be +// writing fields we cannot compute. The whole chain is read instruction by instruction in +// the RE repo; the shape, for the lane that gets the design stats: +// +// THE SHIP. 176 bytes. The object id comes from the network-node id allocator -- +// `(counter << 4) | (node & 0xF)`, per-node counters, PRE-incremented, never issuing 0 -- +// and is written into the object by the id-map insert, not by the constructor. At birth: +// the owner is the queue's owner and the design is the order's design; range, health, +// construction capacity, refuel capacity and repair capacity are copied from the design's +// cached stat words; the plague and one other word are -1 and everything else is zero. +// Two fields matter to a reimplementation: +// * the FLEET LINK IS NULL at birth and is set by the fleet-join step, not here; +// * the TURN-BUILT stamp is the sim's frame counter, i.e. the number of the turn being +// produced -- MEASURED: the six hulls the zuul frame-16 -> frame-17 pair adds all +// carry the NEW turn number, which is independent evidence that the frame counter is +// incremented before the spine runs. +// +// THE FLEET. 288 bytes. A system caches ONE home fleet; every hull built there joins it, +// and a fleet is created only when that cache is empty. So a second turn of building at +// the same system creates no fleet. A created fleet takes an id from the same allocator, +// is born at the system's position, and always carries flag 0x400 -- which is NOT a +// retreat marker (that reading is corrected) and is NOT the design flag of the same +// numeral that game/design/hull.h warns about. The build path adds 0x20 on top. There is +// NO scalar ship count: the wire's count is the length of the fleet's ship vector. +// A fleet created with no name override asks the player's name generator, which is what +// bumps the generator's counter -- one bump per generated name. +// +// Ships are also created OUTSIDE this pass, by the trade manager's encounter spawner. That +// path touches no ShipRecords at all, which is what makes the per-class built counter a +// clean discriminator between player-built hulls and spawned ones. + } // namespace sots::sim