把魔塔的数学摆到台面上——烬塔 EmberTower

本系列:把这两年用 Python 写的十几个小项目挨个开源、挨个说清楚。不讲”我做了什么”,讲”做的过程中被什么打脸、最后怎么想明白的”。 第 3 篇 · 烬塔 EmberTower(前两篇:离阵 / 胶囊医生) 源码:github.com/ghostgorge/EmbserTower 技术栈:Python + pygame,无外部素材,图形全部程序绘制


一、一句话设计目标

每一场仗打之前,屏幕上就写着这一仗要掉多少血、再加几点攻能少掉多少。

魔塔玩家真正在做的事,从来不是操作,而是在资源受限的图上做调度。传统魔塔把这件事留给玩家心算加试错——本作把它做成面板上的算式。

一句话概括:你要做的不是操作,是算清楚钱花在哪一步。

二、战斗模型,和三条被做进 UI 的推论

设勇士攻 A、防 D、魔防 M;怪物血 h、攻 a、防 d、魔攻 k

每回合有效伤害   p = A - d              (p ≤ 0 时不可战胜)
回合数           t = ceil(h / p)
勇士承伤         L = (t - 1 + e) * [ max(0, a - D) * c + max(0, k - M) ]
                 e = 先攻附加回合数,c = 连击倍率

三条推论,每一条都对应界面上的一个常驻面板:

推论 1:承伤是可加的 → “先打谁”根本不是问题

属性不变时,L 只取决于怪物自身,与击杀顺序无关。所以标准魔塔里”先打哪只”从来不是真问题,真问题是在哪一步把金币变成属性

→ 界面右侧常驻”本层全清预计掉血”,就是这个可加量。

推论 2:加攻的收益是阶梯函数

因为有 ceilA 从 30 涨到 34 可能一点收益都没有,涨到 35 突然少打一整回合。第 k 个攻击临界是:

A_k = ceil(h / k) + d        (k = t-1, t-2, ... 1)

→ 【攻击临界】表直接告诉你”再加到多少攻,能省多少血”。拿到《怪物手册》后展开成完整 4 级临界表——信息本身就是战力,这也是那件道具存在的全部理由。

推论 3:加防的收益是线性的,但只对”还没破防”的怪有效

∂L/∂D = -(t - 1 + e) * c    (当 a > D)
       = 0                  (当 a ≤ D)

→ 【防御收益】表给出 +1/+2/+3/+5/+8/+13 防各省多少血。面板底部把两者并排:+1 攻省 X 血 vs +1 防省 Y 血

商店买什么,从此是一道算术题,不是玄学。

三、然后我故意把推论 1 打破了

可加性太干净了。全塔顺序无关的话,这游戏就退化成纯算术——算完照着执行就行,没有临场判断。

所以 9 种怪物词条里,有三种专门用来让”先打谁”重新变得重要

词条效果破坏可加性
先攻额外承受一回合
连击每回合物理 ×2
魔攻无视防御的固定伤害
坚固防御恒为 A-1,每回合只掉 1 点
增伤每回合伤害递增
诅咒战后损失金币否(但影响经济顺序)
吸血战后按当前血量百分比再咬一口——血少的时候打它最划算
反伤每回合按勇士攻击的百分比反弹——攻击越高越疼
分裂死后裂成两只分体——要挑位置,堵在走廊里打

“坚固”是最重要的教学词条:血量极低(60/90/150),但每回合只掉 1 点。它把玩家逼进一个二选一——要么把防御堆到超过它的攻击(零伤白送),要么消耗一枚破甲之楔。 这一条正好对冲了”无脑堆攻击”的惯性。

而”反伤”教的是一句更反直觉的话:变强不总是好事。

四、结构设计:把图论用起来

20 层地形骨架是手写的(每层 11×11),但内容是按图结构放的,放完再由求解器验收。核心是两个概念。

1. 割点 = 闸门

对每层求 articulation point(删掉它就把楼层切成两块):

  • 若某个割点把入口上楼梯分开 → 它是必经闸门 → 放本层最硬的怪。这保证”想上楼就必须打这一场”,而不是靠运气绕过去。
  • 每层最多 3 个闸门怪,其余按”离入口越远越硬”铺开,避免全挤在一角。

2. 剪环 = 造宝库

这里踩了一个坑,值得单独说:手写出来的骨架大多是环——每格都有两条以上的路,天生没有死路。结果是钥匙一把都放不下去,因为任何一扇门都能绕过去。

做法是人工造割点:挑一格 x(离入口最远、只有两个邻居),把它的一个邻居砌成墙、另一个邻居换成门——x 就变成一间必须花钥匙才能进的内室。

每砌一次都复查:“除了 x 以外,全楼是否仍然连通、上楼梯是否仍可达”,不满足就撤销。首领层额外复查**”凿开之后不能绕过首领”**。所以这一步永远不会把塔改坏。

通用教训:结构化布点之前先量一遍地图的”环性”。 全是环的骨架没有割点也没有死路,钥匙/门/宝库一个都放不下去。要么手写死路,要么用剪环人工造割点,且每剪一次都复查连通性。

五、数值:分档的目的是让临界”够得着”

设计基准是”进入第 N 层时勇士应有的属性”,每层约 ×1.24:

11281000111381134600
534271600153302729400
101109038002096680022500

怪物由这张表反推:给定”目标回合数 t”和”目标承伤占当前血量的百分比 pct”,

d = A * 防御占比(0.30~0.60,按怪物定位)
h = (A - d) * (t - 0.5)
a = D + (H * pct / 100) / (t - 1 + e) / c

杂兵 t=3~4, pct=3.5%~5%;精英 t=5~7, pct=7%~9%;首领 t=7~9, pct=24%~34%

属性宝石按区分档:一区 +3、二区 +8、三区 +25、四区 +70。分档的唯一目的是让”攻击临界”在每一区都恰好是 1~3 颗宝石的距离——太细则临界失去意义(反正下一颗就跨过去了),太粗则没有取舍(一颗都够不着)。

同理,四区之间不互通经济:本区的资源必须、也只能在本区消化。这让局部最优接近全局最优,也让数值可以分区独立校准

六、道具:每一件都对应一类数学问题

这是我最喜欢的一张表——道具不是”效果集合”,是方程里的项

道具它解的是哪一项
怪物手册把 2 级临界表展开成 4 级——信息即战力
破甲之楔“坚固”(防御恒为 A-1)的解
引雷符削掉一半生命上限 = 直接把 t 砍半,专治高血厚甲首领
霜冻符回合数 -2 且清掉先攻/连击——针对 ec
爆裂符跳过一场必输的战斗(图上”删一个点”)
地震卷轴清空本层杂兵,代价是没有金币——时间换资源的显式取舍
静默符 / 圣光十字魔攻项 max(0, k - M) 的唯一解
守夜人誓约免疫反伤,把”攻击越高越疼”这条反向规则关掉
矿镐在图上加一条边——可以省一把钥匙,也可以走出死路
飞行靴把塔从一条链变成星形图,回低层商店不再费步数
时之沙回到上一战之前——允许试错,降低”算错一次白给”的挫败

七、验收:一个”老实玩家 AI”

core/solver.py 跑的是和 UI 完全同一套引擎,策略故意做得很笨:

  • 能拿的道具都拿
  • 能打且掉血不超过当前血量 60% 的怪都打(按掉血从小到大)
  • 有钥匙就开门;被”坚固”卡住就用破甲
  • 卡住了就回低层商店,用边际收益 Δ承伤 / 金币 挑最划算的一件买
  • 还卡住就用炸弹

它不做任何超人规划,所以结论是下限它能通关 ⇒ 真人一定能通关。

最近一次 --selftest

层   离层HP    攻    防   魔防     金币
 2    1328     15    11     0      110
 5    2802     30    23     0      892
 8    4058     52    37    20     2213
10    1358     68    61    20     3226   ← 全塔最紧张的一段
15    6181    309   202    80     9391
20   16429   1030   635   540    26050
结果:通关 ✓

第 9→11 层是刻意设计的紧张段:二区首领腐心巫祝带吸血(血越满被咬得越狠),而三区开局的镜灵带反伤(攻击越高越疼)。这两层逼玩家第一次真正去看边际收益面板,而不是无脑堆属性。

自测共 47 项,其中我最看重的一项是**”承伤可加性(顺序无关)对拍”**——随机打乱击杀顺序,总承伤必须严格相等。这条一旦被某次改动悄悄破坏,整个 UI 上的数字就全是骗人的。

八、剧情:每位首领都是本区的考试

四区四位守夜人,一个循环。

名字首领词条主题
1-5铁锈回廊铁锈骑士·维恩先攻 —— 教”攻击临界”
6-10蚀骨深窖腐心巫祝·塞蕾魔攻 + 吸血 —— 教”魔防”和”顺序”
11-15镜影长廊镜中之我反伤 —— 教”变强不总是好事”
16-20灰烬王庭灰烬王增伤 —— 教”必须靠攻击压回合”

每一位首领的机制,都是这一区教学重点的考试。

结局二选一:循环——取走星陨之心走出去,下一位守夜人抬头时,王座上是你的脸;断链——把刀插进那颗心,塔塌了,没有第六位守夜人。

九、v1.0.2:能算不等于能看懂

v1.0 交付后的实机反馈很扎心:地图上一堆 Y B R A D M $ T 字母,玩家根本不知道哪个是什么;36 种怪长得全都是”带眼睛的圆圈 + 一个伤害数字”

我把力气全花在数学可见性上,却忘了信息要先被识别,才谈得上被计算

v1.0.2 全部改在渲染层:

  • 道具变真图标:钥匙、红心血瓶、剑、盾、符文、金币;走到旁边直接冒出中文名
  • 怪物按族群区分形态:黏菌 / 蝙蝠 / 骷髅 / 铁甲卫 / 幽魂 / 法师 / 剑士 / 拟形怪 / 独眼哨 / 巨口……一眼能认;身上的彩点 = 词条
  • 三个查阅界面L 图例、I 道具图鉴、B 怪物图鉴

教训:符号密集的类 roguelike,可读性要靠”按族群造型 + 一块查阅面板”来解决,不要逐个画独立立绘。 36 种怪画 36 张图是不现实的,但归成 10 个族群、再用彩点表达词条,成本可控而且可扩展——加第 37 种怪不需要新画一张图。

十、彩蛋:一行 echo 把刚打好的 exe 覆盖了

最后讲一个 v1.0 的打包事故,因为它足够蠢也足够典型。

build_exe.bat 的收尾横幅我写了这么一行:

echo   DONE  ->  dist\EmberTower.exe

cmd 把 > 当成了重定向。于是刚刚辛苦打包出来的几十 MB exe,被覆盖成了一个 11 字节的文本文件(内容是 DONE - 加换行)。双击报”此应用无法在你的电脑上运行”。

最坑的是:构建日志从头到尾一切正常,唯一的线索是横幅那一行凭空消失了。

两条修法:横幅换措辞去掉裸的 >;并在脚本末尾加体积校验——onefile 正常是几十 MB,低于 1MB 直接报错退出。

.bat 的 echo 行里绝对不能出现裸的 >< 顺带另一条同源的:.bat 必须纯 ASCII + CRLF,cmd 按 GBK 读取,UTF-8 中文注释会被截成半截命令执行。

上手

pip install pygame
python main.py
python main.py --selftest    # 47 项自测,含全塔通关验收

打包双击 build_exe.bat(脚本按 py -3.12 → 3.11 → 3.9 → python 挑解释器)。

按键:方向键/WASD 移动,1-9 用道具,Tab 锁定分析,M 本层怪物,L/I/B 三个图鉴,F5/F9 存读档,连按两下 Shift 出作弊台

三条你必须知道的规则(也是全文的浓缩)

  1. 属性不变时,先打谁后打谁掉的血一样。 要想的是”钱花在哪一步”。
  2. 加攻的收益是阶梯的:加到某个临界才会少打一回合。右侧面板告诉你那个数。
  3. 加防的收益是线性的,但对”魔攻”完全无效,对”坚固”则是一步登天。

📦 源码https://github.com/ghostgorge/EmbserTower


下一篇:回响回廊 EchoHalls——塞尔达式迷宫解谜,核心是”第一遍注定走不完”。回复「继续」。