本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 5 篇 · 砂漏 Grainfall(前四篇:离阵 / 胶囊医生 / 烬塔 / 回响回廊) 源码:github.com/ghostgorge/Grainfall 技术栈:Python + pygame,单文件 exe,10 章 57 关 + 每日挑战 + 试炼塔 + 关卡工坊
一、一句话规则
你碰不到任何一粒砂,你只能转动重力。
棋盘是一个玻璃腔。每一步只有三个选择——顺时针转 90°、逆时针转 90°、翻转 180°。转完之后全场按新重力沉降到稳定。把每一粒彩砂送进同色的汇。
这是我做完离阵之后想做的反面:
| 难在哪 | |
|---|---|
| 离阵 | 十几个走法里挑出对的那个——决策面宽,难在筛选 |
| 砂漏 | 只有三个走法,但你预测不了转完之后是什么样——决策面窄到极致,难在推演 |
一个是”看得清但选不动”,一个是”选得动但看不清”。
二、然后玩家问了一句话
v1.1 发出去之后,收到这么一句反馈:
“为什么砂只往左下滑,不往右下?”
我的第一反应是”这不可能,物理是对称的”。但还是去查了。它是对的。
沉降引擎是这么写的:一趟之内,按”最近的先动”依次结算,并列时按列号升序。
问题就出在那个 tie-break 上。两粒砂并排、下方各有一个斜格可以滑进去,而那个斜格是同一格——按列号升序,左边那粒永远先走一步、占住斜格,于是右边那粒本该原地不动的,却因为左边先动而”看到”了新的空位,跟着滑走了。
镜像一对拍(棋盘左右翻转 + 方向映射,结果必须严格镜像),24 关全部对不上。
根因不是 tie-break 写错了,是结算方式错了
网格物理引擎(沉降 / 下落)在一趟之内不能依次结算。
谁先动会改变别人看到的占用情况,而”谁先动”的那个 tie-break(比如列号升序)就直接变成了左右偏袒。你换成列号降序,偏袒方向就反过来——错误不在方向,在”有方向”这件事本身。
正确做法:一趟之内,每粒砂各自只看”这一趟开始时”的盘面,冲突则都不动。
改完之后镜像对拍 30000 个盘面零不对称。这条对拍从此常驻自测——它是唯一能守住”物理是对称的”这个承诺的东西。
代价:11 关是靠 bug 才能通关的
旧的 58 关在新物理下重新验算,有 11 关直接无解——它们能通关,靠的正是那点左右偏袒。
全部重算 + 补烤之后是 57 关。这个数字我留在 README 里没有凑整,因为它记录了这件事。
三、在此之前,还有一次”不是 bug 的 bug”
时间倒回 v1.0 交付后的第一轮反馈:
“沙漏动画不够直接、旋转后小球位置难以预测、竖向三个球逆时针后直接消失了”
第三条听起来是硬 bug。我去查——守恒律成立,一粒砂都没丢。那三粒是在一趟之内先后滚进了同一个汇,合法消除。
那问题在哪?问题在动画把过程藏起来了。 沉降是一次算到稳定、然后一帧渲染出结果,玩家看到的是”三个球没了”,而不是”它们依次滚进去了”。
v1.1 的修法全在表现层:
- 沉降改成逐趟回放,一趟一趟放给你看
- 消除/合并等事件按帧触发,不再一次性结算
- 计数器跟着动画走,不提前跳数
- 预览默认全程开启
- 加下落速度档位、
F1规则速查
“看不懂”和”算错了”是两种 bug,但玩家只会报同一句话。 收到反馈第一件事是先验证守恒律——确认逻辑对不对,再决定是修引擎还是修动画。这两次反馈一次是表现层、一次是引擎层,如果反过来处理,两次都会白干。
四、关卡:只有三个走法,随机机器人赢得太轻松了
关卡不是手搓的,是随机造 + 四道筛子严格筛(顺序仍然是便宜的排前面):
- 可解(BFS 求最短解)
- 八条周期机器人打不通
- 乱转通关率够低
- 紧步占比够高(安全率 ≤ 0.5 的步数占比)
第 2 道是这个项目特有的,也是淘汰率最高的一道:淘汰了五成以上的候选。
原因很直接:走法只有三个。 “一直顺时针转”这种完全不动脑的固定序列,能打通的关卡多得吓人——毕竟转四次就回到原点,很多局面在这个循环里自己就解开了。所以我准备了八条周期序列(一直顺、一直逆、顺逆交替、顺顺翻、……),任何一条能打通就直接淘汰。
走法很少的谜题必须用”周期序列机器人”筛。 而且机器人和玩家必须共用同一个步数上限——否则你测的是”多转几圈会不会碰巧撞对”,那当然会。
前三章紧步占比是 0——它们没有尖刺也没有闸门,本来就走不死,职责是教会玩家读沉降。难度从第 4 章起飞。这是取舍,不是疏忽。
五、一个只有旋转棋盘才会遇到的排版坑
腔体是会整体旋转的。我一开始按常规算格子尺寸:屏幕短边 / (n + 0.6)。
转到 45° 的时候,正方形棋盘的外接矩形胀到 √2 倍,四个角直接被屏幕切掉了。
修法很简单,除数改成 n * 1.28 + 1.0(1.28 略大于 √2 的一半留余量)。但这类问题的共性值得记:任何会旋转/缩放的整体元素,取景计算必须按最坏姿态算,不能按初始姿态算。
顺带一提,渲染层还定了一条规则:屏幕的”下”永远是重力方向。 转的是腔体,不是相机——如果让相机跟着转,玩家就得在脑子里做坐标变换,那正是三维贪吃蛇那次踩过的坑(后面会有一篇专门讲)。
六、内容与结构
- 战役 10 章 57 关,机关逐章引入
- 每日挑战 种子 = 日期,全世界同一关,连击会断
- 试炼塔 一条命往上爬,没有撤销,只记最高层
- 成就 20 项
- 关卡工坊 自己造关,求解器当场告诉你它是不是一个真谜题,
GFL1:分享码 - 图鉴 每个机关一张自动演示
- 作弊台 连按两下 Shift:
DEMO演示通关 ·STARS满星 ·UNLOCK解锁 ·ACHV全成就 ·SAND加砂钟 ·SKIP·RESET
core/sim.py 沉降引擎(确定性 + 可证明收敛)
core/solver.py BFS + 周期机器人 + 难度分析
core/gen.py 生成器
core/levels.py 烘焙好的 57 关(自动生成,勿手改)
core/meta.py 每日 / 试炼塔 / 成就
core/editor.py 工坊数据层
ui/render.py 腔体渲染(屏幕的"下"永远是重力方向)
关卡工坊里那句”求解器当场告诉你它是不是一个真谜题”是我最满意的功能:它跑的就是上面那四道筛子,你造的关如果被”一直顺时针”打通了,它会当场告诉你。
上手
run.bat 直接玩(需要 Python + pygame)
build.bat 打包成 dist\Grainfall.exe
build-debug.bat onedir + 控制台,exe 打不开时用这个看报错
python main.py --selftest 57 关解重放 + 每日确定性 + 工坊 + 全场景渲染
← 逆转 · → 顺转 · 空格 翻转 · Z 撤销 · R 重来 · H 提示 · F1 规则 · F11 全屏 · Shift Shift 作弊台
存档在 %APPDATA%\Grainfall\,其中 boot.log 是 exe 打不开时的第一现场。
尾巴
这个项目最值钱的一条,是那个左右偏袒的 bug 逼出来的:
网格物理引擎一趟之内不能依次结算。 谁先动会改变别人看到的世界,而”谁先动”的并列规则就是偏袒。必须让每个格子只看这一趟开始时的盘面,冲突则都不动。
守着它的自测叫镜像对拍——棋盘左右翻转 + 方向映射,结果必须严格镜像。30000 盘零不对称。
以及那句更普适的:玩家报的”这不合理”,有一半是表现问题,有一半是真 bug。先验证守恒律,再决定修哪一层。
📦 源码:https://github.com/ghostgorge/Grainfall
下一篇:纯粹数独 PureSudoku——不加成就、不加商店、不加剧情,力气全花在”难度到底怎么量”上。回复「继续」。
