docs(review): measure the sheet's own caveats instead of quoting old ones

The sheet told the reviewer that difficulty and stuck were "the only two
ratings that retest reliably (r = 0.93 / 0.85)". On this set, with 50 retest
pairs to measure against, they are 0.55 and 0.45 — so the line was telling the
reviewer to trust the one number that had just failed. It now computes both
from the round in hand and says the ratings are readable by band only.

The complaints it quoted were round 1's, written about levels this edition no
longer contains. They are pulled from the round's own reports now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
cabinw
2026-09-14 17:16:20 +08:00
co-authored by Claude Opus 5
parent 6af7277179
commit f6e92b05fb
2 changed files with 51 additions and 13 deletions
+10 -10
View File
@@ -62,25 +62,25 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
<div class="wrap"> <div class="wrap">
<h1>五十关 · 人工审核</h1> <h1>五十关 · 人工审核</h1>
<p class="lede">缩略图是<b>开局盘面</b>:绿色的棋子开局就能点,灰色的被挡着。点「试玩」用真游戏打开这一关。</p> <p class="lede">缩略图是<b>开局盘面</b>:绿色的棋子开局就能点,灰色的被挡着。点「试玩」用真游戏打开这一关。</p>
<p class="meta mono">关卡集 levels/campaign-50.json · agent 实测批次 R2 · grok xhigh(50/50 关已评)· 生成于 2026-09-14 17:14</p> <p class="meta mono">关卡集 levels/campaign-50.json · agent 实测批次 R2 · grok xhigh(50/50 关已评)· 生成于 2026-09-14 17:16</p>
<h2>难度曲线(agent 实测)</h2> <h2>难度曲线(agent 实测)</h2>
<table> <table>
<thead><tr><th>档</th><th class="n">已评关数</th><th class="n">难度</th><th class="n">无解感</th><th class="n">盲转/关</th><th class="n">空点/关</th></tr></thead> <thead><tr><th>档</th><th class="n">已评关数</th><th class="n">难度</th><th class="n">无解感</th><th class="n">盲转/关</th><th class="n">空点/关</th></tr></thead>
<tbody> <tbody>
<tr><td>前档 L1–L12</td><td class="n">12</td><td class="n">2.0</td><td class="n">1.1</td><td class="n">0.2</td><td class="n">0.0</td></tr> <tr><td>前档 L1–L12</td><td class="n">12</td><td class="n">2.0</td><td class="n">1.2</td><td class="n">0.2</td><td class="n">0.0</td></tr>
<tr><td>中档 L13–L32</td><td class="n">20</td><td class="n">2.1</td><td class="n">1.2</td><td class="n">0.0</td><td class="n">0.0</td></tr> <tr><td>中档 L13–L32</td><td class="n">20</td><td class="n">2.1</td><td class="n">1.2</td><td class="n">0.0</td><td class="n">0.0</td></tr>
<tr><td>后档 L33–L50</td><td class="n">18</td><td class="n">2.4</td><td class="n">1.7</td><td class="n">0.2</td><td class="n">0.0</td></tr> <tr><td>后档 L33–L50</td><td class="n">18</td><td class="n">2.4</td><td class="n">1.7</td><td class="n">0.2</td><td class="n">0.0</td></tr>
</tbody> </tbody>
</table> </table>
<p class="meta">难度 3 = 刚刚好;无解感越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85),其余几项是模型噪声,不列。</p> <p class="meta">难度 3 = 刚刚好;无解感越低越好。这两项只在<b>按档平均</b>时可读:逐关复测 r = 0.55 / 0.45(本轮 50 对复测现算),好玩与清晰度更低,不列。三档单调上升是这一轮的结果,逐关的高低不要当真。</p>
<h2>这批数字里,哪些能信</h2> <h2>这批数字里,哪些能信</h2>
<div class="note"> <div class="note">
<p><b>机械面是干净的,可以信。</b>agent 跑完 50 关:空点 0.0 次/关、盲转 0.1 次/关、反向转 0.1 次/关,50/50 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。</p> <p><b>机械面是干净的,可以信。</b>agent 跑完 50 关:空点 0.0 次/关、盲转 0.1 次/关、反向转 0.1 次/关,50/50 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。</p>
<p><b>难度评分不能信,它区分不了这批关卡。</b>50 关里有 41 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者<b>直接列成清单</b>,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。</p> <p><b>难度评分不能信,它区分不了这批关卡。</b>50 关里有 42 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者<b>直接列成清单</b>,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。</p>
<p><b>所以难度顺序是机械假设,等你的判定。</b>L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分唯一能用的是它的<b>总体读数</b>:整批偏易一档(2 分 vs 刚刚好的 3 分)。</p> <p><b>所以逐关顺序是机械假设,等你的判定。</b>L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分能用的只有<b>整档</b>:这一轮三档是 2.0 / 2.1 / 2.4,第一次单调上升,但后档 2.4 仍低于「刚刚好」的 3 分。</p>
<p><b>两件事我没有擅自改,留给你定。</b>agent 的 33 条抱怨里反复出现两个主题:路线太长看不完(「68 格 27 弯」「42 格 42 弯」),以及紧贴容易看漏(「看着能出界却会擦到」)。两项都是感知问题,而<b>已上线的 v0.5 在这两项上更严重</b>(最长路线 88 格 vs 这里 74 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。</p> <p><b>两件事我没有擅自改,留给你定。</b>agent 这一轮写了 100 条抱怨,反复出现两个主题:<b>路线太长看不完</b>(「开局 t0 那条 42 格弯路太长,挡谁只能靠摘要判断」「开局t0那条54格路线太长,真正有用的只有路上挡着谁这一句」),以及<b>紧贴容易看漏</b>(「朝向1时t5路上没人却紧贴t4和t6,长直道擦碰差点误点」「第14步转到朝向2,t0看起来能出界但会擦到t2,差点误点」)。两项都是感知问题,而<b>已上线的 v0.5 在这两项上更严重</b>(最长路线 88 格 vs 这里 74 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。</p>
</div> </div>
<h2>逐关</h2> <h2>逐关</h2>
@@ -216,8 +216,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
<span>紧贴棋子</span><b class="">0 枚</b> <span>紧贴棋子</span><b class="">0 枚</b>
</div> </div>
<div class="verdict"> <div class="verdict">
<div class="row"><span>难度 —</span><span>无解感 —</span><span>17 步</span><span>7 决策</span></div> <div class="row"><span>难度 2</span><span>无解感 2</span><span>17 步</span><span>7 决策</span></div>
<div class="worst">第14步转到朝向2,t0看起来能出界但会擦到t2,差点误点</div>
</div> </div>
<a class="play" href="../index.html?campaign=c50&level=8">试玩 →</a> <a class="play" href="../index.html?campaign=c50&level=8">试玩 →</a>
</div> </div>
@@ -641,8 +641,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
<span>紧贴棋子</span><b class="">0 枚</b> <span>紧贴棋子</span><b class="">0 枚</b>
</div> </div>
<div class="verdict"> <div class="verdict">
<div class="row"><span>难度 2</span><span>无解感 2</span><span>13 步</span><span>8 决策</span></div> <div class="row"><span>难度 2</span><span>无解感 1</span><span>13 步</span><span>7 决策</span></div>
<div class="worst">朝向1点完t1后剩下四枚两两对堵,超长轨道绕几十格,一时分不清是还没清干净还是该转</div> <div class="worst">开局长轨道绕三十多格,t1和t2对挡时要扫一遍才确定一枚都走不了、只能转</div>
</div> </div>
<a class="play" href="../index.html?campaign=c50&level=33">试玩 →</a> <a class="play" href="../index.html?campaign=c50&level=33">试玩 →</a>
</div> </div>
+41 -3
View File
@@ -45,6 +45,7 @@ if (existsSync(RUNS)) {
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c])); const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null; 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); 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 // 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), ...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 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 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 bandRow = (label, a, b) => {
const rows = band(a, b); const rows = band(a, b);
const g = k => mean(rows.map(m => Number(m.rating?.[k])).filter(x => !Number.isNaN(x))); 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)} ${bandRow('后档 L33–L50', 32, 50)}
</tbody> </tbody>
</table> </table>
<p class="meta">难度 3 = 刚刚好;无解感越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85),其余几项是模型噪声,不列。</p> <p class="meta">难度 3 = 刚刚好;无解感越低越好。这两项只在<b>按档平均</b>时可读:逐关复测 r = ${f2(retest(m => Number(m.rating?.difficulty)))} / ${f2(retest(m => Number(m.rating?.stuck)))}(本轮 ${retestPairs.length} 对复测现算),好玩与清晰度更低,不列。三档单调上升是这一轮的结果,逐关的高低不要当真。</p>
<h2>这批数字里,哪些能信</h2> <h2>这批数字里,哪些能信</h2>
<div class="note"> <div class="note">
<p><b>机械面是干净的,可以信。</b>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} 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。</p> <p><b>机械面是干净的,可以信。</b>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} 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。</p>
<p><b>难度评分不能信,它区分不了这批关卡。</b>${rated.length} 关里有 ${rated.filter(m => Number(m.rating?.difficulty) === 2).length} 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者<b>直接列成清单</b>,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。</p> <p><b>难度评分不能信,它区分不了这批关卡。</b>${rated.length} 关里有 ${rated.filter(m => Number(m.rating?.difficulty) === 2).length} 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者<b>直接列成清单</b>,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。</p>
<p><b>所以难度顺序是机械假设,等你的判定。</b>L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分唯一能用的是它的<b>总体读数</b>:整批偏易一档(2 分 vs 刚刚好的 3 分)。</p> <p><b>所以逐关顺序是机械假设,等你的判定。</b>L1 到 L50 是按旋转深度、棋子数、互锁程度排的,不是按谁测出来更难。评分能用的只有<b>整档</b>:这一轮三档是 ${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 分。</p>
<p><b>两件事我没有擅自改,留给你定。</b>agent 的 33 条抱怨里反复出现两个主题:路线太长看不完(「68 格 27 弯」「42 格 42 弯」),以及紧贴容易看漏(「看着能出界却会擦到」)。两项都是感知问题,而<b>已上线的 v0.5 在这两项上更严重</b>(最长路线 88 格 vs 这里 ${Math.max(...Object.values(perceptual).map(p => p.longest))} 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。</p> <p><b>两件事我没有擅自改,留给你定。</b>agent 这一轮写了 ${complaints.length} 条抱怨,反复出现两个主题:<b>路线太长看不完</b>(${longQuotes.map(q => `「${esc(q)}」`).join('') || '—'}),以及<b>紧贴容易看漏</b>(${grazeQuotes.map(q => `「${esc(q)}」`).join('') || '—'})。两项都是感知问题,而<b>已上线的 v0.5 在这两项上更严重</b>(最长路线 88 格 vs 这里 ${Math.max(...Object.values(perceptual).map(p => p.longest))} 格)——它是这个游戏本来的性格,不是这次构造引入的。卡片上「最长路线」超 45 格、「紧贴棋子」超 1 枚会标黄,你试玩时留意。</p>
</div> </div>
<h2>逐关</h2> <h2>逐关</h2>