diff --git a/docs/review-50.html b/docs/review-50.html
index 9bd16a8..19b259d 100644
--- a/docs/review-50.html
+++ b/docs/review-50.html
@@ -62,25 +62,25 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
五十关 · 人工审核
缩略图是开局盘面:绿色的棋子开局就能点,灰色的被挡着。点「试玩」用真游戏打开这一关。
-
关卡集 levels/campaign-50.json · agent 实测批次 R2 · grok xhigh(50/50 关已评)· 生成于 2026-09-14 17:14
+
关卡集 levels/campaign-50.json · agent 实测批次 R2 · grok xhigh(50/50 关已评)· 生成于 2026-09-14 17:16
难度曲线(agent 实测)
| 档 | 已评关数 | 难度 | 无解感 | 盲转/关 | 空点/关 |
-| 前档 L1–L12 | 12 | 2.0 | 1.1 | 0.2 | 0.0 |
+| 前档 L1–L12 | 12 | 2.0 | 1.2 | 0.2 | 0.0 |
| 中档 L13–L32 | 20 | 2.1 | 1.2 | 0.0 | 0.0 |
| 后档 L33–L50 | 18 | 2.4 | 1.7 | 0.2 | 0.0 |
-
难度 3 = 刚刚好;无解感越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85),其余几项是模型噪声,不列。
+
难度 3 = 刚刚好;无解感越低越好。这两项只在按档平均时可读:逐关复测 r = 0.55 / 0.45(本轮 50 对复测现算),好玩与清晰度更低,不列。三档单调上升是这一轮的结果,逐关的高低不要当真。
这批数字里,哪些能信
机械面是干净的,可以信。agent 跑完 50 关:空点 0.0 次/关、盲转 0.1 次/关、反向转 0.1 次/关,50/50 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。
-
难度评分不能信,它区分不了这批关卡。50 关里有 41 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者直接列成清单,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。
-
所以难度顺序是机械假设,等你的判定。L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分唯一能用的是它的总体读数:整批偏易一档(2 分 vs 刚刚好的 3 分)。
-
两件事我没有擅自改,留给你定。agent 的 33 条抱怨里反复出现两个主题:路线太长看不完(「68 格 27 弯」「42 格 42 弯」),以及紧贴容易看漏(「看着能出界却会擦到」)。两项都是感知问题,而已上线的 v0.5 在这两项上更严重(最长路线 88 格 vs 这里 74 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。
+
难度评分不能信,它区分不了这批关卡。50 关里有 42 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者直接列成清单,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。
+
所以逐关顺序是机械假设,等你的判定。L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分能用的只有整档:这一轮三档是 2.0 / 2.1 / 2.4,第一次单调上升,但后档 2.4 仍低于「刚刚好」的 3 分。
+
两件事我没有擅自改,留给你定。agent 这一轮写了 100 条抱怨,反复出现两个主题:路线太长看不完(「开局 t0 那条 42 格弯路太长,挡谁只能靠摘要判断」「开局t0那条54格路线太长,真正有用的只有路上挡着谁这一句」),以及紧贴容易看漏(「朝向1时t5路上没人却紧贴t4和t6,长直道擦碰差点误点」「第14步转到朝向2,t0看起来能出界但会擦到t2,差点误点」)。两项都是感知问题,而已上线的 v0.5 在这两项上更严重(最长路线 88 格 vs 这里 74 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。
逐关
@@ -216,8 +216,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 —无解感 —17 步7 决策
-
+
难度 2无解感 217 步7 决策
+
第14步转到朝向2,t0看起来能出界但会擦到t2,差点误点
试玩 →
@@ -641,8 +641,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 2无解感 213 步8 决策
-
朝向1点完t1后剩下四枚两两对堵,超长轨道绕几十格,一时分不清是还没清干净还是该转
+
难度 2无解感 113 步7 决策
+
开局长轨道绕三十多格,t1和t2对挡时要扫一遍才确定一枚都走不了、只能转
试玩 →
diff --git a/scripts/build-review-50.mjs b/scripts/build-review-50.mjs
index a30f79a..4ce37b5 100644
--- a/scripts/build-review-50.mjs
+++ b/scripts/build-review-50.mjs
@@ -45,6 +45,7 @@ if (existsSync(RUNS)) {
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 f2 = x => x == null || Number.isNaN(x) ? '—' : (Math.round(x * 100) / 100).toFixed(2);
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
@@ -81,8 +82,45 @@ const perceptual = Object.fromEntries(levels.map(l => [l.id, {
...legibility(l),
}]));
+// This round played most levels twice, so reliability is measured here rather
+// than quoted: an older, smaller sample had difficulty at r = 0.93, and on this
+// set it is not that.
+const runsByBoard = new Map();
+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.status === 'playing' || !d.level?.tiles) continue;
+ const k = deepLevelKey(d.level);
+ if (!boardKey.has(k)) continue;
+ if (!runsByBoard.has(k)) runsByBoard.set(k, []);
+ runsByBoard.get(k).push(metrics(d));
+ } catch { /* torn read */ }
+ }
+}
+const retestPairs = [...runsByBoard.values()].filter(v => v.length >= 2).map(v => [v[0], v[1]]);
+// Quote the round's own complaints rather than the previous round's, which were
+// written about levels this edition no longer contains.
+const complaints = [...runsByBoard.values()].flat().map(m => String(m.rating?.worst ?? '').trim()).filter(Boolean);
+const pickQuotes = re => [...new Set(complaints.filter(t => re.test(t)))]
+ .sort((a, b) => a.length - b.length).slice(0, 2).map(t => t.length > 34 ? t.slice(0, 34) + '…' : t);
+const longQuotes = pickQuotes(/\d{2,}\s*格|太长|读不完|跟不住|绕/);
+const grazeQuotes = pickQuotes(/紧贴|擦/);
+const sd = a => { const m = mean(a); return Math.sqrt(mean(a.map(v => (v - m) ** 2))); };
+const retest = get => {
+ const ok = retestPairs.filter(p => Number.isFinite(get(p[0])) && Number.isFinite(get(p[1])));
+ if (ok.length < 8) return null;
+ const A = ok.map(p => get(p[0])), B = ok.map(p => get(p[1]));
+ const sa = sd(A), sb = sd(B);
+ if (!sa || !sb) return null;
+ const ma = mean(A), mb = mean(B);
+ return mean(A.map((v, i) => (v - ma) * (B[i] - mb))) / (sa * sb);
+};
+
const rated = levels.map(l => verdicts[l.id]).filter(Boolean);
const band = (a, b) => levels.slice(a, b).map(l => verdicts[l.id]).filter(Boolean);
+const BANDS = [['前档 L1–L12', 0, 12], ['中档 L13–L32', 12, 32], ['后档 L33–L50', 32, 50]];
const bandRow = (label, a, b) => {
const rows = band(a, b);
const g = k => mean(rows.map(m => Number(m.rating?.[k])).filter(x => !Number.isNaN(x)));
@@ -164,14 +202,14 @@ ${bandRow('中档 L13–L32', 12, 32)}
${bandRow('后档 L33–L50', 32, 50)}
-难度 3 = 刚刚好;无解感越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85),其余几项是模型噪声,不列。
+难度 3 = 刚刚好;无解感越低越好。这两项只在按档平均时可读:逐关复测 r = ${f2(retest(m => Number(m.rating?.difficulty)))} / ${f2(retest(m => Number(m.rating?.stuck)))}(本轮 ${retestPairs.length} 对复测现算),好玩与清晰度更低,不列。三档单调上升是这一轮的结果,逐关的高低不要当真。
这批数字里,哪些能信
机械面是干净的,可以信。agent 跑完 ${rated.length} 关:空点 ${f1(mean(rated.map(m => m.deadTaps)))} 次/关、盲转 ${f1(mean(rated.map(m => m.blindRotations)))} 次/关、反向转 ${f1(mean(rated.map(m => m.wrongWayRotations)))} 次/关,${rated.filter(m => m.cleared).length}/${rated.length} 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。
难度评分不能信,它区分不了这批关卡。${rated.length} 关里有 ${rated.filter(m => Number(m.rating?.difficulty) === 2).length} 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者直接列成清单,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。
-
所以难度顺序是机械假设,等你的判定。L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分唯一能用的是它的总体读数:整批偏易一档(2 分 vs 刚刚好的 3 分)。
-
两件事我没有擅自改,留给你定。agent 的 33 条抱怨里反复出现两个主题:路线太长看不完(「68 格 27 弯」「42 格 42 弯」),以及紧贴容易看漏(「看着能出界却会擦到」)。两项都是感知问题,而已上线的 v0.5 在这两项上更严重(最长路线 88 格 vs 这里 ${Math.max(...Object.values(perceptual).map(p => p.longest))} 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。
+
所以逐关顺序是机械假设,等你的判定。L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分能用的只有整档:这一轮三档是 ${BANDS.map(([, a, b]) => f1(mean(levels.slice(a, b).map(l => verdicts[l.id]).filter(Boolean).map(m => Number(m.rating?.difficulty)).filter(Number.isFinite)))).join(' / ')},第一次单调上升,但后档 ${f1(mean(levels.slice(32).map(l => verdicts[l.id]).filter(Boolean).map(m => Number(m.rating?.difficulty)).filter(Number.isFinite)))} 仍低于「刚刚好」的 3 分。
+
两件事我没有擅自改,留给你定。agent 这一轮写了 ${complaints.length} 条抱怨,反复出现两个主题:路线太长看不完(${longQuotes.map(q => `「${esc(q)}」`).join('') || '—'}),以及紧贴容易看漏(${grazeQuotes.map(q => `「${esc(q)}」`).join('') || '—'})。两项都是感知问题,而已上线的 v0.5 在这两项上更严重(最长路线 88 格 vs 这里 ${Math.max(...Object.values(perceptual).map(p => p.longest))} 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。
逐关