本系列:把这两年用 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:加攻的收益是阶梯函数
因为有 ceil,A 从 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:
| 层 | 攻 | 防 | 血 | 层 | 攻 | 防 | 血 | |
|---|---|---|---|---|---|---|---|---|
| 1 | 12 | 8 | 1000 | 11 | 138 | 113 | 4600 | |
| 5 | 34 | 27 | 1600 | 15 | 330 | 272 | 9400 | |
| 10 | 110 | 90 | 3800 | 20 | 966 | 800 | 22500 |
怪物由这张表反推:给定”目标回合数 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 且清掉先攻/连击——针对 e 和 c 项 |
| 爆裂符 | 跳过一场必输的战斗(图上”删一个点”) |
| 地震卷轴 | 清空本层杂兵,代价是没有金币——时间换资源的显式取舍 |
| 静默符 / 圣光十字 | 魔攻项 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 出作弊台。
三条你必须知道的规则(也是全文的浓缩)
- 属性不变时,先打谁后打谁掉的血一样。 要想的是”钱花在哪一步”。
- 加攻的收益是阶梯的:加到某个临界才会少打一回合。右侧面板告诉你那个数。
- 加防的收益是线性的,但对”魔攻”完全无效,对”坚固”则是一步登天。
📦 源码:https://github.com/ghostgorge/EmbserTower
下一篇:回响回廊 EchoHalls——塞尔达式迷宫解谜,核心是”第一遍注定走不完”。回复「继续」。
