Ants Vs. SomeBees (C++ 版本) 介绍

下面有关游戏程序部分的内容,基于 v0.1.0-SeaOtter-patch.2 版本。

本博文刚建立时最新的版本是 v0.1.0-SeaOtter-patch.2,最终提交的产品版本是 v0.1.0-SeaOtter-patch.1。

本项目由于时间问题,未能提高项目的鲁棒性,因此错误的游戏方式可能导致不明原因的错误(如存档键值错误等等),即使是正确的游戏方式也无法保证一定不会出现问题。若有问题可前往 issue 进行反馈。

游戏背景

本项目是一个塔防游戏,基于 CS61A 项目作业 Ants Vs. SomeBees[1](Avsb),使用了 C++ 进行重新实现,同时增加了一些新的功能和特性。

做过 CS61A 项目作业的一下子肯定就知道具体内容了,不过这里还是会进行详细介绍。

基础概念

这个就是游戏界面了,虽然一目了然,但还是让我来依次介绍一下。

首先是最上面一排的功能按钮/信息块:

  • Exit:退出按钮,会同时退出 Web GUI 和后端
  • Restart:重启按钮,会回到游戏主界面,游戏当前内容会被放弃
  • Save:浏览器会下载一个当前局面的 JSON 格式存档文件
  • Food:显示当前食物数目
  • Turn:显示当前轮数

然后是一排 16 个我方蚂蚁单位,具体细节会在稍后介绍。

每个蚂蚁单位上面是其名称,下面的数字是其食物消耗。顺序也是按照所需食物升序进行排列。不能够购买的蚂蚁单位是暗的,如上图因为食物只有 1,所以只有 Remover 这个单位是亮色的,可以进行部署。

下面则是具体进行游戏的地图,除了普通的陆地外还有湿地是特殊类型的位置单位。

就像植物大战僵尸一样,可以将蚂蚁单位部署在地图上,而蜜蜂会从右侧右侧的战争迷雾中出现,并向左边基地移动。同时当蜜蜂抵达地图最左边时,游戏失败。当所有蜜蜂被消灭时,游戏胜利。

有关地图的无关游戏的细节

这个游戏界面也许跟别的 CS61A 项目的实现并不一致。

实际上 CS61A 的项目作业一直在进行演化,光是我见过的 Web GUI 就有三种类型的了。其中一种是非 Web GUI,而且建模比较粗糙,就忽略不提了。另外一种就是我学 CS61A 时的项目了。

我做的时候除了上面一排、蚂蚁单位商店的样式有所不同外,最明显的差距就是没有战争迷雾,所有尚未出动的 Bee 一清二楚。这样有助于玩家掌握游戏进程,当然也少了一点刺激感。

最终我这个 Web GUI 是基于最新的(2024 Fall)CS61A 的 Web GUI 版本,这个后续会谈原因。

我方单位信息如下表所示:

形象 名称 食物消耗 血量 伤害 特殊
Remover 0 0 x 杀死选中位置上非 Queen 的蚂蚁。若对象是 Container,则只杀死 Container
Harvest 2 1 0 每轮收获一个单位的食物
Wall 4 1 0 -
Long 2 1 1 每轮攻击前方 5 格以后范围中的一个蜜蜂单位
Short 2 1 1 每轮攻击前方 3 格以前范围中的一个蜜蜂单位
Thrower 3 1 1 每轮攻击前方一个蜜蜂单位
Scuba 6 1 1 抗水,每轮攻击前方一个蜜蜂单位
Slow 4 1 0 每轮给予前方一个蜜蜂单位「缓慢」效果[2]
Scary 6 1 0 每轮给予前方一个蜜蜂单位「恐惧」效果[3]
Guard 4 2 0 是 Container,可以容纳一个非 Container 的蚂蚁单位
Tank 6 2 1 是 Container,可以容纳一个非 Container 的蚂蚁单位,同时对当前位置所有蜜蜂单位造成伤害
Hungry 4 1 0 每轮若不在咀嚼时,吃掉当前位置的一个蜜蜂单位,并咀嚼 3 轮[4]
Fire 5 3 x (+ 3) 受到攻击时,对当前位置所有蜜蜂单位进行等量伤害反射。死亡时额外附加伤害
Ninja 5 1 1 每轮攻击当前位置所有蜜蜂单位,同时不会阻拦蜜蜂单位继续前进
Laser 10 1 2 - f(x) 每轮攻击前方所有昆虫单位,包括当前位置的 Container,伤害随距离与攻击次数增加而衰减[5]
Queen 7 1 1 抗水,每轮攻击前方一个蜜蜂单位,并给予后方所有蚂蚁单位加成[6]。只能有一个 Queen,Queen 死亡时游戏失败

敌方单位信息如下表所示:

形象 名称 伤害 特殊
Bee 1 -
Wasp 2 -
NinjaBee 1 不会被阻拦
Boss 2 收到的伤害上限为 8,同时会被修正:最终伤害=受到伤害×伤害上限受到伤害+伤害上限\text{最终伤害} = \dfrac{\text{受到伤害} \times \text{伤害上限}}{\text{受到伤害} + \text{伤害上限}}

上面的图片素材全部来源于 CS61A 项目,除了 Remover 都是拷贝自 2024 Fall 版本中的素材,Remover 来自我做的版本。

游戏打包

发布的是一个 Avsb.zip 的 ZIP 压缩包文件,将其解压到 ./Avsb 目录后就是游戏本体,其中包含四个文件或目录:

  • Avsb.exe/Avsb:游戏本体可执行文件(分别是 Windows 和 Linux 版,Windows 版有图标),支持直接打开或命令行运行
  • static:存储游戏所需的静态资源,包括图片、音频等
  • templates:存储游戏的 HTML 模板文件

后续可能将 templates 目录合并进 static 目录,同时可能将 Windows 和 Linux 分开打包。

实际上游戏体验没啥区别,所以下面以 Windows 为例。

命令行

-h--help 选项获取帮助信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ ./Avsb.exe --help
Usage: Ants Vs. SomeBees [--help] [--version] [[--difficulty DIFFICULTY]|[--plan PLAN]] [--water] [--open] [--food FOOD] [--log LEVEL] [--port PORT] [--config CONFIG] [--save]

Optional arguments:
-h, --help shows help message and exits
-v, --version prints version information and exits
-d, --difficulty DIFFICULTY sets difficulty of game (test/easy/normal/hard/extra-hard) [default: "normal"]
-a, --plan PLAN path to custom assault plan JSON file
-w, --water loads a full layout with water
-o, --open automatically open the game in a browser (maybe not work in your OS!)
-f, --food FOOD number of food to start with when testing [default: 2]
-l, --log LEVEL sets log level (0:TEST, 1:INFO, 2:ERROR, 3:NONE) [default: 1]
-p, --port PORT sets the port for the server [default: 18080]
-c, --config CONFIG path to config file [default: "./config.json"]
-s, --save save game configuration to file and exit

-v--version 获得版本信息:

1
2
$ ./Avsb.exe --version
0.1.0-SeaOtter-patch.2
版本信息细节

常见的语义化版本采用的是 major.minor.patch 式版本,即主版本号、次版本号、修订号,而我并不是按这个来的。

虽然一开始确实有遵照语义化版本的意思,但是后面却追加了 patch,自然与语义化版本规范冲突了。

至于为啥要追加尾 patch 而非将第三个版本号上调呢?是因为我懒得发新版本……因为还没学 GitHub Action,而且用的是 XMake 构建工具,Windows 又要下载依赖什么的,比较麻烦,所以暂时还是手动发版。而要是上调版本号,我就还要发新版、写新的更新日志什么的,太麻烦了,就追加 patch 把之前的覆盖了

于是现在采用的是 primary.phase.build,即主序号、阶段号、构建号。完整的版本信息则是 <primary>.<phase>.<build>-<codename>-patch.<patch>,除了 patch 补丁号还多了一个 codename 代号。这个代号比较随意,没有明确的规范,我想怎么给就怎么给,但不会出现两个版本使用相同的代号的情况。

存档的版本不包括补丁号,所以会尽量让补丁不破坏存档的兼容性。不过 v0.1.0-SeaOtter-patch.1 与 v0.1.0-SeaOtter-patch.2 存档是不兼容的,因为存档版本的判断是 patch.2 引入的。

另外实际上真正的 v0.1.0-SeaOtter-patch.2 使用 --version 选项不会显示补丁号,这个是 patch.2 后面的 commit 追加的更新,不过因为不涉及程序、游戏等方面的内容,就没有加新号。

反正就是版本号非常混乱就是了。

-d--difficulty 选项可调节难度,该选项与下面一个对攻击计划进行调节的选项冲突。

一共有 test, easy, normal, hardextra-hard 五个难度,默认是 normal。难度会影响地图与敌方的攻击计划。

地图的长度是写死固定为 10 的,因为 CS61A 就是这么做的。在虚无飘渺的未来可能会加上自定义地图参数的支持。而宽度,test 难度是 1,easy 是 2,其余都是 4。

不过理论上可以通过修改存档自定义地图。但是因为并没有对其进行适配,可能会有问题。

从个人的角度出发也不建议用宽度小于 4 的地图,因为前端设计的是填充的,小于 4 效果会很丑陋。

敌方攻击计划细节

test 难度:

  • 第 2 轮:1 只生命为 3 的 Bee
  • 第 3 轮:1 只生命为 3 的 Bee

easy 难度:

  • 第 3 轮:1 只生命为 3 的 Bee
  • 第 4 轮:1 只生命为 3 的 Wasp
  • 第 5 轮:1 只生命为 3 的 Bee
  • 第 7 轮:1 只生命为 3 的 Bee
  • 第 8 轮:1 只生命为 3 的 NinjaBee
  • 第 9 轮:1 只生命为 3 的 Bee
  • 第 11 轮:1 只生命为 3 的 Bee
  • 第 13 轮:1 只生命为 3 的 Bee
  • 第 15 轮:1 只生命为 3 的 Bee
  • 第 16 轮:1 只生命为 15 的 Boss

normal 难度:

  • 第 3 轮:2 只生命为 3 的 Bee
  • 第 4 轮:1 只生命为 3 的 Wasp
  • 第 5 轮:2 只生命为 3 的 Bee
  • 第 7 轮:2 只生命为 3 的 Bee
  • 第 8 轮:1 只生命为 3 的 NinjaBee
  • 第 9 轮:2 只生命为 3 的 Bee
  • 第 11 轮:2 只生命为 3 的 Bee
  • 第 13 轮:2 只生命为 3 的 Bee
  • 第 15 轮:2 只生命为 3 的 Bee
  • 第 16 轮:1 只生命为 3 的 Wasp
  • Boss 阶段
  • 第 21 轮:2 只生命为 3 的 Bee
  • 第 22 轮:2 只生命为 3 的 Wasp
  • 第 23 轮:2 只生命为 3 的 Bee
  • 第 24 轮:1 只生命为 1.5 的 Bee
  • 第 25 轮:2 只生命为 3 的 Bee
  • 第 26 轮:2 只生命为 3 的 NinjaBee
  • 第 27 轮:2 只生命为 3 的 Bee
  • 第 28 轮:1 只生命为 1.5 的 Bee
  • 第 29 轮:2 只生命为 3 的 Bee
  • 第 30 轮:1 只生命为 20 的 Boss

hard 难度:

  • 第 3 轮:2 只生命为 4 的 Bee
  • 第 4 轮:2 只生命为 4 的 Wasp
  • 第 5 轮:2 只生命为 4 的 Bee
  • 第 7 轮:2 只生命为 4 的 Bee
  • 第 8 轮:2 只生命为 4 的 NinjaBee
  • 第 9 轮:2 只生命为 4 的 Bee
  • 第 11 轮:2 只生命为 4 的 Bee
  • 第 12 轮:1 只生命为 2 的 Bee
  • 第 13 轮:2 只生命为 4 的 Bee
  • 第 15 轮:2 只生命为 4 的 Bee
  • 第 16 轮:2 只生命为 4 的 Wasp
  • Boss 阶段
  • 第 21 轮:3 只生命为 4 的 Bee
  • 第 22 轮:2 只生命为 4 的 Wasp
  • 第 23 轮:3 只生命为 4 的 Bee
  • 第 24 轮:1 只生命为 2 的 Bee
  • 第 25 轮:3 只生命为 4 的 Bee
  • 第 26 轮:2 只生命为 4 的 NinjaBee
  • 第 27 轮:3 只生命为 4 的 Bee
  • 第 28 轮:1 只生命为 2 的 Bee
  • 第 29 轮:3 只生命为 4 的 Bee
  • 第 30 轮:1 只生命为 30 的 Boss

extra-hard 难度:

  • 第 2 轮:1 只生命为 2.5 的 Bee
  • 第 3 轮:2 只生命为 5 的 Bee
  • 第 4 轮:2 只生命为 5 的 Wasp
  • 第 5 轮:2 只生命为 5 的 Bee
  • 第 7 轮:2 只生命为 5 的 Bee
  • 第 8 轮:2 只生命为 5 的 NinjaBee
  • 第 9 轮:2 只生命为 5 的 Bee
  • 第 11 轮:2 只生命为 5 的 Bee
  • 第 12 轮:1 只生命为 2.5 的 Bee
  • 第 13 轮:2 只生命为 5 的 Bee
  • 第 15 轮:2 只生命为 5 的 Bee
  • 第 16 轮:2 只生命为 5 的 Wasp
  • Boss 阶段
  • 第 21 轮:3 只生命为 5 的 Bee
  • 第 22 轮:2 只生命为 5 的 Wasp
  • 第 23 轮:3 只生命为 5 的 Bee
  • 第 24 轮:1 只生命为 2.5 的 Bee
  • 第 25 轮:3 只生命为 5 的 Bee
  • 第 26 轮:2 只生命为 5 的 NinjaBee
  • 第 27 轮:3 只生命为 5 的 Bee
  • 第 28 轮:1 只生命为 2.5 的 Bee
  • 第 29 轮:3 只生命为 5 的 Bee
  • 第 30 轮:1 只生命为 30 的 Boss

-a--plan 选项可指定外部攻击计划的 JSON 文件。

后续可能弃用 -a,改用 -P

攻击计划 JSON 文件细节

下面是 normal 难度攻击计划的节选:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"waves": {
"3": [
{
"health": 3,
"place": "Hive",
"scaredTime": 0,
"slowedTime": 0,
"type": "Bee"
},
...
],
...
"30": [
{
"health": 20,
"place": "Hive",
"scaredTime": 0,
"slowedTime": 0,
"type": "Boss"
}
]
}
}

可以看出来,攻击计划的 JSON 中只有一个一级键 "waves",对应的值是一个对象,存储了攻击波次的具体信息。

攻击波次对象的键是数字(数字字符串),代表攻击的轮次。原则上小于时间的轮次不应出现在当前攻击计划中,即攻击计划是动态波动的。

轮次的键对应一个数组,成员为代表蜜蜂信息的对象。可以看出蜜蜂对象一共有五个键值对,分别代表其当前生命值、所处位置名称、恐惧状态剩余时间、缓慢状态剩余时间、类型。

攻击计划中的蜜蜂的所处位置,都应为蜂房,即 Hive

-w--water 选项允许地图出现湿地,默认是没有湿地。

湿地位置细节

湿地的创建由 wetLayout 函数决定,该函数的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @brief 湿地布局
*
* 从基地开始,创建一系列的 Tunnel,其中每隔一定距离会有湿地。
*
* @param base 基地
* @param registerPlace 注册 Place 的函数
* @param dimensions 布局维度
*/
void wetLayout(AntHomeBase *base, GameState::register_place_f registerPlace, dim dimensions) {
GameState::createLayout(base, registerPlace, dimensions, 3);
}

GameState::createLayout 的声明如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief 创建布局
*
* 从基地开始,创建一系列的 Tunnel,其中每隔一定距离会有湿地。
*
* 具体而言,当 `moatFrequency` 不为 0 时,每隔 `moatFrequency` 步会有一个湿地。
*
* @param base 基地
* @param registerPlace 注册 Place 的函数
* @param dimensions 布局维度
* @param moatFrequency 湿地频率
*/
void GameState::createLayout(AntHomeBase *base, register_place_f registerPlace, dim dimensions, int moatFrequency);

因此可以看出,湿地的出现完全是固定的,没有随机因素。

而湿地频率这个值也是写死的 3,因为 CS61A 就是这么做的。不过后面也可能开放修改。只是如果开放了自定义地图,这个的意义就显得没这么大了。

-o--open 选项以启动时自动打开 Web GUI,无法保证一定奏效。

另外如果已经打开了一个 Web GUI,该选项不会激活对应网页,而是会新打开一个,符合大部分程序 -o--open 选项的行为。

该选项的具体实现

部分代码如下:

1
2
3
4
5
6
7
8
9
    string command;
#ifdef _WIN32
command = format("start http://localhost:{0}", app.port());
#elif __APPLE__
command = format("open http://localhost:{0}", app.port());
#else
command = format("xdg-open http://localhost:{0}", app.port());
#endif
system(command.c_str());

对于 Windows, macOS 与 Linux 平台,分别使用了 start, openxdg-open 命令以尝试打开网页。因此有可能不被支持。

不过在我这里,不管是 Windows11 还是 WSL Ubuntu 24.04,都可以正常打开网页,WSL 打开的是本机的网页。

-f--food 选项可以指定开局的食物数目,默认是 2。

允许的范围就是 int 的范围,也就是说其实完全允许负数。只是小于 2 的初始食物数目会丧失游戏性,因为食物无法继续增长,甚至可能连移除蚂蚁单位都做不到。当然,如果是打算极限游玩定制的存档,那确实没问题,只不过这不是这个选项的管辖范围。

后续可能增加范围的限制?

-l--log 选项设置日志的等级,共有 0, 1, 2, 3 四个选择。

游戏程序的信息大部分都有在日志中进行记录,显示的平台就是终端的 stdout——如果是直接启动的游戏程序,那就是在打开的那个(黑)框框上;如果是在终端命令行启动的,那就是在终端命令行。

日志一共有三个等级[7],按重要程度升序是 TEST[8], INFO 和 ERROR,与日志等级选项前三个对应。

因此设置日志等级的含义就是,只显示不低于设定等级的日志。因此默认的 1,即 INFO,就只会显示 INFO 及以上(INFO, ERROR)的日志信息。而 3,即 NONE 因为甚至大于 ERROR,所以连 ERROR 都不会显示。而我在调试测试过程中需要获得尽可能多的信息,就会选择 0,连 TEST 信息都会显示。

NONE 就一定不会显示所有日志信息吗?

并不是这样的。记录日志的 log 函数声明如下:

1
2
3
4
5
6
7
8
/**
* @brief 记录日志
*
* @param level 日志等级
* @param msg 日志信息
* @param force 是否强制显示
*/
void log(LogLevel level, const string &msg, bool force = false);

可以看到其实有一个 force 选项,只要该选项为 true,即使日志等级不够,也会强制进行显示。

这样做的原因是,这个记录日志的函数 log 其实不仅仅记录了游戏的日志信息,还会输出游戏前的一些问题,例如读取外部设置等。在这个操作过程中可能发生错误,但是此时还没有获得日志等级,就会出错。为了解决这个问题,我想了一个临时的解决方案,那就是不读取设置的显示日志等级,而是允许强制输出,这样就绕过了这个问题。

当然这个做法其实不太好,记录游戏日志的函数应当和程序本身信息的输出函数分开比较合适。

-p--port 选项,设置 Web GUI 的端口,默认是 18080。

如果有端口冲突,可以使用此选项进行修改。

-c--config 选项可以指定外部的设置 JSON 文件,默认会读取 ./config.json[9]

设置的优先级是「命令行指定」>「设置文件」>「默认值」。

-s--save 可以保存当前的设置到 ./config.json

例如说我调试的时候,要湿地,要设置食物为 999,要设置日志等级为 TEST,那我每次都要[10]

$ ./Avsb.exe -w -f 999 -l 0

每次都这样,未免有点麻烦。虽然说在终端中可能可以 Up 来重复,但要是偶尔还要修改呢?或者执行了一些其他命令后又要启动了呢?

这时候就可以在后面加一个 -s 选项,直接保存这样的设置:

$ ./Avsb.exe -w -f 999 -l 0 -s

然后就会在工作目录上出现一个 config.json 文件。

当然这也会同时覆盖掉原有的 config.json 文件,如果有的话。后续可能会增加对于覆盖的确认,那可能还要加一个强制覆盖的选项,如 -S 什么的。

设置 JSON 文件细节

打开上面得到的 config.json 文件,会发现内容如下:

1
2
3
4
5
6
7
8
9
{
"autoOpen": false,
"difficulty": "normal",
"initialFood": 999,
"logLevel": 0,
"planPath": "",
"port": 18080,
"waterEnabled": true
}

这就是设置的全貌了。

虽然一目了然,但还是逐个说明一下:

  • autoOpen:布尔值,设置是否自动打开 Web GUI
  • difficulty:字符串,设置难度
  • initialFood:整数,设置初始食物
  • logLevel:整数,设置日志等级
  • planPath:字符串,设置自定义攻击计划的路径
  • port:整数,设置端口
  • waterEnabled:布尔值,设置是否地图有湿地

基础功能

在弄清楚了游戏的基本概念和命令行后,下面会介绍一点游戏外部的基础功能。

运行游戏后,日志会显示:

其中左边的 [INFO] 代表这是 INFO 级别的日志,同时右侧链接有加粗,稍微好一点的终端,可以通过诸如 Ctrl/Alt + 单击的方式打开链接。另外这个虽然只有 INFO 等级,但是却是强制显示的。

这就是三种日志等级的样式了,TEST 是黄色,INFO 是蓝色,ERROR 是红色。

同时还有昆虫单位的信息:QueenAnt[34](1.00, Tunnel_1_3) 表示:

  • 这是一个 QueenAnt 类型的昆虫单位。类名与名称并不一致,因此不是 Queen
  • 唯一标识符(id)是 34
  • 生命值是 1.00,保留两位小数
  • 所在位置是 Tunnel_1_3,其中 1 代表纵坐标,3 代表横坐标,原点是地图左上角,都是从 0 开始计

即显示的昆虫信息模板是 type[id](health, place)

类名是绿色,生命值是洋红色,位置是青色。

类名与名称的关联

因为类名比较长,为了减少对于游戏界面显示空间的占用,在前端对类名进行了修正,具体代码节选如下:

1
2
3
4
5
6
nameDiv.innerText =
antInfo.type === "BodyguardAnt"
? "Guard"
: antInfo.type === "ThrowerAnt"
? antInfo.type.replace("Ant", "")
: antInfo.type.replace(/Thrower|Ant/g, "");

若是 BodyguardAnt,名称就为 Guard;若是 ThrowerAnt,名称就为 Thrower(删掉 Ant);否则就把 Thrower 或 Ant 的多余信息删掉。

是一个开发过程中赶时间的无奈之举。

颜色具体定义
1
2
3
4
5
6
static const string ANSI_YELLOW = "\x1B[33m";  //!< 黄色
static const string ANSI_BLUE = "\x1B[34m"; //!< 蓝色
static const string ANSI_RED = "\x1B[31m"; //!< 红色
static const string ANSI_GREEN = "\x1B[32m"; //!< 绿色
static const string ANSI_MAGENTA = "\x1B[35m"; //!< 洋红色
static const string ANSI_CYAN = "\x1B[36m"; //!< 青色

打开 Web GUI 后,主界面就是这样的。

对比 CS61A 的版本,多出了下面三个按钮。

四个按钮的具体功效是这样的:

  • START:开始游戏
  • LOAD:加载存档并进入游戏
  • DOCS:显示文档(目前是已失效的 CS61A 项目文档,后续可能修改为本博文)
  • CUSTOM:加载攻击计划(相当于命令行的 --plan 选项)

游戏界面中可以使用 Space 暂停或解除暂停。暂停的时候也可以按上面三个按钮进行对应操作。

不过暂停不意味着静止,即使暂停了,GIF 动图该动的还是会动,BGM 还是会放,在路途中的叶子还是会击中敌方,受到伤害还是会变红色。

前两个选项比较显而易见,下面来具体讲解一下存档的事情。

点击 Save 按钮后,浏览器会立刻下载存档 JSON 文件到下载目录。这个既有在紧张的游戏场景中避免因为选择路径而耽误时间的考虑,也有我懒的因素。

如上图所示,名称格式为 Avsb_save_<time>.json<time> 就是当前时间戳。

然后可以在主界面按 LOAD 按钮,并选择对应存档 JSON 文件读取恢复游戏。

恢复存档后的游戏状态一定与保存存档时的完全一致吗?

并不是!上面有提到过每个昆虫具有唯一标识符 id,这个重新加载存档并不会保持不变。其他的基本是和保存时一致。

存档 JSON 细节

下面是一个存档的示例节选,因为比较庞大,所以注解都以注释的形式写在里面了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
{
// 蜜蜂入口。在蜜蜂从蜂房 Hive 出动时,会随机从中挑选一个出发
"bee_entrances": [
"Tunnel_0_9",
"Tunnel_1_9",
"Tunnel_2_9",
"Tunnel_3_9"
],
// 维度,分别为纵、横数目
"dimensions": [
4,
10
],
// 食物数目
"food": 985,
// 当前蜜蜂总数,如果不对应真实情况会引发游戏结束状态的误判
// 有待提升鲁棒性
"numBees": 34,
// 所有位置的具体信息
"places": [
// 一个普普通通的位置对象
{
// 名称,普通陆地就是 Tunnel
"name": "Tunnel_3_9",
// 位置信息
"place": {
// 所含蜜蜂,此处为空
"bees": [],
// 入口位置名称,此处为蜂房
"entrance_name": "Hive",
// 出口位置名称
"exit_name": "Water_3_8",
// 位置名称
"name": "Tunnel_3_9",
// 位置类型,普通的 Place
"type": "Place"
}
},
...
{
"name": "Tunnel_1_3",
"place": {
// 若位置有蚂蚁单位,则会有 "ant" 键与对应蚂蚁对象
"ant": {
// 不同类型的蚂蚁单位可能有其额外的存档属性,例如 Container 类型有 "antContained" 键保存其容纳的蚂蚁对象
"antContained": {
"buffed": false,
"health": 1,
"place": "Tunnel_1_3",
"type": "ThrowerAnt"
},
// 表示蚂蚁是否给加成
"buffed": false,
// 生命值
"health": 2,
// 所在位置
// 这个属性好像没啥用
"place": "Tunnel_1_3",
// 类型
"type": "TankAnt"
},
"bees": [],
"entrance_name": "Tunnel_1_4",
"exit_name": "Water_1_2",
"name": "Tunnel_1_3",
"type": "Place"
}
},
...
{
"name": "Tunnel_0_3",
"place": {
"ant": {
"buffed": false,
"health": 3,
"place": "Tunnel_0_3",
// 换了 FireAnt
"type": "FireAnt"
},
"bees": [],
"entrance_name": "Tunnel_0_4",
"exit_name": "Water_0_2",
"name": "Tunnel_0_3",
"type": "Place"
}
},
...
{
// 这个是湿地的示例
"name": "Water_0_2",
"place": {
"bees": [],
"entrance_name": "Tunnel_0_3",
"exit_name": "Tunnel_0_1",
"name": "Water_0_2",
// Water 类型
"type": "Water"
}
},
...
{
"name": "Tunnel_0_9",
"place": {
// 该地有蜜蜂
"bees": [
{
"health": 3,
"place": "Tunnel_0_9",
"scaredTime": 0,
"slowedTime": 0,
"type": "Bee"
}
],
"entrance_name": "Hive",
"exit_name": "Water_0_8",
"name": "Tunnel_0_9",
"type": "Place"
}
},
{
"name": "Hive",
"place": {
// 攻击计划的 JSON 格式上面介绍过了
"assaultPlan": {
"waves": {
...
}
},
"bees": [
...
],
"entrance_name": "",
"exit_name": "",
"name": "Hive",
"type": "Hive"
}
}
],
// 当前时间
"time": 3,
// 版本,若存档版本号与游戏版本不匹配会发出 ERROR 进行警告
// 会尽量让相同版本的存档兼容
// 当然,v0.1.0-SeaOtter-patch.1 与 v0.1.0-SeaOtter-patch.2 的存档不兼容
"version": "0.1.0-SeaOtter"
}

前端调试

这部分是有关前端调试方面的东西,比较底层与杂乱无章。可以折叠来跳过。

前端调试

静态资源文件都是暴露出来的,因此可以自行修改以满足需要。同时为了调试方便,也准备了一些预先定制的功能函数,以便在浏览器控制台进行调试。

然而因为时间比较紧张,前端代码比较混乱,没有进行代码复用,用两处的代码真的就是复制两遍。反正就是有各种问题,我后面完工后再测试前端的调试函数,似乎有点问题。反正就是不保证能用就是了。

主要提供的功能函数在 static/script.js。首先在开头定义了一些变量:

1
2
3
4
5
6
let enablePolling = true;
let isServerRunning = true;
let isPaused = false;
let failedRequestCount = 0;
const MAX_FAILED_REQUESTS = 100;
const REQUEST_TIMEOUT = 3000;

首先需要对前后端交互的机制进行介绍,采用的就是普通的轮询机制,每隔一定时间(50ms),就会执行 updateStats 函数,前端向后端的 /update_stat 路由请求一下,然后后端就会返回当前的部分游戏状态(可购买的蚂蚁单位、食物数目、时间)。然后每隔更长的一段时间(5s),就会执行 insectsTakeActions 函数,向后端 /ants_take_actions/bees_take_actions 请求一下,后端模拟一轮。

了解了这个后就可以看上面定义的变量了。enablePolling 表示当前是否正在轮询,默认是 true。可以改为 false 以默认进入「单步调试」状态。剩下的变量最好不要动。

然后就是两个常量,轮询是比较消耗资源的,我看了看 CS61A 的项目,即使是结束了也还在不停地进行轮询。于是我就加入了自动停止的机制,在失败一定次数后,认定为断连,停止服务器,不再继续轮询。

这个值目前设置得比较大,因为比较小的话会很容易判定为断连。

然后就是提供的几个工具函数了:

1
2
3
4
function manualUpdate() { ... }
function manualInsectActions() { ... }
function manualOneTurn() { ... }
function togglePolling() { ... }
  • manualUpdate 函数执行 updateStats 函数,手动更新游戏状态。如果不执行这个函数,前端显示的可能与后端不符
  • manualInsectActions 函数执行 insectsTakeActions 函数,手动进行一轮。可以用这个函数进行单步调试
  • manualOneTurn 函数先执行一次 insectsTakeActions 函数,再执行一次 updateStats,完成一轮并进行状态更新
  • togglePolling 函数切换轮询状态

然后就可以开启调试模式,一步一步执行 manualInsectActionsmanualOneTurn 推演,在原程序下断点跟踪或检测错误了,抑或是慢慢检查日志的信息。

当然,也可以以此把这个游戏当成是回合制游戏,可以等一轮完全准备好了[11],再使用 manualOneTurn 进入下一轮。

我虽然加入了关闭程序就同时终止服务器,停止轮询的代码,但是似乎并不奏效,还是要等到超时、请求失败积累而停止。所以说要关闭的话,顺手 Ctrl + W 把网页关闭,抑或是选择按 Exit 按钮进行退出比较合适。


  1. 该链接已失效,在每年课程周期也许会恢复。 ↩︎

  2. 缓慢状态下的蜜蜂单位,只会在偶数轮进行移动。一次缓慢效果的停滞时间为 3 次,可以叠加。 ↩︎

  3. 恐惧状态下的蜜蜂单位,会尝试返回蜂巢(若已到达地图最右侧,则不会继续返回蜂巢,而是会停留在原地)。一次恐惧效果的持续时间为 2 轮,不可叠加。 ↩︎

  4. 咀嚼一般可以直接杀死一个蜜蜂单位,但是对于有伤害修正效果的蜜蜂来说则不然。 ↩︎

  5. 最终伤害=基础伤害0.25×距离攻击次数/16\text{最终伤害} = \text{基础伤害} - 0.25 \times \text{距离} - \text{攻击次数} / 16↩︎

  6. 被加成的蚂蚁单位,基础伤害翻倍。 ↩︎

  7. 没有 WARNING 等级,因为我也不知道哪里要警告,不如全 ERROR 了。 ↩︎

  8. 为什么不是 DEBUG 而是 TEST 呢?这是历史原因。一开始并没有提供给用户设置日志等级的选项,而是通过设置宏来决定日志等级,但 DEBUG 宏与编译调试部分的宏有冲突,虽然可以通过为宏名增加更清晰的注解,如 LOG_DEBUG,但最终还是选择了改名为 TEST,并沿用到现在。 ↩︎

  9. 相对的是工作路径,而非可执行文件的位置!如果是直接运行那二者没有区别。但如果是在命令行运行,工作路径不在可执行文件所在的目录(PATH_TO_AVSB/Avsb),那么这个相对的就是工作路径而非可执行文件的位置!按理来说相对可执行文件会比较合适,但我懒得做了,因为肯定比工作路径麻烦点。 ↩︎

  10. 其实在实现外部设置之前,确实是这样做的。只是呢也不是手动输入,靠的是 VS Code 的设置,预先设置好了运行和调试的选项。 ↩︎

  11. 即使是暂停了轮询依旧可以部署、移除,放在湿地的非抗水蚂蚁单位依旧会立刻死亡。因为部署蚂蚁用的不是 /ants_take_actions 路由,不归 insectsTakeActions 管,所以依旧会响应。不过我刚刚测试了一下,可能会有 500 Internal Server Error 问题,有机会可以研究一下。 ↩︎