lane Y: the standalone's generator against lane Z's calibrated oracle pairs, and the tail's turn record
The standalone now models the turn's dominant generator cost -- 16 of a measured 18-22 words -- and lands 4 and 2 short of the two calibrated pairs, which is exactly the per-call-site ledger's split for those turns. The state block is byte-identical; only left differs. The answer to 'does it match the oracle' is no, by a stated amount, and tools/rng_oracle_check.py is the instrument that says so. The tail's last phase is modelled for the six turn-record fields recoverable from the wire and checked against the record the game itself archived: 480 fields over 80 player-records, 0 mismatches. It stays blocked; --commit-blocked shows exactly which five fields are missing and what they cost. By-product, and probably worth more than the phase: the stored bankruptcy elimination limit is injective in the maximum-income sum it is built from, so every save states the per-system output term that blocks ComputeBudget. tools/max_income_oracle.py inverts it -- 25 player-records over the corpus -- and recovers the protection factor as 3.3 from the saves rather than from the data files. It also shows the engine's -0.15 divisor disagrees with the game on 6 of those 25. divergence unchanged: 209->204 and 108->103, 5 closed / 0 regressed on both pairs.
This commit is contained in:
parent
351acc07c3
commit
c471a5515f
9 changed files with 1504 additions and 58 deletions
296
findings/control-flow/standalone-generator-and-turn-record.md
Normal file
296
findings/control-flow/standalone-generator-and-turn-record.md
Normal file
|
|
@ -0,0 +1,296 @@
|
|||
# The standalone's generator, measured against the oracle — and the tail's turn record
|
||||
|
||||
Lane Y, 2026-09-08. Host work, no VM held. Engine worktree `wip/yield`
|
||||
(`sots-engine/docs/Y-standalone-rng-and-tail.md` carries the predictions, committed before the
|
||||
build). Program `sots` / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
|
||||
|
||||
**The headline, and it is binary. The standalone's generator does NOT land on the oracle's state.**
|
||||
It lands **4 words short** on lane Z's turn-4→5 pair and **2 words short** on the turn-5→6 pair, and
|
||||
those two numbers are exactly the per-call-site ledger's split for those turns. The 624-word state
|
||||
block is byte-identical in both cases (no boundary is crossed); only `left` differs, 379 against 375
|
||||
and 359 against 357.
|
||||
|
||||
| calibrated pair | oracle words | standalone | residual | `left` ours / oracle | state matches |
|
||||
|---|---|---|---|---|---|
|
||||
| `z2-endturn` → `z2-autosave` (turn 4→5) | 20 | **16** | **4** | 379 / 375 | **no** |
|
||||
| `z-t6-endturn` → `z-t6-autosave` (turn 5→6) | 18 | **16** | **2** | 359 / 357 | **no** |
|
||||
|
||||
Tool: `tools/rng_oracle_check.py`; output kept at `verify/results/standalone/rng-oracle.{txt,json}`.
|
||||
|
||||
---
|
||||
|
||||
## 1. What is now modelled: the trade-raid block, 16 words
|
||||
|
||||
Lane Z established that `ServerTradeManager::GenerateTradeRaidEncounters` 0x00893290 loops the
|
||||
server's player vector and rolls two `Mars::RNG::Chance` calls per player, at 0x00893426
|
||||
(`TRADE_RAID_ODDS_PLAYER`, 0.2f) and 0x00893513 (`TRADE_RAID_ODDS_NPC`, 0.05f), with **no back edge
|
||||
containing either site**. That is what makes it modellable by a lane holding no game: the cost is a
|
||||
**bound**, not a fitted observation, and it does not depend on the probabilities — both are strictly
|
||||
inside (0, 1), so `Chance` takes neither early-out and spends exactly one word.
|
||||
|
||||
The player vector's size is on the wire: it is the `NumPlrs`-counted array, **8** on the Human saves
|
||||
and 7 on the Zuul ones. `sots-engine/src/app/trade_raid.{h,cpp}` implements the loop, and
|
||||
`sots-engine/tests/app/test_trade_raid.cpp` pins the count against a scripted generator.
|
||||
|
||||
**The word cost is counted, not assumed.** The implementation increments its counter *inside* the
|
||||
chance helper, past the two early-outs, rather than once per call. That matters because the three
|
||||
odds are game-data constants: a data file that pushes one of them to 0 or 1 removes the **draw**, not
|
||||
just the outcome, and a count-the-calls implementation would be silently one word per player out.
|
||||
One test case pins exactly that.
|
||||
|
||||
### 1.1 The 16 is now confirmed by a third instrument
|
||||
|
||||
Lane Z's 16 came from two instruments that both watch the running game (a boundary hook and a set of
|
||||
return-address detours). Subtracting the standalone's structurally-bounded 16 from the **file**
|
||||
oracle's 20 and 18 leaves **4 and 2** — which is precisely what the per-site table predicts for those
|
||||
two turns (2 detection words on both, plus 2 research words on turn 5 and 0 on turn 6). So the
|
||||
trade-raid count is now corroborated by a route that touches no hook at all: file bytes minus a
|
||||
static bound.
|
||||
|
||||
This is weaker than it looks in one specific way, and it is worth saying: the residual is a
|
||||
*subtraction*, so it confirms the **sum** 16 + (detection + research), not each term separately. If
|
||||
the trade-raid block cost 15 and something unmodelled cost 5, the same residual would appear. What
|
||||
rules that out is the static bound, not this arithmetic.
|
||||
|
||||
## 2. What is not modelled, named rather than netted off
|
||||
|
||||
`TurnResult` now carries `rngUnaccounted` next to `rngWords`, and the report prints them as two
|
||||
separate facts. The same convention as `closed` / `regressed`, for the same reason.
|
||||
|
||||
| site | owner | cost | why it is not modelled |
|
||||
|---|---|---|---|
|
||||
| 0x0050329d | ← `DetectEncounters`, depth 4 | 1/turn on 3 of 3 measured turns | no derived rule; lane I bounds the containing family at `\|contacts\| × \|detectors\|`. Fitting 2/turn to three observations is not a model |
|
||||
| 0x007929a4 | ← `DetectEncounters`, depth 3 | 1/turn on 3 of 3 | same |
|
||||
| 0x00587888 | `TechTree::ProcessResearch`+0x1c8 | 0–1 | downstream of the research allocation, which is downstream of `ComputeBudget`, which is blocked on the population → base-output term |
|
||||
| 0x0088df4f | `ServerPlayer::RollResearchEvent`+0x2f | 0–1 | the standalone's phase P10 holds the gate but the *threshold* reads research progress after the same blocked allocation, so it evaluates false where the game evaluates true |
|
||||
| 0x008939ee | `ServerTradeManager::CreateRaidEncounter` | 0 or 1 per **successful** roll | undetermined; no roll succeeded on any measured turn |
|
||||
|
||||
**The last row is the one that will bite.** Because the standalone skips the two research draws that
|
||||
happen *earlier* in the turn than the trade-raid block, its raid rolls consume the wrong words, so
|
||||
its hit decisions are not the game's. The run reports the hit count and says explicitly that it is
|
||||
not a prediction. On the two calibrated pairs no roll succeeded in our stream either, so no
|
||||
target-selection draw was reached; that is luck, not a property.
|
||||
|
||||
**Ordering, stated for the record.** The generator state a save carries encodes only the *count*, so
|
||||
the residual above is order-independent. Order still matters for two things: the *values* drawn (and
|
||||
hence every decision taken from them), and the word cost of any bounded-integer draw whose rejection
|
||||
loop can run more than once. Getting the count right is necessary, not sufficient.
|
||||
|
||||
## 3. `--commit-rng` now has something to commit — and still tells the truth
|
||||
|
||||
Lane S2 left the write-back unimplemented on the reasoning that an advanced-but-incomplete state is
|
||||
wrong differently from an untouched one. That reasoning is satisfied only for the accounted words, so
|
||||
the flag discipline is kept and the run now prints, every time:
|
||||
|
||||
* the words consumed, and whether they were written back;
|
||||
* the unaccounted sites, as a list, never subtracted from the total;
|
||||
* a standing warning that the committed state is a **lower bound** and that its drawn values are not
|
||||
the game's.
|
||||
|
||||
By default the generator is still untouched, and `tests/app/test_turn.cpp` still asserts that.
|
||||
|
||||
---
|
||||
|
||||
## 4. The tail's last phase — the turn record, and it is testable without a game
|
||||
|
||||
Lane K read `StrategyServer::FinalizeTurnRecords` (tail phase 36, `FUN_0078a0e0`, 416 B) field by
|
||||
field and noted that it also runs on load, so it is "testable without a turn". It is better than
|
||||
that: **the archive it writes into is on the wire.** It is `/Sim/turnstats/nply[]/hist/stats[]` —
|
||||
`PlayerTurnStats` in the engine's schema — one element per turn per player, and **every save carries
|
||||
an element for its own frame**. So the phase can be checked against bytes the original produced.
|
||||
|
||||
### 4.1 Six fields, recovered and confirmed on 80 player-records
|
||||
|
||||
| record field (wire) | lane K's offset | rule | status |
|
||||
|---|---|---|---|
|
||||
| `trn` | — | the frame counter | confirmed |
|
||||
| `col` | `+0x18` (int16 in memory) | the number of owned-system ids | confirmed |
|
||||
| `sav` | `+0x14` | the player's savings | confirmed |
|
||||
| `inc` | `+0x0c` | savings **minus previous-turn savings** | confirmed |
|
||||
| `tch` | `+0x28` (int16) | tech-tree states equal to 4 | confirmed |
|
||||
| `pop` | `+0x20`/`+0x24` (int64) | Σ over owned systems of `Pop + pbon` | confirmed |
|
||||
|
||||
`sots-engine/tests/app/test_turn_record.cpp` runs this over `$SOTS_SAVES_DIR`: **11 saves, 80
|
||||
player-records, 480 fields compared, 0 mismatches, 0 dangling owned-system ids.** The standalone also
|
||||
runs the same check *inside every turn* and prints it as a phase note, so a future schema change that
|
||||
breaks the mapping announces itself on the next run rather than at the next audit.
|
||||
|
||||
**Two corrections to `combat-done-tail.md` §5, both from the corpus.**
|
||||
|
||||
1. **`+0x0c` is not "net savings change" in the sense of a budget line** — it is literally
|
||||
`Sav − PvSav`, and `PvSav` is a wire field stamped before this turn's savings are written. The
|
||||
distinction matters because it makes the field computable from the save alone.
|
||||
2. **The wire names `inc` and `tdinc` are the opposite way round from the obvious reading.** `inc`
|
||||
is `+0x0c` (the savings delta) and `tdinc` is `+0x10` (the trade income lane T found). Reading
|
||||
`tdinc` as the turn's delta income and `inc` as a general income would put both fields on the
|
||||
wrong offsets.
|
||||
|
||||
**And one term lane K's reading left open is now pinned.** `Σ (sys->+0x194 + sys->+0x18c)` maps to
|
||||
the wire pair `Pop + pbon` — the live population plus the pending population bonus. The competing
|
||||
candidate, `Pop + dcs`, matched **101 of 112** player-records and was rejected; `Pop + pbon` matched
|
||||
**112 of 112**. Neither `Pop` alone nor `PvPop` alone matches: `PvPop` fits the Human saves and fails
|
||||
the Zuul ones, and `Pop` does the reverse. A one-save check would have concluded either.
|
||||
|
||||
### 4.2 Five fields it cannot fill, and what each is blocked on
|
||||
|
||||
| field | blocked on |
|
||||
|---|---|
|
||||
| `almem` (`+0x08`) | the alliance/vision mask rebuilt by `ProcessTurn` phase 4 — a stub |
|
||||
| `tdinc` (`+0x10`) | trade income from `ComputeBudget` — blocked behind the per-system money output |
|
||||
| `bat` (`+0x1a`) | the tail's battle tally, phase 4 — a stub |
|
||||
| `sacq` / `slost` | counted lists, **count 0 in every save in the corpus** — rule 6 applies: the element meaning is unobserved, not known |
|
||||
| the per-hull-class census (`+0x2a`..`+0x34`) | each design's hull size and its `0x400` class flag; neither is on the wire, both come from the game data |
|
||||
|
||||
### 4.3 The block is a measurement now, not an argument
|
||||
|
||||
The phase builds the record and does **not** commit it. `--commit-blocked` writes it anyway, so the
|
||||
claim can be checked. On `turn1-state` → `turn2-state`:
|
||||
|
||||
| run | closed | regressed |
|
||||
|---|---|---|
|
||||
| default | **5** | **0** |
|
||||
| `--commit-blocked` | 29 | 17 |
|
||||
|
||||
and every one of the 17 is in the unmodelled set: `almem` ×8, `inc` ×3, `sav` ×3, and three leaves of
|
||||
the ship census. Nothing else regresses. On `turn2` → `turn3` the same run is 13 closed / 19
|
||||
regressed (17 turnstats + the two `Status` leaves lane S2 already knew about).
|
||||
|
||||
So the honest statement is **not** "committing this makes everything worse" — on the first pair it is
|
||||
net +7 for the turnstats subsystem and on the second it is net −9. It is: *the record is right in six
|
||||
fields and wrong in five, and which way the net falls depends on the save*. That is precisely what
|
||||
netting `closed` and `regressed` into one number would have hidden, and it is why the phase stays
|
||||
blocked until `ProcessTurn` phase 4 and `ComputeBudget` land. **When they do, phase 36 closes 24
|
||||
leaves on the reference pair with nothing left over.**
|
||||
|
||||
---
|
||||
|
||||
## 5. A by-product worth more than the phase: `BnkEl` inverts to the blocked input
|
||||
|
||||
`ServerPlayer::UpdateBankruptcyLimits` computes
|
||||
`BnkEl = max(trunc(maxIncome / -0.15000000596046448), -2000000000)` where
|
||||
`maxIncome = Σ over owned systems of max(ComputeMaxIncome(s), 0)` — the *same* per-system money
|
||||
output that blocks `ComputeBudget`, phase P01, phase P02 and half the turn record.
|
||||
|
||||
The map from `maxIncome` to `BnkEl` has a slope of about 6.67, so **it is injective and invertible**:
|
||||
each stored `BnkEl` has at most one integer preimage. Inverted over the corpus:
|
||||
|
||||
| save | player | `BnkEl` | recovered `maxIncome` | `BnkPr / maxIncome` |
|
||||
|---|---|---|---|---|
|
||||
| turn1-state | 16 | −1590613 | **238592** | 3.299997485 |
|
||||
| turn2-state | 16 | −1594593 | **239189** | 3.299997073 |
|
||||
| turn3-state | 16 | −1598566 | **239785** | 3.299997915 |
|
||||
| turn1-state | 32 | −1811273 | **271691** | 3.299998896 |
|
||||
| human-turn2 | 16 | −9064672 | **1359701** | 3.299999779 |
|
||||
| zuul-turn16 | 16 | −1270619 | **190593** | 3.299995278 |
|
||||
|
||||
25 player-records over the corpus invert cleanly; the 55 players that own nothing carry `BnkEl = 0`
|
||||
and are reported as such rather than inverted. `tools/max_income_oracle.py` does this over any save,
|
||||
with `verify/results/standalone/max-income-oracle.json` as the corpus run. **Two things follow:**
|
||||
|
||||
1. **Lane N has a per-save oracle for the term it is chasing.** Every save in the corpus states, for
|
||||
every player that owns anything, the exact sum of its systems' maximum money outputs — no VM, no
|
||||
hook, no game. A candidate output formula can be summed over the owned systems and compared to a
|
||||
number the original wrote.
|
||||
2. **`BANKRUPTCY_PROTECTION_LIMIT_FACTOR` is 3.3**, recovered from the saves rather than from the
|
||||
data files. All 25 ratios land in [3.299994751, 3.299999872] — 3.3 minus one truncation.
|
||||
`combat-done-tail.md` §4.1 correctly flagged the constant as DB-sourced and not a binary literal;
|
||||
this is an independent measurement of its value.
|
||||
|
||||
### 5.1 The divisor defect is live on the corpus, and it is not one ulp
|
||||
|
||||
`sots-engine/src/game/sim/economy.cpp` divides by `-0.15`. Lane K's correction — the constant in the
|
||||
image is the double `-0.15000000596046448`, i.e. `(double)(float)-0.15f` — was described as being
|
||||
"one ulp out on large empires". **That understates it, and this lane's own prediction repeated the
|
||||
understatement and is corrected here.**
|
||||
|
||||
`trunc(m / -0.15)` and `trunc(m / -0.15000000596046448)` differ for **every `m` divisible by 3**,
|
||||
starting at `m = 3` (−20 against −19). The cause is not precision loss at scale: `m / 0.15` is an
|
||||
exact integer whenever `3 | m`, and the two constants straddle it, so one truncates down and the
|
||||
other does not. On top of that floor, ordinary drift adds more as the value grows:
|
||||
|
||||
| `maxIncome` around | fraction of integers where the two constants disagree |
|
||||
|---|---|
|
||||
| 10³ … 1.2 × 10⁶ | **33.3 %** (exactly the multiples of 3) |
|
||||
| 1.36 × 10⁶ … 2 × 10⁶ | 66.7 % |
|
||||
| ≥ 3 × 10⁶ | **100 %** |
|
||||
|
||||
On the corpus this is not hypothetical: **6 of the 25 player-records above would get a different
|
||||
`BnkEl` from the engine's current constant than the game wrote** — both players of `human-turn2`
|
||||
(`maxIncome` 1359701 and 1556834, neither divisible by 3, both in the 66.7 % band) and of
|
||||
`human-turn3`, plus `zuul-turn15` player 32 and `zuul-turn16` player 16 (both multiples of 3).
|
||||
`tools/max_income_oracle.py` flags them.
|
||||
|
||||
**This lane did not fix it**: `src/game/sim/economy.cpp` belongs to another lane in flight. The fix
|
||||
is one character class — replace the literal `-0.15` in `ComputeBankruptcyLimits` with the
|
||||
round-tripped float constant — and it wants a test case at `maxIncome = 3`.
|
||||
|
||||
---
|
||||
|
||||
## 6. What still stands between the standalone and a byte-match, re-ranked
|
||||
|
||||
Ranked by *leaves closed per unit of work*, with what each unblocks.
|
||||
|
||||
| # | blocker | what it costs | what it closes |
|
||||
|---|---|---|---|
|
||||
| 1 | **the per-system money output** (population → base output) | one formula, and §5 now hands it a per-save oracle | P01, P02, P05, P06, T31, and 5 of the 11 fields of the turn record. It is upstream of ~82 of the 204 remaining leaves and of 2 of the 4 unaccounted generator words |
|
||||
| 2 | **`ProcessTurn` phase 4, the alliance/vision mask** | one bit-mask rebuild, no RNG, no events | `almem` on every player's turn record — 8 of the 17 leaves that stop phase 36 committing |
|
||||
| 3 | **encounter detection** (`ProcessTurn` phase 31) | the largest remaining *generator* gap: 2 words a turn with no derived rule | the last non-research residual against the oracle. Cheapest experiment: bracket `DetectEncounters` with the contact and detector counts in the record, which lane I already specified |
|
||||
| 4 | **the ship-design catalogue** (hull size + the `0x400` flag) | a data-file read, not a code read | the turn record's census, the ship-record leaves, and part of `T24` |
|
||||
| 5 | **`ModCount`** | it advances 12–44 times a turn from writers spread across both drivers | 1 leaf, but it is in `Summary.Checksum`'s input, so no byte-match is possible without it |
|
||||
| 6 | the tail's remaining 34 stubs | large | the rest |
|
||||
|
||||
**The generator specifically.** After the trade-raid block there are exactly **two** classes of
|
||||
missing word left on a quiet turn: the detection pair (#3) and the research pair (#1). Close either
|
||||
and the residual halves; close both and the standalone's generator state matches the oracle's on
|
||||
these two pairs, at which point `/Sim/RNG` — currently 1 leaf of the 204 — closes, and it is the leaf
|
||||
that gates every *subsequent* turn.
|
||||
|
||||
## 7. What this lane did not do, listed as loudly as the results
|
||||
|
||||
* **Nothing here was compared against the live game.** `verified` in the completion metric stays 0
|
||||
for everything this lane touched, and the catalog test enforces that. The turn-record check
|
||||
compares against **bytes the original wrote**, which is strictly weaker than a live compare and
|
||||
strictly stronger than nothing; it is labelled that way in the source.
|
||||
* **The 16-word count is exercised on 8-player saves only.** The two calibrated pairs are the same
|
||||
game. Lane Z's free prediction — that a 7-player Zuul save should cost 14 rather than 16 at these
|
||||
two sites — is still untested, and this model would make it testable the moment a Zuul pair with a
|
||||
known word cost exists.
|
||||
* **The two raid gates are hypotheses.** The NPC-raid gate is player-independent and was open on 8 of
|
||||
8 measured turns; the refugee gate was shut on all 8. Neither is on the wire. If either flips, the
|
||||
turn costs ±8 words on an 8-player save and the residual moves by that much — which is how it will
|
||||
announce itself.
|
||||
* **`sacq` / `slost` have count 0 in every save.** Rule 6: their element meaning is unobserved, and
|
||||
the turn record model treats them as absent rather than as empty-and-understood.
|
||||
* **The turn-record self-check compares six fields, not eleven.** A green run over 480 fields is 480
|
||||
comparisons of the six fields we hold; it says nothing about the five we do not, and the phase note
|
||||
lists them every run for that reason.
|
||||
* **The `BnkEl` inversion assumes the stored value was written by this formula and not clamped.** Any
|
||||
player whose `maxIncome` is large enough to hit the −2,000,000,000 floor inverts to nothing; none
|
||||
in the corpus does, and the tool would return an empty preimage rather than a wrong one.
|
||||
* No shim TU was touched, so no CT111 cross-build was required for this work.
|
||||
|
||||
---
|
||||
|
||||
## 8. Three questions for whoever holds the VM (earned rule 18)
|
||||
|
||||
Rule 18 landed while this work was in flight and it applies to every residual above. This lane holds
|
||||
no VM, so everything here was measured against the **files** — the two calibrated pairs and the
|
||||
eleven-save corpus — rather than derived, but three of the open items are hook-shaped and would take
|
||||
minutes on the game.
|
||||
|
||||
1. **Bracket `StrategyServer::DetectEncounters` 0x007d7f70** the way lane Z bracketed the tail, with
|
||||
the contact and detector counts in the record. That settles the 2-word detection pair, which is
|
||||
the *last* non-research residual between the standalone's generator and the oracle's on a quiet
|
||||
turn. Lane I already specified the experiment and its falsifiers.
|
||||
2. **Hook `ServerTradeManager::CreateRaidEncounter` 0x008938a0.** A word count cannot separate "no
|
||||
roll succeeded" from "the candidate list was empty"; the callee separates them instantly. Until
|
||||
it does, the trade-raid model's 16 is a bound on a *quiet* turn only, and the first successful
|
||||
raid moves the count by an unknown 0 or 1.
|
||||
3. **Run one End Turn on any Zuul save with the site instrument.** The player vector holds 7 there,
|
||||
so the two trade-raid sites should cost **14** rather than 16. That is the cheapest available
|
||||
check on what the loop actually iterates, and this lane's model makes it a prediction rather than
|
||||
an observation: the standalone will report 14 on those saves today.
|
||||
|
||||
And one that is not mine but is next to my work: **the `Player.Status` writer between tail phase 31
|
||||
and the autosave** — the phase writes 1, the file carries 4, and it is the leaf that blocked lane
|
||||
S2's first committed phase. A watchpoint on `ServerPlayer+0x164` across the tail names the writer.
|
||||
149
tools/max_income_oracle.py
Normal file
149
tools/max_income_oracle.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Recover each player's maximum-income sum from the bankruptcy limit a save carries.
|
||||
|
||||
`ServerPlayer::UpdateBankruptcyLimits` stores
|
||||
|
||||
BnkEl = max( trunc(maxIncome / -0.15000000596046448), -2000000000 )
|
||||
BnkPr = max( -trunc(BANKRUPTCY_PROTECTION_LIMIT_FACTOR * maxIncome), BnkEl )
|
||||
|
||||
where `maxIncome` is the sum, over the player's owned systems, of `max(ComputeMaxIncome(s), 0)` --
|
||||
the per-system money output that blocks `ComputeBudget`, both savings phases and half the turn
|
||||
record. The first map has a slope of about 6.67, so it is injective: each stored `BnkEl` has at
|
||||
most one integer preimage, and every save in the corpus therefore STATES the blocked term for
|
||||
every player that owns anything.
|
||||
|
||||
That makes this a per-save oracle for a formula nobody has yet written: sum a candidate
|
||||
per-system output over the owned systems and compare it with the number here.
|
||||
|
||||
Two caveats the tool reports rather than hides:
|
||||
* a player whose limit hit the -2,000,000,000 floor inverts to nothing, and is listed as such
|
||||
rather than given a wrong answer;
|
||||
* the divisor is the round-tripped float `-0.15000000596046448`, NOT `-0.15`. Using `-0.15`
|
||||
changes the result for every `maxIncome` divisible by 3, and the tool flags the records where
|
||||
the two constants disagree.
|
||||
|
||||
tools/max_income_oracle.py # every save in verify/results/saves
|
||||
tools/max_income_oracle.py PATH.sav ... # named saves
|
||||
tools/max_income_oracle.py --json OUT
|
||||
"""
|
||||
import argparse
|
||||
import glob
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import sys
|
||||
|
||||
RE_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
SAVE_DIR = os.path.join(RE_ROOT, "verify", "results", "saves")
|
||||
sys.path.insert(0, os.path.join(RE_ROOT, "verify", "save-reader"))
|
||||
import save_reader as sr # noqa: E402
|
||||
|
||||
DIVISOR = -0.15000000596046448 # the double in .rdata: (double)(float)-0.15f
|
||||
NAIVE = -0.15 # what a reimplementation writes by mistake
|
||||
FLOOR = -2000000000
|
||||
|
||||
|
||||
def kids(n):
|
||||
return getattr(n, "children", []) or []
|
||||
|
||||
|
||||
def name(n):
|
||||
return getattr(n, "name", None)
|
||||
|
||||
|
||||
def field(n, key):
|
||||
for c in kids(n):
|
||||
if name(c) == key:
|
||||
return getattr(c, "value", None)
|
||||
return None
|
||||
|
||||
|
||||
def invert(bnkel):
|
||||
"""Integer maxIncome values that map to this stored limit, [] when none do."""
|
||||
if bnkel == 0:
|
||||
return [0]
|
||||
if bnkel <= FLOOR:
|
||||
return [] # clamped: the preimage is unbounded, so refuse to answer
|
||||
centre = int(-bnkel * 0.15)
|
||||
return [m for m in range(max(centre - 8, 0), centre + 9)
|
||||
if math.trunc(m / DIVISOR) == bnkel]
|
||||
|
||||
|
||||
def scan(path):
|
||||
res = sr.read_save(path)
|
||||
sim = [c for c in kids(res.tree) if name(c) == "Sim"][0]
|
||||
ch = kids(sim)
|
||||
rows = []
|
||||
for i, c in enumerate(ch):
|
||||
if name(c) != "Player":
|
||||
continue
|
||||
pid = getattr(ch[i - 1], "value", None)
|
||||
el, pr = field(c, "BnkEl"), field(c, "BnkPr")
|
||||
owned = sum(1 for k in kids(c) if name(k) == "OwnId")
|
||||
if not el:
|
||||
rows.append({"playerID": pid, "ownedSystems": owned, "BnkEl": el, "BnkPr": pr,
|
||||
"maxIncome": None, "why": "limit is zero (no owned systems)"})
|
||||
continue
|
||||
cands = invert(el)
|
||||
if len(cands) != 1:
|
||||
rows.append({"playerID": pid, "ownedSystems": owned, "BnkEl": el, "BnkPr": pr,
|
||||
"maxIncome": None,
|
||||
"why": "clamped at the floor" if el <= FLOOR
|
||||
else "%d preimages" % len(cands)})
|
||||
continue
|
||||
m = cands[0]
|
||||
rows.append({
|
||||
"playerID": pid,
|
||||
"ownedSystems": owned,
|
||||
"BnkEl": el,
|
||||
"BnkPr": pr,
|
||||
"maxIncome": m,
|
||||
"protectionFactor": (-pr / m) if m else None,
|
||||
"naiveDivisorDisagrees": math.trunc(m / NAIVE) != math.trunc(m / DIVISOR),
|
||||
})
|
||||
return rows
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("saves", nargs="*")
|
||||
ap.add_argument("--json")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
paths = args.saves or sorted(glob.glob(os.path.join(SAVE_DIR, "*.sav")))
|
||||
if not paths:
|
||||
print("no saves given and none in " + SAVE_DIR, file=sys.stderr)
|
||||
return 2
|
||||
|
||||
out = {}
|
||||
factors, disagree, solved = [], 0, 0
|
||||
for p in paths:
|
||||
rows = scan(p)
|
||||
out[os.path.basename(p)] = rows
|
||||
print("== " + os.path.basename(p))
|
||||
for r in rows:
|
||||
if r["maxIncome"] is None:
|
||||
print(" pid=%-5s owns %-3d BnkEl=%-12s -- %s"
|
||||
% (r["playerID"], r["ownedSystems"], r["BnkEl"], r["why"]))
|
||||
continue
|
||||
solved += 1
|
||||
factors.append(r["protectionFactor"])
|
||||
disagree += 1 if r["naiveDivisorDisagrees"] else 0
|
||||
print(" pid=%-5s owns %-3d BnkEl=%-12d maxIncome=%-10d factor=%.9f%s"
|
||||
% (r["playerID"], r["ownedSystems"], r["BnkEl"], r["maxIncome"],
|
||||
r["protectionFactor"],
|
||||
" [-0.15 would differ]" if r["naiveDivisorDisagrees"] else ""))
|
||||
if factors:
|
||||
print("\n%d player-record(s) inverted; protection factor in [%.9f, %.9f]"
|
||||
% (solved, min(factors), max(factors)))
|
||||
print("%d of them would get a DIFFERENT stored limit from the naive -0.15 divisor"
|
||||
% disagree)
|
||||
if args.json:
|
||||
with open(args.json, "w") as f:
|
||||
json.dump(out, f, indent=2)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
213
tools/rng_oracle_check.py
Normal file
213
tools/rng_oracle_check.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Does the standalone's generator land where the oracle's did?
|
||||
|
||||
Lane Z left two calibrated pairs -- a pre-turn save, a post-turn save, and a word count
|
||||
verified from the file bytes alone (`twists = 0`, so the number does not route through
|
||||
anyone's twist implementation). This tool runs the standalone on the pre-turn save with
|
||||
`--commit-rng` and compares three things against the post-turn save:
|
||||
|
||||
* the 624-word state block, byte for byte;
|
||||
* `left`, the count of words still unread in the block;
|
||||
* the resulting absolute position, which is what a word count actually is.
|
||||
|
||||
The verdict is binary and the residual is stated in words, never as a percentage. A
|
||||
residual of N means the standalone models N fewer words than the turn really spends, and
|
||||
lane Z's per-call-site ledger says which sites those are.
|
||||
|
||||
tools/rng_oracle_check.py # both calibrated pairs
|
||||
tools/rng_oracle_check.py --binary PATH # a sots_turn built elsewhere
|
||||
tools/rng_oracle_check.py --pair IN OUT WORDS # an ad-hoc pair with a known cost
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
RE_ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
||||
PAIR_DIR = os.path.join(RE_ROOT, "verify", "results", "shim", "tailrng")
|
||||
sys.path.insert(0, os.path.join(RE_ROOT, "verify", "save-reader"))
|
||||
import save_reader as sr # noqa: E402
|
||||
|
||||
N, M = 624, 397
|
||||
|
||||
# (pre-turn, post-turn, words the game spent, what it is)
|
||||
PAIRS = [
|
||||
("z2-endturn.sav", "z2-autosave.sav", 20, "ref-turn2, turn 4 -> 5"),
|
||||
("z-t6-endturn.sav", "z-t6-autosave.sav", 18, "ref-turn2, turn 5 -> 6"),
|
||||
]
|
||||
|
||||
DEFAULT_BINARIES = [
|
||||
os.path.expanduser("~/sots-engine-wt-yield/build-host/src/app/sots_turn"),
|
||||
os.path.expanduser("~/sots-engine/build-host/src/app/sots_turn"),
|
||||
"/srv/re-lab/build/sots-engine-y/src/app/sots_turn",
|
||||
]
|
||||
|
||||
|
||||
def twist(mt):
|
||||
mt = list(mt)
|
||||
for kk in range(N - M):
|
||||
y = (mt[kk] & 0x80000000) | (mt[kk + 1] & 0x7FFFFFFF)
|
||||
mt[kk] = mt[kk + M] ^ (y >> 1) ^ (0x9908B0DF if y & 1 else 0)
|
||||
for kk in range(N - M, N - 1):
|
||||
y = (mt[kk] & 0x80000000) | (mt[kk + 1] & 0x7FFFFFFF)
|
||||
mt[kk] = mt[kk + (M - N)] ^ (y >> 1) ^ (0x9908B0DF if y & 1 else 0)
|
||||
y = (mt[N - 1] & 0x80000000) | (mt[0] & 0x7FFFFFFF)
|
||||
mt[N - 1] = mt[M - 1] ^ (y >> 1) ^ (0x9908B0DF if y & 1 else 0)
|
||||
return mt
|
||||
|
||||
|
||||
def _find_rng(node, out):
|
||||
if getattr(node, "name", None) == "RNG":
|
||||
out.append(node)
|
||||
for c in getattr(node, "children", []) or []:
|
||||
_find_rng(c, out)
|
||||
|
||||
|
||||
def rng_state(path):
|
||||
"""(mt[624], left) out of a save's generator frame."""
|
||||
res = sr.read_save(path)
|
||||
hits = []
|
||||
_find_rng(res.tree, hits)
|
||||
if not hits:
|
||||
raise SystemExit("no generator frame in " + path)
|
||||
n = hits[0]
|
||||
raw = n.raw or b"".join(c.raw for c in (getattr(n, "children", []) or []) if c.raw)
|
||||
if not raw:
|
||||
raise SystemExit("generator frame in %s carries no bytes" % path)
|
||||
for off in range(0, len(raw) - 2500 + 1):
|
||||
mt = [int.from_bytes(raw[off + 4 * i:off + 4 * i + 4], "little") for i in range(N)]
|
||||
left = int.from_bytes(raw[off + 2496:off + 2500], "little", signed=True)
|
||||
if 0 <= left <= N:
|
||||
return mt, left
|
||||
raise SystemExit("cannot parse the generator blob of %s (%d B)" % (path, len(raw)))
|
||||
|
||||
|
||||
def words_between(a, b, max_twists=64):
|
||||
"""Words consumed moving from state a to state b, exact across block boundaries."""
|
||||
(mta, la), (mtb, lb) = a, b
|
||||
cur, tw = mta, 0
|
||||
while tw <= max_twists and cur != mtb:
|
||||
cur = twist(cur)
|
||||
tw += 1
|
||||
if cur != mtb:
|
||||
return None, None
|
||||
return 624 * tw + (la - lb), tw
|
||||
|
||||
|
||||
def find_binary(explicit):
|
||||
if explicit:
|
||||
return explicit if os.path.exists(explicit) else None
|
||||
for p in DEFAULT_BINARIES:
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
return shutil.which("sots_turn")
|
||||
|
||||
|
||||
def run_pair(binary, src, oracle, expected, note, workdir):
|
||||
out_sav = os.path.join(workdir, "post-" + os.path.basename(src))
|
||||
metric = os.path.join(workdir, "metric.json")
|
||||
cmd = [binary, src, "--out", out_sav, "--metric", metric, "--commit-rng"]
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
row = {"input": os.path.basename(src), "oracle": os.path.basename(oracle),
|
||||
"note": note, "oracleWords": expected, "exit": proc.returncode}
|
||||
if proc.returncode != 0:
|
||||
row["error"] = proc.stderr.strip()[:2000]
|
||||
return row
|
||||
|
||||
before = rng_state(src)
|
||||
after_oracle = rng_state(oracle)
|
||||
after_ours = rng_state(out_sav)
|
||||
|
||||
oracle_words, oracle_twists = words_between(before, after_oracle)
|
||||
our_words, our_twists = words_between(before, after_ours)
|
||||
|
||||
row.update({
|
||||
"oracleWordsFromFiles": oracle_words,
|
||||
"oracleTwists": oracle_twists,
|
||||
"ourWords": our_words,
|
||||
"ourTwists": our_twists,
|
||||
"blockIdentical": after_ours[0] == after_oracle[0],
|
||||
"leftOurs": after_ours[1],
|
||||
"leftOracle": after_oracle[1],
|
||||
"stateMatches": after_ours[0] == after_oracle[0] and after_ours[1] == after_oracle[1],
|
||||
})
|
||||
row["residual"] = None if our_words is None or oracle_words is None else oracle_words - our_words
|
||||
if os.path.exists(metric):
|
||||
with open(metric) as f:
|
||||
m = json.load(f)
|
||||
row["standaloneRngWords"] = m.get("run", {}).get("rngWords")
|
||||
row["standaloneRngCommitted"] = m.get("run", {}).get("rngCommitted")
|
||||
row["unaccounted"] = m.get("run", {}).get("rngUnaccounted", [])
|
||||
return row
|
||||
|
||||
|
||||
def render(rows):
|
||||
out = []
|
||||
out.append("# does the standalone's generator land on the oracle's?\n")
|
||||
for r in rows:
|
||||
out.append("## %s -> %s (%s)" % (r["input"], r["oracle"], r["note"]))
|
||||
if r.get("error"):
|
||||
out.append(" standalone failed: " + r["error"].splitlines()[0])
|
||||
out.append("")
|
||||
continue
|
||||
out.append(" oracle spent %s word(s) (%s twist(s), from the files)"
|
||||
% (r["oracleWordsFromFiles"], r["oracleTwists"]))
|
||||
out.append(" standalone %s word(s) (metric says %s)"
|
||||
% (r["ourWords"], r.get("standaloneRngWords")))
|
||||
out.append(" residual %s word(s) NOT modelled" % r["residual"])
|
||||
out.append(" state block identical: %s left: ours %s, oracle %s"
|
||||
% ("yes" if r["blockIdentical"] else "no", r["leftOurs"], r["leftOracle"]))
|
||||
out.append(" GENERATOR STATE MATCHES ORACLE: %s"
|
||||
% ("YES" if r["stateMatches"] else "no"))
|
||||
for u in r.get("unaccounted", []):
|
||||
out.append(" - unaccounted: " + u)
|
||||
out.append("")
|
||||
matched = sum(1 for r in rows if r.get("stateMatches"))
|
||||
out.append("verdict: %d of %d calibrated pair(s) match the oracle's generator state exactly"
|
||||
% (matched, len(rows)))
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
ap = argparse.ArgumentParser(description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
ap.add_argument("--binary")
|
||||
ap.add_argument("--pair", nargs=3, metavar=("INPUT", "ORACLE", "WORDS"))
|
||||
ap.add_argument("--json", help="write the machine-readable result here")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
binary = find_binary(args.binary)
|
||||
if not binary:
|
||||
print("rng_oracle_check: sots_turn not found; build the host preset first",
|
||||
file=sys.stderr)
|
||||
return 2
|
||||
|
||||
if args.pair:
|
||||
pairs = [(args.pair[0], args.pair[1], int(args.pair[2]), "ad-hoc")]
|
||||
else:
|
||||
pairs = [(os.path.join(PAIR_DIR, a), os.path.join(PAIR_DIR, b), w, n)
|
||||
for a, b, w, n in PAIRS]
|
||||
|
||||
rows = []
|
||||
with tempfile.TemporaryDirectory(prefix="rngoracle-") as wd:
|
||||
for src, oracle, words, note in pairs:
|
||||
if not (os.path.exists(src) and os.path.exists(oracle)):
|
||||
print("missing calibrated pair: %s / %s" % (src, oracle), file=sys.stderr)
|
||||
return 2
|
||||
rows.append(run_pair(binary, src, oracle, words, note, wd))
|
||||
|
||||
text = render(rows)
|
||||
print(text, end="")
|
||||
if args.json:
|
||||
with open(args.json, "w") as f:
|
||||
json.dump({"binary": binary, "pairs": rows}, f, indent=2)
|
||||
# Exit 0 whether or not it matches: this is a measurement, not a gate. A non-zero exit
|
||||
# is reserved for the tool being unable to measure.
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
|
|
@ -53,6 +53,7 @@ PAIRS = [
|
|||
]
|
||||
|
||||
DEFAULT_BINARIES = [
|
||||
os.path.expanduser("~/sots-engine-wt-yield/build-host/src/app/sots_turn"),
|
||||
os.path.expanduser("~/sots-engine-wt-standalone/build-host/src/app/sots_turn"),
|
||||
os.path.expanduser("~/sots-engine/build-host/src/app/sots_turn"),
|
||||
"/srv/re-lab/build/sots-engine-s2/src/app/sots_turn",
|
||||
|
|
|
|||
689
verify/results/standalone/max-income-oracle.json
Normal file
689
verify/results/standalone/max-income-oracle.json
Normal file
|
|
@ -0,0 +1,689 @@
|
|||
{
|
||||
"human-turn2-orders.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 10,
|
||||
"BnkEl": -9064672,
|
||||
"BnkPr": -4487013,
|
||||
"maxIncome": 1359701,
|
||||
"protectionFactor": 3.299999779363257,
|
||||
"naiveDivisorDisagrees": true
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 10,
|
||||
"BnkEl": -10378892,
|
||||
"BnkPr": -5137552,
|
||||
"maxIncome": 1556834,
|
||||
"protectionFactor": 3.299999871534152,
|
||||
"naiveDivisorDisagrees": true
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"human-turn3-noderoute.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 10,
|
||||
"BnkEl": -9122659,
|
||||
"BnkPr": -4515716,
|
||||
"maxIncome": 1368399,
|
||||
"protectionFactor": 3.299999488453295,
|
||||
"naiveDivisorDisagrees": true
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 10,
|
||||
"BnkEl": -10444699,
|
||||
"BnkPr": -5170126,
|
||||
"maxIncome": 1566705,
|
||||
"protectionFactor": 3.2999996808588725,
|
||||
"naiveDivisorDisagrees": true
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"turn1-state.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -1590613,
|
||||
"BnkPr": -787353,
|
||||
"maxIncome": 238592,
|
||||
"protectionFactor": 3.2999974852467813,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -1811273,
|
||||
"BnkPr": -896580,
|
||||
"maxIncome": 271691,
|
||||
"protectionFactor": 3.299998895804425,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 576,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -665806,
|
||||
"BnkPr": -329574,
|
||||
"maxIncome": 99871,
|
||||
"protectionFactor": 3.2999969961250013,
|
||||
"naiveDivisorDisagrees": false
|
||||
}
|
||||
],
|
||||
"turn2-state.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -1594593,
|
||||
"BnkPr": -789323,
|
||||
"maxIncome": 239189,
|
||||
"protectionFactor": 3.299997073444013,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -1815833,
|
||||
"BnkPr": -898837,
|
||||
"maxIncome": 272375,
|
||||
"protectionFactor": 3.2999981642955483,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 576,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -665806,
|
||||
"BnkPr": -329574,
|
||||
"maxIncome": 99871,
|
||||
"protectionFactor": 3.2999969961250013,
|
||||
"naiveDivisorDisagrees": false
|
||||
}
|
||||
],
|
||||
"turn3-state.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -1598566,
|
||||
"BnkPr": -791290,
|
||||
"maxIncome": 239785,
|
||||
"protectionFactor": 3.299997914798674,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -1820206,
|
||||
"BnkPr": -901002,
|
||||
"maxIncome": 273031,
|
||||
"protectionFactor": 3.299998901223671,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 576,
|
||||
"ownedSystems": 1,
|
||||
"BnkEl": -665806,
|
||||
"BnkPr": -329574,
|
||||
"maxIncome": 99871,
|
||||
"protectionFactor": 3.2999969961250013,
|
||||
"naiveDivisorDisagrees": false
|
||||
}
|
||||
],
|
||||
"zuul-turn15-orders.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1271166,
|
||||
"BnkPr": -629227,
|
||||
"maxIncome": 190675,
|
||||
"protectionFactor": 3.299997377736987,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1820399,
|
||||
"BnkPr": -901097,
|
||||
"maxIncome": 273060,
|
||||
"protectionFactor": 3.299996337801216,
|
||||
"naiveDivisorDisagrees": true
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"zuul-turn16-noderoute.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1270619,
|
||||
"BnkPr": -628956,
|
||||
"maxIncome": 190593,
|
||||
"protectionFactor": 3.2999952778958304,
|
||||
"naiveDivisorDisagrees": true
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1819586,
|
||||
"BnkPr": -900695,
|
||||
"maxIncome": 272938,
|
||||
"protectionFactor": 3.299998534465703,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"zuul-turn17-orders2.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1270066,
|
||||
"BnkPr": -628682,
|
||||
"maxIncome": 190510,
|
||||
"protectionFactor": 3.2999947509317096,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1818586,
|
||||
"BnkPr": -900200,
|
||||
"maxIncome": 272788,
|
||||
"protectionFactor": 3.2999985336598385,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"zuul-turn17-rollpending.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1270066,
|
||||
"BnkPr": -628682,
|
||||
"maxIncome": 190510,
|
||||
"protectionFactor": 3.2999947509317096,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1818586,
|
||||
"BnkPr": -900200,
|
||||
"maxIncome": 272788,
|
||||
"protectionFactor": 3.2999985336598385,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"zuul-turn23-fleet23.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1266033,
|
||||
"BnkPr": -626686,
|
||||
"maxIncome": 189905,
|
||||
"protectionFactor": 3.299997367104605,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1813746,
|
||||
"BnkPr": -897804,
|
||||
"maxIncome": 272062,
|
||||
"protectionFactor": 3.299997794620344,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
],
|
||||
"zuul-turn5-species5.sav": [
|
||||
{
|
||||
"playerID": 16,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1277766,
|
||||
"BnkPr": -632494,
|
||||
"maxIncome": 191665,
|
||||
"protectionFactor": 3.2999973912816634,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 32,
|
||||
"ownedSystems": 2,
|
||||
"BnkEl": -1725413,
|
||||
"BnkPr": -854079,
|
||||
"maxIncome": 258812,
|
||||
"protectionFactor": 3.299997681714913,
|
||||
"naiveDivisorDisagrees": false
|
||||
},
|
||||
{
|
||||
"playerID": 496,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 512,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 528,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 544,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
},
|
||||
{
|
||||
"playerID": 560,
|
||||
"ownedSystems": 0,
|
||||
"BnkEl": 0,
|
||||
"BnkPr": 0,
|
||||
"maxIncome": null,
|
||||
"why": "limit is zero (no owned systems)"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
# standalone vs the oracle
|
||||
|
||||
generated 2026-09-08T14:30:05Z binary /home/alex/sots-engine-wt-standalone/build-host/src/app/sots_turn
|
||||
generated 2026-09-08T15:35:00Z binary /home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn
|
||||
|
||||
phases: 14/44 of the two turn drivers modelled, 7 committed (implemented 2, partial 5, blocked 7, stub 30)
|
||||
2/37 of the post-combat tail modelled
|
||||
phases: 14/44 of the two turn drivers modelled, 8 committed (implemented 2, partial 6, blocked 6, stub 30)
|
||||
3/37 of the post-combat tail modelled
|
||||
|
||||
## turn1-state.sav -> turn2-state.sav (real End Turn)
|
||||
baseline (do nothing) 209 leaves diverge
|
||||
|
|
|
|||
51
verify/results/standalone/rng-oracle.json
Normal file
51
verify/results/standalone/rng-oracle.json
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
{
|
||||
"binary": "/home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn",
|
||||
"pairs": [
|
||||
{
|
||||
"input": "z2-endturn.sav",
|
||||
"oracle": "z2-autosave.sav",
|
||||
"note": "ref-turn2, turn 4 -> 5",
|
||||
"oracleWords": 20,
|
||||
"exit": 0,
|
||||
"oracleWordsFromFiles": 20,
|
||||
"oracleTwists": 0,
|
||||
"ourWords": 16,
|
||||
"ourTwists": 0,
|
||||
"blockIdentical": true,
|
||||
"leftOurs": 379,
|
||||
"leftOracle": 375,
|
||||
"stateMatches": false,
|
||||
"residual": 4,
|
||||
"standaloneRngWords": 16,
|
||||
"standaloneRngCommitted": true,
|
||||
"unaccounted": [
|
||||
"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",
|
||||
"the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
"a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined"
|
||||
]
|
||||
},
|
||||
{
|
||||
"input": "z-t6-endturn.sav",
|
||||
"oracle": "z-t6-autosave.sav",
|
||||
"note": "ref-turn2, turn 5 -> 6",
|
||||
"oracleWords": 18,
|
||||
"exit": 0,
|
||||
"oracleWordsFromFiles": 18,
|
||||
"oracleTwists": 0,
|
||||
"ourWords": 16,
|
||||
"ourTwists": 0,
|
||||
"blockIdentical": true,
|
||||
"leftOurs": 359,
|
||||
"leftOracle": 357,
|
||||
"stateMatches": false,
|
||||
"residual": 2,
|
||||
"standaloneRngWords": 16,
|
||||
"standaloneRngCommitted": true,
|
||||
"unaccounted": [
|
||||
"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",
|
||||
"the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
"a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
23
verify/results/standalone/rng-oracle.txt
Normal file
23
verify/results/standalone/rng-oracle.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# does the standalone's generator land on the oracle's?
|
||||
|
||||
## z2-endturn.sav -> z2-autosave.sav (ref-turn2, turn 4 -> 5)
|
||||
oracle spent 20 word(s) (0 twist(s), from the files)
|
||||
standalone 16 word(s) (metric says 16)
|
||||
residual 4 word(s) NOT modelled
|
||||
state block identical: yes left: ours 379, oracle 375
|
||||
GENERATOR STATE MATCHES ORACLE: no
|
||||
- unaccounted: 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
|
||||
- unaccounted: the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)
|
||||
- unaccounted: a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined
|
||||
|
||||
## z-t6-endturn.sav -> z-t6-autosave.sav (ref-turn2, turn 5 -> 6)
|
||||
oracle spent 18 word(s) (0 twist(s), from the files)
|
||||
standalone 16 word(s) (metric says 16)
|
||||
residual 2 word(s) NOT modelled
|
||||
state block identical: yes left: ours 359, oracle 357
|
||||
GENERATOR STATE MATCHES ORACLE: no
|
||||
- unaccounted: 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
|
||||
- unaccounted: the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)
|
||||
- unaccounted: a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined
|
||||
|
||||
verdict: 0 of 2 calibrated pair(s) match the oracle's generator state exactly
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": "sots-standalone-status/1",
|
||||
"generated": "2026-09-08T14:30:05Z",
|
||||
"binary": "/home/alex/sots-engine-wt-standalone/build-host/src/app/sots_turn",
|
||||
"generated": "2026-09-08T15:35:00Z",
|
||||
"binary": "/home/alex/sots-engine-wt-yield/build-host/src/app/sots_turn",
|
||||
"reference": {
|
||||
"input": "turn1-state.sav",
|
||||
"oracle": "turn2-state.sav",
|
||||
|
|
@ -31,24 +31,24 @@
|
|||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 2,
|
||||
"partial": 5,
|
||||
"blocked": 7,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"stub": 30,
|
||||
"modelled": 14,
|
||||
"committed": 7
|
||||
"committed": 8
|
||||
},
|
||||
"tailPhases": {
|
||||
"total": 37,
|
||||
"verified": 0,
|
||||
"implemented": 1,
|
||||
"partial": 0,
|
||||
"blocked": 1,
|
||||
"stub": 35,
|
||||
"modelled": 2,
|
||||
"blocked": 2,
|
||||
"stub": 34,
|
||||
"modelled": 3,
|
||||
"committed": 1
|
||||
},
|
||||
"rng": {
|
||||
"wordsModelled": 0,
|
||||
"wordsModelled": 16,
|
||||
"wordsPerTurnUnattributed": "18-20 (lane Z, in flight)"
|
||||
},
|
||||
"pairs": [
|
||||
|
|
@ -64,18 +64,24 @@
|
|||
"roundtrip (untouched): byte-identical (591376 bytes)",
|
||||
"",
|
||||
"phases",
|
||||
" turn drivers (the milestone's denominator): 14 of 44 modelled, 7 committed",
|
||||
" verified 0 implemented 2 partial 5 blocked 7 stub 30",
|
||||
" post-combat tail (written to the autosave, tracked separately): 2 of 37 modelled",
|
||||
" verified 0 implemented 1 partial 0 blocked 1 stub 35",
|
||||
" turn drivers (the milestone's denominator): 14 of 44 modelled, 8 committed",
|
||||
" verified 0 implemented 2 partial 6 blocked 6 stub 30",
|
||||
" 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 16",
|
||||
" generator words consumed 0 (state loaded)",
|
||||
" leaves NOT written by a blocked phase 64",
|
||||
" 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",
|
||||
" - the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
" - a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined",
|
||||
" ! 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/tmpatg5108q/post-turn1-state.sav (65145 bytes gzipped, 591376 inflated)",
|
||||
"metric -> /tmp/tmpatg5108q/metric-turn1-state.sav.json"
|
||||
"wrote /tmp/tmp_cz5dsbf/post-turn1-state.sav (65145 bytes gzipped, 591376 inflated)",
|
||||
"metric -> /tmp/tmp_cz5dsbf/metric-turn1-state.sav.json"
|
||||
],
|
||||
"coverage": {
|
||||
"input": true,
|
||||
|
|
@ -145,32 +151,38 @@
|
|||
"standalone": {
|
||||
"schema": "sots-standalone-metric/1",
|
||||
"input": "/home/alex/sots-re/verify/results/saves/turn1-state.sav",
|
||||
"output": "/tmp/tmpatg5108q/post-turn1-state.sav",
|
||||
"output": "/tmp/tmp_cz5dsbf/post-turn1-state.sav",
|
||||
"spine": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 2,
|
||||
"partial": 5,
|
||||
"blocked": 7,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"stub": 30,
|
||||
"modelled": 14,
|
||||
"committed": 7
|
||||
"committed": 8
|
||||
},
|
||||
"tail": {
|
||||
"total": 37,
|
||||
"verified": 0,
|
||||
"implemented": 1,
|
||||
"partial": 0,
|
||||
"blocked": 1,
|
||||
"stub": 35,
|
||||
"modelled": 2,
|
||||
"blocked": 2,
|
||||
"stub": 34,
|
||||
"modelled": 3,
|
||||
"committed": 1
|
||||
},
|
||||
"run": {
|
||||
"leafWrites": 7,
|
||||
"blockedLeafWrites": 16,
|
||||
"rngWords": 0,
|
||||
"rngLoaded": true
|
||||
"blockedLeafWrites": 64,
|
||||
"rngWords": 16,
|
||||
"rngLoaded": true,
|
||||
"rngCommitted": false,
|
||||
"rngUnaccounted": [
|
||||
"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",
|
||||
"the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
"a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined"
|
||||
]
|
||||
},
|
||||
"phases": [
|
||||
{
|
||||
|
|
@ -617,11 +629,11 @@
|
|||
"driver": "StrategyServer::ProcessTurn",
|
||||
"id": "S31",
|
||||
"name": "EncounterDetectionAndStatusRestore",
|
||||
"status": "blocked",
|
||||
"status": "partial",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 2,
|
||||
"rng": 0
|
||||
"rng": 16
|
||||
},
|
||||
{
|
||||
"driver": "StrategyServer::OnAllCombatDone_Tail",
|
||||
|
|
@ -987,10 +999,10 @@
|
|||
"driver": "StrategyServer::OnAllCombatDone_Tail",
|
||||
"id": "T36",
|
||||
"name": "FinalizeTurnRecords",
|
||||
"status": "stub",
|
||||
"ran": 0,
|
||||
"status": "blocked",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
"blockedWrites": 48,
|
||||
"rng": 0
|
||||
},
|
||||
{
|
||||
|
|
@ -1018,18 +1030,24 @@
|
|||
"roundtrip (untouched): byte-identical (603360 bytes)",
|
||||
"",
|
||||
"phases",
|
||||
" turn drivers (the milestone's denominator): 14 of 44 modelled, 7 committed",
|
||||
" verified 0 implemented 2 partial 5 blocked 7 stub 30",
|
||||
" post-combat tail (written to the autosave, tracked separately): 2 of 37 modelled",
|
||||
" verified 0 implemented 1 partial 0 blocked 1 stub 35",
|
||||
" turn drivers (the milestone's denominator): 14 of 44 modelled, 8 committed",
|
||||
" verified 0 implemented 2 partial 6 blocked 6 stub 30",
|
||||
" 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 10",
|
||||
" generator words consumed 0 (state loaded)",
|
||||
" leaves NOT written by a blocked phase 58",
|
||||
" 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",
|
||||
" - the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
" - a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined",
|
||||
" ! 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/tmpatg5108q/post-turn2-state.sav (66883 bytes gzipped, 603360 inflated)",
|
||||
"metric -> /tmp/tmpatg5108q/metric-turn2-state.sav.json"
|
||||
"wrote /tmp/tmp_cz5dsbf/post-turn2-state.sav (66883 bytes gzipped, 603360 inflated)",
|
||||
"metric -> /tmp/tmp_cz5dsbf/metric-turn2-state.sav.json"
|
||||
],
|
||||
"coverage": {
|
||||
"input": true,
|
||||
|
|
@ -1099,32 +1117,38 @@
|
|||
"standalone": {
|
||||
"schema": "sots-standalone-metric/1",
|
||||
"input": "/home/alex/sots-re/verify/results/saves/turn2-state.sav",
|
||||
"output": "/tmp/tmpatg5108q/post-turn2-state.sav",
|
||||
"output": "/tmp/tmp_cz5dsbf/post-turn2-state.sav",
|
||||
"spine": {
|
||||
"total": 44,
|
||||
"verified": 0,
|
||||
"implemented": 2,
|
||||
"partial": 5,
|
||||
"blocked": 7,
|
||||
"partial": 6,
|
||||
"blocked": 6,
|
||||
"stub": 30,
|
||||
"modelled": 14,
|
||||
"committed": 7
|
||||
"committed": 8
|
||||
},
|
||||
"tail": {
|
||||
"total": 37,
|
||||
"verified": 0,
|
||||
"implemented": 1,
|
||||
"partial": 0,
|
||||
"blocked": 1,
|
||||
"stub": 35,
|
||||
"modelled": 2,
|
||||
"blocked": 2,
|
||||
"stub": 34,
|
||||
"modelled": 3,
|
||||
"committed": 1
|
||||
},
|
||||
"run": {
|
||||
"leafWrites": 7,
|
||||
"blockedLeafWrites": 10,
|
||||
"rngWords": 0,
|
||||
"rngLoaded": true
|
||||
"blockedLeafWrites": 58,
|
||||
"rngWords": 16,
|
||||
"rngLoaded": true,
|
||||
"rngCommitted": false,
|
||||
"rngUnaccounted": [
|
||||
"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",
|
||||
"the research-allocation draw is downstream of the budget, which is blocked on the per-system money output (0 or 1 word)",
|
||||
"a successful raid roll may draw one further word to pick its target; no roll succeeded on any measured turn, so the cost of a success is 0 or 1 and undetermined"
|
||||
]
|
||||
},
|
||||
"phases": [
|
||||
{
|
||||
|
|
@ -1571,11 +1595,11 @@
|
|||
"driver": "StrategyServer::ProcessTurn",
|
||||
"id": "S31",
|
||||
"name": "EncounterDetectionAndStatusRestore",
|
||||
"status": "blocked",
|
||||
"status": "partial",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 2,
|
||||
"rng": 0
|
||||
"rng": 16
|
||||
},
|
||||
{
|
||||
"driver": "StrategyServer::OnAllCombatDone_Tail",
|
||||
|
|
@ -1941,10 +1965,10 @@
|
|||
"driver": "StrategyServer::OnAllCombatDone_Tail",
|
||||
"id": "T36",
|
||||
"name": "FinalizeTurnRecords",
|
||||
"status": "stub",
|
||||
"ran": 0,
|
||||
"status": "blocked",
|
||||
"ran": 8,
|
||||
"writes": 0,
|
||||
"blockedWrites": 0,
|
||||
"blockedWrites": 48,
|
||||
"rng": 0
|
||||
},
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue