用python复刻了经典游戏马里奥医生——胶囊医生 Dr. Capsule

本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 2 篇 · 胶囊医生 Dr. Capsule(上一篇:离阵 Arrow Exodus) 源码:github.com/ghostgorge/DrMarioClone 技术栈:Python + pygame,零素材文件——音频用 numpy 实时合成,图形全程序化绘制,代码与音乐均为原创


一、先说这个玩法真正的分水岭

《马里奥医生》看着就是个落物消除,跟《俄罗斯方块》《魔法气泡》像是一家人。但它有一条规则,一条就把它和所有同类分开了:

半重力。

  • 病毒永不下落,焊死在瓶里
  • 一颗完整胶囊只要有任意一半被支撑,整颗就悬停——哪怕另一半下方是悬空的
  • 只有被消除拆散的孤立半块才自由下落

这条规则的后果非常大:你可以刻意搭出悬空平台,把某个颜色送到本来够不到的位置。这是本作独有的空间技巧,《魔法气泡》里不存在。

代价也很直接:因为消除后大部分东西不塌,连锁上限被压到 1–3 级。所以这游戏的爽点不在长连锁,而在”用最少的药清掉最后那只病毒”。

我把这条规则做成了可切换的:多做了一个全重力变体模式(消除后解链、该塌的都塌),把连锁深度还给硬核玩家。两种物理并存,也顺带成了自测里最好用的对照组——后面会讲。

其余的还原项:8×16 瓶、4×(level+1) 病毒(Lv20 上限 84)、≥4 才消除(3 连不触发)、LOW/MED/HI 三档下落速度、每 10 颗胶囊提速、单次落子多杀病毒的分数倍增。

对战送出的垃圾是可消除的单色半块——这点值得单独说:垃圾本身能被算进连线,也就是说防御里天然蕴含反击,局面永远保留逆转空间。这跟”往你瓶里倒不可消除的灰块”是两种游戏。

另外病毒生成器带三级放宽约束阶梯,保证开局绝不存在已成型或接近成型的连线——你的每一分都来自自己的操作,没有一分是白送的。

二、三个真 bug,每一个都改变了对玩法的理解

1. 配色袋的方向做反了

一开始照《俄罗斯方块》的 7-bag 思路做防缺色洗牌。实测一跑:纯色胶囊率从 33% 掉到了 20%。

这恰好饿死了残局最需要的牌型——收尾时你要的往往就是一颗纯色胶囊,正好补上那只孤零零的病毒。防缺色机制把”平均”做出来了,却把”你真正等的那张牌”给稀释了。

改成「均匀分布 + 每色缺色计数器强制补色」:纯色率回到 1/3,缺色上限锁在 9 个半块。

教训:随机性的目标不是”公平”,是”不要饿死玩家的主要战术”。

2. AI 的落点枚举假设错了

原本枚举落点时要求”第 0 行能水平就位”。听着合理,实际上堆高之后大量合法选项被砍掉——而被砍掉的,恰好是好玩家最依赖的塞缝落点(从上面斜插进一个洞里)。

改成 BFS 枚举可达状态并回放实际动作路径,保证模拟出来的落点就是执行得出来的落点。

3. 最值钱的一条:失败条件是局部的,评估函数却是全局的

AI 在 Lv6 打了 88 颗胶囊、把病毒磨到只剩 10 只,然后输了。

dump 出棋盘才看清是怎么死的:它把出生列堆死了,而 0/1/2/4 列从上到下全是空的。

原因很清楚:这游戏里只有出生点那两格能杀死你,是一个局部条件。而我的评估函数用全局 max(height) 来表达”堆太高了”,惩罚上限只有 48 分——而一个”三连含病毒”的收益是 85 分。于是 AI 完全理性地做出了选择:为一条潜在连线堵死瓶口是划算的。

加上出生列的二次惩罚之后,通关率 22% → 28%,Lv0–4 稳定通关。

这一条比我试过的所有权重调整加起来都值钱。它也是原作机制里一条从没被写出来的隐含设计约束:瓶颈是唯一的死因,所有布局决策都必须先服务于它。

更普适的版本:当失败条件是局部的,评估函数就必须显式表达那个局部量。 用全局统计量去近似一个局部约束,优化器一定会找到那条缝钻过去——它不是笨,是你给的目标写错了。

修好之后 AI 的失败模式从”崩盘”变成了”收不掉尾”。这本身就是一次质变。

三、AI 到底有多强(实测,不是估计)

关卡表现
Lv0稳定通关
Lv2–4能打 150–260 颗胶囊,把病毒磨到只剩 1–2 只,然后卡在残局
Lv16+很快阵亡

卡残局的原因是结构性的:最后几只散落的病毒需要特定颜色落在特定格子,而一到二层的贪心搜索没有办法为此做规划——它看不到”再等三颗胶囊就能凑到那个黄色”。

所以我不吹它是强 AI。它作为对战对手完全够用:能活很久、持续送垃圾、不会自杀式崩盘。够了。

性能上做了一件事值得说:完整 2-ply 搜索一次要约 90ms,60fps 下是肉眼可见的卡顿。解法不是砍搜索深度,而是跨帧增量搜索——胶囊从生成到落地有 100 多帧,把候选评估按每帧固定预算摊进去。棋力一点没变,帧时间稳定了。

实测帧耗时(预算 16.7ms):单人 avg 4.6ms、双人 avg 7.6ms、对战电脑 avg 8.5ms / worst 15.8ms。worst 离预算太近,所以又把 AI 每帧预算下调一档。

四、自测:一个”测了个寂寞”的教训

半重力 / 全重力的差异,最初我是这么测的:让 AI 跑若干局,比两种物理下的连锁深度。

测出来的纯是噪声——因为 AI 从不主动搭连锁。我测的不是物理规则,是 AI 的癖好。

现在改成构造局面做确定性验证:摆出一个”消除后有半块悬空”的精确局面,半重力下整颗胶囊悬停、级联停在 1 连锁;全重力下解链坠落、级联继续到 2 连锁。一次断言,零噪声。

规则层的差异要用构造局面测,不要用 AI 对局采样。 采样测出来的是策略分布,不是规则。

三套自测:

python selftest_board.py    # 25 项规则测试:半重力、消除、病毒生成、配色袋、16000 次落子压力测试
python selftest_session.py  # 对局层:旋转闭环、踢墙、锁定延迟、Hold、AI 通关、对战互送垃圾
python smoke_test.py        # 全应用烟雾:四模式 × 两语言 × 所有开关组合,注入合成按键

build_exe.bat先跑自测再打包,自测不过就不打包

v1.0.1 修的是自测自己的毛病

v1.0.0 交付后,打包在用户机器上直接失败了。而所有玩法测试全绿——挂掉的是这么一条断言:

integration run finished quickly —— 171.9s,阈值 120s,goto :fail

我本地跑 48 秒,用户机器上 Python 3.9 跑 AI 集成测试要 170–180 秒。于是构建被一条与正确性完全无关的墙钟时间断言卡死了。

教训:自测里不要写墙钟时间断言。 性能要测就单独测、单独报告,绝不能挂在构建链路上——不同机器、不同解释器版本、不同后台负载,能差三倍以上。

五、其他实现细节

P0(不做就是残缺):3 格预览 + Hold 槽、配色袋防缺色、色块叠加形状符号 ★/●/■(色觉辅助——三色消除游戏不做这个等于把一部分玩家关在门外)、锁定延迟 20 帧 + 落点幽灵投影。

P1:残局配色辅助(剩 ≤3 只病毒时偏置抽色,但保留 20% 随机以免退化成无脑)、LEVEL×SPEED 正交自选难度、瓶身变红 + BGM 变速的双通道危险预警、一键重开。

明确没做:体力条、影响操作结果的角色技能、付费道具。理由是这类设计稀释技巧归因,而技巧归因是消除类留存的唯一根基——玩家必须能确信”这局输了是我操作的问题”,否则重开的动机就没了。

四个模式:单人闯关(0–20 关,按用药效率给 1–3 星)、对战电脑(三档 AI)、对战双人(同屏两瓶)、每日挑战(种子取自当天日期,全世界同一局面)。

单人模式下两套键位都能用,随手用哪套都行:WASD+QEZ 或方向键 + 小键盘。双人对战时 1P 用 A 套、2P 用 B 套。连按两次 SHIFT 出作弊面板。

六、已知边界

Lv20 极难:84 只病毒占满 104 格合法区域,81% 密度,只剩 3 行操作空间。这是 4×(level+1) 公式在 8×16 瓶里的数学必然,原作同样如此——属于耐力测试,不是设计缺陷。AI 在 Lv16+ 基本必死。

中文显示依赖系统字体,按 Windows → macOS → Linux 顺序试一串字体名,并用 Font.metrics() 探测汉字字形是否真的存在(比按名字瞎试可靠得多)。全都找不到时自动退回英文界面并在设置页提示,而不是满屏豆腐块。

尾巴

这个项目最后留给我的是两句话:

一、当失败条件是局部的,评估函数必须显式包含那个局部量。 否则你的 AI 会理性地钻过去,而你会以为它笨。

二、随机性设计的目标不是公平,是别饿死玩家的主要战术。

至于半重力——它是那种”看起来是妥协、其实是全部设计核心”的规则。少了它,这就只是又一个消除游戏。

📦 源码https://github.com/ghostgorge/DrMarioClone 🕹 双击 run_game.bat 直接玩;build_exe.bat 出单文件 exe(会先跑自测);exe 双击没反应用 build_debug_exe.bat 排查。不要用 Anaconda 环境打包,脚本检测到会警告并暂停。


下一篇:烬塔 EmberTower——魔塔类,把这个玩法背后的数学摆到台面上。回复「继续」。