30 列 17 行的网格,起点在左上角,终点在右下角。
空地图上,A* 给出的最短路径是 41 格,一次寻路访问 234 个节点。
我只在终点旁边补了两座塔,路径立刻变成 43 格 ------ 怪物得先跑到终点外侧,再折回来撞门。
这就是网格塔防和固定路线塔防的分水岭:路不是画好的,是每次算出来的。

一、A* 在这里到底算了什么
寻路的棋盘就是 30×17 的格子,每格 32 像素。
塔占一格,占住就不可通行,起点格和终点格永远禁建。
启发式用的是曼哈顿距离 h = |Δ列| + |Δ行|,四方向代价都是 1,所以这是在无权重网格上跑 A*。
开放列表用的是二叉堆,每次弹出 f 最小的节点,弹出时进 closed 表做去重。
同一张地图我改了三次塔位,把页面上读到的数字列成一张表。
| 地图上的塔 | A* 算出的路径 | 本次访问节点数 |
|---|---|---|
| 一座都没有 | 41 格 | 234 |
| 摆在路两侧的 8 座塔 | 41 格 | 277 |
| 再堵住终点旁边两格 | 43 格 | 349 |
| 终点三个入口全堵住 | 不可达 | 不返回路径 |
访问节点数从 234 涨到 349,是因为终点入口只剩一个的时候,A* 要先把近处那一圈格子探完,才肯绕到远角去。
二、为什么值得看:塔防的可玩性就藏在这条路里
固定路线的塔防,玩家只能沿着画好的那条路摆塔,位置好不好全靠背图。
网格寻路的塔防,玩家能主动改路,把主路一卡,怪物立刻换道。
代价是玩家要自己负责:把路围死,怪物就过不来了。
页面因此加了一条规则,建塔之前先预校验,会把路堵死的格子直接拒绝。

三、三种塔和 20 波数值
塔只有三种,分工不重叠。
| 塔 | 造价 | 伤害 | 射程 | 攻速 | 溅射 / 减速 |
|---|---|---|---|---|---|
| 箭塔 | 50 金 | 9 | 115 | 0.38 秒/发 | 单体,无溅射 |
| 炮塔 | 80 金 | 26 | 105 | 1.45 秒/发 | 溅射半径 55 |
| 减速塔 | 45 金 | 4 | 95 | 0.85 秒/发 | 溅射 40,减速 42% 持续 1.8 秒 |
每波的数量、血量、速度、赏金都是公式算出来的,我把第 1、5、10、15、20 波抄下来核对。
| 波次 | 怪物数量 | 单只血量 | 移动速度 | 单只赏金 |
|---|---|---|---|---|
| 第 1 波 | 7 只 | 20 HP | 42 px/s | 4 金 |
| 第 5 波 | 16 只 | 49 HP | 46.6 px/s | 12 金 |
| 第 10 波 | 27 只 | 144 HP | 52.35 px/s | 27 金 |
| 第 15 波 | 38 只 | 421 HP | 58.1 px/s | 46 金 |
| 第 20 波 | 49 只 | 1225 HP | 63.85 px/s | 68 金 |
开局给 120 金、20 点生命,每波清完再补一笔奖励,金额是 20 加当前波数乘 4。
第一波就能看出经济设计的用意:120 金只够买两座箭塔,或者一座箭塔加一座减速塔。
四、准备环境:进入码道 Web 版,把需求写成一段话
码道有三种使用方式:WebUI(浏览器对话)、TUI(终端命令行)和桌面 IDE(IDE 插件)。本文用 WebUI 版演示。
浏览器打开码道 Web 版:devcloud.cn-north-4.huaweicloud.com/chat?source...
登录之后直接在对话框里说需求,不用装任何东西。
我的需求原文是这样的,读的时候也能看出我想验证什么。
text
用 HTML5 Canvas 做一个单文件塔防小游戏:只允许一个 index.html,纯原生 JS + CSS,
不引任何外部库、字体、图片或网络请求。怪物沿固定路径从起点走到终点,路上可建 3 种塔
(箭塔/炮塔/减速塔),每波怪物数量与血量递增,共 20 波。界面要有金币、生命、波数、
建塔面板、升级与卖出、开始与暂停、胜负判定,还要有一个页面内置的「自检」按钮,
点一下跑完全部检查并显示通过数。完成后打开预览,让我直接看到运行效果。
第一版生成得很快,但怪物走的是我写死在代码里的折线。
我把固定路径这一步推翻,又补了一句要求:换成网格 A* 寻路,塔占格阻挡通行,每波开始重算,建塔前预校验。
五、验证与踩坑
页面底部那个「自检」按钮,点一下会跑 17 项断言,这次是全绿。

几条我认为最有用的自检项,原文抄在这里:
5/ A* 空地图可达:起点(0,2)→终点(29,14),访问 234 节点6/ 路径每步 1 格且首尾正确:路径 41 格 · 曼哈顿步长均 =116/ A* 与 BFS 最短长度一致:缺口图 11 步 · 全堵图 均不可达 · 随机图 5 组均一致17/ 堵死场景能正确判不可达:缺口图可达 · 整墙图 A*=null · 补唯一缺口判堵死=true
第 16 项是我特意让它写的。A* 的启发式一旦写错,最短路径长度就会和 BFS 对不上,这条断言相当于给寻路加了一道回归测试。
下面三条是我实际踩到的,不是设计说明。
坑一:堵中间格不会让路变长。 我在主路上建了一座减速塔,怪物确实换了条道走,但路径长度还是 41 格。
网格四方向、代价都是 1,曼哈顿距离是多少,最短路径就是多少格,绕开一个孤立的障碍不会多走一步。
真正让它绕远路,得逼它做一次折返。把终点旁边两格堵掉,只留最外侧那个入口,路径才从 41 格变成 43 格。

坑二:波次进行中建塔,怪物会瞬移。 第 6 波打到一半时我建了一座塔,路径从一条横线拐到了另一条。
紧接着读坐标,原本停在 (285,112) 的那只怪变成了 (260,144),几只怪的 y 一起从 112 跳到 144,正好一格 32 像素。
原因是实现里怪物只存了「沿路径走了多远」,位置每帧再从当前路径上采样,路一换,它们就被贴到新路上。
想修的话,得在建塔时对每只怪物做一次位置重投影,或者干脆规定波次进行中不允许改路。
坑三:金币不够时的提示分先后。 建塔的检查顺序是先判钱、再判堵路。
我试过在金币只剩 14 金时放炮塔,提示是「金币不足,需要 80」,根本没走到堵路那一关。
所以想验证堵路判断,得先把金币攒够。

六、本地复现
这个页面只有一个 index.html,没有任何构建步骤,下载下来双击就能玩。
想按目录起个本地服务看的话,两条命令就够:
bash
git clone https://atomgit.com/deli007/demo_park.git
cd demo_park/codearts-tower-defense-game && python -m http.server 8000
玩法是先在左侧面板选一种塔,再点路径旁边的空格建造,点已有的塔可以升级或卖出。
自检按钮在第 6 波之后仍然能跑。我这轮是在打过 6 波、场上 10 座塔的状态下点开它的,结果还是 17/17 通过。
实际战绩是:10 座塔守到第 6 波,生命还是 20/20,一次都没漏。第 5 波那 16 只怪用了 20.2 秒清完。
直接下载试玩
这个 Demo 已经完整开源(源代码、本文档、全部截图),托管在公开仓库,可直接下载来玩:
- Demo Park 仓库:atomgit.com/deli007/dem...
- 本案例目录:atomgit.com/deli007/dem...
打开上面的链接,进入 codearts-tower-defense-game 文件夹,把 index.html 下载到本地,用浏览器打开即可运行,无需安装任何依赖。
七、用码道做这个页面的一点体会
第一版生成的塔防是能玩的,但它选了一条最省事的路:把路径写死成折线。
这种写法视觉上没问题,截图也好看,可它把塔防最核心的决策拿掉了。
第二遍我把要求写得更具体,说清了塔占格阻挡通行、每波开始重算、建塔前预校验,它才把 A* 补上。
体会是想要什么机制,就得在需求里点名,别指望一句「做个塔防」能落到机制层面。
把验收方式一起写进需求也省事。我在第一句话末尾加了「点一下跑完全部检查并显示通过数」,它就自己搭了那套 17 项自检。
八、总结
这篇的所有数字都来自页面本身:41 / 43 格、234 / 349 个节点、17 项自检、20.2 秒。
如果你想自己加关卡,最省力的两个方向是改波次公式,和在 tryBuild 里加新的拒绝条件。
下一步我打算把波次进行中改路的瞬移问题修掉,再给塔加一个目标优先级设置,看看同一张图能玩出几种流派。