Z: the first instrumented battle in this campaign costs zero RNG words

A Von Neumann encounter at Gallandro on turn 54 gave the workload the finding
said did not exist. Auto-resolved, with P10 committed before the click.

P10 predicted a non-zero tail cost and was wrong: res_no_battle flipped to 0
for the first time in 55 turns, the fleet was destroyed, and the generator
moved by zero. The bracket residual stayed 0, so combat proper drew nothing
either -- all 22 words were inside ProcessTurn, exactly as on a peaceful turn.

That is the strong form of lane J's static reading, and it means a
reimplementation can model a turn's RNG while modelling nothing about combat.
One auto-resolved encounter against an NPC is not combat in general, and 10.2
says so at length.
This commit is contained in:
alex 2026-09-08 10:14:53 -04:00
parent 932f366597
commit 9fa1ee2600

View file

@ -305,19 +305,9 @@ would have said so by construction rather than by anyone noticing.
## 8. What is not settled, listed as loudly as the results
* **The combat resolver has still never run under an instrument.** Every encounter this workload produced
had the no-battle flag set, so `ApplyEncounterResult` was a no-op every time; its measured 0 words says
nothing whatever about combat's RNG cost, and the residual-0 result above holds only for turns with no
battle.
Lane J read all 7,641 bytes of it in parallel with this run (`combat-resolver.md`; and note **7,641**, not
the 7,499 Ghidra reports — method rule 17). Its conclusion pairs with this one exactly: **the resolver has
no unconditional draw.** All three sites in its subtree are conditional — a node-cannon `NextInt`, an
inlined `NextFloat` per back-engineering candidate, and a `NextInt` per successful roll of that. So lane
J's cheap first prediction is directly testable with this instrument: **a plain fleet battle with no node
cannon and no salvage should cost the same 18–22 words as a peaceful turn.** That is the next run this
hook family should do, and it needs a workload nobody has built yet: a save where two hostile fleets
actually meet.
* ~~The combat resolver has never run under an instrument.~~ **It has now, once — see §10. It cost 0
words.** What remains unsettled is everything a single auto-resolved encounter cannot speak for; §10.2
lists it.
* **Node-line expiry did not fire.** See §9 for the quantified distance rather than an absence.
* **A turn with a genuinely empty encounter vector was not observed** (§4). Both saves produce exactly one
sighting encounter on every turn. The tail-runs-every-turn claim is settled; the no-encounters variant is
@ -410,3 +400,58 @@ Compare mode was **not** run on this hook for the same reason: with a prediction
`ours` would advance the scratch generator by nothing, diff clean, and prove only that the harness works.
The model becomes checkable the first time a line expires, and the descriptor is ready for that day.
---
## 10. A real battle, measured — and it costs nothing
The turn-54 End Turn of the long run stopped on an **Encounter at Gallandro**: the player's five ships
(3 DE Colonizer, 2 DE Armor) against a **Von Neumann**. That is the workload §8 said did not exist and lane
J's `combat-resolver.md` asked for — every encounter in 54 turns until this one had `res->+0x4` set, making
`ApplyEncounterResult` a whole-function no-op. **Auto Resolve** was chosen (the dialog's four options are
Fight Manually / Auto Resolve / Fight Manually If Opponent Does / Retreat), and the prediction was committed
to `sots-engine/docs/Z-tail-rng.md` §7 with the dialog still on screen and unclicked.
| turn | `res_no_battle` | `ApplyEncounterResult` words | tail words | `ProcessTurn` words | bracket | residual |
|---|---|---|---|---|---|---|
| 52 | 1 | 0 | 0 | 18 | 18 | 0 |
| 53 | 1 | 0 | 0 | 18 | 18 | 0 |
| 54 | 1 | 0 | 0 | 16 | 16 | 0 |
| **55** | **0** | **0** | **0** | 22 | **22** | **0** |
**P10 predicted a non-zero tail cost and was wrong.** The first battle this campaign has ever instrumented
moved the strategic generator by **zero words**, and the bracket residual stayed 0 — so combat proper
(`RunCombatRound` / the combat server, which run between `ProcessTurn` and the tail and are hooked by
nobody) drew nothing either. Every one of the turn's 22 words was inside `StrategyServer::ProcessTurn`, just
as on a peaceful turn.
That is the *strong* form of lane J's reading. Lane J established from the instruction stream that the
resolver has **no unconditional draw** — its three sites are a node-cannon `NextInt`, an inlined `NextFloat`
per back-engineering candidate, and a `NextInt` per successful roll of that. This run shows that on an
ordinary encounter **none of the three fires**, and lane J's own cheap prediction — *a plain fleet battle
should cost the same as a peaceful turn* — holds exactly.
### 10.1 Why this matters to the standalone
A reimplementation that models a strategic turn's RNG and **nothing about combat** reproduces the generator
correctly through a battle. Combat's effect on the save is entirely in the state it writes, not in the
generator it advances. That is a much cheaper milestone than "read the 7,641-byte resolver first", and it
was not knowable before this run: the honest prior was lane K's "draw counts are entirely combat-dependent
and unknown".
### 10.2 What one battle does not settle — and it is a lot
* **One encounter, auto-resolved.** `Auto Resolve` may not take the same path as a manually fought battle;
the tactical engine has its own `Mars::CombatSim` generator at `sim+0x108` (§7) which nothing here
watches. A manually fought battle is a different experiment and has still never been run.
* **The opponent was a Von Neumann**, an NPC pseudo-player, not a rival empire's war fleet. No node cannon
was present, so R1 could not fire; whether R2's salvage roll was skipped because no candidate had a
non-zero salvage slot, or because the arm was not reached at all, is not distinguishable from a word
count of 0.
* **A cost of 0 is the easiest number to produce by accident.** It is exactly what a hook that compared
nothing would report. The reasons to believe it here are that the same hook reported 18–22 for
`ProcessTurn` on the same turn, that `res_no_battle` flipped to 0 for the first time in 55 turns on
exactly the turn the battle happened, and that the player's fleet was destroyed — the battle demonstrably
occurred. It is still one observation.
* **No `EVENT_*` or state-side check was made.** These hooks declare the generator and nothing else, so
this says the battle was RNG-free and says nothing about whether it was *computed* correctly.