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:
@@ -34,6 +34,7 @@ const BATCHES = [
|
||||
{ tag: 'Codex CLI · gpt-6-astra low', label: 'gpt-6-astra · low · 新盘面', short: 'astra', format: 'new' },
|
||||
{ tag: 'Codex CLI · spark xhigh', label: 'codex-spark · xhigh · 新盘面', short: 'spark新', format: 'new' },
|
||||
{ tag: 'Kimi CLI · k3 high', label: 'kimi-k3 · high · 新盘面', short: 'kimi', format: 'new' },
|
||||
{ tag: 'pi · Grok 4.6 xhigh · 新门槛', label: 'grok-4.6 · 新关卡集(终局门槛后)', short: 'grok新关', format: 'new' },
|
||||
];
|
||||
|
||||
// Batches run under the fixed board format, i.e. everything that can be
|
||||
|
||||
@@ -65,9 +65,17 @@ for (const { id: family } of ROUTE_FAMILIES) for (const radius of RADII) {
|
||||
// locked opening, a first turn that only works one way, or any stuck state
|
||||
// where neither adjacent orientation opens anything.
|
||||
const grownEnough = rows.filter(r => r.stats.pieces >= MIN_PIECES);
|
||||
const playable = grownEnough.filter(r => r.audit.unsolvable === 0 && r.stats.firstTurnDirections === 2 && r.stats.lockedShare <= .9);
|
||||
// The endgame gate: with three pieces or fewer left, at least two of the six
|
||||
// orientations must release something. One is a 1-in-6 guess with a dead end
|
||||
// behind every other door — solvable, legible, and still the thing that made
|
||||
// an agent give up. It rejects exactly the levels every model stalled on.
|
||||
const playable = grownEnough.filter(r => r.audit.unsolvable === 0 && r.stats.firstTurnDirections === 2
|
||||
&& r.stats.lockedShare <= .9 && r.audit.endgameMinOpen >= 2);
|
||||
playable.sort((a, b) => (a.audit.doubleDead - b.audit.doubleDead) || (b.stats.minRelease - a.stats.minRelease) || (b.stats.lockedShare - a.stats.lockedShare) || (a.apt - b.apt));
|
||||
console.log(`\n【可玩性门槛】长成 ${grownEnough.length} 关 → 首转双向 ${grownEnough.filter(r => r.stats.firstTurnDirections === 2).length} → 开局锁住 ≤90% ${grownEnough.filter(r => r.stats.firstTurnDirections === 2 && r.stats.lockedShare <= .9).length} → 无不可解可达态 ${playable.length}`);
|
||||
const g1 = grownEnough.filter(r => r.stats.firstTurnDirections === 2);
|
||||
const g2 = g1.filter(r => r.stats.lockedShare <= .9);
|
||||
const g3 = g2.filter(r => r.audit.unsolvable === 0);
|
||||
console.log(`\n【可玩性门槛】长成 ${grownEnough.length} 关 → 首转双向 ${g1.length} → 开局锁住 ≤90% ${g2.length} → 无不可解可达态 ${g3.length} → 终局可走朝向 ≥2 ${playable.length}`);
|
||||
const kept = [];
|
||||
const perFamily = {};
|
||||
for (const row of playable) {
|
||||
@@ -91,8 +99,8 @@ console.log(` 每次旋转最少释放 ≥2 枚的关: ${all.filter(s => s.minR
|
||||
console.log(` 首转只有一侧能开路的关: ${all.filter(s => s.firstTurnDirections === 1).length}/${all.length}`);
|
||||
console.log(` 猴子 动作/棋子 中位 ${med(rows.map(r => r.apt)).toFixed(2)} (v0.5 全库 1.67 · v0.4 手作 3.42)`);
|
||||
console.log(`\n【写入 ${kept.length} 关】`);
|
||||
console.log(' id 格 枚 R 锁住% 每转释放 首转方向 双向死转态 最长盲转 猴子APT hero弯');
|
||||
for (const { level, stats, audit, apt } of kept) console.log(' ' + level.id.padEnd(36) + pad(level.cells.length, 3) + pad(stats.pieces, 4) + pad(stats.rotations, 3) + pad((stats.lockedShare * 100).toFixed(0) + '%', 7) + ' ' + stats.releasePerTurn.join('/').padEnd(12) + pad(stats.firstTurnDirections === 1 ? '单向' : '双向', 6) + pad(audit.doubleDead, 10) + pad(audit.maxDeadTurns, 9) + pad(apt.toFixed(2), 9) + pad(level.prototype.lock.heroBends, 7));
|
||||
console.log(' id 格 枚 R 锁住% 每转释放 首转方向 双向死转态 最长盲转 终局路口 猴子APT hero弯');
|
||||
for (const { level, stats, audit, apt } of kept) console.log(' ' + level.id.padEnd(36) + pad(level.cells.length, 3) + pad(stats.pieces, 4) + pad(stats.rotations, 3) + pad((stats.lockedShare * 100).toFixed(0) + '%', 7) + ' ' + stats.releasePerTurn.join('/').padEnd(12) + pad(stats.firstTurnDirections === 1 ? '单向' : '双向', 6) + pad(audit.doubleDead, 10) + pad(audit.maxDeadTurns, 9) + pad(audit.endgameMinOpen, 9) + pad(apt.toFixed(2), 9) + pad(level.prototype.lock.heroBends, 7));
|
||||
console.log('\n 「双向死转态」= 可达的卡住状态里,左右各转一次都打不开任何棋子的状态数;「最长盲转」= 从某个可达卡住态朝错误方向连转、遇到第一个有效朝向前经过的空朝向数。');
|
||||
|
||||
mkdirSync(dirname(OUT), { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user