feat(levels): reject one-orientation endgames, and measure the result
Every model that played the D5 set complained about the same moment: two pieces left, interlocked, and nothing on the board saying which way to turn. codex-spark took it to the end — it cycled all six orientations on L18 and gave up on a level that was solvable, with clarity rated 5. Replaying the position shows exactly one of the six orientations releases anything. deadTurnAudit now reports endgameMinOpen: across reachable states with three pieces or fewer left, the fewest orientations that release anything. It is computed inside the walk that was already happening, so it costs nothing. The generator requires at least two. The gate picks out precisely the four levels every model stalled on, with no false positives — the other fourteen already had two or three. Regenerating drops the spiral family entirely: its structure produces single-exit endgames by construction, not by unlucky seed. Measured on the regenerated set, rule bot first and then grok-4.6 xhigh over all 18: blind rotations 1.17 -> 0.29 per level extra rotations 1.22 -> 0.35 dead taps 2 -> 0 run time 55.5s -> 34.7s stuck rating 1.89 -> 1.41 (this metric repeats at r = 0.85) "which way do I turn" complaints 22% -> 12% of levels The gate is a floor, not a difficulty dial: difficulty also fell, 2.39 -> 2.12, because the levels it removes are the hard ones. And it treats a symptom — the t0/t3 interlock is what the four-piece chain in createDeepLock builds by construction, so the complaint is halved rather than gone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+26
-1
@@ -179,15 +179,38 @@ export function deepLevelStats(level) {
|
||||
// sequence). Removal only reduces occupancy and turns are reversible, so no
|
||||
// reachable state can be unsolvable — the audit proves it rather than assuming
|
||||
// it — and it measures how many blind turns a player can run into.
|
||||
// A level is judged on its endgame with this many pieces or fewer left.
|
||||
const ENDGAME_PIECES = 3;
|
||||
|
||||
export function deadTurnAudit(level, maxStates = 20000) {
|
||||
const boards = Array.from({ length: 6 }, (_, i) => rotateBoard(level, i));
|
||||
const keyOf = (o, rem) => o + ':' + [...rem].sort().join(',');
|
||||
const start = { o: 0, rem: new Set(level.tiles.map(t => t.id)) };
|
||||
const seen = new Map([[keyOf(0, start.rem), start]]), queue = [start];
|
||||
let stuckStates = 0, doubleDead = 0, maxDeadTurns = 0, unsolvable = 0;
|
||||
// The endgame is where every model complained: two pieces left, nothing on
|
||||
// the board saying which way to turn. Count how many of the six orientations
|
||||
// release anything at all — one is a 1-in-6 guess with a dead end behind
|
||||
// every other door, which is what made an agent give up on a solvable level.
|
||||
const openCache = new Map();
|
||||
let endgameMinOpen = 6, endgameTightStates = 0, endgameWorst = null;
|
||||
const openOrientations = rem => {
|
||||
const k = [...rem].sort().join(',');
|
||||
if (openCache.has(k)) return openCache.get(k);
|
||||
const tiles = level.tiles.filter(t => rem.has(t.id));
|
||||
let n = 0;
|
||||
for (let o = 0; o < 6; o++) if (available(boards[o], tiles).length) n++;
|
||||
openCache.set(k, n);
|
||||
return n;
|
||||
};
|
||||
while (queue.length && seen.size < maxStates) {
|
||||
const state = queue.shift();
|
||||
if (!state.rem.size) continue;
|
||||
if (state.rem.size <= ENDGAME_PIECES) {
|
||||
const open = openOrientations(state.rem);
|
||||
if (open <= 1) endgameTightStates++;
|
||||
if (open < endgameMinOpen) { endgameMinOpen = open; endgameWorst = [...state.rem].sort().join(','); }
|
||||
}
|
||||
const tiles = level.tiles.filter(t => state.rem.has(t.id)), free = available(boards[state.o], tiles);
|
||||
if (!free.length) {
|
||||
stuckStates++;
|
||||
@@ -206,7 +229,9 @@ export function deadTurnAudit(level, maxStates = 20000) {
|
||||
if (!seen.has(k)) { const next = { o, rem: state.rem }; seen.set(k, next); queue.push(next); }
|
||||
}
|
||||
}
|
||||
return { states: seen.size, stuckStates, doubleDead, maxDeadTurns, unsolvable, exhausted: seen.size >= maxStates };
|
||||
return { states: seen.size, stuckStates, doubleDead, maxDeadTurns, unsolvable,
|
||||
endgameMinOpen: endgameMinOpen === 6 && !openCache.size ? null : endgameMinOpen,
|
||||
endgameTightStates, endgameWorst, exhausted: seen.size >= maxStates };
|
||||
}
|
||||
|
||||
export const deepLevelKey = level => level.tiles.map(t => `${t.q},${t.r},${t.dir}`).sort().join('|');
|
||||
|
||||
Reference in New Issue
Block a user