下篇一图流:模拟器 94 关通关,到求解器层面 100 关全通
一、先说结果,因为它变了
10 月 1 日凌晨,《驿路巡点》在 iOS 和 Android 两个模拟器上连续跑完 0--99 关:94 关通关,6 关卡死。到 10 月 2 日,几批改动合进主线(我们自己仓库的主分支)之后,仓库里的求解器(给定一关,自动判断它有没有解的脚本)显示这 6 关全部有解;那次合并时的改动说明里也自述双端已全通(来源见 4.2)。求解器层面是:100 关,无一关无解。
这不是改数字:中间隔着一个根因、一次流程失误,和一个已经查清的疑点。如果你也要把一批关卡数据搬到新平台,这三类问题也许可以当作自查清单。
先说清范围:本文的"通关"分两个范围------双端模拟器连续测试里 94 关通关(不是人工试玩);求解器层面 100 关全通,只说明有解。模拟器是 iOS(iPhone 16 / iOS 18.0)和 Android(arm64)各一次单进程连续跑,两个范围都没有真机。
现在这件事已经收尾:卡住的 6 关在求解器层面已经全部有解,后来一轮复测里也没有再失败;复测的范围和保留放在文末。
二、先回到 10 月 1 日:卡住的 6 关
2.1 现象
iOS 和 Android 上卡住的是同一批 6 关。这轮评测是脚本自动跑的:团队里负责测试的那个 Grok Bot 用自己照游戏规则写的求解器给每关算出点击序列,再用 idb(iOS)和 adb(Android)把点击逐格注入模拟器,靠画面像素判断是否出现通关页,然后点"下一关";过不去的 6 关只走一段局部路线,截图后继续下一关。100 关里有 33 关带机制(单向门、渡口、泽地格、符节、符节闸、塌桥、负重,各带多少关见图 2)。其中泽地格(slide_cells)是走上去会沿原方向滑到底的格子,仓库规则里叫"滑行"。
卡住的 6 关在图 3 里标红(玩家号 = 全局 id + 1,对应关系见第三节的表)。
它们带的是渡口(ferry:成对的两格,路线走进甲格就被强制跳到乙格,单向),其中玩家号 60、64 另有单向门,68 另有泽地格。
图 2|机制分布

图 3|卡死的 6 关在哪里

图 4|卡死的其中一关(模拟器画面)

2.2 根因:字段被丢了
一句话说清:导入关卡数据的转换脚本把机制字段丢了,渡口(ferry)是要害;引擎一侧当时也没有渡口规则------数据和引擎两边都缺,这批关自然过不去。9 月 30 日的根因分析见图 5。图里没写的一点:引擎一侧搜索的范围是共用引擎、CJMP 页面层和鸿蒙入口的所有 .cj、.ets 源文件。
2.3 为什么导入 100 关时没发现
我觉得这条是流程问题,也可能是整件事里代价最大的部分。
测试覆盖写在了 20 关的假设上,而实际数据是 100 关。所以它可能比技术 bug 更值得记。
图 5|根因链,和没拦住的检查

三、当时解释不了的一处,现在能解释了
根因报告里留了一个疑点:渡口关一共 8 个,为什么只有 6 个通不了?另外 2 个为什么没进这 6 个?
在主线上用仓库里的求解器重新求解,答案有了。
渡口关共 8 个。把这 8 关的 ferry 键去掉,计数上限调高后重新求解,每关最多数 100 个解:
| 全局 id | 玩家号 | 关名 | 去渡口后的解数 | | --- | --- | --- | --- | | 52 | 53 | 荆州·樊城·第 1 驿 | 0 | | 57 | 58 | 荆州·公安·第 1 驿 | 3 | | 59 | 60 | 荆州·公安·第 2 驿 | 0 | | 60 | 61 | 赤壁·樊口 | 0 | | 63 | 64 | 赤壁·陆口·第 1 驿 | 0 | | 67 | 68 | 赤壁·柴桑·第 2 驿 | 0 | | 87 | 88 | 汉中·南郑·第 2 驿 | 2 | | 89 | 90 | 南中·滇池·第 2 驿 | 0 |
求解器默认数到 2 个就停,不调高计数上限时 57 也只会显示"至少 2 个"。这两关不依赖渡口也能通,所以它们不在 6 个卡住的里面。6/8 这个数字不是随机的。
根因报告里还有两处当时写着"无法确认"的:sols_no_ferry 这个字段在 id 60、63 上是 5,但去掉渡口重跑是 0 解,对不上;以及渡口是单向还是双向需团队里负责设计的那个 Grok Bot 确认。前者现在明确了------去掉渡口就是 0 解,该字段不可用。后者在引擎实现里已经定为单向进甲、强制跳乙。
四、修复的过程:两次相邻的合并
回头看,这 6 关的修复落在两次相邻的合并里:先是数据和求解器那批改动,合并进基座(应用的基础部分:关卡数据、字体、图片和页面导航)和形状组件棋盘所在的开发分支;23 分钟后,这个开发分支整体并进主线。
4.1 先合并的一批:数据、求解器,以及引擎解析和棋盘机关代码
这批改动的说明开头写的范围是"阶段 1:只含数据 schema + 求解器 + 测试"(schema 指关卡数据的格式),并写明"未碰引擎和 App"。但它合并时已经追加到 37 个提交、55 个文件、新增 9098 行、删除 342 行,其中包括 mobile/shared/src 下引擎的规则与解析代码,以及 mobile/cjmp/lib 下的 board_mech.cj(棋盘机关绘制)。正文里的那句话,到合并时已经不成立。它最初的三件事是:
-
转换脚本 build_levels.py 为 33 个机制关补发 ferry / one_ways / slide_cells / seal_gates / token_cells / collapse / burden 七个可选键,其余 67 关与改动前逐字节相同
-
求解脚本 solve_levels.py 去掉 20 关的上限,强制 100 关,独立实现七种机制
-
规则测试 test_rules.py 把 20 关旧断言全换成 100 关,新增各机制单测
这一步是团队里负责写代码的那个 Grok Bot 自测,不是正式测试,数字见图 6。图 6 里没有的有:全程约 24 秒(改动说明里自述),我们这次在自己的一台 Linux 机器上复跑 100 关是 23.3 秒;原来过不去的 6 关各恰有 1 解;关卡设计时存下的 100 条通关路径回放 100/100 通过;规则测试另有 9 个跳过(2 个要靠环境变量开启全 100 关扫描,7 个是单关求解超过 10 秒而被保护性跳过,跳过不等于通过;我们这次复跑也是 9 个)。
我想说清楚这批改动中途的状态:它还在阶段 1 的时候,负责测试的 Grok Bot 的一份报告原文写着 "the engine does not read the new keys yet",那时引擎确实还没读新键。这份报告核对的是中途的一个提交,不是合并时的状态。
4.2 把几批改动并进主线
10 月 2 日 16:38(北京时间)的这次合并,把那个开发分支(基座、形状组件棋盘,以及已经合并进来的数据和求解器改动)并进了主线。它替代了原来那份棋盘的合并请求------那份是被误关的:基座那份合并后删了自己的分支,棋盘那份正挂在这条分支上,GitHub 就把它自动关掉了。
这次合并时的改动说明里自述了"已全量验证 PASS",口径见图 6;那是自测的说法,没有双端原始日志可对,所以我们只认两个数:双端模拟器 94 关通关,和求解器层面 100/100。负责测试的 Grok Bot 10 月 3 日那份补充验证报告,口径也在图 6 里。
图 6|四种"通过"的口径

引擎侧的实现在我们这次核对里能对上:mobile/shared 里有 REJECT_FERRY(进甲被拒:乙已在 path 里)和 FerryDef 等数据类,json_level.cj 解析这七个键;mobile/shared-test 下按机制拆了 ferry_test、replay_test 等用例。
9 月 30 日那版引擎"0 命中"的状态,到这两次合并之后反转为"七种机制全在"。从 10 月 1 日凌晨的 94 关通关,到 10 月 2 日并进主线,中间是两次相隔 23 分钟的合并,不是回滚重做。
五、问题分三类
框架的问题(CJMP 的):Canvas 没编进去、registerFont 拿不到、@r 不认识、形状渲染细节、编译器随机崩溃、模板 JNI 里写了固定值。能绕,绕完棋子能动。
我们自己的问题(引擎和数据):引擎没有渡口规则,转换脚本丢了字段。这两处没法在 CJMP 一侧绕开,要改我们自己的引擎和数据。
我们自己的流程问题:求解器和测试都只按 20 关写,导入 100 关时没拦住。它让前两类里本该早发现的问题晚发现了。
94 关到 100 关这一趟,我的体会是:CJMP 的缺口大多在"画得像"和"跑得动"上,这些都能绕;真正决定"能不能通关"的,是引擎有没有那条规则------规则是可以补的,卡住不等于终结。
六、到这里收尾
到这里,这三篇就收尾了:卡住的 6 关已经全部有解,后来的复测里也没有再失败。从 10 月 1 日凌晨卡住的 6 关到现在,补上的都是我们自己这边的东西:引擎里的渡口规则、转换脚本丢掉的字段,以及只按 20 关写的求解器和测试。
最后补一句范围。本文的 94 关和 100 关,都不是在 10 月 5 日合进主线的那批改动上测的。那批改动上又做过一轮复测,只测了 iOS 和 Android:Android 三个宽度、iOS 三个尺寸,各 100 关(其中 1 关按设计跳过),跑完的关里没有失败项。跳过的是哪一关、判不准的是哪几项,报告里没有逐项列明,这里也就不往细里写。这一轮的结论记为"部分验证",原因是有几项测试工具判不准,另有几项是界面细节上的判断,都不是游戏本身的问题。文中提到的所有测试都在模拟器上跑,没有用真机。
回头看,我觉得最值得记下来的还是流程那一条。
以上是我们现在的理解,排查和补救的思路可能还有更好的,欢迎大家在评论区讨论。