#!/usr/bin/env node // Build docs/level-board.html — the task board for "50 good levels". // // node scripts/build-board.mjs // // Task state lives in bench/board.json and is edited by hand as work moves. // Everything else on the page is measured live from the level set and the run // archive, so the board cannot claim progress the artifacts do not show. import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { solveRotations } from '../src/core.js'; import { metrics } from '../src/bench.js'; import { deadTurnAudit } from '../src/deep-lock.js'; const board = JSON.parse(readFileSync(resolve('bench/board.json'), 'utf8')); const OUT = resolve('docs/level-board.html'); const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const f1 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 10) / 10).toFixed(1); const f2 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 100) / 100).toFixed(2); const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null; // ---------- live measurement ---------- const SET = resolve('levels/prototypes/d5.json'); let pool = { n: 0, rot: {}, apt: null, families: {}, blind: null, wrong: null, endgame: {} }; if (existsSync(SET)) { const raw = JSON.parse(readFileSync(SET, 'utf8')); const levels = Array.isArray(raw) ? raw : raw.levels; pool.n = levels.length; for (const l of levels) { const r = l.prototype?.stats?.rotations ?? solveRotations(l).rotations; pool.rot[r] = (pool.rot[r] || 0) + 1; const fam = l.id.replace('proto-d5-', '').replace(/-r\d+-s\d+$/, ''); pool.families[fam] = (pool.families[fam] || 0) + 1; const a = l.prototype?.audit ?? deadTurnAudit(l); pool.endgame[a.endgameMinOpen] = (pool.endgame[a.endgameMinOpen] || 0) + 1; } pool.apt = mean(levels.map(l => l.prototype?.monkeyActionsPerPiece).filter(x => typeof x === 'number')); } // Latest rule-bot sweep over whatever the current set is. const RUNS = resolve('bench/runs'); const runsBy = tagTest => { if (!existsSync(RUNS)) return []; const out = []; for (const f of readdirSync(RUNS)) { if (!f.endsWith('.json') || f === 'index.json') continue; try { const d = JSON.parse(readFileSync(resolve(RUNS, f), 'utf8')); if (tagTest(d, f)) out.push(metrics(d)); } catch { /* torn read */ } } return out; }; const bot = runsBy((d, f) => f.startsWith('restruct-')); if (bot.length) { pool.blind = mean(bot.map(m => m.blindRotations)); pool.wrong = mean(bot.map(m => m.wrongWayRotations)); } // Agent batches, newest label wins. const agentBatches = {}; for (const f of (existsSync(RUNS) ? readdirSync(RUNS) : [])) { if (!f.endsWith('.json') || f === 'index.json') continue; try { const d = JSON.parse(readFileSync(resolve(RUNS, f), 'utf8')); if (!d.agent || !d.rating) continue; (agentBatches[d.agent] ||= []).push(metrics(d)); } catch { /* torn read */ } } const TASKS = board.phases.flatMap(p => p.tasks); const counts = TASKS.reduce((m, t) => (m[t.status] = (m[t.status] || 0) + 1, m), {}); const doneN = counts.done || 0; const pct = Math.round(doneN / TASKS.length * 100); const STATUS = { done: ['完成', 'good'], doing: ['进行中', 'live'], todo: ['待办', 'idle'], blocked: ['受阻', 'bad'], }; const depthRows = Object.entries(pool.rot).sort((a, b) => Number(a[0]) - Number(b[0])); const html = `
| 最少旋转数 | 候选关数 | 说明 |
|---|---|---|
| ${r} 转 | ${n} | ${Number(r) <= 3 ? '浅' : Number(r) <= 5 ? '中' : '深'} |
| 旋转深度没有梯度——生成器把最少旋转写死了,这正是「太简单」的根因 | ||
路线家族分布:${Object.entries(pool.families).map(([k, v]) => `${esc(k)} ×${v}`).join(' · ') || '—'} 终局可走朝向分布:${JSON.stringify(pool.endgame)}
${esc(board.definition.why)}
| 关号区间 | 旋转数 | 目标 difficulty 评分 |
|---|---|---|
| ${esc(b.band)} | ${esc(b.rot)} | ${esc(b.difficulty)} |
| 轮 | 改了什么 | 测出什么 |
|---|---|---|
| ${r.n} | ${esc(r.change)} | ${esc(r.result)} |
| 批次 | 关数 | difficulty | stuck | 盲转/关 | 多转/关 |
|---|---|---|---|---|---|
| ${esc(tag)} | ${ms.length} | ${f2(g('difficulty'))} | ${f2(g('stuck'))} | ${f2(mean(ms.map(m => m.blindRotations)))} | ${f2(mean(ms.map(m => m.rotations - m.minimumRotations)))} |
difficulty 3 = 刚刚好;stuck 越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85)。
` : ''}