玩家问”为什么砂只往左下滑”,我查出整套物理都在偏袒左边——砂漏 Grainfall

本系列:把这两年用 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,但玩家只会报同一句话。 收到反馈第一件事是先验证守恒律——确认逻辑对不对,再决定是修引擎还是修动画。这两次反馈一次是表现层、一次是引擎层,如果反过来处理,两次都会白干。

四、关卡:只有三个走法,随机机器人赢得太轻松了

关卡不是手搓的,是随机造 + 四道筛子严格筛(顺序仍然是便宜的排前面):

  1. 可解(BFS 求最短解)
  2. 八条周期机器人打不通
  3. 乱转通关率够低
  4. 紧步占比够高(安全率 ≤ 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——不加成就、不加商店、不加剧情,力气全花在”难度到底怎么量”上。回复「继续」。