-| 拟合 difficulty ~ 机械特征,用来重选关卡 | 响应几乎无方差(46/50 同分),回归的是常数 |
+| 拟合 difficulty ~ 机械特征,用来重选关卡 | 响应几乎无方差(83/99 同分),回归的是常数 |
| legibility(一眼看穿的成本)解释难度评分 | 相关 -0.02,无效;降级为纯测量 |
| 路线交叠度当难度轴 | 中位已达 89%,没有上升空间 |
| 换开局朝向来提高判断成本 | 127 个候选里只有 7 个变好 |
diff --git a/docs/review-50.html b/docs/review-50.html
index 297824b..9bd16a8 100644
--- a/docs/review-50.html
+++ b/docs/review-50.html
@@ -62,7 +62,7 @@ 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:12
+
关卡集 levels/campaign-50.json · agent 实测批次 R2 · grok xhigh(50/50 关已评)· 生成于 2026-09-14 17:14
难度曲线(agent 实测)
@@ -70,7 +70,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
| 前档 L1–L12 | 12 | 2.0 | 1.1 | 0.2 | 0.0 |
| 中档 L13–L32 | 20 | 2.1 | 1.2 | 0.0 | 0.0 |
-| 后档 L33–L50 | 18 | 2.3 | 1.8 | 0.2 | 0.0 |
+| 后档 L33–L50 | 18 | 2.4 | 1.7 | 0.2 | 0.0 |
难度 3 = 刚刚好;无解感越低越好。这两项是评分里唯二复测可信的(r = 0.93 / 0.85),其余几项是模型噪声,不列。
@@ -78,7 +78,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
这批数字里,哪些能信
机械面是干净的,可以信。agent 跑完 50 关:空点 0.0 次/关、盲转 0.1 次/关、反向转 0.1 次/关,50/50 关通关。「动了半天没结果」这类结构缺陷已经被生成期的闸门挡掉了。
-
难度评分不能信,它区分不了这批关卡。50 关里有 26 关被打成 2 分,而这些关卡机械上差着三倍(决策数 0–8)。原因不在关卡,在 bench:文字盘面会把每颗棋子的路线、挡路者、紧贴者直接列成清单,agent 从来不需要用眼睛顺着线走一遍——而那正是这个游戏的难点所在。agent 自己在报告里写过这句:「挡路名单已经写全,不用顺着格子数」。
+
难度评分不能信,它区分不了这批关卡。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 枚会标黄,你试玩时留意。
@@ -182,8 +182,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 —无解感 —12 步6 决策
-
+
难度 2无解感 112 步6 决策
+
开局 t0 那条 44 格绕来绕去的路线,摘要能看清挡子,但弯太多跟不上
试玩 →
@@ -216,8 +216,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 2无解感 217 步7 决策
-
第14步转到朝向2时t0看似能出界,但紧贴t2会擦到,长路线上不太容易预见到
+
难度 —无解感 —17 步7 决策
+
试玩 →
@@ -233,8 +233,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —17 步7 决策
-
+
难度 2无解感 217 步7 决策
+
t1 走后 t0/t2 互挡,右转朝向2时 t0 看似能出却擦到 t2,空转了一步
试玩 →
@@ -250,8 +250,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —16 步6 决策
-
+
难度 2无解感 116 步6 决策
+
第12步右转后t0和t2仍互相挡,还得再转一次才拆开;开局长路线(31–49格)也只能靠挡子摘要读
试玩 →
@@ -267,8 +267,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —15 步8 决策
-
+
难度 2无解感 115 步8 决策
+
朝向1剩四枚时 t2 擦到 t0、t0 路上又有 t2,看起来互相卡死,只能再转
试玩 →
@@ -284,8 +284,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —15 步8 决策
-
+
难度 2无解感 115 步8 决策
+
开局 t0 那条 53 格 16 弯的超长路线把盘面读得很晕,真正能点的其实只有边上的 t8/t9/t11
试玩 →
@@ -301,8 +301,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 115 步8 决策
-
开局 t0/t2 路线 25–28 格还缠着四五枚棋子,只能靠摘要判断挡谁,盘面本身看不清
+
难度 2无解感 215 步7 决策
+
朝向1清完t1/t6/t7后,t0↔t2互压、t4↔t5互擦两对死锁叠在一起,不确定再转一次能不能解开
试玩 →
@@ -318,8 +318,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —14 步7 决策
-
+
难度 2无解感 114 步7 决策
+
开局 t3 那条 62 格 26 弯的超长轨道几乎读不完,只能靠摘要看挡子
试玩 →
@@ -335,8 +335,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 115 步11 决策
-
开局 t0 那条 42 格弯路太长,挡谁只能靠摘要判断
+
难度 2无解感 115 步7 决策
+
开局 t0 那条 42 格长路列了九个挡子,其实先清边上的 t8/t9/t11 就行,长路干扰判断
试玩 →
@@ -352,8 +352,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子2 枚
-
难度 2无解感 115 步8 决策
-
开局 t0/t1/t3 路线 50 多格,只能靠末尾挡子列表判断,中间弯折几乎读不动
+
难度 2无解感 115 步7 决策
+
开局 t0/t1/t3 路线长达五十多格十六个弯,只能靠路上棋子摘要点,几乎没法在脑子里画出轨道
试玩 →
@@ -369,8 +369,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 3无解感 115 步7 决策
-
第二次旋转前剩 5 枚全靠擦碰互卡,一时不确定是漏点了谁还是该转
+
难度 2无解感 215 步10 决策
+
朝向1清完t1后,t2路上压着t3、t3又紧贴t2,两枚互相卡死,只能转盘
试玩 →
@@ -386,8 +386,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 —无解感 —15 步7 决策
-
+
难度 2无解感 115 步7 决策
+
开局 t0/t1 路线 50 多格、挡子名单很长,只能先点边上三枚再转
试玩 →
@@ -403,8 +403,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子3 枚
-
难度 —无解感 —15 步10 决策
-
+
难度 2无解感 115 步10 决策
+
t0/t3 绕几十格的轨道只能靠摘要,空间上绕到哪、会擦谁完全脑补不出来
试玩 →
@@ -421,7 +421,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 2无解感 117 步7 决策
-
开局 t0 那条 55 格 16 弯太长,脑子对不上盘面;朝向 0 时 t2/t4 互堵,转一次没拆开还要再转
+
开局 t0 那条 55 格 16 弯完全读不过来,只能靠「路上有谁」的摘要;t2/t4 对卡看起来像死结,其实该转
试玩 →
@@ -438,7 +438,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 2无解感 115 步7 决策
-
开局 t0/t1 那两条 50 多格的超长路线把盘面挤得很满,真正能走的短路径要翻下去找
+
开局 t0/t1 各绕 50 多格互相穿过,只能靠「路上有」摘要判断,路线本身几乎扫不动
试玩 →
@@ -454,8 +454,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子2 枚
-
难度 —无解感 —17 步10 决策
-
+
难度 2无解感 117 步10 决策
+
开局 t0/t9 路线 50 多格,只能靠摘要判断谁挡谁,格子序列本身看不过来
试玩 →
@@ -471,8 +471,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子3 枚
-
难度 —无解感 —15 步7 决策
-
+
难度 2无解感 115 步7 决策
+
开局 t0/t3/t5 路线 50+ 格叠在一起,虽然能走的棋子标得很清楚,但中间层谁挡谁很难建立直觉
试玩 →
@@ -488,8 +488,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —15 步10 决策
-
+
难度 2无解感 115 步10 决策
+
开局 t0/t1 路线 50 多格 16 弯,空间感几乎没有,只能靠路上有谁来点
试玩 →
@@ -505,8 +505,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 215 步8 决策
-
朝向1清完后剩5枚互相卡住,t4看着能走却擦到t0,一时不知道该转还是继续点
+
难度 3无解感 115 步7 决策
+
朝向1清完t1/t7/t6后t4只擦t0、t0又被t4压着,像死锁,其实再右转就开了
试玩 →
@@ -523,7 +523,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 2无解感 115 步7 决策
-
朝向2时 t2 只差1格出界却擦到 t1,t1 路上又压着 t2,互锁要转才看出来
+
朝向1清完t7/t6后6枚仍互卡,一时分不清是漏看了能走的还是必须再转
试玩 →
@@ -539,8 +539,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 —无解感 —15 步7 决策
-
+
难度 2无解感 115 步7 决策
+
开局 t0 那条 54 格路线列了十来个挡子,眼花,好在先点边上没人的就对了
试玩 →
@@ -556,8 +556,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子2 枚
-
难度 —无解感 —15 步7 决策
-
+
难度 2无解感 115 步7 决策
+
开局 t6 那条 68 格 28 弯的轨道太长,盘面一眼读不清,只能靠挡子列表剥层
试玩 →
@@ -573,8 +573,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 —无解感 —16 步7 决策
-
+
难度 2无解感 216 步7 决策
+
朝向0清完边上3枚后,t1/t2对堵还擦t8、t7/t8也对堵,长轨道看不出该先解哪对,只能转
试玩 →
@@ -591,7 +591,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 2无解感 119 步10 决策
-
开局 t0/t4 那两条 50 多格路线把几乎所有棋子都列进去,一时不知道从哪下手
+
开局 t0 一条 54 格长路几乎压着全盘,短出口要在一堆长路线里翻;后面几次卡住都是右转就开了,没真正选过方向
试玩 →
@@ -607,8 +607,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 215 步8 决策
-
朝向1时 t4 和 t5 路线都出界却互相擦着,谁也走不了,得再转一次才解开
+
难度 2无解感 115 步7 决策
+
开局 t4 那条 63 格螺旋把盘面读得很乱,其实只要先清边上能出的再转就行
试玩 →
@@ -624,8 +624,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 —无解感 —17 步8 决策
-
+
难度 2无解感 117 步8 决策
+
朝向1时 t1 和 t7 都写路上没棋子,其实互相擦着谁也走不了,得再转才开
试玩 →
@@ -658,8 +658,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子2 枚
-
难度 —无解感 —17 步7 决策
-
+
难度 2无解感 117 步7 决策
+
朝向1时t5路上没人却紧贴t4和t6,长直道擦碰差点误点
试玩 →
@@ -675,8 +675,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 217 步11 决策
-
第14步转到朝向4没有出口,t1仍擦到t2,短暂犹豫还要不要继续转
+
难度 3无解感 217 步10 决策
+
只剩 t1 t2 互相卡住,转到朝向 4 仍擦碰没出口,一时以为还要绕回去
试玩 →
@@ -692,8 +692,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 3无解感 219 步9 决策
-
清完朝向3的五枚后,t5 路上看似没人却擦到 t2,剩下三枚互卡只能再转
+
难度 2无解感 219 步9 决策
+
朝向2时剩8枚互相挡成环,扫了一圈才确定一枚都点不了
试玩 →
@@ -709,8 +709,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 2无解感 118 步9 决策
-
开局 t3、t6 路线 35–36 格织在一起,只能靠路上有/紧贴摘要点,盘面本身几乎读不出
+
难度 2无解感 218 步9 决策
+
朝向2剩5枚时 t2擦t5、t5擦t0、t0和t3互压,一时看不出该转还是还能点
试玩 →
@@ -726,8 +726,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子2 枚
-
难度 —无解感 —18 步11 决策
-
+
难度 3无解感 218 步11 决策
+
朝向4只剩t2和t3互相堵、提示没有新出口,以为要转回去,其实再转一格就开了
试玩 →
@@ -743,8 +743,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子2 枚
-
难度 2无解感 220 步11 决策
-
朝向3清掉t6/t7/t0后剩5枚两两对卡,一时看不出还要再转两次才解开
+
难度 3无解感 220 步11 决策
+
朝向3清完t0后剩下t1↔t3、t2↔t5两对互锁,一时看不出再转一次能否拆开
试玩 →
@@ -760,8 +760,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 3无解感 219 步10 决策
-
清完 t8/t9 后 t2 与 t6、t0 与 t3 两对互卡,只能再转一次碰朝向
+
难度 2无解感 119 步9 决策
+
开局t3/t7路线三十多格,只能靠摘要看挡子,具体绕法扫不过来
试玩 →
@@ -778,7 +778,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 2无解感 220 步9 决策
-
开局 t0 那条 39 格、11 个挡子的长路线读不清,只能靠摘要先清边上的自由子
+
朝向2剩7枚时t0和t3互压、t5擦t7,短暂犹豫是再转还是还有能点的
试玩 →
@@ -794,8 +794,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 2无解感 119 步9 决策
-
开局 t3/t9 路线三十多格互相编织,挡子名单太长,只能先清边上能走的再转
+
难度 2无解感 219 步9 决策
+
朝向3清完t3/t6/t7后,剩下t0/t2/t4互相压着擦着,看不出谁能先走,只能再转
试玩 →
@@ -812,7 +812,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 3无解感 220 步9 决策
-
开局清完外围后 t2/t10、t9/t11 两对对压,t1 那条 43 格长线看得晕,一时不确定是该硬拆顺序还是该转
+
朝向2时 t0 和 t3 对堵、旁边 t7 还擦着 t3,看起来像死结,只能再转一次才开
试玩 →
@@ -828,8 +828,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 —无解感 —19 步9 决策
-
+
难度 3无解感 219 步9 决策
+
朝向2清完t8/t9后t0和t3对堵、t5还擦着t4,一时拿不准是继续右转还是该回转
试玩 →
@@ -846,7 +846,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 2无解感 120 步10 决策
-
开局 t3 那条 40 格 38 弯的路线太长,扫一眼抓不住谁挡谁
+
朝向0清完外围后 t0 和 t1 对堵,超长路线缠在一起,看不出该往哪转,只能盲转右边
试玩 →
@@ -862,8 +862,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 219 步9 决策
-
朝向2时 t0 和 t3 对堵、其余全擦挡,一时以为还要来回试朝向
+
难度 2无解感 119 步9 决策
+
开局 15 枚路线动辄三十多格互相织着,只能先清边上三枚再盲转;之后一直右转就能开,没真正选过方向
试玩 →
@@ -879,8 +879,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子3 枚
-
难度 3无解感 223 步15 决策
-
最后两枚转回朝向0仍对压,要再转到朝向1才双双出界,中间空转了一圈
+
难度 3无解感 223 步14 决策
+
最后只剩 t0 和 t3 时朝向 5、0 都互相卡住,要再转到朝向 1 才同时能出
试玩 →
@@ -896,8 +896,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子0 枚
-
难度 2无解感 222 步11 决策
-
朝向4剩 t0/t4/t5 互相卡住,t5 被 t0 挡、t0 又被 t5 挡,一度怀疑转错了方向
+
难度 2无解感 122 步11 决策
+
朝向2时盘面只剩互相缠着的超长路线,t5路上没人却擦到t1/t7,一时不确定是漏看了能走的还是该转
试玩 →
@@ -913,8 +913,8 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
紧贴棋子1 枚
-
难度 2无解感 223 步15 决策
-
最后 t0/t3 互挡,转回朝向 0 提示没有新出口,以为转错了
+
难度 2无解感 223 步14 决策
+
第20步转回朝向0后t0和t3仍对压,短暂以为要回头改出子顺序
试玩 →
@@ -931,7 +931,7 @@ footer{margin-top:50px;padding-top:18px;border-top:1px solid var(--rule);color:v
难度 3无解感 224 步15 决策
-
开局长路线动辄三十多格三十多个弯,只能靠「路上有谁」摘要,跟不出实际形状
+
开局长路线三十多格第一眼理不清;后段只剩 t0/t1/t3 互锁时犹豫要不要再转一整圈
试玩 →
diff --git a/scripts/build-process-report.mjs b/scripts/build-process-report.mjs
index c3115d8..ae8e97d 100644
--- a/scripts/build-process-report.mjs
+++ b/scripts/build-process-report.mjs
@@ -7,7 +7,7 @@ import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { trace } from '../src/core.js';
import { metrics } from '../src/bench.js';
-import { legibility } from '../src/deep-lock.js';
+import { deepLevelKey, legibility } from '../src/deep-lock.js';
const esc = s => String(s ?? '').replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
const mean = a => a.length ? a.reduce((x, y) => x + y, 0) / a.length : null;
@@ -29,11 +29,38 @@ for (const f of existsSync(resolve('bench/runs')) ? readdirSync(resolve('bench/r
runs.push({ raw: d, m: metrics(d) });
} catch { /* torn read */ }
}
-const r1 = runs.filter(r => r.raw.agent === 'R1 · grok xhigh' && byId.has(r.raw.level?.id));
-const rated = r1.filter(r => r.m.rating && Number.isFinite(Number(r.m.rating.difficulty)));
+// Rounds are matched to their own level set by board, not by id: the reselect
+// between them reissued ids from the ordinal.
+const inSet = new Set(levels.map(deepLevelKey));
+const oldLevels = (() => { try { return JSON.parse(readFileSync(resolve('levels/campaign-50-r1.json'), 'utf8')).levels; } catch { return []; } })();
+const inOld = new Set(oldLevels.map(deepLevelKey));
+const round = (agent, set) => runs.filter(r => r.raw.agent === agent && r.raw.level?.tiles && set.has(deepLevelKey(r.raw.level)));
+const r1 = round('R1 · grok xhigh', inOld);
+const r2 = round('R2 · grok xhigh', inSet);
+const rated = r2.filter(r => r.m.rating && Number.isFinite(Number(r.m.rating.difficulty)));
const byDiff = rated.reduce((m, r) => (m[r.m.rating.difficulty] = (m[r.m.rating.difficulty] || 0) + 1, m), {});
const topDiff = Object.entries(byDiff).sort((a, b) => b[1] - a[1])[0] ?? ['—', 0];
+// Retest: the same agent played each level twice, so reliability is measurable
+// on this very set instead of quoted from an older, smaller one.
+const pairsBy = new Map();
+for (const r of r2) { const k = deepLevelKey(r.raw.level); if (!pairsBy.has(k)) pairsBy.set(k, []); pairsBy.get(k).push(r); }
+const pairs = [...pairsBy.values()].filter(v => v.length >= 2).map(v => [v[0], v[1]]);
+const sd = a => { const m = mean(a); return Math.sqrt(mean(a.map(v => (v - m) ** 2))); };
+const pearson = (A, B) => { 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 retest = get => {
+ const ok = pairs.filter(p => Number.isFinite(get(p[0])) && Number.isFinite(get(p[1])));
+ if (ok.length < 8) return { r: null, n: ok.length };
+ return { r: pearson(ok.map(p => get(p[0])), ok.map(p => get(p[1]))), n: ok.length };
+};
+const sig = r => (r.raw.steps ?? []).map(s => `${s.action?.type}:${s.action?.id ?? s.action?.direction ?? ''}`).join('|');
+const identical = pairs.filter(([a, b]) => sig(a) === sig(b)).length;
+const ratingDisagreed = pairs.filter(([a, b]) => a.m.rating?.difficulty != null && b.m.rating?.difficulty != null && a.m.rating.difficulty !== b.m.rating.difficulty);
+const disagreedOnSamePlay = ratingDisagreed.filter(([a, b]) => sig(a) === sig(b)).length;
+
+const bandOf = (rows, a, b) => rows.filter(r => { const i = levels.findIndex(l => deepLevelKey(l) === deepLevelKey(r.raw.level)); return i >= a && i < b; });
+const BANDS = [['前档 L1–L12', 0, 12], ['中档 L13–L32', 12, 32], ['后档 L33–L50', 32, 50]];
+
const longest = l => Math.max(...l.tiles.map(t => trace(l, t, [], true).steps.length));
const c50Longest = Math.max(...levels.map(longest));
const loadPool = p => { try { const r = JSON.parse(readFileSync(resolve(p), 'utf8')); return Array.isArray(r) ? r : r.levels; } catch { return []; } };
@@ -43,25 +70,35 @@ const traps = levels.map(l => legibility(l).grazeTraps);
const decisions = rated.map(r => r.m.decisions ?? 0);
const R1 = {
- n: r1.length, rated: rated.length,
- cleared: r1.filter(r => r.m.cleared).length,
- dead: mean(r1.map(r => r.m.deadTaps ?? 0)),
- blind: mean(r1.map(r => r.m.blindRotations ?? 0)),
- wrong: mean(r1.map(r => r.m.wrongWayRotations ?? 0)),
+ n: r2.length, rated: rated.length,
+ cleared: r2.filter(r => r.m.cleared).length,
+ dead: mean(r2.map(r => r.m.deadTaps ?? 0)),
+ blind: mean(r2.map(r => r.m.blindRotations ?? 0)),
+ wrong: mean(r2.map(r => r.m.wrongWayRotations ?? 0)),
flat: topDiff, decMin: Math.min(...decisions), decMax: Math.max(...decisions),
+ r1n: r1.length, r1blind: mean(r1.map(r => r.m.blindRotations ?? 0)),
+ r1diff: mean(r1.map(r => Number(r.m.rating?.difficulty)).filter(Number.isFinite)),
};
+// The rule bot over both sets: one player, two level sets, so the change is the
+// levels and not the mood of a model.
+const bot = tag => runs.filter(r => String(r.raw.run ?? '').startsWith(tag));
+const BOT = tag => { const v = bot(tag); return v.length ? {
+ n: v.length, blind: mean(v.map(r => r.m.blindRotations ?? 0)), dead: mean(v.map(r => r.m.deadTaps ?? 0)),
+ streak: Math.max(...v.map(r => r.m.longestBlindStreak ?? 0)), dec: mean(v.map(r => r.m.decisions ?? 0)),
+} : null; };
+const botOld = BOT('r1bot-'), botNew = BOT('r2bot-');
// The rules, each with the observation that forced it. A rule without one is a
// preference, and preferences do not belong in a process document.
const RULES = [
- { n: 1, rule: 'agent 轮次用来抓缺陷,不用来排难度',
- ev: `R1 的 ${R1.rated} 关里有 ${R1.flat[1]} 关难度评分是 ${R1.flat[0]} 分,而这些关卡的决策数从 ${R1.decMin} 到 ${R1.decMax}。评分不是不准,是不区分。`,
- why: '文字盘面把每颗棋子的路线、挡路者、紧贴者列成清单,agent 不必用眼睛顺线走一遍——而那正是这游戏的难点。agent 自己写过:「挡路名单已经写全,不用顺着格子数」。',
- now: '难度排序改为机械假设(旋转深度、棋子数、互锁),由人工审核裁定;agent 轮次只看死局、盲转、转错、空点、演出。' },
- { n: 2, rule: '先用规则机器人扫,再花 agent 时间',
- ev: '机械旋转指标已被证明测的是关卡而不是玩家:4 次运行、3 家厂商,18 关里 17 关结论一致。',
- why: '既然同一关在不同模型下得出同样的机械读数,那这个读数不需要模型来产生。',
- now: '12 秒的规则机器人扫描替代一整轮 agent 跑批;agent 只用于抽样确认和读自由文本抱怨。' },
+ { n: 1, rule: '难度评分只能按档看,不能逐关看',
+ ev: `${R1.rated} 局里有 ${R1.flat[1]} 局打的是 ${R1.flat[0]} 分,而这些关卡的决策数从 ${R1.decMin} 到 ${R1.decMax}。逐关复测 r = ${f2(retest(r => Number(r.m.rating?.difficulty)).r)},而且 ${ratingDisagreed.length} 对分歧里有 ${disagreedOnSamePlay} 对走的是同一条轨迹——同样的动作、同样的结果,模型给自己打了不同的分。但按档平均约 20 局后,曲线第一次显形了(见上表)。`,
+ why: '文字盘面把每颗棋子的路线、挡路者、紧贴者列成清单,agent 不必用眼睛顺线走一遍——而那正是这游戏的难点。agent 自己写过:「挡路名单已经写全,不用顺着格子数」。剩下的评分噪声,只有靠聚合才压得下去。',
+ now: '逐关排序改为机械假设(旋转深度、棋子数、互锁),由人工审核裁定;评分只用来读整档的高低。' },
+ { n: 2, rule: '先用规则机器人扫,再花 agent 时间;同一关不要跑两遍',
+ ev: `第二轮意外把每关跑了两次,得到 ${pairs.length} 对复测:其中 ${identical} 对(${pct(identical, pairs.length)})重放了完全相同的动作序列,机械指标复测 r = 1.00。规则机器人跑完 50 关只要 1 秒。`,
+ why: 'agent 近乎确定性,重复同一关几乎不产生新信息。为「提高可信度」而重复跑,是把预算花在已经知道的答案上。',
+ now: '全量验收交给规则机器人;agent 每关只跑一次,省下的预算用来多覆盖关卡。' },
{ n: 3, rule: '门槛写在生成期,不写在筛选期',
ev: '把开局门槛提到「可点≥3 枚、演出≥0.8s」后筛选写出了 0 关——池子的中位数是 2 枚 / 0.57s。',
why: '筛选只能从池子里挑,挑不出池子里没有的东西。分布不对时,要改的是构造。',
@@ -135,9 +172,9 @@ a{color:var(--rail)}
关卡制作流程复盘
这一轮做 50 关的过程里,最有用的结论是一个否定结论:原计划的「agent 打分 → 拟合权重 → 重选关卡」这个闭环合不上,而且不是样本量的问题。下面是它为什么合不上,以及流程该改成什么样。
-
实测批次 R1 · grok-4.6 xhigh · ${R1.n} 关 · 生成于 ${new Date().toLocaleString('sv-SE').slice(0, 16)}
+
实测批次 R2 · grok-4.6 xhigh · ${R1.n} 局(50 关,多数跑了两次)· 对照批次 R1 ${R1.r1n} 局 · 生成于 ${new Date().toLocaleString('sv-SE').slice(0, 16)}
-
R1 实测到了什么
+
第二轮实测到了什么
通关
${R1.cleared}/${R1.n}
无死局
@@ -145,7 +182,56 @@ a{color:var(--rail)}
转错 / 关
${f2(R1.wrong)}
目标 0
难度评分
${R1.flat[1]}/${R1.rated}
都是 ${R1.flat[0]} 分 — 不区分
-
机械面干净,说明生成期的闸门是有效的:「动了很多次没结果」这类结构缺陷已经被挡在关卡集之外。难度评分那一格是这一轮真正的发现——它不是不准(复测 r = 0.93,相当稳),它是不区分:机械上差着三倍的关卡,它一律给 ${R1.flat[0]} 分。
+
机械面干净,说明生成期的闸门是有效的:「动了很多次没结果」这类结构缺陷已经被挡在关卡集之外。难度评分那一格是这两轮真正的发现:机械上差着三倍的关卡,它一律给 ${R1.flat[0]} 分。更早的记录里这项的复测可信度是 0.93,那是个更小的样本——在这一批 ${pairs.length} 对复测上实测是 ${f2(retest(r => Number(r.m.rating?.difficulty)).r)},所以它既不区分、也不像以为的那么稳。
+
+
两轮之间改了什么,效果如何
+
第一轮唯一可用的读数是「整批偏易」,加上抱怨里反复出现的「转了没反应」。据此只改了两件事:给连续空转设闸门(最多一次转动没反应,此前有 11 关能连着两次),以及把难度曲线写成旋转深度的配额而不是指望它从排序里自己长出来。
+
+| 同一个规则机器人,两套关卡集 | 第一版 | 第二版 |
+
+${botOld && botNew ? `
+| 盲转 / 关 | ${f2(botOld.blind)} | ${f2(botNew.blind)} |
+| 最长连续盲转 | ${botOld.streak} | ${botNew.streak} |
+| 空点 / 关 | ${f2(botOld.dead)} | ${f2(botNew.dead)} |
+| 决策 / 关(要做的事) | ${f1(botOld.dec)} | ${f1(botNew.dec)} |
+` : '| 规则机器人数据缺失 |
'}
+
+
+
要做的事一样多,浪费掉的动作少了一半。这是同一个玩家的对照,所以变的是关卡不是模型的心情。
+
+| grok 实测,按档 | 难度 | 无解感 | 盲转/关 | 旋转深度 |
+
+${BANDS.map(([label, a, b]) => {
+ const rows = bandOf(r2, a, b);
+ const g = k => mean(rows.map(r => Number(r.m.rating?.[k])).filter(Number.isFinite));
+ const rot = mean(levels.slice(a, b).map(l => l.campaign?.rotations ?? 0));
+ return `| ${label} | ${f2(g('difficulty'))} | ${f2(g('stuck'))} | ${f2(mean(rows.map(r => r.m.blindRotations ?? 0)))} | ${f1(rot)} |
`;
+}).join('\n')}
+
+
+
第一轮三档的难度评分是一条平线。第二轮后档升到了 ${f2(mean(bandOf(r2, 32, 50).map(r => Number(r.m.rating?.difficulty)).filter(Number.isFinite)))},无解感也跟着抬起来——这是评分第一次表现出梯度,但只在按档平均之后。
+
+
哪些信号复测得住
+
下面的 r 是在这一批关卡上现算的:同一个 agent 把每关玩了两次,${pairs.length} 对。不是引用更早、更小的样本。
+
+| 信号 | 复测 r | n | 怎么用 |
+
+${[['盲转次数', r => r.m.blindRotations, '可用:驱动了真实修复'],
+ ['转错方向次数', r => r.m.wrongWayRotations, '可用'],
+ ['实际旋转数', r => r.m.rotations, '可用'],
+ ['步数', r => r.m.steps, '可用'],
+ ['决策数', r => r.m.decisions, '可用'],
+ ['无解感评分', r => Number(r.m.rating?.stuck), '只按档看'],
+ ['难度评分', r => Number(r.m.rating?.difficulty), '只按档看,约 20 局起'],
+ ['好玩评分', r => Number(r.m.rating?.fun), '不可用'],
+ ['清晰度评分', r => Number(r.m.rating?.clarity), '不可用']].map(([name, get, use]) => {
+ const t = retest(get);
+ const cls = t.r == null ? '' : t.r >= 0.9 ? 'yes' : t.r >= 0.45 ? '' : 'no';
+ return `| ${name} | ${t.r == null ? '—' : f2(t.r)} | ${t.n} | ${use} |
`;
+}).join('\n')}
+
+
+
机械指标全部 r ≈ 1.00,但那多半是因为 agent ${pct(identical, pairs.length)} 的情况下重放了完全相同的动作序列——这本身才是有用的那条结论:重复跑同一关买不到新信息。评分则相反:同一条轨迹,${disagreedOnSamePlay} 对给出了不同的难度分。
五条规则,各自的证据
${RULES.map(r => `
@@ -161,12 +247,12 @@ ${RULES.map(r => `
| 指标 | 可用 | 依据 |
-| 盲转 / 多余转 / 转错方向 | 可用 | 复测 r ≈ 0.82–0.92;驱动了真实修复(1.17 / 1.22 / 0.44 → 0) |
+| 盲转 / 多余转 / 转错方向 | 可用 | 本批复测 r = ${f2(retest(r => r.m.blindRotations).r)};驱动了真实修复(盲转 1.17 → ${f2(R1.blind)}) |
| 是否通关、空点次数 | 可用 | 直接观察,无主观成分 |
| 自由文本抱怨 | 可用 | 33 条里两个主题高度重复,指标抓不到;但须对照基线(规则 5) |
-| 难度评分 | 只能看总体 | 稳定但不区分;单关排序不可用,整批读数(偏易一档)可用 |
-| 无解感评分 | 谨慎 | 复测 r = 0.85,与无效动作相关 0.66,可当挫败感的代理 |
-| 好玩 / 清晰度 / 旋转评分 | 不可用 | 复测 r ≤ 0.56,是模型噪声 |
+| 难度评分 | 只按档看 | 逐关复测 r = ${f2(retest(r => Number(r.m.rating?.difficulty)).r)},且同一条轨迹会打出不同分;按档平均约 20 局才显出梯度 |
+| 无解感评分 | 只按档看 | 复测 r = ${f2(retest(r => Number(r.m.rating?.stuck)).r)};按档看时与难度同向抬升,可当挫败感的代理 |
+| 好玩 / 清晰度评分 | 不可用 | 复测 r = ${f2(retest(r => Number(r.m.rating?.fun)).r)} / ${f2(retest(r => Number(r.m.rating?.clarity)).r)},是模型噪声 |