#!/usr/bin/env node
// Build docs/review-50.html — the sheet the 50 levels are handed over on.
//
// node scripts/build-review-50.mjs [--agent="R1 · grok xhigh"]
//
// One card per level: the board as it opens, what the construction guarantees,
// and what the agent that played it reported. Every card links into the real
// game at that level, because a level is judged by playing it.
import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { trace, rotateBoard } from '../src/core.js';
import { layout } from '../src/geometry.js';
import { metrics } from '../src/bench.js';
const args = Object.fromEntries(process.argv.slice(2).map(a => { const [k, ...v] = a.replace(/^--/, '').split('='); return [k, v.length ? v.join('=') : true]; }));
const AGENT = args.agent ?? 'R1 · grok xhigh';
const OUT = resolve(args.out ?? 'docs/review-50.html');
const raw = JSON.parse(readFileSync(resolve('levels/campaign-50.json'), 'utf8'));
const levels = raw.levels;
// Agent verdicts, keyed by level id.
const verdicts = {};
const RUNS = resolve('bench/runs');
if (existsSync(RUNS)) {
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 (d.agent !== AGENT || !d.level?.id) continue;
verdicts[d.level.id] = metrics(d);
} catch { /* torn read */ }
}
}
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null;
const f1 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 10) / 10).toFixed(1);
// A flat SVG of the opening position: tracks in grey, pieces as arrows. Small
// enough to read fifty of them on one page.
const ARROWS = ['↑', '↗', '↘', '↓', '↙', '↖'];
function thumbnail(level, size = 168) {
const view = layout(level.cells, size, size);
const { size: r, ox, oy } = view;
const px = (q, rr) => ({ x: ox + r * 1.5 * q, y: oy + r * Math.sqrt(3) * (rr + q / 2) });
const cells = level.cells.map(c => {
const { x, y } = px(c.q, c.r);
const pts = Array.from({ length: 6 }, (_, i) => {
const a = Math.PI / 180 * (60 * i);
return `${(x + r * Math.cos(a)).toFixed(1)},${(y + r * Math.sin(a)).toFixed(1)}`;
}).join(' ');
return `
缩略图是开局盘面:绿色的棋子开局就能点,灰色的被挡着。点「试玩」用真游戏打开这一关。
| 档 | 已评关数 | 难度 | 无解感 | 盲转/关 | 空点/关 |
|---|