@@ -386,19 +426,19 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v
05Agent 说了什么
评分是 1–5 的数字,worst 是它自己写的一句话。下面按反复出现的主题归类——价值在重复模式,不在单条内容。
-
擦碰看不出来 · 13 次提及
+
擦碰看不出来 · 30 次提及
L3 switchback(grok旧):开局 t2/t11 路上看着没人其实会擦到,差点空点;只剩 t0 和 t3 对堵时短暂以为卡住了
L6 weave(grok旧):t5走后(第9步)t2和t3都像能出界,其实互相擦碰,只能再转
L6 weave(grok新①):朝向1时 t2 和 t3 互相擦碰、t0 被 t2 压着,短暂不确定是再转还是还有能点的
L6 weave(grok新②):第9步清完后t2和t3互相擦挡,谁先走都不行,只能转
-
不知道该转哪边 · 9 次提及
+
不知道该转哪边 · 19 次提及
L1 switchback(grok新关):朝向3右转后没有新出口,t0和t3仍互相挡,短暂怀疑该不该改左转
L5 switchback(grok旧):第 13 步右转后朝向 3 没新出口,t0 和 t3 仍互相挡,不确定该不该继续同向转
L5 switchback(grok新①):第15步右转后朝向3没有新出口,最后两枚还互压,一度担心转错了方向
L5 switchback(spark旧):第 8-9 步清掉 t5,t4,t2 后只剩 t0/t3 互压,朝向2不再有出口,开始不确定该是继续走还是转哪边
-
长路线读不动 · 25 次提及
+
长路线读不动 · 32 次提及
L2 weave(grok新关):开局 t0 绕了 21 格还压着 6 枚,一眼看不出清障顺序,只能先点边上的 t4/t6
L3 switchback(kimi):第 10 步后 t0/t2/t3/t10 四枚互相压成环,盘面上看不出解法,只能靠旋转试出新朝向
L9 petals(grok新关):开局 t0/t3 各绕 19 格互相穿过,一时看不出该先剥哪层
@@ -409,7 +449,7 @@ footer{margin-top:64px;padding-top:22px;border-top:1px solid var(--rule);color:v
生成期硬门槛 · 零成本
用已有求解器,在生成时就卡掉:最少旋转 ≥ 2、开局锁住率 ≤ 90%、首转两个方向都能推进、无死局状态。不合格的根本不进候选池。
规则机器人全量扫 · 1000 关 12 秒
产出盲转 / 多转 / 转错方向。按 盲转 ≥ 2 或 多转 ≥ 2 打标。这条规则在 D5 上正好命中 orbits+spiral 那几关,和两个大模型跑两小时得到的结论一致。
-Agent 只跑被标记的 + 5% 随机抽样
不是 1000 关全跑。收集三样东西:difficulty(r=0.93,可用来给 1000 关排序)、stuck(r=0.85,体感无解的直接信号)、以及 worst 文字(重复模式能挖出系统性缺陷)。丢掉 rotation / clarity / fun——复测 r 只有 0.32 / 0.19 / 0.56,是模型噪声。按实测 36 s/关、4 路并行,150 关约半小时。
+Agent 只跑被标记的 + 5% 随机抽样
不是 1000 关全跑。收集三样东西:difficulty(r=0.93,可用来给 1000 关排序)、stuck(r=0.85,体感无解的直接信号)、以及 worst 文字(重复模式能挖出系统性缺陷)。丢掉 rotation / clarity / fun——复测 r 只有 0.32 / 0.19 / 0.56,是模型噪声。按实测 43 s/关、4 路并行,150 关约半小时。
真人标定锚点 · 5–10 关
Agent 的难度排序可信(复测 r=0.93),但绝对值可能整体偏移——它觉得「刚好」的关,真人可能觉得太简单。所以真人只需要玩少数几关当锚点,把 agent 的相对排序映射到真实难度刻度上,不必逐关评。这比原本设想的省很多人力。
diff --git a/scripts/build-bench-report.mjs b/scripts/build-bench-report.mjs
index 9c0326c..d6de301 100644
--- a/scripts/build-bench-report.mjs
+++ b/scripts/build-bench-report.mjs
@@ -18,13 +18,18 @@ const RUNS = resolve('bench/runs');
// ---------- data ----------
const set = JSON.parse(readFileSync(resolve('levels/prototypes/d5.json'), 'utf8'));
const levels = Array.isArray(set) ? set : set.levels;
-const meta = Object.fromEntries(levels.map((l, i) => [l.id, {
- n: i + 1,
+// Levels are keyed by id, never by position: regenerating the set renumbers
+// everything, and keying by ordinal silently dropped every run played against
+// an earlier set. Runs carry their own level, so ids seen only in the archive
+// still resolve.
+const describe = (l, i) => ({
+ n: i == null ? null : i + 1,
family: l.id.replace('proto-d5-', '').replace(/-r\d+-s\d+$/, ''),
lock: l.prototype ? Math.round(l.prototype.stats.lockedShare * 100) : null,
minRot: l.prototype ? l.prototype.stats.rotations : null,
cells: l.cells.length, tiles: l.tiles.length,
-}]));
+});
+const meta = Object.fromEntries(levels.map((l, i) => [l.id, describe(l, i)]));
const BATCHES = [
{ tag: 'Grok Build CLI · xhigh', label: 'grok-4.6 · 旧盘面', short: 'grok旧', format: 'old' },
@@ -44,12 +49,18 @@ const NEW_TAGS = ['pi · Grok 4.6 xhigh', 'pi · Grok 4.6 xhigh · 复测', 'Kim
const runs = readdirSync(RUNS).filter(f => f.endsWith('.json') && f !== 'index.json')
.map(f => { try { return JSON.parse(readFileSync(resolve(RUNS, f), 'utf8')); } catch { return null; } })
- .filter(Boolean).filter(d => meta[d.level?.id]);
+ .filter(Boolean).filter(d => d.level?.id?.startsWith('proto-d5-'));
+// Levels retired by a regeneration still have runs worth comparing.
+let retired = 0;
+for (const d of runs) if (!meta[d.level.id]) { meta[d.level.id] = describe(d.level, null); retired++; }
+// Give every level a stable ordinal: current set first, retired ones after.
+let ord = levels.length;
+for (const k of Object.keys(meta)) if (meta[k].n == null) meta[k].n = ++ord;
const byTag = {};
for (const b of BATCHES) {
const rows = {};
- for (const d of runs) if (d.agent === b.tag) rows[meta[d.level.id].n] = { m: metrics(d), lv: meta[d.level.id], run: d.run };
+ for (const d of runs) if (d.agent === b.tag) rows[d.level.id] = { m: metrics(d), lv: meta[d.level.id], run: d.run, id: d.level.id };
byTag[b.tag] = rows;
}
const present = BATCHES.filter(b => Object.keys(byTag[b.tag]).length > 0);
@@ -82,7 +93,7 @@ const METRICS = [
function pair(tagA, tagB) {
const A = byTag[tagA] || {}, B = byTag[tagB] || {};
- const shared = Object.keys(A).filter(k => B[k]).map(Number).sort((a, b) => a - b);
+ const shared = Object.keys(A).filter(k => B[k]).sort((x, y) => meta[x].n - meta[y].n);
return { shared, A, B };
}
@@ -184,9 +195,9 @@ const THEMES = [
];
function notes() {
const outp = [];
- for (const b of present) for (const [L, r] of Object.entries(byTag[b.tag])) {
+ for (const b of present) for (const r of Object.values(byTag[b.tag])) {
const w = r.m.rating?.worst; if (!w) continue;
- outp.push({ batch: b.short, L: Number(L), fam: r.lv.family, text: String(w) });
+ outp.push({ batch: b.short, L: r.lv.n, fam: r.lv.family, text: String(w) });
}
return outp.sort((a, b) => a.L - b.L);
}
@@ -203,11 +214,12 @@ const clearRateNote = allCleared
const liveTags = NEW_TAGS.filter(t => Object.keys(byTag[t] || {}).length);
const triple = m => [m.rotations - m.minimumRotations, m.blindRotations, m.wrongWayRotations].join('/');
const crossRows = [];
-for (let L = 1; L <= levels.length; L++) {
- const cells = liveTags.map(t => byTag[t][L] ? triple(byTag[t][L].m) : null);
- if (cells.some(c => c == null)) continue;
- const lv = Object.values(meta).find(x => x.n === L);
- crossRows.push({ L, family: lv.family, cells, agree: new Set(cells).size === 1 });
+const crossIds = [...new Set(liveTags.flatMap(t => Object.keys(byTag[t] || {})))]
+ .filter(id => liveTags.every(t => byTag[t]?.[id]))
+ .sort((a, b) => meta[a].n - meta[b].n);
+for (const id of crossIds) {
+ const cells = liveTags.map(t => triple(byTag[t][id].m));
+ crossRows.push({ L: meta[id].n, id, family: meta[id].family, cells, agree: new Set(cells).size === 1 });
}
const agreeN = crossRows.filter(r => r.agree).length;
const crossModel = crossRows.length < 3 ? '
同盘面批次不足,跨模型比较尚未成立。
' : `