# The balance ledger

The game's title screen says its balance was tuned on 7,313,561 simulated battles.
This file is where that number is accounted for, because a claim nobody can check is
just a number in a nice font.

Every one of those battles has a log in `sim-runs/` and a seed in `combat_sim.py`. You can
re-run any campaign in this table and get the numbers back.

## What is published here

- `combat_sim.py` - the simulator itself. It reimplements the game's combat maths in
  Python so a design question can be answered in minutes instead of playtests. Run a lab
  with `python tools/combat_sim.py lab9 - 400`.
- `sim-runs/*.log` - the raw stdout of the campaigns listed below, gates and all,
  including the runs that FAILED. The failures are the point; they are what changed the
  design.

## The campaigns behind the number

Each lab freezes its gates in the docstring **before** the first run. A gate that fails
sends the design back, not the gate.

| campaign | battles | what it asked | verdict | evidence |
|---|---:|---|---|---|
| lab9 round 1 | 256,000 | do the 18 shipped growth-tree nodes hold up, node by node | 3 gates FAILED | `sim-runs/lab9_run.log` |
| lab9 round 2 | 256,000 | after halving two keystones | 3 gates FAILED | `sim-runs/lab9_run2.log` |
| lab9 round 3 | 256,000 | after redesigning the observability fork | 2 gates FAILED | `sim-runs/lab9_run3.log` |
| lab9 round 4 | 256,000 | after the breaker stopped erasing death | **all 7 PASS** (verdict is sample-size-dependent - see OPEN 2026-08-31) | `sim-runs/lab9_run4.log` |
| lab10 round 1 | 102,400 | is the incident-response kit worth carrying | K1 FAILED | `sim-runs/lab10_run.log` |
| lab10 round 2 | 102,400 | after the kit's real lifetimes and numbers | K1 fails by design, K2-K5 pass | `sim-runs/lab10_run2.log` |
| lab11 baseline | 480,000 | what does a crowded room do to the rooms players walk into | D1 and D3 FAILED | `sim-runs/lab11-lab12-baseline-2026-08-23.log` |
| lab12 baseline | 519,942 | six fights with no rest: what do packs COST | E2 FAILED | same log |
| lab11 shipped | 480,000 | the same question after the two remedies | D1 fails, D3 borderline | `sim-runs/lab11-lab12-shipped-2026-08-23.log` |
| lab12 shipped | 520,719 | the walk after the two remedies | E1 fails, E2-E5 pass | same log |
| lab11 with AoE | 480,000 | does the room-hitting INDEX STORM undo the packs | no: +0.2pp KO, unchanged | `sim-runs/lab11-lab12-with-aoe-2026-08-23.log` |
| lab12 with AoE | 520,735 | the walk, with the AoE the game actually ships | E1 fails, E2-E5 pass | same log |
| lab11 instrument fixed | 480,000 | the same questions, on an instrument that can see a pack | D1, D3 fail | `sim-runs/lab11-lab12-instrument-fixed-2026-08-23.log` |
| lab12 instrument fixed | 514,714 | the walk, measured properly for the first time | E1 PASSES; E2, E4 fail badly | same log |
| lab11 corrected | 480,000 | after the party-outnumbers rule and the right rosters | D1, D3 fail (both known) | `sim-runs/lab11-lab12-corrected-2026-08-23.log` |
| lab12 corrected | 520,947 | the walk as the game actually ships it | **ALL GATES PASS** | same log |
| lab11 zoned | 480,000 | on a TESTED instrument, with honest walk fixtures | D1, D3 fail (both known) | `sim-runs/lab11-lab12-zoned-2026-08-23.log` |
| lab12 zoned | 607,704 | the walk, once the party stops being given what it has not earned | **ALL GATES PASS** | same log |

**Published total: 7,313,561 battles**, every one of them reproducible from the script
and the seeds in it. That is the whole number the game claims.

**Only the last two rows describe the shipped game**, and they are the first rows in this
table produced by an instrument that has tests of its own. Everything above the "instrument
fixed" pair was measured by a simulator that could not see the thing it was measuring - see
the retraction below.

## The time this file did not add up

On 2026-08-23 the bolded total above, and the headline on the game's title screen, both
read 7,834,585 while the campaign table summed to 7,313,561 - out by 521,024. A
mis-addition entered when the instrument-fixed rows landed and was carried forward through
two more updates, because every one of those updates edited the total instead of
recomputing it.

The number is corrected. More usefully, `site_check.py` now ADDS THE COLUMN itself and
fails if the table, this file's stated total, and every figure quoted inside the game do
not agree. The one check nobody had run was a reader with a calculator, and that reader is
now part of the build.

## RESOLVED 2026-08-31: two more fidelity defects in the instrument, found 2026-08-24

An adversarial code review of the tooling found two ways `combat_sim.py` models a game that
is not the one `game.html` ships. Both are disclosed here before they are fixed, because
fixing them changes the numbers in the table above and that requires a fresh pre-registered
re-run, not a quiet edit.

**1. A character can act twice in one round.** The simulator resets `acted` for everyone at
the top of each round and sets it on the PARTNER of a dual tech - but never on the ally who
led it. So a fast ally takes its own turn, and a slower ally's dual tech can then pick that
same ally up as a partner and swing it again. The game forbids this explicitly: it marks the
acting ally `acted` as its turn resolves, and ships a test asserting either partner may lead
only while both are unacted. The effect is a systematic OVERESTIMATE of party output in
every campaign that uses dual techs.

**2. Rounding goes the other way on the commonest hit in the game.** The damage roll ends in
`round(base*sm)` with `sm` of 1.5 about 80% of the time. Python's `round` is banker's
rounding - `round(4.5)` is 4 - while the game uses `Math.round`, where 4.5 is 5. Any odd
attack value diverges on the mainline damage path.

Neither is a rounding error in the colloquial sense: they are the same defect class as the
pack bug retracted below - the instrument quietly not being the thing it claims to model.
They were found by pointing a reviewer at the tooling and asking what could not fail, which
is now the third time that has been worth more than reading the code again.

**Until the re-run happens, every pack-era number in the table above should be read as
carrying an unquantified bias in the party's favour.** The fix for (1) is one line; the cost
is re-running the campaigns and re-registering the gates, which is a session of its own.

**Resolution (2026-08-31):** both fixes landed under the pre-registration below - the acted
flag is now set as the leader's turn starts, and js_round mirrors Math.round on all three
damage-path rounds - each seen RED in tools/test_combat_sim.py before the fix existed. The
re-run followed the same day; results in "The re-run happened" below.

## OPEN: lab9's verdict depends on the sample size, found 2026-08-31

Re-running the ledger's own documented command at different N flips the lab9 verdict on
Monte-Carlo noise alone: at N=400 (256,000 battles) gate G6 "drama survives" FAILS; at N=800
(512,000) G6 passes but G7 "forks are real" FAILS; at N=1500 (960,000) both pass. The cause is
real but small - the two variants differ by about 0.02 turns on the optimal-play axis, so a
strict-dominance gate sits inside the noise band at small N. The row above keeps its recorded
verdict because that is what the logged run returned, and it now carries the annotation. The
fix belongs to the same pre-registered re-run as the two defects above: pin each gate to a
declared N or a confidence interval, so a verdict names its own resolution.

**Update, same day, on the FIXED sim (R3 below):** at N=1500 (960,000 battles) G6 "drama
survives" now FAILS - one cell, stress_mis x V-auto, KO 73.2% vs control 60.7%: +12.5pp
against the 12pp ceiling. The exceedance is 0.5pp with ~0.85pp of Monte-Carlo noise on that
cell (~0.6 sd), so the verdict still sits inside the noise band - one N higher than before.
G6 has no rung on lab9's remedy ladder, so no node changes; the open remedy is unchanged
(pin the gate to a declared N or a confidence interval) and needs its own pre-registration.

## PRE-REGISTERED 2026-08-31: the fidelity fixes, and the re-run that judges them

Written BEFORE the fixes were applied and before any battle of the re-run was rolled.

**The fixes.** (1) `battle()` marks the acting ally `acted` at the start of its turn - one
line; `partner_of` already refuses acted allies, which closes the double-swing. (2) A
`js_round(x) = floor(x + 0.5)` mirrors `Math.round` on the three damage-path rounds
(mainline `round(base*sm)`, the 2.25 crit path, v3b) - Python's banker's rounding diverged
on odd attack values. (3) A fidelity catch-up, because the shipped game grew a GEAR LADDER
on 2026-08-31: `LADDER_ATK` / `LADDER_ATK_HERO` knobs add flat atk in `mk_actor`, modeling
the Dragon-tier ceiling (companions +6 over the crafted model, hero +2). A flat-uplift
WORST CASE, not per-item modeling - read it as a bound.

**The re-run, on the fixed sim, seeds unchanged (wave-2 base 90290827):**
- R1 campaign validation: wave 2, RUNS_PER sized to the hardware, target on the order of
  10 million battles total across this re-run. Band: a row whose win moves more than 2.0
  points or KO more than 3.0 points against the published wave-2 row is FLAGGED for a
  separate, logged re-tune decision - never silently adjusted.
- R2: lab12, the five-gate shipped-model suite, defaults.
- R3: lab9 at N=1500 - resolving the OPEN item above by pinning the gates at the N where
  they resolve.
- R4: the ladder worst case - wave 2 with {"LADDER_ATK":6,"LADDER_ATK_HERO":2},
  measurement only. Expected: walkovers get more walkover; the question that matters is
  whether unknown5's last drama survives a full-Dragon party.

Totals and verdicts land in rows below when the run completes; the OPEN sections above
resolve only then.

## The re-run happened: results, same day (2026-08-31)

| run | battles | verdict | log |
|---|---|---|---|
| R1 wave-2 validation | 6,104,000 | all 4 iterations VALIDATION PASS | `sim-runs/prereg20260831_R1_wave2.log` |
| R2 (the lab12 five-gate suite) | 134,299 | ALL GATES PASS | `sim-runs/prereg20260831_R2_lab12.log` |
| R3 (the lab9 suite, N=1500) | 960,000 | G6 FAIL by 0.5pp vs ~0.85pp noise - recorded above, no re-tune | `sim-runs/prereg20260831_R3_lab9.log` |
| R4 ladder worst case | 2,616,000 | measurement below | `sim-runs/prereg20260831_R4_ladder.log` |

**Validation battles this re-run: 9,814,299.** They are accounted here, separately, and are
NOT added to the tuned-on total above: zero knobs and zero enemy stats changed, so the
corpus the balance was TUNED on is exactly what it was. A validation pass that quietly
inflated the headline would be the opposite of what this file is for.

**R1 becomes the first logged wave-2 per-row baseline** (the original wave-2 rows lived in a
private journal; the band check in the pre-registration could not execute against them, a
gap this row closes). Sub-100% rows: unknown5 only - OPT 99.9%, NOISY 99.6%. Top KO rows
(OPT): unknown5 41.8%, unknown6 16.2%, monolith5 3.4%. Full table in the R1 log; seed base
90290827, knobs frozen. The next re-run's band check (win +/-2.0pp, KO +/-3.0pp) runs
against THESE rows.

## The ladder economy, measured (2026-08-31, adversarial-QA arithmetic)

The gear ladder's throttle is now a measured fact, not a vibe. Reproduced by summation of
the shipped tables (every number re-derivable from game.html):
- Non-grind campaign income: **1,861 tokens** (scripted fights 1,565 + loot and chests 276 +
  starting 20), plus an **AUDIT ceiling of 786** (max(3, round(tok/2)) once per battle).
- Full ladder catalogue (every rung, all 9 lines): **5,310 tokens** - 2.9x non-grind income
  by design; buying every rung is a completionist flex, not the intended path.
- Skip-buy to Dragon on all 9 lines: **1,800 tokens** - inside income plus a modest AUDIT
  habit, which is the intended endgame shape.
- Repeatable postgame faucets: the grand chain (~380 first run, up to ~190 per rerun) and
  the Unknown refight (~150 per loop) - grinding exists but nothing requires it.
- Campaign XP ceiling: **1,191 XP (+48 bridge questions) = LV17** on a thorough run. The
  gate taper of 2026-08-31 (Adamant 14, Rune 15, Dragon 16) follows from this number: the
  original 16/20/24 gates put the top two rungs past the campaign entirely, the opposite
  of the ladder's front-loaded intent. Dragon at LV16 (floor 1,112 XP) is reachable by a
  thorough player with margin.
- One coherence note: the transmuted Frontier Aegis was raised def 3 -> 4 the same day so
  the story's sigil-forged capstone TIES Dragon Plate instead of losing to it - it costs
  sigils and a story, never tokens, and Peg's fanfare now says exactly that.

**R4, the question that mattered - does the gear ladder break the game?** Under the flat
worst case (every companion at Dragon, +6 atk; hero +2), across 2,616,000 battles: every
walkover stays a walkover and nothing new drops below 2 turns; the hacker chain collapses
the most (NOISY -1.0t); and the drama holds - unknown5 saturates to 100.0% win but keeps
36.8% KO at optimal play and 46.8% under noise, unknown6 keeps 16.8%/26.1%. The drama
survives; the loss risk does not. Zero re-tunes demanded, and the Stage 2 decision this
run gates - companion armor - inherits these numbers as its baseline.

## Why the headline number went DOWN

It used to say 25,000,000+. That figure was true and it was mostly uncheckable: about
21.8M of it came from earlier campaigns - encounter tuning, the boss table, limit breaks,
dual techs, the companion roster, the crafting economy - that ran in sessions whose logs
live in a private engineering journal rather than in this repo.

A reader could not verify them, and this site's whole argument is that a system should
prove it worked. A big number I am asking you to take on faith undercuts that argument
more than a smaller one strengthens it, so the headline now counts only what ships with a
log and a seed.

The earlier work still happened and the design still rests on it. It is simply no longer
being counted in a number presented as evidence.

From here the number only goes up, and it goes up one archived campaign at a time.

## Why the failures are in the table

Most of the campaigns in that table failed at least one of their own gates, and the
failures are the reason the design is what it is. From the growth-tree rounds:

- A free death-save was taking the KO rate on an underleveled party from 17.9% to 2.0%. It
  now catches only a blow that fells a healthy ally, never a slow bleed.
- One side of a three-cost fork beat the other on all ten rows. A fork with a strictly
  better side is not a choice, so the losing side was redesigned, not buffed.
- One node ran **slower** than owning no tree at all and dropped fights the control won.

Two gates were changed after seeing results, and both changes are disclosed in
`combat_sim.py` where they live: one was **tightened**, and one was **re-scoped** away from
fights under three turns, where saving a single round is 0.84 of the control by arithmetic
alone and the gate was measuring rounding rather than power creep.

## RETRACTION: every pack number above the last two rows was measured blind

This is the biggest thing this ledger has had to say, so it goes near the top of the
failures rather than at the end.

**The simulator gave a whole pack one attack per round.** When the simulator learned about
packs, a per-body loop was added at the end of the round - and put behind `if not
foe_done`. There was already an interleave path that fires one foe action and sets
`foe_done` as soon as an ally slower than the foe is reached. REFUTER is speed 3 and WIKI
is 4, so that path fires in very nearly every battle, and the per-body loop was dead code
from the day it was written. A four-pack hit exactly as hard as one foe.

It is the mirror image of a defect that was found and fixed in the game itself the same
day - `nextUnit` discarding the foe index so one body swung four times. The fix went into
the game, and then the same mistake was written into the instrument that was supposed to
check the game.

**Two walks were also measured with parties that cannot walk them.** `tileAt` gates the
1999 rift on `S.won` and the internet gateway on `S.haxKnown`, and `ending()` sets `S.won`
alongside `S.stage=4`. So those zones are only ever walked after the finale, by a
post-victory party in full kit - and they were being simulated at level 7-8 with partial
gear and stage tags of 2 and 3.

Both errors pushed the same way: they made packs look weaker than they are.

What the corrected instrument says, on the same six walks:

| | measured blind | measured properly |
|---|---:|---:|
| resources left, one foe per room | 50.1% | 60.3% |
| resources left, packs as shipped | 41.4% | 44.3% |
| the drop (gate E1, needs >= 10) | 8.7pp - FAIL | **16.0pp - PASS** |
| walks ending under half | 78.9% | 74.9% |
| worst single walk's wipe rate | 6.8% | 5.6% |

**The gate that had been failing all along was passing all along.** Every previous entry in
this file that quotes an 8.7-point drop, or says E1 could not be satisfied without pushing
some walk over the wipe ceiling, was describing an instrument rather than a game.

There is a design consequence worth stating plainly. That failing gate was the entire
reason for a planned enemy-scaling system - five independent designs were drafted and
adversarially reviewed to close a 1.3-point gap that did not exist. **No enemy's numbers
were changed, and none needed to be.** The adversarial pass on those designs is what found
the bug; not one of the designs survived its own review, and that turned out to be the most
valuable thing the exercise produced.

One real change did come out of it. With a pack finally able to swing, the opening walk -
a HERO and one Ranger - wiped about one run in five, and the rule that had allowed an even
fight became "the party always outnumbers the room". It binds only at party sizes two to
four and costs the late game nothing.

## The simulator has tests now

`tools/test_combat_sim.py`, run by `site_check.py` as part of the gate. Twenty-three checks
in three classes, and each class earned its place by finding something on its first run.

**Fidelity** parses `game.html` and compares - rosters, enemy attacks, the pack table, the
solo list. Two implementations of the same combat maths drift, and this found three enemies
the simulator modelled under names the game does not use.

**Sensitivity** asks the only question that matters of a measuring device: if the thing I
am measuring changed, would this number move? It took two attempts. The obvious version -
a four-pack costs more per BATTLE - passes WITH the pack bug in place, because four foes
carry four times the health so the fight runs four times as long. It measured the pack's
health bar rather than its damage. The real check measures damage per ROUND, and it was
verified the only way that counts: the bug was put back and the suite was watched failing
on it, reading 1.38 per round at one body and 1.38 at four.

**Fixtures** check that a measured party is one the game can actually produce. This found
the internet walk being handed SHARONDUH, who joins at the END of that arc; missing LITO,
who joins in the statement that opens the GATEWAY; and carrying two transmutes gated on a
flag it has not set. All three made the arc look easier than it is.

With honest fixtures the Firewall Bastion wiped 11.0% against a 10% ceiling - the only walk
that failed, invisible until the party stopped being given things it had not earned. The
remedy was pack odds, as the ladder requires, applied to the zone that failed rather than
to all four that share its stage. A test asserts every other zone draws exactly the
distribution it drew before, so the change is provably local.

## Two gates that still fail, and why they were not quietly moved

**lab11 D1** asked for the KO rate on wandering fights to rise by 3 points once rooms
could hold up to four. It rose by 0.2. That answer is true and the question was wrong:
a trash fight is not meant to threaten a wipe, and lab11 handed the party a full bar
before every single battle, so it could not see a cost even in principle. lab12 exists
because of that failure.

**lab11 D3** asks that no wandering row stay above 85% wins at a forced four-pack. It fails
on `w_gremlin`, which wins 1% - and that row is a two-body party facing four foes, which
the party-outnumbers rule makes structurally impossible. The gate measures a configuration
the game cannot produce. Earlier it merely wobbled across the threshold on the seed;
now it fails unmistakably, for a reason that is about the gate rather than the game.

Re-scoping D3 to the rows where four foes can actually occur is the obvious repair, and it
is a gate change made after seeing results, so it belongs to a fresh pre-registration.

**lab12 E1 used to be on this list and no longer is.** It asked for a 10-point drop and the
game delivers 16.0. It was recorded here as an honest failure for as long as the instrument
was wrong, which is exactly how it should have looked from the inside.

No number was ever moved to make a gate green. That is the whole point of writing them
down first - and it is what made the retraction above possible to write, because the
failing number was still sitting there when the instrument was fixed.

## Two things the last campaign was run specifically to find out

**Did the published numbers describe a game nobody plays?** Room-hitting INDEX STORM
shipped after the campaign above had already run, and handing the party a room-wide attack
right after making rooms crowded is exactly the kind of change that quietly invalidates a
measurement. So the simulator learned the AoE and the whole campaign ran again.

It changed almost nothing: attrition across the walk went from -8.7 to -8.6 points, wipes
stayed at 2.5%, walks ending under half moved 78.9% to 78.3%. The reason is visible in the
turn counts - wandering fights end in two to three turns, and the storm needs the foe
revealed, unfogged and five MP, so it rarely gets cast before the room is already clear.
The suspicion was worth a million battles; the answer was that the earlier numbers stood.

**D3 is a gate that cannot give a stable answer, on a row that cannot happen.** It asks
that no wandering row fall below 85% wins at a forced four-pack. `w_vague` lands on 85%
one run and 84% the next - the verdict is decided by the seed, not by the design. And a
four-pack of `w_vague` is unreachable in play: stage 0 tops out at two foes, and a
two-body party is capped at two regardless.

Both facts are recorded rather than fixed. Re-scoping D3 to the rows where four foes can
actually occur is the obvious repair, and it is a gate change made after seeing results,
so it belongs to a fresh pre-registration rather than to a quiet edit in the run that
found it.
