feat(levels): two-sided endgame release, and a board to track the 50-level run

The chain built its own worst moment: hero and tail deadlock at orientations 0
and +2 and open only at +1, with +3/+4/+5 left entirely to chance. That is why
some levels released at exactly one of six orientations, and why every model
kept writing "I don't know which way to turn" — at +2 one direction was right
and the other five were dead ends.

createDeepLock now traces candidates at all six orientations and requires the
tail to also release at +3, certified with the real swept tracer. A player
stuck at +2 turns either way and something moves. Measured by the rule bot
over the regenerated set: blind rotations, wrong-way turns and extra rotations
all go to zero, from 1.17 / 0.44 / 1.22 per level. Endgame orientation counts
shift from {1:4, 2:10, 3:4} to {2:3, 3:13, 4:1}.

It costs yield: switchback r3 and crossing r3 now produce nothing, and the set
comes out at 17 levels rather than 18. The test witnesses moved with it — a
witness is whichever seed still satisfies the construction, so they are
re-derived from the sweep rather than pinned.

docs/level-board.html is the task board for getting to 50 levels. Task state is
in bench/board.json; every number on the page — candidate count, rotation
quota, blind turns, monkey actions-per-piece, the agent batches — is measured
from the level set and the run archive at build time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
cabinw
2026-09-14 16:22:08 +08:00
co-authored by Claude Opus 5
parent 8a736766fd
commit acc374e276
6 changed files with 5112 additions and 7747 deletions
+17 -5
View File
@@ -18,12 +18,16 @@ function candidatePool(base, boards, maxBends) {
const pool = [];
for (const cell of base.cells) for (let dir = 0; dir < 6; dir++) {
const tile = { q: cell.q, r: cell.r, dir };
const routes = [0, 1, 2].map(turn => trace(boards[turn], tile, [], true));
if (routes.some(route => route.status !== 'exit')) continue;
const bends = routes.map(countBends);
// All six orientations: the chain only constrains 0/1/2, but the endgame
// is played at 3/4/5 too, and leaving those to chance is what produced
// locks that open at exactly one of the six.
const routes = boards.map(board => trace(board, tile, [], true));
if (routes.slice(0, 3).some(route => route.status !== 'exit')) continue;
const bends = routes.map(r => r.status === 'exit' ? countBends(r) : Infinity);
if (bends[1] > maxBends) continue;
pool.push({
tile, position: positionKey(tile), bends, cells: routes.map(route => route.steps.length),
exits: routes.map(route => route.status === 'exit'),
at: routes.map(route => new Set(route.steps.slice(1).map(positionKey))),
});
}
@@ -38,17 +42,22 @@ export function createDeepLock(base, {
seed = 0, minHeroBends = 6, maxHeroBends = 70, maxChecks = 2000000,
} = {}) {
const rng = random(seed);
const boards = [0, 1, 2].map(turn => rotateBoard(base, turn));
const boards = [0, 1, 2, 3, 4, 5].map(turn => rotateBoard(base, turn));
const pool = candidatePool(base, boards, maxHeroBends);
const candidates = shuffled(pool, rng);
const heroes = candidates.filter(c => c.bends[1] >= minHeroBends);
let checks = 0, formalChecks = 0;
for (const a of heroes) {
// D holds A at 0 and at +2, and never stands on A's +1 departure.
// A and D deadlock at 0 and at +2 and open at +1 — that is the chain. The
// added requirement is that +3 opens too: a player stuck at +2 turns one
// way or the other and something moves either way, instead of one
// direction being right and the other five a dead end.
const tails = candidates.filter(d => d.position !== a.position
&& a.at[0].has(d.position) && d.at[0].has(a.position)
&& a.at[2].has(d.position) && d.at[2].has(a.position)
&& !a.at[1].has(d.position));
&& !a.at[1].has(d.position)
&& ((a.exits[3] && !a.at[3].has(d.position)) || (d.exits[3] && !d.at[3].has(a.position))));
for (const d of tails) {
// C holds A at +1 and leaves alone at +2 once B is gone.
const seconds = candidates.filter(c => c.position !== a.position && c.position !== d.position
@@ -73,6 +82,9 @@ export function createDeepLock(base, {
if (afterSecond.some(tile => trace(boards[2], tile, afterSecond).status === 'exit')) continue;
if (trace(boards[1], hero, afterSecond).status !== 'exit') continue;
if (trace(boards[1], tail, [tail]).status !== 'exit') continue;
// Certify the two-sided release with the real swept tracer, not the
// cell-occupancy approximation the filter above uses.
if (![hero, tail].some(t => trace(boards[3], t, afterSecond).status === 'exit')) continue;
const level = { ...base, tiles, moveLimit: tiles.length };
const solution = solveRotations(level);
if (!solution.solvable || solution.exhausted || solution.rotations !== 3) continue;