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:
+17
-5
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user