Skip to content

RL agent ladders for cold-start skill prediction: sprint, minesweeper, and what transferred - #1

Merged
mwburke merged 68 commits into
mainfrom
claude/game-rl-agents-skill-e87e58
Aug 10, 2026
Merged

RL agent ladders for cold-start skill prediction: sprint, minesweeper, and what transferred#1
mwburke merged 68 commits into
mainfrom
claude/game-rl-agents-skill-e87e58

Conversation

@mwburke

@mwburke mwburke commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

68 commits building the agent track end to end across two new environments, plus the experiments that tested whether any of it predicts human skill.

What was built

Two validated environments (simulacrum workflow: spec → reference → batched → differential battery)

  • tetris_sprint — input-level 40 LINES on a virtual clock, 12/12 battery PASS
  • minesweeper — expert/intermediate/beginner, 10/10 battery PASS, 13 invariants

Agent generators

  • Sprint: scripted quad-playing teacher on a strategy dial, keystroke distillation, placement distillation, DQN
  • Minesweeper: DQN v1/v2, reverse curriculum, supervised mine-probability net, deduction oracle

Analysis: E6 transfer test, label-efficiency curves, accuracy cliff, 192-rung three-dial grid

Headline findings

E6 (sprint) — the simulator lost to one number, then won it back. Ranking players by pps alone scores ρ=0.932 against the agent-fitted model's 0.660. But that baseline is not cold-start legitimate: knowing which axis to rank by requires labelled human data. Priced honestly, a model fitted only on agents is never beaten by one fitted on humans at any sample size tested — worth more than 300 labelled games.

Signal density, not reward shape, is the RL bottleneck. Same net, same env: 1 sparse scalar per action gave 0 safe cells after 2M steps; 480 dense labels per state gave 81 after 16k updates.

The accuracy cliff. Nothing finishes a sprint below 95% per-placement imitation accuracy, so imitation accuracy cannot serve as a skill dial — it is a step function, not a gradient.

Minesweeper's ladder decorrelates where sprint's did not. Across 192 oracle-judged rungs, speed × judgement r = −0.03 (sprint: +0.85). Win rate rides the judgement axis (+0.64), not speed (+0.03).

Corrections made mid-flight

Three claims I published and then had to retract: column 9 was never unreachable, the env validation gate was never actually green (re-run: PASS), and every distilled number before the sampling fix was measuring an argmax deadlock. All documented in EXPERIMENTS.md rather than quietly fixed.

Not included, deliberately

Checkpoint weights (regenerable) and data/human/ (belongs to its publishers; personal data even pseudonymised).

🤖 Generated with Claude Code

mwburke and others added 30 commits August 7, 2026 01:09
Generalises the Connect Four pipeline to all four games so the comparison the
project turns on - what property of a game decides how early skill is legible -
can be run with the method held fixed.

New: games.py extended with restart/ply-capping/reward normalisation, nets.py
(conv for board games, MLP for vector games), selfplay.py, ladder.py, profile.py,
predict.py, tactics.py, and a features/ package with a distinct feature set per
game. Connect Four is deliberately built twice; the pgx port is differential-
tested against the PettingZoo-validated original feature by feature.

Three bugs the measurement caught rather than the code:

- One-step targets do not work on long episodes. The first backgammon ladder
  spanned 32 Elo with the untrained network ranked third - everything trained,
  nothing learned, no error anywhere. Targets are now n-step, set per game
  against episode length; backgammon went from 0.57 to 0.85 against random and
  its ladder from 32 to 184 Elo.
- Counting hands won would have ranked a fold-everything Leduc policy first.
  Pairwise results now fit the mean normalised result, identical to
  wins + draws/2 for the games that are simply won or lost.
- Leduc's pot tops out at 13, not the 11 a random rollout samples, so the
  reward scale was wrong for exactly the biggest pots. Derived now, not sampled.

Tactical density is measured by two-ply search rather than asserted, which
shows the asserted "medium" for backgammon was wrong (0.5%).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three features were flagged as not earning their place. Checking each against
the data rather than against the prior that put it there:

- bears_off fired on 0.000 of profiled moves in every tier and is removed. A
  backgammon game runs a couple of hundred plies; the telemetry window is the
  first twenty moves, so nobody in it is bearing off.
- is_pass is likewise constant in Othello's window and is removed. Passes are
  still handled - they just are not reported.
- pip_gain was removed on the argument that the die value is the dice's choice
  rather than the player's, and the argument was wrong: accuracy fell 6.5
  points at one move and 5 at two. Which die you are still able to play is
  decided by the position you left yourself. Restored.

Adds moves_rearmost (running the back checker against building at home), which
is the choice pip_gain was mistaken for. Keeping both beats the original set at
every move budget: n=1 0.492, n=2 0.606 (was 0.602), n=20 0.362 (was 0.332).

Also corrects two docstrings that asserted received wisdom the measurement
contradicts: Othello's discs-flipped tell runs the wrong way here (1.545 ->
1.638 up the ladder), and backgammon's top tier over-stacks more than its
bottom, not less.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Orders the open questions cheapest-invalidating-first. Two items (the
cross-game speed/efficiency heuristic, and handicapping versus undertraining
in the games already built) need no new environment and could each retire a
large part of the remaining work.

Records the constraint that emerged while planning: nothing requiring the
target game's human data may be used to build the method, since at a real
launch it does not exist. Human data tests, once, at the end.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The lightweight cross-game heuristic and the agent-ladder work answer different
questions, and neither is contingent on the other's result. The heuristic is
the baseline the agent track is scored against - an agent number means little
alone and a lot against a known simple baseline - so it goes first for
calibration, not as a gate.

Adds E7, the head-to-head that makes the comparison explicit, and a track
column so the commitment is legible at a glance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Renumbers the head-to-head and deployment experiments to E6 and E7, and
removes the Tenhou row from the data source table.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Builds the lightweight track. coldopen/human/ ingests rank-labelled public
records; coldopen/crossgame.py runs the transfer matrix that E1 is actually
about - fit on game A, predict skill in game B, with the within-game diagonal
only there as the ceiling to measure the off-diagonal against.

The client is deliberately slow: one request a second, an identifying
User-Agent with a contact route, disk cache so iterating on features costs
nobody anything, and Retry-After honoured. Player ids are hashed at the
boundary; nothing downstream can leak an identity it never received.

TETR.IO sampling had to be redesigned. Walking the leaderboard from the top
returned sixty accounts that were all X+, which is exactly the population the
cold-start problem is not about. It now enters the leaderboard at eighteen
rating points chosen to land in each rank.

Pilot result on 54 players across all 18 ranks: Spearman 0.892 against real
rating from a single round, 0.956 from three, using two features and a ridge
regression grouped by player. That is the baseline the agent track must beat.

Confirms the cross-person speed/efficiency correlation the E5 ladder design
assumes (rho = 0.878). Refutes one prior: VS/APM is flat across ranks, so VS
carries no skill signal beyond APM.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…result

The two-axis transfer matrix looks like a strong positive - a model fitted on
StarCraft II ranks TETR.IO players at 0.911, against a 0.974 within-game
ceiling. The ablation says that number is not evidence of a transferable shape.

Each axis alone transfers as well as or better than the pair (speed only
0.926, efficiency only 0.927, both 0.911 for skillcraft->tetrio), and the same
holds in the other direction. Combining the axes helps within a game - 0.974
against 0.924 for speed alone in TETR.IO - and hurts across them. A
standardised single-axis model has nearly nothing to transfer, since ranking by
a z-score needs no fitting, so its cross-game score is that axis's within-game
correlation in disguise; speed-only scores an identical 0.661 both within
SkillCraft and transferred from Tetris, which is the giveaway.

So the strong form of E1 is not supported. What is domain-general is that each
axis correlates with skill; the weighting between them is game-specific.

The ablation now runs as part of the standard output rather than as a one-off,
because a pair that does not beat its own best single axis has not shown
anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Lichess supplies the cleanest instance of the schema yet and needs no key,
no scraping and no rate limit: speed from clock deltas, efficiency from
Stockfish annotations, and an Elo computed from results and nothing else.
Monthly dumps are tens of gigabytes but the games are independent, so the
reader streams and stops - 12,304 player-rows cost a few hundred megabytes.

With three games the ablation becomes decisive. A no-fit control that ranks
players by the z-scored axis, fitting nothing, scores identically to the
fitted single-axis transfer - and those fitted scores are the same whatever
game they were fitted on. Every column is constant across sources, so
single-axis "transfer" carries nothing; it is the target's own correlation
in disguise.

That leaves the two-axis pair as the only place a learned relationship could
live, and it loses to the best single axis in five of six ordered pairs. The
reason is visible in the per-game breakdown: chess at a fixed time control is
0.10 speed against 0.36 efficiency, StarCraft is 0.66 against 0.44, TETR.IO is
0.93 against 0.93. The weighting is a property of the game, not of skill.

The weak claim survives and is still useful: standardise whichever axis a game
exposes and a skill estimate comes free, with no model and no data from the
target game. Worth 0.93 in Tetris, 0.66 in StarCraft, 0.36 in chess.

Also fixes a test that was wrong rather than a bug: dropping the evaluation on
one move charges both players, because giving an advantage straight back is
itself a mistake.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
nethack.alt.org publishes every game it has ever recorded as xlogfiles - 8.6
million games in under 100 MB - and it is the only corpus found with many games
by the same identified player. That makes it the only place the cold-start
question can be asked directly instead of by proxy: predict how good somebody
turns out to be from the first games they ever played.

Labels are held out in time, not just in rows. Skill is scored from the second
half of a career and predicted from the first, so the observation window is
never part of what it predicts. Scoring a career from all of its games would
not look like a bug, it would look like an excellent result.

967 careers, 46,660 games: one game gives rho 0.303 against later career, three
0.369, ten 0.497, twenty 0.547.

With four games the E1 asymmetry is clear and sharper than the earlier reading.
Efficiency - output per action - is positive and useful everywhere: 0.93 Tetris,
0.65 NetHack, 0.44 StarCraft, 0.36 chess. Speed is game-specific: 0.93 in
Tetris and 0.66 in StarCraft but 0.17 in untimed NetHack and 0.10 in chess at a
fixed clock. Time pressure is the obvious mechanism.

Importing another game's weighting is destructive rather than merely unhelpful:
the speed-dominant SkillCraft fit applied to NetHack scores 0.158, four times
worse than ignoring SkillCraft and using NetHack's own efficiency axis.

Honours the two parsing traps the archive documents: the 3.4.3-era file uses
colon separators, and the field set widens over the years, so fields are read
by name and all are optional.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dataset would have been the strongest entry - five games under one
collection protocol, which is the confound every pair in the current table
still carries - but the domain has been re-registered as an unrelated site and
the data path is gone. Leads recorded rather than chased, along with the reason
they may not be worth it: the trajectories carry no player identifier, so they
could not answer the per-career question NetHack now does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds three ways to degrade the strongest checkpoint instead of catching an
undertrained one: epsilon (throw the move away, the control that computer chess
already knows feels unhuman), temperature (sample from the action values, so
mistakes stay ordered), and blindspot (hide a band of the board, modelling
attention rather than decision noise). They expose act() and are delegated to
from epsilon_actions, so the league, profiler and classifier need no changes.

At a matched Elo range - 689 against the trained ladder's 736 - the
temperature-handicapped ladder gives 0.287 from one move where the undertrained
one gives 0.468, and rises to 0.384 by sixteen where the other reaches 0.619.
Skill is much less legible early when weakness is decision noise rather than an
undertrained value function, which was the written prediction.

That makes the project's headline result partly a property of the generator
rather than of the game, and it sharpens the standing prediction that transfer
to humans will compress: human error is ordered, so if people resemble the
temperature ladder then one move is worth nearer 0.29 than 0.47.

Two side findings. The handicapped ladder ranks better than the trained one
(concordance 0.978 against 0.912), since the dial controls strength directly
while training only correlates with it. And blindspot has a floor at about 0.80
against random however blind it is, because a consistent policy beats random
whatever it is consistent about - this project's opening trap, reappearing.

Blindspot was reparameterised after the first version failed to degrade play at
all: what matters is how much of the board goes unattended, not how often.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Full runs for temperature, epsilon and blindspot at 300 games per league pair
and 600 sessions per tier. All four ladders span 612-736 Elo, so the comparison
is at matched measured strength.

Undertraining is by far the most legible from one move: 0.468 against 0.281,
0.249 and 0.307. The project's headline finding is therefore substantially a
property of how the ladder was built rather than of the game.

The prediction that handicapping would produce MORE structured errors is wrong,
and backwards. Measuring blunder-profile concentration puts undertraining
highest (0.33, 0.34, 0.25, 0.37 across the weak tiers) and the noise handicaps
lowest (0.12-0.21). An undertrained network has systematic gaps - it has learned
something about the centre and nothing about blocking - so its mistakes have a
shape. Noise spread evenly across positions spreads mistakes evenly across error
types.

That reverses the mechanism and strengthens the conclusion: undertrained agents
are easy to classify early BECAUSE their errors are systematic, and one move can
expose the profile of what they have not learned. Noise-degraded agents need
observations to estimate a rate.

Blindspot has its own signature - flat to three moves, a jump to 0.536 at five,
then a plateau - which is what an occasional catastrophic miss looks like.

The useful output is that the three generators have distinguishable fingerprints
in curve shape and error concentration. Computing the same two signatures on a
human corpus turns "which failure model are humans?" into a measurement, and the
human side of that needs no agents at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes, one of which withdraws an earlier result.

The tier-spacing control: the first E2 comparison matched only the total Elo
range, which let the trained ladder's tiers sit 1.4x further apart than the
handicapped ones - and tier gap is the main driver of how hard classification
is, a point this project makes about Leduc and then failed to apply to itself.
Generators now run through one code path with tiers pinned to fixed ratings.
The finding survives the control and is stronger for it: at a mean tier gap of
121 against temperature's 139, so with the confound now working against it, the
trained ladder still gives 0.522 from one move where temperature gives 0.292
and epsilon 0.249.

The distillation ladder: train a student to imitate the strongest checkpoint
and snapshot it as it learns. This is a third kind of generator, and possibly
the closest of the three to how people are bad - a partially-trained imitator
has picked up the common patterns and not the rare ones, which is what "strong
at what you have drilled, weak elsewhere" means.

It also solves a range problem the others cannot. Handicapping only reaches
down from a finished agent and undertraining only reaches up from random; a
student spans both ends in one run. On Connect Four it goes from 0.127 teacher
agreement at initialisation to 0.954 after four thousand steps, smoothly.

That range property is what makes it the answer for NetHack, where the overlap
check found random agents sitting at the 5th percentile of human games while
AutoAscend's median sits around the 85th.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All five now run through one code path with tiers pinned to the same six
ratings, so tier spacing cannot explain the difference. The trained ladder ends
up with one of the narrowest gaps (121) and still nearly doubles every other
generator at one move: 0.522 against 0.283 distilled, 0.308 blindspot, 0.292
temperature, 0.249 epsilon.

Distillation is what makes this decisive. It is also a form of undertraining -
a student snapshotted partway to competence - and it behaves like the handicaps,
not like the RL ladder. So the legibility is not a property of being partly
trained. It is specific to RL checkpoints, most likely because an early DQN has
a systematically distorted value function (something about the centre, nothing
about blocking) whose blunder profile is a fingerprint, where a cloning student
is wrong in scattered places instead.

Error concentration separates them the same way: the trained ladder stays
concentrated at every strength, while distillation and blindspot are
concentrated only at the weak end and become uniform as they improve - which is
the more plausible description of a person.

Three curve families fall out - trained (high from move one), distilled and
blindspot (jump at five, plateau), temperature and epsilon (slow climb) - and
they are fingerprints a human corpus can be matched against without needing any
agents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The claim that most of the signal arrives on move one was measured on a ladder
built by snapshotting self-play, and rebuilding it four other ways at matched
tier spacing puts the same number at 0.25-0.31 instead of 0.52. The
outward-facing document should not keep stating the strong version.

Also states plainly that no game yet has both an agent ladder and human data,
so the transfer question remains untouched rather than merely pending.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nformatively

Othello turned out to be the one game where both halves already existed. The
French Othello Federation publishes 125,000 tournament games with full move
sequences, free, and Othello already had an agent ladder, a reference network
and a feature extractor - so both populations could go through identical code.

Two traps handled: WTHOR omits passes, so replaying the list literally
desynchronises the board and misattributes every later move; and it ships no
ratings, so skill is fitted from the results with the same Bradley-Terry code
the agent league uses, which keeps both populations on one scale. 60 of 60
games replay legally and the fitted human spread is sensible.

The comparison then fails three ways at once, and each says what to fix.

The corner features are nonzero in 0.013% of human move rows: the window is a
player's first twenty moves and competent Othello contests corners in the
endgame. The set that separates agents measures events that do not happen when
humans are watched. Agents only produce them by playing badly enough to give
corners away in the opening.

ref_agreement is inverted - stronger humans agree with the reference less -
because the reference is a mediocre DQN and tournament players deviate from it
by being better. A yardstick has to outrank everyone it measures.

And the ladder tops out at 641 Elo over random while every WTHOR player is a
tournament entrant, so the populations never meet.

This is the manifold-overlap risk the plan names as the central threat,
measured on the first real attempt, and invisible from the agent-only
experiments - all four of which looked healthy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The versus engine was the expensive part - kick tables, T-spin corner rules,
garbage cancelling order, the multiplier combo, the garbage cap - and it turns
out not to be needed.

A 40 LINES record already carries inputs and piecesplaced, so inputs/piece is
finesse: keystrokes spent against the minimum the placement needed. Rules
derived, no opponent, no attack table, no engine. Same class of feature as
missed_win.

Checked across the ladder rather than assumed: 47 of 54 sampled players have a
sprint record with coverage at every rank, finesse runs 6.71 at D to 2.60 at X+
and quad rate 0.14 to 0.76, both monotone.

The design this permits avoids all three failures Othello hit. The label is
league TR, earned in versus, while the features come from sprint - different
modes, so the label cannot restate the features, which is what disqualified
Jstris. Finesse and quad rate need only the rules, so no yardstick can invert.
And they fire from the first piece, so no window mismatch.

What is given up is VS/APM, the downstacking signal, which E1 already measured
as flat across every rank. The sacrificed part is the part already shown inert.

One consequence recorded: the simulator has to be input-level rather than
placement-level, since finesse only exists if the agent emits keystrokes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The simulacrum workflow end to end: spec.md as the single source of truth,
a readable reference implementation, an independently written batched torch
implementation, and the full validation battery - PASS, eligible for training.
Bit-exact differential over 8 seeds x 300 steps, batch independence, twelve
invariants swept, auto-reset, determinism, replay, compiled parity. A third
hand-transcription of the SRS tables guards both implementations against a
shared transcription slip.

One step is one keystroke, and time is deterministic accounting: each action
costs declared integer milliseconds plus a per-agent LATENCY parameter, with
gravity and guideline lock delay running on the virtual clock. Finesse exists
because steps are keystrokes; sprint time and PPS exist because the clock is
state; the reward (clear bonus minus elapsed ms) is the human sprint objective
rather than an analogue; and the speed/accuracy coupling that E5 called for
emerges from the physics instead of being wired in.

The invariant sweep caught what the differential test could not: both
implementations faithfully implemented the same broken spec rule - a piece
lifted off a ledge at the lock-delay reset cap kept stale lock delay. Fixed in
the spec first ("leaving the ground discards lock delay"), then in both.

Throughput: the harness headline of 1x vs reference is an artifact (its
reference loop skips observation building). With observations, 13.8k steps/s
reference vs 74k compiled at n=1024 - a 5x training-shape speedup, an order of
magnitude above the wall rate the Connect Four ladder actually trained at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
coldopen/tetris.py extracts per-episode telemetry from batched rollouts by
watching the env from the outside: a queue shift means a piece was consumed,
and a consumed piece is a lock unless the hold swapped it in, with terminal
corrections for the finishing or topping-out lock that never spawns. The
feature set mirrors what TETR.IO's API reports for a human 40L record, so the
comparison is column for column. Seven tests drive scripted action streams
with known counts.

coldopen/train_sprint.py is single-player DQN with the lessons already paid
for: n-step returns (the clear bonus is many steps downstream of the stacking
that earns it) and log-spaced checkpoints. New here: potential-based shaping
on holes and stack height, training-only, optimal-policy-preserving - random
play essentially never clears a line, which is the sparse-reward wall the
Tetris literature warns about. Every reported number is unshaped.

coldopen/human/tetrio_sprint.py pulls each sampled ladder player's best 40L
record through the cached polite client: same sample as the league ingestion,
league TR as the label, sprint statistics as the features.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extending a training run should cost a flag, not a retrain: the newest
checkpoint in --out seeds the network, the schedule skips already-saved tags,
and the log appends. The replay buffer and epsilon restart cold - a brief
re-exploration bump, cheaper than serializing a 600MB buffer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Finesse spans 4.86 to 2.81 inputs per piece, quad rate 0.03 to 0.69, and -
unpredicted - hold usage rises about 4x with skill. Holds per piece is a pure
planning signal with no speed component, making it a fifth human-comparable
feature. This table is the manifold the agent ladders must land on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eacher

The 3M-step run ended at 0.00 mean lines: a brief blip of clears at 227k
never returned, and the 1.2M-3M curve is flat. Keystroke-level control needs
about five coordinated decisions per placement and ten good placements per
line, which epsilon-greedy exploration essentially never strings together -
the documented Tetris RL wall, now measured here. The checkpoints are kept:
they are the undertrained-RL generator's floor rungs.

The teacher that breaks the wall is scripted, in the AutoAscend pattern:
enumerate every (rotation, column) placement, score the resulting board with
Dellacherie-style hand weights (holes, height, bumpiness, clears), emit the
keystroke sequence - DAS to walls, taps elsewhere - and replan per piece. No
human data anywhere, so ladders built from it stay cold-start legitimate.

First evaluation: finishes 15/16 sprints, 41.5s mean, finesse 3.02
inputs/piece. Against the human table that is an odd and informative shape -
finesse near rank x (3.07), quad rate near rank d (0.01 vs 0.03), holds below
the entire human range (0.0 vs 0.07 at the floor). The bot is off-manifold as
a package even though each axis alone looks human; recorded as the first
overlap observation rather than smoothed over.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured first: dialling the env's LATENCY from 17ms to 1400ms left the
scripted teacher at 40.1 lines, 100% finish and 3.05 inputs/piece at every
setting. Only pps and elapsed time moved. Gravity displaces pieces vertically
and a hard drop lands them at the bottom regardless, so deliberating slowly
never costs a placement. The dial produces slow-but-flawless players, and E1
established humans are not that - the slowest ranks have the WORST finesse
(4.86 against 2.81 at the top).

So the coupling is now explicit. SprintBot.skill moves two knobs together:
Gaussian noise on candidate placement scores (ordered mistakes, the property
E2 found separates temperature-style degradation from uniform noise), and
fumbled keystrokes - taps walked back, which cost finesse without changing
where the piece lands.

The resulting ladder covers the human range on three axes: finesse 3.05-4.72
against a human 2.81-4.86, pps 7.07-0.23 against 6.01-0.84, sprint time
15s-464s against 17s-135s. Quad rate does not: every rung sits at ~0.01 while
humans span 0.03 to 0.69. Recorded as the open manifold gap.

Also fixes a latent SprintBot bug: a plan surviving auto-reset was applied to
the next episode's board.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Measured the compounding-error failure rather than assuming it: the cloned
student reached 0.995 per-action agreement with the teacher and still cleared
4.3 lines against the teacher's 40, never finishing a sprint. Over ~300
keystrokes even that accuracy gives 0.995^300 = 22% odds of a clean run, and
one wrong keystroke lands the student on a board its teacher never built,
where its next action is worse than a guess.

DAgger (Ross et al. 2011) closes the distribution mismatch: roll the student
out, ask the teacher what it would play in the states the student actually
reached, aggregate, keep training. Rounds interleave with the snapshot
schedule so ladder rungs spread across the whole curriculum. --rounds 1
reproduces plain cloning for comparison.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…idn't

The agent-versus-human overlap check reported every generator "100% inside
the human range" on quad rate, holds and B2B. All three were false. Pooled
human quad rate spans 0.00-1.00, so a bot pinned at 0.00 - below every human
alive - passes a range test. Replaced it with coverage (which human rank does
this rung look like?) and coherence (do the rung's features agree?). The v1
bot scored 8.0 ranks of median disagreement: rank-16 finesse, rank-0 stacking.

The cause was the objective. W_LINES paid for any immediate clear, so the bot
cashed singles and never stacked four rows. v2 keeps a well, pays for quads,
charges for partial clears while safe, and holds when the swap is better.

Two bugs found by reading rather than by a failing test:
- the placement search ran x in range(-2, 9), so column 9 was unreachable for
  any piece whose cells all sit at dx = 0 - including the vertical I that a
  quad requires;
- no hold logic at all, leaving holds/piece below the weakest human rank.

Swept the well-defence weights: at W_WELL_FILL = -14 the bot defended the well
to the death (10 lines, 0% finish); at -6 it finishes every sprint with the
same quad rate (41.4 lines, 100%, quad 0.61 against rank x+'s 0.69).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
DQN from scratch never cleared a line in 3M steps, which leaves the
"trained RL" ladder at zero on every rung and stage 5 with nothing to compare
distillation against. The cause is structural, not a tuning miss: a quad needs
nine columns stacked flat and a vertical I dropped into the tenth, about forty
correlated keystrokes. Random exploration never assembles that, so every
trajectory scores alike and there is no gradient. Potential-based shaping on
holes and height did not bridge it.

--init warm starts the Q-net from a distilled student, the standard
clone-then-improve answer to hard exploration. The student's head was trained
as classification logits and is read as Q-values: a scale mismatch, not a
semantic one, since the argmax is what the policy uses and Bellman updates
rescale the head quickly. Needs a low --eps-start or the decay window is spent
destroying the policy it was handed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… humans

Two measurements forced the redesign. Latency alone is a pure speed dial
(identical finesse at 17ms and 1400ms), and degrading judgement with noise
instead killed the bot: at noise 8 every rung below skill 0.7 topped out. A
ladder whose lower rungs cannot finish measures survival, not skill.

So skill now interpolates the OBJECTIVE between expert well-and-quad play and
beginner take-any-clear. Both survive; they differ in what they are trying to
achieve. The un-fitted ladder spans 15.4s to 272.3s with in/pc 3.27 to 4.32,
quad 0.665 to 0.059 and pps 7.46 to 0.40, every rung finishing.

Reverted two constants I had fitted to the human table, which would have made
the overlap test circular:
- hold margins 0.3/4.0, set because human hold usage never falls below 0.070;
- strategy = skill ** 0.7, set because a linear blend put quad rate at 0.265
  where the rank-matched humans sit at 0.487.

Both fitted values are recorded in EXPERIMENTS.md rather than deleted: the gap
between fitted and un-fitted is how much of the overlap would have been
manufactured. At the top rung it turns out to be almost nothing (15.5s/0.666
fitted against 15.4s/0.665 un-fitted), which is the useful part of the result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…estion

Distillation kept failing in a way plain agreement numbers hid: a student at
98.6% agreement on the teacher's own states scored 25.8% on the states it
actually reached, cleared 1.8 lines, and spent 60 inputs per piece without
locking. Adding DAgger rounds made it worse, not better.

The cause is that the plan-following teacher is not a Markov policy. Which
keystroke it plays depends on how far through its stored plan it is, and plan
progress is not in the observation. So "what would the teacher do in this
state?" - exactly the question DAgger relabelling asks - has no well-defined
answer. Hold made the damage visible: the teacher re-decided to hold on every
step the student declined to, so 39.6% of relabelled targets came back `hold`
against the student's 1.1%, one persistent disagreement flooding the set.

markov=True derives the action from observable state alone: pick the target
placement, emit the one keystroke that moves toward it. It plays identically
to the plan-following version (39.2 lines, in/pc 3.27, quad 0.674, hold 0.222
either way), so this costs nothing but a re-search per step.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Agreement measured on the teacher's own states was 98.6% while the policy was
unusable: 1.8 lines, 60 inputs per piece, and 25.8% agreement on the states
the student actually reached. Watch the second number, not the first.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coverage, the gate Othello failed, now passes: every feature traverses most of
the human ladder with 0% off-manifold (pps 14%), against a v1 whose quad rate
was pinned at rank d and whose hold usage sat below every human alive.

Coherence is still 8.0 ranks of median disagreement, but a leave-one-out pass
names the two causes, and neither is a strategy error:
- holds per piece runs 4.1 ranks weak. The hold policy is a one-ply greedy
  swap check; humans use the slot to plan, and keep using it down to rank d.
- inputs per piece runs 2.9 ranks strong. The emitter computes the exact
  keystroke sequence, so even weak rungs have finesse real weak players lack.

Quad rate, the feature v1 got most wrong, is now the best calibrated of the
five at +0.4 ranks, with nothing fitted to achieve it.

Also memoise the Markov target per (board, piece): it is unchanged while a
piece falls, so this restores one search per piece instead of one per
keystroke, with identical play.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
mwburke and others added 28 commits August 8, 2026 17:09
The encoding is the student's entire action space, so an off-by-one would
mislabel every example and present as "the net just didn't learn". And a
refactor that quietly wired own-state agreement to pool agreement would remove
the only signal that caught three distillation failures, where the two
diverged by 4-13x.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
load_checkpoints hardcoded a 10-way head, so placement checkpoints would not
load at all. They also need a different rollout: a placement student picks a
target and a deterministic emitter walks to it, so unlike the keystroke
students it has no limit cycle to escape and stays greedy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The section had grown in the order things happened, which buried the ladder
result behind the distillation failure and the validation-gate incident. Now:
what the humans look like, what was wrong with the metric and the teacher,
where the cold-start line runs, THEN the result, then the generator that
failed, then the process failure.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Averaged over 300 steps of each policy's own play:

  boards     max height   holes   bumpiness
  teacher        6.85      0.45     10.01
  student        9.78     13.50     13.86

The teacher essentially never makes a hole, so the pool contains almost no
example of what to do on a board with thirteen of them - and that is the only
kind of board the student ever sees. It is not failing to learn the function;
it has never been shown the domain it operates in. 97.2% agreement on the
teacher's pool against 26.7% on its own boards is the same fact as a metric.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llegal ones

markov_action emits HARD_DROP only once the piece reaches (target_rot,
target_x). The teacher never asks for an unreachable target because its search
only returns legal ones - but an unmasked student can, and then the piece
never arrives, never drops, and the policy stalls forever. That is what the
run was showing: inputs per piece climbed to 50.8 against the teacher's 3.3
while own-state agreement fell from 0.268 to 0.136, which I had been reading
as DAgger failing to help.

LEGAL is a precomputed [7, 44] column-bounds mask, verified to agree exactly
with the full search on an empty board (34-36 legal placements per piece).
Applied in placement_policy and in own_state_agreement, so the diagnostic
measures the same policy that plays.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…locked

Masking targets to legal columns was necessary but not sufficient - inputs per
piece came back at 86.4 against the teacher's 3.3. Two causes, both about
reachability rather than legality:

- The student re-predicted every step, and the observation renders the active
  piece at its current height, so the prediction flipped as the piece fell and
  the emitter turned around mid-approach. The target is now committed once per
  (board, piece) and cached, exactly as the Markov teacher memoises its search.
  Training already supplies the invariance this leans on - the same board and
  piece appear at every fall height with the same label - but the student only
  approximates it, and caching makes it exact.

- A legal destination is not reachable from everywhere. Beside a tall stack
  the wanted rotation or sideways step can be physically blocked, and
  markov_action only drops once the piece arrives, so it tapped forever.
  markov_action now takes y and hard-drops when the move it wants does not
  fit. The teacher is bit-identical either way (40.8 lines, in/pc 3.33, quad
  0.685, hold 0.339), because its targets are always reachable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…aps at 31

Ran the control I should have run first: replace the student's net with the
teacher's own answers and see what the pathway can do at best. Without hold it
scores 31.0 lines at a 50% finish rate and quad rate 0.349, against the
teacher's 40.8 and 0.685. That is a ceiling, not a training gap - and half the
oracle's runs top out, while every one of the 396 human sprint records is a
finish. A student without hold could never reach the human manifold however
well it learned.

With hold as a 45th action the same oracle scores 36.8 lines, 83% finish,
quad 0.621, holds 0.337 - close enough to the teacher to be worth training
against.

Hold steps are now labelled rather than dropped from collection, legality is
computed per step from hold_used rather than per piece, and teacher_labels()
gives DAgger and the diagnostic one shared definition of "what the teacher
would do" so they cannot drift apart.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
oracle_ceiling() runs the pathway with the teacher's own answers substituted
for the student's. It separates "the student has not learned" from "the
pathway cannot do better", and those are indistinguishable from the outside -
which is what cost most of the work on this generator. The check takes about a
minute and now runs at the top of every distillation, with its result stored
alongside the training log so no curve can be read without it.

It is what caught hold missing from the action space: a 31-line, 50%-finish
ceiling against a human population where every record is a finish.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Corrupting a known fraction of the oracle's placements isolates imitation
accuracy from every other variable - same pathway, emitter and teacher:

  accuracy  lines  finish
     1.00    36.8    0.83
     0.98    40.2    0.92
     0.95    27.8    0.42
     0.90    10.2    0.00
     0.80     5.6    0.00
     0.60     0.8    0.00
     0.40     0.3    0.00

Nothing finishes below 95% per-placement accuracy, and every one of the 396
human records is a finish. So the whole human-relevant range is compressed
into accuracy 0.95-1.00, and everything below 0.90 is indistinguishable
rubble. A ladder needs a dial whose intermediate settings give intermediate
play; imitation accuracy is not one here.

Second finding in the same data: the DAgger student at 0.834 agreement cleared
1.3 lines where random corruption at 0.80 clears 5.6. A learned policy is
worse than noise at the same error rate, because its errors are systematic
rather than independent - it is wrong the same way on similar boards, so the
mistakes compound instead of averaging out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Distillation is disqualified as a ladder generator for sprint, for a reason
that generalises: a ladder needs a dial whose intermediate settings produce
intermediate play, and imitation accuracy is a step function here - nothing
finishes below 95% per-placement accuracy, while every human record finishes.
That is also why the scripted dial works: it varies strategy, which degrades
smoothly, not accuracy, which does nothing until nearly perfect.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sprint_transfer.py fits a ridge model on agent episodes labelled by their own
virtual sprint time - no human quantity anywhere - and applies it once to the
396 human records. Reported beside the controls E1 showed are indispensable:
rank by one standardised feature with no model at all, a human-fitted
cross-validated ceiling, and a speed-versus-judgement ablation, because a
model that only rediscovers "faster is better" has not earned a simulator.

Also records what "early" can mean here: sprint gives a full telemetry row
from ONE game against a rank that takes many matches to establish, which is
the cold-start question in its natural form. The within-game version ("how
many pieces in?") is not answerable from this data - the API returns
game-level summaries, not replays, so a human record cannot be truncated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test the project exists for, and it is negative.

  method                          rho vs true rank   needs a simulator
  pps alone, no model                      +0.932   no
  inputs_per_piece alone                   +0.773   no
  human-fitted 5-feature ridge             +0.923   no (needs labels)
  agent-fitted 5-feature ridge             +0.660   yes

Ranking 396 players by one number beats the agent-fitted model by 0.27. The
human-fitted model also fails to beat pps alone, so the extra features add
nothing even with labels and cross-validation - this is not agents failing to
capture something humans have.

The ablation reproduces E1's costume finding exactly: fitting pps ALONE on
agents gives rho +0.932, identical to the no-fit ranking. A one-axis fitted
model is the no-fit ranking. Judgement features alone give +0.635, so they do
carry real signal; adding them to pps is what destroys it (+0.660).

Diagnosis, and it is the useful part. Every feature has the right sign in both
populations. But the agent features are far more collinear than human ones -
holds x pps +0.90 against +0.46, quad x pps +0.85 against +0.47 - because a
single coupled skill dial makes every feature move together. Ridge splits
weight arbitrarily among redundant predictors (it gave pps a NEGATIVE
coefficient) and that arbitrary split does not transfer.

A one-dimensional ladder produces one-dimensional telemetry. The coupling that
made each rung human-shaped is exactly what breaks multivariate transfer.
Testing a decoupled skill x latency grid next.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ble here

Decoupling the ladder into a skill x latency grid identifies pps's true effect,
so the learned weight goes from -0.40 to +0.73 and transfer jumps 0.660 ->
0.927. It converges on the trivial solution without exceeding it: pps alone,
no model, is 0.932.

The signal beyond speed is real but useless for ranking. Controlling for pps,
holds still correlates +0.435 with rank, finesse +0.393, b2b +0.346. Yet an
exhaustive search over all 16 subsets containing pps, fitted on humans with
5-fold CV, found nothing that beats pps alone (0.9334). The residual describes
variation within a rank, not the ordering between ranks.

Design conclusion, which is the part worth keeping: 40 LINES is a time trial,
so its ground truth is nearly a single observable axis and there is no headroom
for a simulator to fill. That was knowable in advance from E1's own tetrio
column (0.926 from speed alone). The check to run BEFORE building an
environment: how well does the best single observable feature already predict
rank? At 0.9 there is no room. Same discipline as the overlap gate, one level
earlier.

E1 measured Lichess at 0.358 from its best single axis and SkillCraft at 0.661
against sprint's 0.932, so the agent track should be pointed there instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
I scored the agent-fitted model against "rank by pps" and concluded the
simulator earned nothing. That comparison is invalid. To rank players by pps
you must first know pps is the discriminating axis and which direction it
runs, and that knowledge comes from labelled human data - precisely what does
not exist at a cold start. The baseline was allowed to peek at the test set's
labels; the method under test was not.

What the grid model actually learned from ZERO human labels: weights
[0, 0, 0, 0, +0.73]. It found on its own that pps is the axis and the other
four are noise. That is not a failure to beat the baseline - it IS the
baseline, derived without the labels the baseline needs.

The honest question is how many labelled games buy the same thing. Same
held-out records for every method, 200 splits per size:

  labelled games   human-fitted   agent grid (0 labels)
               5         +0.828                  +0.927
              20         +0.907                  +0.927
              80         +0.918                  +0.927
             300         +0.920                  +0.922

A model fitted only on agents is not beaten by a model fitted on humans at any
sample size tested, so the simulator is worth more than 300 labelled games. At
cold-start sizes the gap is large. The coupled ladder's 0.660, which read as a
failure against 0.932, is a usable ranking from the very first game with no
rating, no match history and no labels anywhere in its construction.

Still standing: the collinearity diagnosis (build ladders with as many
independent axes as the skill being modelled), and that no subset beats pps on
rank ordering even fitted on humans, so sprint really is close to a one-axis
game. Withdrawn: that the simulator earned nothing, and that a
single-axis screen should gate whether an environment gets built.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Checked minesweeper.online before crawling it:

  User-agent: *
  Allow: /
  Disallow: /chat
  Disallow: /chat-history
  Disallow: */password-reset/*
  Disallow: */invoice/*

Permissive for what this project wants, with no Crawl-delay declared. But
urllib.robotparser matches rules in FILE ORDER and returns the first hit, so
"Allow: /" wins for every path and it reports /chat as crawlable. An
enforcement layer that says yes to a forbidden path is worse than no layer,
because it gets believed.

Replaced with an RFC 9309 matcher: longest matching pattern wins, Allow breaks
exact ties, * and $ supported, named user-agent groups beat the wildcard
group. Verified against the live policy - all eight paths now classify
correctly, including /chat which the stdlib waved through.

The check sits in front of the network in get_json, so a disallowed path
raises Disallowed without a request being made. Cache hits skip it because no
request happens either way.

tests/test_robots.py pins the live policy verbatim, so a change on their side
surfaces as a failure rather than as silent over-crawling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…base

robots.txt permits /ranking, /best-players and /player/* with no Crawl-delay,
and the ranking pages carry exactly the axes E1 needs - 3BV/s for speed, Eff
for efficiency, with leaderboard position as the skill label.

But the data is not reachable over plain HTTP. Every variant returns a
byte-identical 29,946-byte SPA shell reading "Loading data...": our UA, curl's
default, no UA, with and without a connect.sid cookie, with standard Accept
headers, and with ?page= / ?level= / path forms. No bot mitigation is present
(plain nginx) and the JS bundle references no data API. In a real browser the
DOM grows from ~30KB to ~49KB with the table, while the only same-origin XHR
is /authorize.

And /authorize creates a user account per call:

  {"userId": 56457114, "isNewUser": true,
   "user": {"createdAt": "2026-08-10T05:23:21.316Z", ...}}

Consecutive calls returned 56457114 then 56457116. Roughly four anonymous user
rows were created while diagnosing what was supposed to be a read-only
question.

So the binding constraint is not politeness or robots.txt, both of which are
satisfiable. It is that every available route either drives a real browser
thousands of times or writes to their production user table. Stopping pending
a reply from support@minesweeper.online or an official read-only endpoint.

coldopen/human/minesweeper_notes.md records the findings, including what to do
differently if it resumes: reuse one session, pseudonymise at ingest, and
treat display names as untrusted text.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Expert minesweeper as a solo timed env, for measuring per-click judgement
alongside 3BV/s and click efficiency. Four design forks settled with the user:

- guaranteed opening: mines are drawn from cells outside the closed 3x3 around
  the first click, so count(first_cell) == 0 by construction and no game is
  lost to first-move luck;
- standard boards, not no-guess, matching the leaderboard we would test
  against - the solver then has to separate a FORCED guess from an avoidable
  one, which is the more informative feature;
- reveal + flag + chord, because efficiency above 100% requires chording and
  efficiency is one of the two E1 axes;
- the deduction oracle lives in the telemetry layer, not the env: constraint
  solving every step would make the batched env unusable, and the env does not
  need the answer for its own dynamics.

One design point comes straight from E6. If every action cost the same,
elapsed time would be exactly t*(COST+LATENCY), making 3BV/s equal to
efficiency/(COST+LATENCY) - the two axes would be the same number in different
units. That is the collinear-telemetry trap that broke sprint's multivariate
transfer. Distinct per-kind costs (reveal 30, flag 30, chord 60, plus LATENCY)
break the identity so speed and efficiency can vary independently.

Mine keys are drawn at reset but applied at the first click, making the board a
deterministic function of (seed, first_cell) and independent of how many flags
preceded it. Keys rather than sampled positions because "the M smallest among
eligible" cannot depend on draw order.

13 invariants and 1 RNG slot enumerated; schema.json mirrors the state table;
every field is integral so no x-atol is declared anywhere.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Readable single-instance implementation: dataclass state, explicit ifs, every
block tagged with the spec section it implements. No vectorization anywhere,
so it stays an independent witness for the differential test against fast.py.

Verified by hand against the spec's guarantees:
- opening guarantee holds, count(first_cell) == 0 and the first click opened
  49 cells;
- exactly M=99 mines placed, none of them revealed while dead == 0;
- win reachable (228 steps, won=1, reward +999770 = 1000000 - 230);
- death reachable (reward -1000230 = -230 - 1000000);
- determinism: same (seed, episode, actions) gives identical JSON, different
  seeds differ;
- to_json/from_json round-trips exactly;
- a no-op chord still costs 260ms and increments t, which efficiency depends
  on - an env that discarded useless clicks could not measure clicks-per-work;
- chord is a no-op until flagged neighbours equal the count, then reveals in
  ascending index order;
- flags block the flood: a flagged cell inside an opening stays hidden and
  holds back everything beyond it.

MINE_KEYS uses rng.draw_bits directly (64 uniform bits) rather than masking to
uint32 - every transformation is another place the two implementations can
disagree. Spec amended to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fast.py written from spec.md, never from reference.py. The differential test
caught a real independent-misreading on seed 1: a mine can have neighbour
count 0 (count excludes the cell itself), and the batched flood seeded from
any revealed zero-count cell - including the mine that just ended the episode
- revealing the ring around it. The spec's Reveal sequence stops at step 3 for
a mine; one ~mines term in the flood seed fixes it. Two other traps handled by
design: mine keys masked to 32 bits (torch reinterprets draw_bits' uint64 as
SIGNED, so unmasked ascending sorts disagree for half the key space), and the
flood gated per-instance on "this step revealed a zero" (a global fixpoint
would reveal cells when a flag is REMOVED).

Battery: 10/10 PASS, validation_report.json committed - the gate before
training, checked as an artifact this time.

FullyConvNet: conv body -> 1x1 head, [B,3,H,W] output whose row-major flatten
IS the kind-major action encoding. 49.5k parameters against BoardNet's 4.2M
for the same job, and locality verified by perturbation. Minesweeper deduction
is local translation-invariant pattern-matching, which is exactly what weight
sharing buys; the Flatten+Linear head forced 1440 spatial actions through a
128-unit bottleneck and had to relearn each pattern per location.

Also adds train_minesweeper.py: DQN with an exploration mask over spec-defined
no-ops and potential-based shaping on revealed-safe progress.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The exploration wall in numbers: an expert win is ~381 correct reveals with
zero fatal ones, and random play dies in a handful of clicks - sprint's wall
again, met head-on this time instead of discovered after 3M wasted steps.

ms_curriculum.py (after Salimans & Chen 2018): reset agents into boards with
only k safe cells left; a Pacer moves the start line backwards whenever the
windowed win rate clears a threshold, one-directional so the curriculum cannot
thrash. The validated env is not touched - the subclass defers to the
validated reset and then overwrites masked instances; its re-derived mine
placement is pinned byte-for-byte to fast.py's by a parity test. Injected
starts satisfy every invariant except I9, which defines t==0 as ungenerated -
a curriculum start is deliberately mid-game, so training runs debug-off and
validation stays on the base class.

Start states come from a pluggable source, which is the answer to "use scraped
replays as a curriculum later":
- SyntheticStates (running now): the env's own boards with all but k random
  safe cells revealed. Cold-start legitimate, usable in the main ladder.
- minesweeper_replays.py (contract now, data later): fixed JSON format for
  scraped games; clicks replay through the REFERENCE env so one validated
  flood/chord implementation defines what "after click i" looks like, and
  every visited state becomes a start state. Contamination rules fixed in
  advance: separate ladder directory, disjoint player split by hash,
  pseudonymised at the scraper, and the comparison framed as a point on the
  label-efficiency curve rather than a second cold-start method.

Trainer switched to the FCN head and the curriculum env; local replay buffer
because train_sprint's binds sprint's shapes at import. Fixture correction
caught by our own spec: a flagged safe cell blocks the flood, so the toy
replay does not win - both captured states are live.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…-signal

The DQN's measured failure mode by 116k steps: deaths falling (43 -> 18 on
curriculum probes) with zero wins - it learns to avoid revealing rather than
to locate safety, because with 99 hidden mines against a few hidden safe
cells, "never reveal" beats an uninformed reveal, and a 1-in-1440 sparse
Q-target gives almost no gradient toward finding the safe cell.

mineprob.py replaces the sparse signal with a dense one. The env knows its
mines, so every visited state yields 480 labelled cells free: FCN ->
per-cell P(mine), BCE masked to unrevealed cells, policy = reveal argmin.
States are collected under the net's own policy - the collector IS the
policy, so there is no train/rollout gap to close (the tetris lesson), and
the label is a pure function of the board, so there is no ill-posed teacher
(the other tetris lesson). No human data anywhere; checkpoints form a
cold-start judgement ladder whose dial is board-reading quality, with
epsilon-lapses available as the degradation knob at the judgement level.

Smoke test: 300 supervised updates already reveal 30-48 safe cells per game
against the DQN's 0-24 after 262,000 env steps.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1's 2M-step 0.000-win result was the optimum of the reward it was given: an
uninformed reveal is a mine with prior ~21% (expected value ~-210k) against
flagging forever at -230 a step, and the +1 shaping bonus for a correct
reveal was 0.4% of the step cost. Flag-spin was rational.

Three training-only changes, env and spec untouched:
- --shaping-weight 300: correct reveals become material, not rounding error;
- --death-penalty 20k replaces the -1M term during training. -1M targets do
  not just discourage death - generalization smears massive negatives across
  ALL reveal Q-values, burying the safe-vs-unsafe spread the net must learn;
- --aux-weight 1.0: a 4th head channel predicts mines, BCE against the env's
  own layout masked to unrevealed cells, on the live batch each loop. The A/B
  against mineprob showed the bottleneck is gradient density, not reward
  shape: same net and env, 480 labels/state reached 81.7 safe cells in 7k
  updates while 1 scalar/action reached 0 in 2M steps.

mineprob final curve: 25 -> 81 safe cells over 16k updates, win rate still 0
- it reads boards locally but cannot yet finish one. Its checkpoints are a
monotone-ish judgement ladder regardless; wins need either longer training,
more depth, or the forced-guess endgame the solver will quantify.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eplays

The sprint lesson applied before it bites this time: every human leaderboard
record is a completed game, and no agent wins Expert yet - so the comparable
ladders move to Beginner (9x9, 10) and Intermediate (16x16, 40), which have
their own leaderboards on minesweeper.online. The env was always
parameterized and the FCN is fully convolutional, so one architecture covers
all three sizes unchanged.

"The differential test passed" is only true of parameters it ran, and the
battery runs Expert defaults - tests/test_ms_boards.py pins a miniature
differential (300 steps x 2 seeds, bit-exact JSON) plus an invariant sweep at
each extra size, and guards that an oracle can actually win a 9x9.

The replay ingest contract now requires an `outcome` field with lost and
abandoned games first-class. Player profiles record failures (win rate =
wins/attempts); leaderboards cannot. The E4 metric is win rate +
progress-at-death + telemetry rather than time-on-wins, so losses on both
sides make the comparison denser instead of narrower.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y layer

ms_solver.py decides the headline judgement feature by logic, not opinion:
unary fixpoint, pairwise-subset rules, and exact enumeration over frontier
components up to 16 cells, with `complete` reported honestly when a component
was too big to enumerate. The oracle sees only what the player saw - revealed
counts, never the layout, never the flags - so it runs identically on agent
episodes and future scraped human games. Soundness is tested against live env
games it cannot peek at: it never calls a mine safe or a safe cell a mine.

ms_telemetry.py extracts both feature families per episode: the
leaderboard-comparable numbers a human record carries (win, time, 3BV, 3BV/s,
efficiency) and the judgement numbers only the oracle produces (proven-safe
rate, avoidable-guess rate, forced-guess rate, blunder rate). The oracle is
consulted BEFORE each click on the position as seen, so verdicts use exactly
the information available at decision time.

First judged measurement, top beginner checkpoint: 80.7% of reveals provably
safe, 11.8% avoidable guesses, 4.7% blunders, 2.8% forced guesses - which is
the win-rate ceiling explained, not just observed. 184 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Six checkpoints x three lapse rates, every reveal judged on the position as
seen. Training moves proven-safe 0.10 -> 0.83 and blunders 0.08 -> 0.01; the
lapse dial at a fixed checkpoint pushes blunders back to 0.19 while
proven-safe falls more slowly. Two axes, distinguishable in telemetry - the
anti-collinearity property sprint lacked, measured rather than hoped for.
Agent side of E4 complete; human data remains the blocked input.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
192 oracle-judged rungs (6 checkpoints x 8 lapse rates x 4 latencies).
Coverage now spans time 0.38-25s and 3BV/s 0.34-10.4; latency moves speed
with judgement flat, training and lapses move judgement with speed flat, and
win rate rides the judgement axis. The collinearity that sank sprint's
multivariate transfer is absent by construction and confirmed by measurement.
Lapse dial cliffs between 0.05 and 0.10 - sample that range densely in the
human-comparison run, with more than 16 episodes per rung.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ding them

coldopen/ladders is gitignored because .pt files are large and regenerable.
train_log.json is neither: it is the measured curve, and several of this
project's findings ARE curves - beginner mineprob 3% -> 67%, the DQN's flat
zero across 2M steps, mineprob expert 25 -> 81 safe cells, the sprint
distillation plateau. Those were committed only as prose in EXPERIMENTS.md
with no data behind them.

All 18 curves are now mirrored into analysis/training_curves/ with an INDEX,
and .gitignore says to repeat that after any run whose curve gets cited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Debug snapshots from fixing the tetris I11 lock-residual bug and confirming
the curriculum's deliberate I9 exception. Both are documented in EXPERIMENTS.md
and pinned by tests; the raw dumps served their purpose.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mwburke
mwburke merged commit 47ad061 into main Aug 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant