本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 4 篇 · 回响回廊 EchoHalls(前三篇:离阵 / 胶囊医生 / 烬塔) 源码:github.com/ghostgorge/EchoHalls 技术栈:Python + pygame,8 章 48 关,关卡程序生成 + 六道筛子验收
一、迷宫游戏最容易犯的错
地图再大再花,玩家沿着墙摸一圈也就出去了。
贴墙法(左手法则)是个几百年前就有的万能解。只要迷宫是单连通的,闭着眼睛贴着一侧墙走,一定能走到出口。你花三天手工雕的 60×60 迷宫,玩家用一条不需要动脑的规则就通关了。
所以这个项目从第一行代码起就奔着一个判据做:
闭着眼睛现场探路的人,走不完。
一句话设计目标:做一个”第一遍注定走不完”的迷宫。
二、三样东西咬在一起
- 迷雾——暗层里光只照 2~5 格,你看不到全图。
- 步数上限——每关给的步数只比最短解多 10%~50%。
- 记忆留存——走过的格子以暗色永久留在图上,同一关重试之间保留。
于是游戏循环变成了:
第一遍 = 侦察 必然超步数,但你摸清了岔路在哪
第二遍 = 执行 照着脑子里(和图上暗色)的路直着走过去
失败不是惩罚,是信息采集。 这就是”回响”的意思——你走过的每一步都在墙上留了回声。
这三样缺一样都不成立:只有迷雾没有步数上限,玩家可以慢慢磨;只有步数上限没有迷雾,第一遍就看得见全图;有前两样但不留记忆,那就是纯粹的折磨,重试跟重来没区别。
一命通关模式「无灯之夜」 把这套循环整个拆掉:不能撤销、地图记忆不保留、48 关连着打,任何一次超步或掉光心就从第 1 关重来。它考的是你真的记住了没有。
三、验收:生成不算数,验收才算数
关卡是程序生成的,但每个候选要连过六道筛子。顺序按”便宜的排前面”——这一条是从上一个项目带过来的教训,贵的筛子放前面会白白烧掉十倍时间:
| 顺序 | 筛子 | 淘汰什么 | 代价 |
|---|---|---|---|
| 1 | A* 求最短解 | 无解的 | 毫秒级 |
| 2 | 步数区间 | 太短(没内容)或太长(磨人)的 | 免费 |
| 3 | 贪心机器人 | 一根筋直冲出口就能过的 | 快 |
| 4 | 贴墙法机器人(左右手各一遍) | 经典万能解能过的 | 快 |
| 5 | 盲探机器人 ×15 次 | 一次探路就能过的 | 慢 |
| 6 | 道具必需性 / 机关深度 | 道具是摆设的 | 中 |
第 5 道筛子,和我一开始搞错的地方
这是全套里最关键的一个,也是我纠正过的一个错误。
最初我拿”不用道具也能解”当淘汰条件——想法是”如果不用道具就能过,说明道具是白给的”。结果全军覆没,一关都过不了。
原因很蠢但很重要:不带道具的完美求解器不是笨办法,它是一个更强的求解器。 任何谜题不在道具上的关卡它都能过。拿它当淘汰条件,等于要求”这关必须只能靠道具解”,那是另一种游戏。
对迷宫来说,真正的笨办法是闭着眼睛现场探路:
- 优先踩没走过的格子
- 走进死胡同就退回来
- 绝不主动往尖刺上撞
- 但会用道具
任何迷宫这么走早晚都出得去——问题是要走多少步。于是判据自然浮现:跑 15 次(随机打破并列),统计”在步数上限内走出去”的比例。
这个比例就是盲探一次通关率。游戏里右侧面板直接把它显示给玩家看。
通用教训:设计”笨办法机器人”时,笨的方向要选对。 削掉能力(不许用道具)得到的是一个残废的强者;要削的是信息(看不见全图)和规划(走一步看一步)。前者测的是关卡的道具依赖,后者才是”这关需不需要动脑”。
步数上限才是真正的难度旋钮——它要卡在”探一遍走不完、背下来才走得完”之间。
四、实测曲线
| 章 | 名字 | 主题 | 最短解 | 步数上限 | 紧度 | 盲探一次通关率 |
|---|---|---|---|---|---|---|
| 1 | 石廊 | 走路、钥匙、锁门 | 44–58 | 60–82 | 0.73 | 37% |
| 2 | 灯下 | 光只照三格 | 50–78 | 68–102 | 0.76 | 0% |
| 3 | 尖牙 | 尖刺按拍子、地板会塌 | 64–81 | 81–101 | 0.80 | 1% |
| 4 | 推石 | 压板与闸门 | 46–64 | 58–79 | 0.80 | 0% |
| 5 | 冰室 | 冰上刹不住 | 40–66 | 51–82 | 0.81 | 1% |
| 6 | 深坑 | 羽毛跳、钩爪 | 56–76 | 71–92 | 0.82 | 1% |
| 7 | 暗记 | 全黑 + 三次回想 | 79–143 | 95–161 | 0.85 | 2% |
| 8 | 回响 | 全都来一遍 | 68–102 | 76–109 | 0.90 | 0% |
第 1 章是教学,故意留着 37%——前三关允许无脑走过去,玩家得先相信自己会走路。从第 2 章起压到 0~2%。
紧度(最短解 / 步数上限)从 0.73 爬到 0.90:第 8 章的步数余量只有 10%,几乎不允许任何一次走错回头。
五、机关:每一个都对应图论里的一个操作
塞尔达谱系的那套元素,我全部按”它在数学上是什么”来选:
| 元素 | 它在数学上是什么 |
|---|---|
| 锁门 / 钥匙 | 图上的强制拓扑顺序(门放割点,钥匙放门这一侧) |
| 裂墙 / 炸弹 | 同上,但资源是消耗品 |
| 压板 / 石块 / 闸门 | 不可逆动作——推错方向就回不来 |
| 火把 / 灯笼 | 光照资源 |
| 尖刺 | 时间维度:按 (步数 + 相位) % 周期 < 占空 起落,原地”等”一步也是解法 |
| 碎地板 | 单向边——走过就没了 |
| 深坑 | 需要羽毛(跨一格)或钩爪(拉一串) |
| 羽毛跳 | 跳过一个点(2 步) |
| 钩爪 | 加一条长边——朝一个方向找铆点,把自己拉过去,中间可以是坑 |
| 冰面 | 走一步 = 走一段,路径不再由你逐格控制 |
| 石块推进坑 | 用一个不可逆动作换一条边 |
| 回想 | 把”信息”标价成”步数” |
最后一条是整个设计的支点。 暗层里你随时可以花 3 步买一次全图闪现,所以”记不住”永远有解,只是要付钱。第 7 章只给 3 次,第 8 章只给 2 次。
这解决了记忆类玩法最大的隐患:记忆力差不该等于玩不了。 把信息明码标价之后,记性好的人省下步数用来试错,记性差的人花步数买确定性——两条路都通,只是紧度不同。
六、工程:关卡不存地图,只存种子
core/rules.py 规则内核。状态压成不可变元组,UI 和求解器共用同一份规则
core/solver.py A* 求最短解 + 四种笨办法机器人
core/gen.py 迷宫骨架(递归回溯 + 编织)+ 母题布点 + 章节配置
core/baked.py 自动生成:48 关的种子表(不要手改)
core/session.py 一局的运行时:迷雾、记忆、回想、撤销
tools/bake.py 扫种子、验收、写 baked.py
生成器是确定性的,同一个种子永远长出同一座迷宫,所以 48 关只存种子 + 章节配置。改生成器就得重烤(python tools/bake.py,约 5 分钟)。
A* 的启发式:状态空间爆炸先加启发式,再谈上限
一开始用纯 Dijkstra,推石关(两个石块可自由移动)状态直接炸到 6×10⁷,20 万节点的上限根本不够看。
解法是加一张**”把门、坑、闸全当通路,只留墙”的到出口距离表**当下界。它永远不会高估真实代价,所以是合法的 A* 启发式。同样的关卡从”跑不出来”变成毫秒级。
另一个几乎免费的优化:没有尖刺的关卡把周期设成 1,直接省掉状态里一个白白翻四倍的维度。
母题布点要反着定位
三个坑,都是”顺着放”必踩的:
- 压板必须放在闸门的同一侧——否则要先过闸门才能开闸门,80% 的候选直接无解。
- 推石要反着定位:先定压板,再沿某个方向往回数 k 格放石块,并确认石块背后还有一格能站人。随机撒点会大量产出推不动的死局。
- 深坑两端都要能挂钩爪——只在一端放铆点,过去了就回不来。
共同点:先定”目标位置”,再往回推”起始位置”。 顺着随机撒点然后祈祷有解,命中率低一个量级。
七、成就:刻意不做重复劳动型
28 项,分四类:进度(每章一枚 + 全通)、精度(最短步数通关 1/10/30 关、一遍过 1/10 关)、自律(不用撤销、不用回想、毫发无伤)、硬核(无灯之夜活到第 3/6 章 + 隐藏的全通)。
刻意没有做任何”重复劳动型”成就——走 N 步那一枚是唯一例外,且门槛很低。
理由很简单:这个游戏的乐趣在于把一条路走短,不在于时长。 加一个”累计行走 10000 步”的成就,等于鼓励玩家做与核心乐趣完全相反的事。
尾巴
这个项目留下的最有价值的一条,是关于怎么设计验收机器人:
削能力得到的是残废的强者,削信息才得到真正的笨办法。
判断”这关需不需要动脑”,不要问”少了某个工具还能不能过”,要问”看不见全图、走一步看一步的人,能不能在预算内出去“。
以及那条一以贯之的:筛子的顺序值十倍时间,便宜的必须排前面。
📦 源码:https://github.com/ghostgorge/EchoHalls
下一篇:FANGCUN——回复「继续」。
