Closed 2 on the reference pair and 4 on pair 2, regressed 0, with no operator
input. Measured, both pairs, never netted.
- S00 stamps PvSav from Sav on every live player, before any phase can move it.
0 closed on pair 1 (a no-op there), 2 on pair 2.
- T31 recovers the per-player difficulty column by recomputing BnkEl from the
colony state the input save was written from and comparing against the BnkEl
the save carries. That removes the --ai-player flag as a blocker and turns the
phase's self-check into a real one: it used to compare its POST-turn result
against the PRE-turn stored value, so its 6-of-8 only ever covered the six
players whose limit does not move. The load-time check passes for every live
player of all eleven corpus saves. T31 Blocked -> Partial; BnkPr still needs
the tuning constant.
- BANKRUPTCY_PROTECTION_LIMIT_FACTOR is multiplied in as fmul dword ptr, so it
is a float32 in the image; the engine narrows it now. Zero leaves move on this
corpus -- all seven of its BnkPr records land where the two constants agree --
and three hand-written test expectations moved (rule 23).
docs/PL-players-residual.md carries the decomposition, the predictions written
before the build, where they were wrong, and the ranked remainder.
Adds the half of the AI's turn that is arithmetic rather than judgement: what an
order looks like in the command block, which orders advance the save's
modification counter and by how much, and the phase/pass skeleton the decisions
hang in.
The counter's per-command cost turns out to have a sharp boundary. Applying an
element of command lists 1..16 advances it; applying an element of lists 17..27
does not, and one of the six flag-gated single commands is free as well. So a
uniform per-element cost model is wrong on any turn that touches the free half.
Two behaviours here are not conveniences and change the output:
* every submitted block costs at least one, because the send-buffer build sets
the research-rate gate unconditionally whatever the player did. On a quiet
board that is the largest term in the turn's delta -- four of the ten command
bumps on the reference turn are exactly this, and one of the four is the
human's;
* an AI fleet order costs three where the interface's costs two, because the
AI's bridge issues the fleet-task command twice, mode 0 then mode 1, and the
adder keys on (fleet, mode).
The phase spine records the one thing a literal port gets wrong: the turn submits
at phase 28 of 34, the submit latches the client closed before it builds the send
buffer, and every order the last five phases issue -- one of which is a colonize
order -- is refused. Tested through the client rather than by asserting a flag.
The task walk reproduces the two passes: rank once, walk twice, and refuse every
write in the first pass at the client rather than trusting the caller to check.
Nothing here decides anything. Which tasks exist and what each one wants are
questions about the board, and no part of this models the board; the module
supplies the order API, the pass gate and the cost function, and a caller
supplies the decisions.
game/ai tests 233 -> 423 checks; ctest 51/51 -> 53/53. Not linked into the
standalone driver, whose divergence on the reference pair is unchanged at 128.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
Reference pair turn1->turn2: 81 leaves closed, 0 regressed (was 78/0).
Pair turn2->turn3: 39 closed, 0 regressed (was 36/0). With
--commit-blocked=T31 --ai-player 1: 83/0 and 41/0.
game/sim/colony: GrowCivilianPopulations models ServerSystem's civilian
growth sub-pass. The whole system's delta is clamped to 20,000,000 -- an
int64 column of the population-type table, built in the executable from
its own literals -- and on both reference pairs that clamp, not the growth
curve and not any carrying capacity, is what decides the value: the
uncapped delta is 7.5x it and the capacity headroom 25x it. So the pass
commits with no tuning table loaded, and says by how much each unmodelled
input would have to be wrong before it mattered.
The one input genuinely off the wire is the per-species civilian capacity
factor. It is handled by running the pass twice, once with the modelled
capacity and once with the system's own wire-known dcs limit, and
committing only when the two agree. Imperial growth is deliberately NOT
committed: it is a no-op on this corpus and would need a capacity the
corpus can bound from below but not from above.
game/sim/economy: both interest rates in ComputeBudget are WIDENED FLOAT
literals, (double)0.01f and (double)0.15f, and are then truncated -- so a
treasury of exactly 50,000 earns 499, not 500. This module used the exact
decimals, which left the human's savings one money high on the first
reference pair and exact on the second. Sixteen hand-computed test
expectations moved by one; they were derived from the model, not measured.
The live ComputeBudget compare (4,437 calls, 0 divergences) did not catch
this because it presented only 20 distinct states and none sat on a
rounding boundary.
game/sim/colony: ShipRepairCost, the last unmodelled input of the output
turn path. The demand is still 0 -- its two design fields are cached stats
the save does not carry -- but the zero is now evidenced rather than
silent: S13 reports the candidate set, and the independent colony keeps a
ten-ship fleet over a colony whose savings close exactly at zero demand.
Gates run as separate commands: clean-room OK, host ctest 49/49, and the
CT111 shim cross-build exit 0 (required: game/sim is compiled into the
shim). The host build and report were also re-run on CT111 and produced
identical numbers.
docs/G3-civilian-growth.md; notes repo
findings/subsystems/population-growth.md.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
The engine has modelled the event log since lane E, but the standalone never wrote
any of it into the save it produces. This wires the two together for the one event
on the reference pair the standalone can compute, and corrects the condition.
What a turn actually posts, measured over all eleven saves: two events on
turn1 -> turn2 and three on turn2 -> turn3, and only two players in the whole
corpus ever hold an event at all. Order is readable off the ids -- the build pass
posts before the research pass. See docs/EV-events.md section 1.
The condition had two of the original's three tests. The missing one is "no tech
finished on this turn or later", and it is what keeps the event off the turn a tech
lands; zuul-turn23 exercises it. The third input, whether the player holds a target
at the moment of the check, is not on the wire -- three of the four real players
acquire one during the turn, which is AI research selection -- so the operator's
--ai-player roster stands in for it as a stated hypothesis and the phase stays
blocked without it.
No prose in the engine: the record's text is resolved through a caller-supplied
lookup over the operator's own installed string table, and a run without a data
root posts nothing rather than writing a record it cannot fill.
Measured (CT111 host build, real data root), closed and regressed never netted:
turn1 -> turn2 209 -> 127 closed 82 (+4 this lane), regressed 0
turn2 -> turn3 108 -> 69 closed 39 (+3 this lane), regressed 0
The posted record agrees with the oracle on all eight of its fields. Controls: with
the roster withheld the phase over-fires and regresses 16 leaves on pair 1; with the
string table withheld it posts nothing and regresses none.
Gates run separately: tools/clean_room_check.sh OK; host ctest 50/50; CT111 shim
cross-build OK (exports 66 names identical to binkw32.dll, staged in
/srv/re-lab/shim/dist-ev); CT111 host ctest 50/50.
209 -> 158 (closed 51, regressed 0) and 108 -> 87 (closed 21, regressed 0), identical to the
pre-lane baseline, measured on CT111. Falsification hypotheses 1 and 2 refuted by measurement
(three empty BQ frames per reference save, every hbq false, the one command block empty);
hypothesis 3 refuted by the corpus oracle; hypothesis 4 stands as a labelled hypothesis.
construction.h also carries the ship/fleet birth shape as read but NOT implemented, with the
reason stated: the newborn hull copies four cached stat words the engine does not yet compute.
`ComputeBudget` takes a system's money from two different functions. Projected mode
calls `ComputeMaxIncome`, which lane E1 closed 25/25 against the BnkEl oracle. The
TURN calls `ComputeOutput` with the system's own rate sliders, where the build queue,
the ship-repair pass and the infrastructure -> terraform -> money cascade are all
live and E1's proof that the cascades are zero does not apply.
Read from the instruction stream, both ranges disassembled to the next function start:
* `sim::ComputeSystemOutput` -- the channel algebra of `ComputeOutputFromRates`, with
every rounding site (round-half-even per channel, truncating for the construction
and money slots) and the association of every x87 sum as the original has them.
* `sim::IdealSuitability` -- the owner's own field, the server's species baseline for
an independent colony, and the per-system `dsu` override.
* `sim::RepairShipsInOrbit` -- the round robin, which is provably equivalent to
`points - min(points, demand)`: the per-pass share is at least 1, so the only early
exit needs every remaining cost to be zero.
* two corrections to `ConstructionPoints` and `SplitLeftover`: the station bonus is
ignored unless strictly positive and its association is `k x (b x cons) + cons`, and
the leftover weights sum as `wi + (wf + wt)`.
The load-bearing fact: the leftover construction points come back to the TRADE
channel, so a colony with an empty build queue earns the same money whichever way its
sliders point. The engine now runs both paths on every load and reports the
difference; on the 11-save corpus every delta decomposes to the unit into the build
queue's points priced through the money chain.
P01/P02 move from blocked to partial and are committed:
turn1-state -> turn2-state 209 -> 157 closed 52 regressed 0 (was 51 / 0)
turn2-state -> turn3-state 108 -> 86 closed 22 regressed 0 (was 21 / 0)
One leaf per pair, and it is the easy one: the independent colony, whose population
does not grow and whose orders the turn does not change. The human's savings are
still short by the civilian growth `S11` does not commit, and the AI's by its own
orders. The ship-repair demand is taken as 0 because `Ship::RepairCost` is unread.
sots-re: findings/subsystems/output-turn-path.md, ghidra/addresses.d/lane-c3.json
H02 StampTreatyTurns -- the diplomacy ledger's "this treaty was last in force on turn
N" stamp, over every ordered pair of players that holds one, creating the entry on
demand. It is a host step, not a phase of either turn driver: its sole caller is the
command-application step, which runs after the frame counter has advanced and before
both drivers. On a turn with no combat and no diplomatic command it is the only writer
of these fields, which is why a whole per-player dipstats vector is its output.
Read from the instruction stream; three things an earlier reading had wrong are
corrected in findings/subsystems/treaty-turn-stamp.md: the stamped value is the TURN and
not the modification counter, the relation codes are 3=allied / 2=NAP / 1=cease-fire and
not the reverse, and the bit is the player's index field rather than its position in the
player vector -- the opposite convention from the shared-vision mask two files away.
Measured, closed and regressed reported separately and never netted:
turn1 -> turn2 (reference) 209 -> 132 closed 77 (was 51), regressed 0
turn2 -> turn3 108 -> 73 closed 35 (was 21), regressed 0
human-turn2 -> turn3 closed 76 (was 64), regressed 0
zuul-turn15 -> turn16 closed 30 (was 18), regressed 0
zuul-turn16 -> turn17 closed 29 (was 17), regressed 0
The last three are pairs from a different game at turns 2, 15 and 16 that the model was
never fitted to, and it closes exactly the twelve ordered treaty pairs each of them holds.
The rule also reproduces the ledger of ten of the eleven corpus saves entry for entry,
including each entry's order and every stamped value; the eleventh is the turn-1 save
whose ledger no turn has yet written, and it is the reference pair's input.
app_test_treaty pins the five things the corpus cannot separate: the relation codes (no
save exercises cease-fire), the bit's source, the missing alliance-id guard, the -1
initialiser on a fresh entry, and the append-at-the-end order that makes re-running the
step idempotent instead of duplicating rows.
The betrayal half of the same function needs the turn's diplomatic commands; with no
command stream it is provably a no-op and is not modelled.
First module of game/ai, and the first piece of Rung B that is not scaffolding. It is the two
halves of the AI's task selection that are pure: the 33-value task type enumeration and the
ranking that decides which goal the AI acts on first.
* the priority table, verbatim -- higher runs first, and it is the entire default policy;
* the five overrides, kept out of the table on purpose. The two artifact tasks ignore their
table entries (1 and 2) and return 1260/1261; a port that only copied the table would rank
them last instead of fourth and fifth. The two tuned invade priorities are INPUTS
(TaskPriorityPolicy), not constants, because their loader is not yet identified;
* Rank() as a stable descending sort. The original sorts a std::list, so stability is the
behaviour, not a choice -- ties keep creation order;
* CreationOrder(species, policyNonZero), because that is what breaks the ties. Four arms: the
NPC species builds nothing, Hiver is the only arm with the gate families, Zuul the only one
with NodeBore, everyone else shares a fourth. Both defensive families are gated on the
player's policy value and DefendGateIncoming is Hiver-only on top of that.
193 checks in tests/game_ai, every expected value read off the original's tables rather than
produced by running this code. ctest 46/46 -> 47/47; clean-room check OK.
Derivation: sots-re findings/subsystems/ai-task-system.md (lane AI2), sections 1-3.
game/sim/construction.{h,cpp}: Game::ShipRecords (four per-hull-class arrays plus the
per-design vector, sized by ENUMERATION against the wire, not by what the code touches)
and the completion bookkeeping BuildQueue::ProcessTurn performs -- the per-class built
counter, whose indexed increment has EXACTLY ONE writer in the whole image, and the
find-or-append per-design record keyed by the design's object id. RunSystemConstruction
wraps the point pass and keeps each completion's design id, which the point pass alone
does not report.
game/sim/colony: corrected from the instruction stream -- with points <= 0 the entry test
branches to the epilogue, so the REMOVAL SWEEP IS SKIPPED TOO. Carried as a labelled
hypothesis: no corpus save can reach the state that shows it.
app/construction_phase.{h,cpp}: S11's build-queue sub-pass, reported on its own line
because what blocks it is not what blocks the rest of the colony turn. It is blocked on
the per-system output term for points; it is NOT what the archived ship census waits on.
tests/game_sim/test_construction.cpp: 65 checks, including a corpus oracle the campaign
already owned and had not noticed -- zuul-turn16-noderoute -> zuul-turn17-rollpending is a
real consecutive-turn pair in which six orders complete and one is partially advanced.
The test SOLVES for the point total rather than assuming it, so a non-FIFO order, a
per-order budget or skip-instead-of-stop each falsify it.
Written before any code (earned rule 2). Both reference saves carry three BQ frames with
zero orders, every hbq false and the only TurnCommands_v5 block empty, so the pass has
nothing to advance; the archived destroyer is created inside the turn by the AI. Includes
the falsification list and names the oracle pair the campaign already owns.
The second chain ComputeBudget needs. The output term (lane N) is a system's
OUTPUT; what the budget and the bankruptcy limits sum is its MONEY, which runs
that total through TradePointsToMoney. Read instruction by instruction, every
range disassembled to the next function start.
The income law is not the output law with a different constant: money per head
is typeIncomeMod / 14000 -- no 1.8, no 500000 -- and it truncates TWICE per
(group, species) row, once inside the per-row term and once after the morale and
addiction factors. Summing the species first and truncating once is wrong on any
colony with more than one species.
The multiplier that was "not on the wire" (formula-gaps.md Q3) resolves to a
three-row table the executable BUILDS IN CODE from .rdata float literals, exactly
like lane N's pop-type table: {int id; float ai[3]; float other[3]}, selected per
player by is-AI && !NPC. Every corpus save carries aidf == 1, whose AI income
column is 1.1f -- the x1.1 the BnkEl oracle measured. Its other two columns are a
fleet-maintenance divisor and a research multiplier, both of which already had a
home in BudgetInputs and no source.
Verified against the 25-record BnkEl oracle, which inverts the stored limit to
the true sum and therefore needs no VM: 6/25 before, 25/25 after. Falsified three
ways -- moving the AI flag off the AI players costs 11 records, moving it onto the
humans costs the other 11, and forcing one species' resource pair on all of them
costs every Zuul record.
app: T31 UpdateBankruptcyLimits now runs the whole roll-up and self-checks it
every run against the BnkEl the input save already carries -- 8 of 8 players on
turn1-state with --ai-player 1. It stays blocked on two things that are not the
formula: ServerPlayer+0xf9 (is this player AI?) is a game-setup input the save
does not carry, and BnkPr needs BANKRUPTCY_PROTECTION_LIMIT_FACTOR from the data
files. Committing it closes nothing on the reference pair -- the limits move
because the CIVILIAN POPULATION grows and that growth is not committed -- so
measured with --commit-blocked=T31 --ai-player 1: 0 closed, 0 REGRESSED, i.e.
209 -> 204 and 108 -> 103 unchanged.
P01 is NOT unblocked, and the roadmap's item 1 was wrong about that: ComputeBudget
takes its per-system money from ComputeOutput with the system's OWN rate sliders,
not from ComputeMaxIncome. Only its projected mode uses the max-income form. On
the turn path the repair pass runs and the unspent-industry and
unspent-terraforming cascades into the money channel are live, so the proof that
both are zero does not apply. The catalog text says so now.
Two things the 25/25 does NOT cover, and they are labelled in the code: the
suitability money cost is multiplied by zero on every corpus colony (all sit at
their species' ideal), and the slave, addiction, morale, station and
capacity-surplus branches are unexercised.
Three phases, one input. A star system carries four per-player masks and three of
them agree on nearly every system of every save the corpus holds, so a model built
on the wrong one looks right until it does not. The gate is the DERIVED
active-presence mask -- fleet-here OR gate-here OR owner, recomputed on every
arrival and departure -- not the sticky one and not the explored one.
S29 SystemObservedStamp the system's own last-observed turn (whole function)
T17 RebuildPlayerViewTree the per-(system, player) observation record: who saw
the system, on what turn, and what encounter was there
T21 UpdateSurveyAndStats the explored sweep: seen this turn implies surveyed
game/sim/visibility is pure and knows nothing about save shapes; app/visibility_phase
wires it to them. The mask is READ FROM THE SAVE and never rebuilt: neither reference
pair moves a mask leaf, so the loaded value is the value these phases would see, and
rebuilding it from an unmodelled movement pass would be a change with no evidence.
Measured, closed and regressed reported separately and never netted:
turn1-state -> turn2-state 209 -> 158 closed 51, regressed 0
turn2-state -> turn3-state 108 -> 87 closed 21, regressed 0
of which this lane closed 46 and 16 (the rest were already closed at main). The 46
are the brief's 32-leaf target in full -- 8 record counts, 8 player ids, 8 turn
stamps, 8 encounter ids -- plus 8 system stamps and 6 explored masks.
Three further pairs the model was never fitted to, all zero regressions:
human-turn2 -> human-turn3 353 -> 311 closed 42 (a different game, 21 systems)
zuul15 -> zuul16 276 -> 264 closed 12
zuul16 -> zuul17 341 -> 329 closed 12
The corpus's one discriminating row is a host test rather than a comment: a system
whose last visiting fleet has gone carries the sticky and explored bits set, the
active bit clear, and a stamp frozen a turn behind. The test asserts the freeze AND
asserts what the wrong gate would have produced, so a future edit that swaps the
mask fails loudly instead of quietly agreeing with five saves.
Labelled hypothesis, with the workload named in the header: the encounter id is
recovered from the encounter fleet at the system, because the field the original
reads is set once at map generation and is not on the wire. It agrees on all six
encounter fleets in the corpus and no save can separate it -- none kills an
encounter while leaving its system visible.
Not written, deliberately: the colony-ownership stamp that moves beside these.
Its gate is demonstrably NOT the active mask (one system in the corpus has a zero
mask and moves it anyway), the formula is not held, so it is reported, not written.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
The tail's last phase archives a per-player record whose 13 modelled fields were 7.
The six ship counts join them: every player's designs (normal and legacy, one id space)
are classified against the section catalog, and the global fleet list is walked keyed by
Flt.PID against each player's OBJECT id, not its vector position.
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, so the standalone grows a data root -- `--data DIR`, or $SOTS_DATA_DIR. No game data
is embedded, and without a root the six counters report themselves unmodelled instead of
being written as six zeros that a wrong model would also produce.
`--commit-blocked=IDS` and `--commit-blocked-except=IDS` narrow the commit switch to named
phase ids. All-or-nothing across every blocked phase reports one closed count and one
regressed count for all of them at once, which is the netting the campaign does not do.
MEASURED, closed and regressed never netted, state_checksum leaves:
turn1 -> turn2 default 209 -> 204 closed 5 regressed 0 (unchanged)
turn1 -> turn2 --commit-blocked=T36 no data closed 29 regressed 9
turn1 -> turn2 --commit-blocked=T36 with data closed 29 regressed 7
turn2 -> turn3 default 108 -> 103 closed 5 regressed 0 (unchanged)
turn2 -> turn3 --commit-blocked=T36 with data closed 13 regressed 7
The prediction written before the code said the regressed list would fall to 6 and 8 under
a full --commit-blocked. It fell to 7 and 9. The prediction's first falsification case is
what happened: two census leaves closed and the third did not, because the census is of the
fleet list as it stands and no phase the standalone runs creates a ship. The archived count
is higher than ours by exactly one destroyer on BOTH pairs for the one player whose build
queue completes that turn, while the self-check on the input turn is exact. That leaf is
short by the turn's construction, not wrong about classification.
app_test_turn_record now compares the six counters against the record the game archived:
11 saves, 80 player-records, 1040 fields, 480 of them census leaves, 0 mismatches. That is
lane D2's 480/480 reproduced through this code path, which visits a design's slots in the
original's in-memory order (mission, command, engine) rather than the wire's.
T36 stays Blocked, and on two named things, neither of them in this phase:
sav and inc come from P01/P02, blocked on the per-system money output; and shpt[0] is short
by the ships the turn builds. The archived record is one struct on the wire, so those words
cannot be left out while the rest is written -- committing is all-or-nothing at the record,
and there is no field-granular knob that could change that. Seven confidently-wrong leaves
are not worth 29 that later lanes close for free.
COVERAGE, as loudly as the verdict: only 32 of the 480 archived census leaves are nonzero
anywhere in the corpus -- per leaf (cls0 shpt/satt, cls1 shpt/satt, cls2 shpt/satt) =
18/3, 0/0, 11/0. cls1 entirely and satt for cls2 have never been observed nonzero: three of
the six counters are unexercised hypotheses. The four loss/kill words of each group are zero
throughout and are written as zeros with no model behind them. Hull size is an assignment in
slot order, and design rule A6 means no save can tell the memory order from the wire order.
verified stays 0: nothing here was compared against a running game.
Gates, separately: clean_room_check OK; host ctest 45/45 without SOTS_SAVES_DIR and 45/45
with it. No src/shim file touched; the shim cross-build was NOT run (no i686 mingw here).
The design record's section array is [mission, command, engine]; the wire is
[command, mission, engine]. Hull size is an assignment inside the original's
per-section loop, and that loop runs over the array, so the last resolved
section in MEMORY order wins -- engine, else command, else mission.
The first cut of this walked the wire order, which gives a different answer for
a design with an empty engine slot and both other slots filled. Rule A3 makes
that shape invalid, so no design in the corpus can tell the two apart and the
480/480 census result is unchanged either way -- which is exactly why the order
is now a named constant with the reasoning attached instead of whichever loop
was already to hand.
derive_stats takes hull_size after its loop for the same reason; the
defence-platform flag is an OR and stays in the loop.
52 design unit tests (the mixed-class case now pins memory order and adds an
engine-slot case), census still 480/480 on 11 saves, realdata still 127/127 and
197/197. host ctest 43/43, clean-room OK.
The two derived words the per-player turn record's ship census counts by, and
the design serializers that three lanes had been told did not exist.
HOW DESIGNS PERSIST. Game::ShipDesign derives from Game::ShipDesignDef and
reaches IStreamable through adjustor thunks, so a design is written by TWO
serializers: the base emits FAIDes/DHide/DWep/DName and exactly three section
frames (command, mission, engine on the wire), the derived one appends Dtc, the
Dwgv flag and, only when that flag is set, a weapon-group frame. The earlier
"the writer makes no stream call at all" note named an address that is in no
vftable at all. Corrected in shapes.h.
THREE sections, not five. The "two reserved slots" were Dtc and Dwgv swept into
the section list by the reference reader's catch-all tail; the constructor
builds a three-element array. design.h's comment is corrected and the fixture
loader now accepts 3-5 raw_slots so old fixtures still load; the array keeps
five inert entries deliberately, since touching the slot enum reaches rules.cpp
and another lane's tests for no behavioural gain.
DWep and Dwgv are bools, not ints -- both writers call the bool primitive. With
four-character tags a bool item and an int item are the same size on the wire
and 0/1 the same bytes, so no save can tell them apart. Byte-neutral: the typed
round trip is still byte-identical on all 11 saves at 100% named coverage.
HULL SIZE is the section_class of the last resolved section in memory slot
order, mapped Destroyer/Cruiser/Dreadnought -> 0/1/2 case-insensitively, with
absent or unrecognised meaning 0 rather than an error. The DEFENCE-PLATFORM
flag is one bit of a 64-bit role-flag word OR-ed across the design's sections.
Neither is on the wire; both are rebuilt from the section catalog.
MEASURED, not assumed: the new game_design_census test rebuilds the six census
counters per player and compares them against the record the game archived for
each save's own frame. 11 saves, 503 designs, 480 leaves, 0 mismatched, 0 ships
with an unresolvable design, 0 designs where first- and last-resolved section
disagree on hull size. COVERAGE IS THIN AND THE TEST SAYS SO: only 32 of the
480 leaves are nonzero, and three of the six census leaves (both cruiser rows
and dreadnought platforms) are never exercised by any save in the corpus -- the
test prints the per-leaf nonzero counts and names them unexercised rather than
verified.
Nothing is wired into the turn record: src/app is another lane's this cycle, so
this is evaluated and reported, not written.
host ctest 43/43 (was 42/42; +1, skips cleanly without the env). With a data
root set, game_data_realdata and mars_text_realdata fail identically on main --
both are the absent Locale/EN/Strings.csv, not this change. clean-room OK.
Reference readers fixed openly in the RE repo: save_reader 49/49,
design rules 32/32, stock_designs.json regenerated (raw_slots 5->3 and dWep
int->bool are the only field changes across all 127 designs).
The spine's fourth phase, read byte-for-byte and implemented:
almem[i] = (1 << i) | (ALid != -1 ? AL : 0)
with i the player's POSITION IN THE PLAYER VECTOR, not its index field. Both
inputs are on the wire and so is the output, through the turn-record archive,
so the phase is checkable against bytes the original wrote:
app_turn_record: 11 saves, 80 player-records, 560 fields, 0 mismatches
(was 480 fields over six fields; almem is the seventh)
The eight zero masks of the corpus's earliest archived turn are PREDICTED, not
excluded: the archiving phase also runs on load, and the load path does not run
the spine. BuildTurnRecord takes spineRan and models it, so all 80 records are
compared.
Three parts of the rule the corpus cannot separate -- the bit index, the OR,
and the ALid guard -- are pinned in app_alliance with the separating inputs no
save provides, and app_turn_record prints that it could not separate them.
Divergence, closed and regressed reported separately:
turn1->turn2 default 209 -> 204 closed 5, regressed 0
turn1->turn2 --commit-blocked 209 -> 189 closed 29, regressed 9 (was 17)
turn2->turn3 default 108 -> 103 closed 5, regressed 0
turn2->turn3 --commit-blocked 108 -> 106 closed 13, regressed 11 (was 19)
T36 stays blocked: nine leaves would still be wrong (inc x3, sav x3 behind the
budget; three census leaves behind the design catalogue). It now closes all 24
turnstats leaves on the reference pair, so it becomes a clean +24 once those
two land.
Prediction and falsification committed first in 49ae628.
Gates run separately: clean-room OK; host ctest 43/43. No src/shim touched.
The strategic layer does not search for a route: the player or the AI picks the
destinations and the engine classifies each consecutive pair, deciding the waypoint
kind and whether the order is legal. This models that classifier as pure functions.
The waypoint kind of any leg that is neither a gate transit nor a node route is a
pure function of the owning species -- which is the whole answer to why kind 2 has
never been observed. Kind 2 is the Liir drive; the two node-drive races are Human
and Zuul, both of which map to kind 3, and every observation so far was taken on
one of those two.
Also modelled: the three refusal bits versus the nine advisory ones, the gate
transit that waives the grounded-fleet refusal, the projection radius that splits
gate-to-gate from gate-to-gateless, the single-hop node line lookup and bore, and
the fuel check whose range is squared at full precision while the distance is
narrowed -- the one floating-point asymmetry here that flips a decision.
The leading-destination drop is reproduced with its original off-by-one behind an
explicit flag rather than silently fixed.
120 hand-computed checks. Host ctest 43/43; clean-room check OK.
Reads the whole colony output chain off the instruction stream (every range
disassembled to the next function start) and compares two of its functions
against the running game.
The population -> output law is linear and is carried by the executable:
output points per head are typeOutputModifier x 1.8 / 500000, and the
three-row population-type table is built in code rather than loaded, so the
imperial (1.0) and civilian (0.33f) modifiers are facts about the binary.
A system's total output is a SUM of three terms, not one multiplicative
chain. The station bonus scales only the imperial term and morale only the
civilian one, so OutputModifiers no longer carries either; they belong to
GroupOutputInputs. The function previously described as the base-output term
is the over-harvest RESOURCE demand, and it is corrected in place.
Live on VM140, both hooks in compare mode over two species and two workloads:
GroupOutput 13,105 calls / 0 divergences; ComputeTotalOutput 11,252 calls /
1 divergence of one ulp, in a value its caller rounds to an integer. Both
functions declare a whole-object Guard: 0 undeclared writes in 24,357 calls,
which is what makes the side-effect-free claim a measurement.
sim::Narrow forces the double rounding a 32-bit x87 build otherwise skips;
without it every civilian row came out one ulp low.
Also fixes ComputeBankruptcyLimits' elimination divisor, which was the
decimal -0.15 rather than the image's widened float -0.15000000596046448.
The two disagree for every maximum income divisible by 3 and for essentially
every empire above ~3,000,000.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ