下载 App
《忆如泉涌》开发日志 #03:第三个玩法上线,以及被测试抓住的bug
修改于10/203 浏览综合



前两篇里,涌现分别以两种形态出现:
- #01:难度的随机分布——同一套规则,排列不同,难度自己长出来;
- #02:规则内部的冲突——“看字色不看字义”,让大脑的两条通路打架。
这一篇是第三种:让两个本来独立的信息维度强行绑定。
同时,这一篇还有个不太光彩但很真实的记录:我新加的回归测试,抓住了一个自己写的布局 bug。先说玩法,再说 bug。


一、涌现的第三种形态:数字 vs 字号
看这张截图。四个数字:3、9、5、7。
正确顺序是什么?3 → 5 → 7 → 9,按数值从小到大。
但你的眼睛会先被什么吸引?3 和 9 特别大,5 和 7 特别小。
这就是新玩法意形分离的核心:数值是判断依据,字号是干扰信息。
规则一句话:忽略字体大小,按数值从小到大点击。 点对了数字消失,点错了弹出红圈;清空一组出下一组,每完成 3 组,数字数量 +1;整局 60 秒,按完成组数排名。
为什么这是"涌现",而不是"加系统"
注意,我们没有设计任何一道题。
- 数值从池子里随机抽;
- 字号从倍率区间里随机分;
- 数值和字号的对应关系是随机的——最大数不一定最大字,最小数也不一定最小字;
- 位置也是随机散布的。
我们做的只有一件事:把"数值"和"字号"这两个本来独立的信息维度,强行绑在同一个对象上。 冲突是这两个维度绑在一起后自己涌出来的,不是我们编排的。
这一点在策划案里写死了一条很关键的规则:允许偶然出现字号与数值同向或反向的排列,不按答案顺序剔除这类题面。
意思是:如果随机生成的一组恰好是"数值越大字越大"(也就是字号不构成干扰),我们不会因为它太简单就重抽。 因为一旦开始按"难度"筛选题面,随机就被人为干预了,涌现也就变味了——那就成了设计师在出题,不是规则在生成。
和色彩辨识的区别在哪
有人可能会问:色彩辨识不也是"忽略一个维度、看另一个维度"吗?
有本质区别:
| 玩法 | 干扰维度 | 判断维度 | 冲突来源 |
|---|---|---|---|
| 色彩辨识 | 字义 | 颜色 | 语义自动激活 |
| 意形分离 | 字号 | 数值 | 视觉大小直觉 |
色彩辨识的冲突是语义层面的——你读到"黄"就自动想到黄色,这是大脑的自动反应。
意形分离的冲突是知觉层面的——大东西看起来"更重要",这是更底层的视觉直觉。
一个要你压住"读到的意思",一个要你压住"看到的尺寸"。同一种结构,长在两个不同的认知层级上。


二、难度曲线:这次是"无上限"的
三个玩法的难度设计完全不同,这点我觉得挺有意思:
| 玩法 | 难度怎么变化 | 有上限吗 |
|---|---|---|
| 舒尔特方格 | 玩家选段位,3×3 到 7×7 | 有,7×7 封顶 |
| 色彩辨识 | 玩家选段位,10/20/30 题 | 有,三段封顶 |
| 意形分离 | 游戏自己涨,每 3 组 +1 个数字 | 没有上限 |
意形分离不分段位,所有玩家面对同一套规则。难度是局内自己长出来的:
当前组序号 = 已完成组数 + 1
本组数字数量 N = 4 + floor(已完成组数 / 3)
第 1~3 组 4 个,第 4~6 组 5 个,第 7~9 组 6 个……一直涨下去。
策划案里明确写了一条:不在 9 个数字处封顶。 后期会自然出现两位数。
这意味着:意形分离的成绩没有"满分区"。 你玩得越好,下一组就越难,直到 60 秒耗尽。这跟舒尔特方格"刷新个人最佳时间"的追求完全不同——那边是逼近一个极限,这边是看你能在难度爬升中撑多久。
涌现在这里的表现是:难度曲线本身是玩家行为的结果。 高手会更快进入高组数,面对更密集的数字;新手则一直在低组数徘徊。同一条规则,给不同的人完全不同的挑战。


三、那个被测试抓住的 bug
现在说不太光彩的部分。
给舒尔特方格加了一组布局回归测试(T10),专门验证"高缩放、安全区、五种棋盘规模"下棋盘不会溢出。跑出来:
test_result: FAILED
assertions: {'total': 5832, 'passed': 5831, 'failed': 1}
FAIL: T10 scale=1.00 段位1棋盘横向溢出
5832 项断言里挂了 1 项。
这条断言检查的是:棋盘右边缘 x + w 不能超过视口宽度 vw。在一段(3×3)、缩放 1.00 的条件下,棋盘横向溢出了。
实测出来的真因:迭代预算不够,不是边距算错
我一开始以为是"棋盘宽度没把两侧页面边距算全"。实测之后发现想错了,真因完全不一样。
先看探针打印的现场数据(视口 2040×1080,横屏):
viewport=2040x1080 pagePad=76 gap=41 trayPad=45
board x=851.0 w=1312.0 boardRight=2163.0 vw=2040
棋盘右边缘 2163,超出视口 2040,溢出 123px。
再看求解单格尺寸的那段代码,逻辑是:
起始 cell = viewW / cols -- 2040 / 3 = 680
每轮 cell = cell - 1
最多循环 300 次
问题是:它从"整宽单格"开始,每次只减 1 像素。
- 起点:2040 / 3 = 680
- 循环上限 300 次 → 最低只能降到 380
- 而横屏真正需要的单格尺寸是 约 271(解 BoardOuterSize(cell,3) ≤ (viewW − pagePad×3)/2 得到)
- 680 → 271 需要约 409 次递减,超过了 300 的预算
所以循环在 cell=380 时就耗尽了次数,带着一个"还没收缩到位"的尺寸退出了。不是算错,是没算完。
为什么偏偏是"段位1"
这个 bug 只在 cols 最少时触发,原因很直观:
| 段位 | cols | 起始 cell | 需要递减次数 | 300 预算够吗 |
|---|---|---|---|---|
| 一段 | 3 | 680 | ~409 | ❌ 不够 |
| 二段 | 4 | 510 | ~296 | ✅ 刚好够 |
| 三段 | 5 | 408 | ~210 | ✅ |
| 四段 | 6 | 340 | ~150 | ✅ |
| 五段 | 7 | 291 | ~110 | ✅ |
格子越少,起始 cell 越大,要走的距离就越远。 一段需要 409 步,预算只有 300,于是只有它挂。二段需要 296 步,差一点点就也要挂——这条断言其实是在悬崖边上抓住了一个 bug。
为什么这条断言有价值
因为这个 bug 肉眼几乎不可能发现。
3×3 的格子很大,横屏下多出 123px,看起来只是"棋盘偏右一点"或"右边贴边"而已——你不会觉得是 bug,只会觉得设计如此。
而且它只在特定缩放 × 特定段位的组合下出现。手测的话,谁会想到去逐个试"缩放 1.0 / 2.05 / 3.0"×"一段到五段"这 15 种组合?
这就是断言的意义:它不累,而且不偷懒。 15 种组合一次跑完,并且直接告诉我"是一段、是横屏、是缩放 1.0"——排查范围一下从"整个布局系统"缩到"一个循环的迭代上限"。
顺带说一个测试里踩的坑
写这组测试的时候我发现一个现象,值得记录:
测试里调用的 UI.SetScale() 和截图时的实际缩放,不一定一致。
从日志里能看到,挑战页实际运行时报告的缩放是 1.49 或 1.54(取决于设备),而测试里我手动设的是 1.0 / 2.05 / 3.0。
也就是说,测试覆盖的缩放区间,和真机的实际缩放,是两套数字。 这不算错(测试本来就是扫边界),但它提醒我:断言通过 ≠ 真机没问题,真机还得单独截图验证。
这也解释了为什么我在每个玩法上都保留了实机截图这一步——逻辑测试和视觉验证,谁也替代不了谁。


四、三个玩法的测试现状
跑完三套无头断言,当前数据:
| 玩法 | 断言总数 | 通过 | 失败 |
|---|---|---|---|
| 舒尔特方格 | 5832 | 5831 | 1 ← 上面那个布局 bug |
| 色彩辨识 | 286 | 286 | 0 |
| 意形分离 | 3509 | 3509 | 0 |
意形分离这套断言覆盖的是它最麻烦的部分:
- 组数递增:验证第 1、3、4、6、7、9、10 组分别生成 4、4、5、5、6、6、7 个数字;
- 数值池:同组内数值唯一、正整数;
- 容量判定:数字排不下时,必须报"技术异常作废",不能偷偷减少数字数量;
- 红圈时长:错点后红圈 1 秒,数字不消失;
- 60 秒边界:到点即结束,完成 0 组也是有效成绩;
- 作废:退出、中断、转屏都要作废,不入榜;
- 榜单隔离:意形分离的榜和另外两个玩法完全隔离。
其中容量判定这条我要特别说一下。
意形分离的数字会一直增加,理论上迟早会多到屏幕上排不下。策划案把这件事列成了发布前阻断项,并明确写死:容量不足时报技术异常、本局作废,绝不允许"悄悄少放几个数字"或"缩小到点不中"来蒙混过关。
因为一旦允许偷偷减数字,排行榜就失去意义了——A 玩家的第 20 组是 10 个数字,B 玩家的第 20 组被系统缩成 8 个,两个人比的根本不是同一件事。
宁可作废,不可作弊。 这条底线是写进代码里的,不是写在文档里好看的。


五、还修了几个真机问题
意形分离从实装到能跑,中间修了三个真实的真机 bug,记录一下:
1. 题面整片空白
和色彩辨识那次同源:字号随视口放大,在某些设备上大到无法光栅化,文字直接不显示。修复方式是给字号加硬上限,并且让控件边长由字号反推——这样"框"和"字"始终成比例,不会出现框很大但字是空的。
2. 倒数卡在 3 不动
这个比较隐蔽。原因链条是:
- App.Update 检测到视口尺寸变化 → 重建当前页面;
- 而倒计时是页面里的局部变量,页面一重建就被重置;
- 于是形成"重建 → 倒数归零 → 再重建"的死循环,倒数永远停在 3。
修复做了两件事:
- App 层加去抖:新尺寸必须连续稳定若干帧才认定真的变化(真机上安全区收敛会让视口小幅抖动);
- 倒数进度改存在对局对象上,不再放页面里,重建也不会重置。
还加了一道兜底:dt 异常时按 60fps 步长推进,累计帧数超上限强制结束倒数——保证真机上不会永久卡在准备阶段。
3. 页面溢出
日志里能看到 Overflow in 'Panel': children=413x126, container=1992x93 这样的警告。这是 Yoga 的 flexShrink 默认值为 0 导致的——子元素不肯收缩,就把容器撑爆了。给相关行设 flexShrink = 0(要固定高度的)或允许收缩,问题解决。


六、当前状态
| 项 | 状态 |
|---|---|
| 舒尔特方格 | ✅ 实装,5831/5832 断言(1 项布局 bug 待修) |
| 色彩辨识 | ✅ 实装,286/286 断言 |
| 意形分离 | ✅ 实装,3509/3509 断言 |
| 主菜单三玩法入口 | ✅ |
| 三玩法独立排行榜 | ✅ |
| 本地存档 + 待提交补交 | ✅ |
| 多分辨率 / 安全区适配 | ✅ 已验多种视口 |
下一步:
- 修掉 T10 那个布局溢出 —— 根因已经定位(单格尺寸求解器的 300 次迭代预算不够,一段横屏需要约 409 次),改法很清楚:要么调大预算,要么把"每次减 1"换成二分/直接求解;
- 意形分离的容量极限测试 —— 这是发布前阻断项,得实测出"到底能撑到第几组",而不是理论上算;
- 三玩法的难度曲线对比 —— 三个玩法一个比极限速度、一个比准确率、一个比爬升高度,它们的成绩分布形状完全不同。这个我想用真实数据画出来看看。
第 3 条是我最期待的。因为如果三个玩法的成绩曲线真的长得不一样,那就说明"涌现"不只是玩法内的——整个游戏层面也在涌现。


本文为《忆如泉涌》开发日志第 3 篇,记录 2026-10-19 的第三玩法实装与回归测试进展。
[图片:暂不支持黏贴,请手动导入]
真机自测
设备:手机
尺寸:390 × 867 (9:20)
72%













