我们首先会介绍游戏运行的逻辑,然后会介绍 AI 的实现,最后会介绍一些打包的细节。

After go

这个游戏的逻辑是这样的:

当游戏开始后,initGame() 函数会被调用,这个函数会初始化游戏的一些参数,比如棋盘的大小,棋盘的数组,以及棋盘的 UI,还会调用一次重绘函数,来绘制棋盘。

最后,initGame() 函数会调用 startGame() 函数,这个函数会启动游戏的主循环。

在游戏的主循环中,会不断的接收玩家的输入,然后判断玩家的输入是否合法,如果合法,就会调用 AfterGo() 函数,这个函数会判断游戏是否结束,如果游戏结束,就会调用 gameOver() 函数,这个函数会弹出一个对话框,告诉玩家游戏结束了,然后会调用 initGame() 函数,重新开始游戏。

如果游戏没有结束,就会调用 changePlayer() 函数,这个函数会切换玩家,然后调用重绘函数,来重绘棋盘。

不同的游戏模式下,changePlayer() 函数的行为是不一样的,比如人机对战模式下,changePlayer() 函数会调用 AI 的函数,来让 AI 下棋,而 PVP 模式下,这个函数只会切换玩家(通过更改一个全局变量的值)。

游戏的主循环会一直运行,直到游戏结束。

AI

让我们来简述一下 AI 的思路:

首先,AI 会遍历棋盘上的每一个点,然后判断这个点是否可以下棋,如果可以下棋,就会计算这个点的得分,然后将这个点的得分和坐标存入一个数组中。

然后,AI 会遍历这个数组,找到得分最高的点,然后下棋。

我们将会用到的算法有:

评分表

我们需要给每一个点评分,这样才能知道哪个点是最好的点。这就需要我们建立一个评分表,给每一个点一个分数,这个分数代表了这个点的价值。

一个很经典的评分表是这样的:

类型 分数
活二 100
死二 10
活三 1000
死三 100
活四 10000
死四 1000
活五 100000

这里只是一个简化的描述,实际写起来还需要考虑各种情况

极大极小值搜索

极大极小值搜索是一种博弈树搜索算法,它是极小化极大算法的一种变体,用于在两个对手之间进行零和游戏的博弈树搜索,是一种广泛用于人工智能、博弈论、决策论、统计学和哲学的决策规则,用于最小化最坏情况(最大损失)的可能损失。当处理收益时,它被称为“最大化”——最大化最小收益。最初为多人零和博弈论制定,涵盖了玩家轮流行动和玩家同时行动的情况,它也被扩展到了更复杂的游戏和在不确定性存在的情况下的一般决策。

具体来说,极大极小值搜索是一种递归的搜索算法,用于计算博弈树的最佳走法。它假设对手也在使用极大极小值搜索,因此在每一层,它都会选择最小化对手可能获得的最大值的走法。在极大极小值搜索的最后一层,它会选择最大化自己可能获得的最大值的走法。

Alpha-Beta 剪枝

Alpha-Beta 剪枝是一种用于减少极大极小值搜索的节点数的算法。它可以应用于任何零和游戏,包括棋类游戏和许多其他的策略游戏。它减少了需要评估的节点数,从而提高了搜索效率。这是一种对抗性搜索算法,主要应用于机器游玩的二人游戏(如井字棋、象棋、围棋)。当算法评估出某策略的后续走法比之前策略的还差时,就会停止计算该策略的后续发展。该算法和极小化极大算法所得结论相同,但剪去了不影响最终决定的分枝。

五子棋的 AI

五子棋的 AI 有很多种,这里我们使用的是最简单的一种,就是上面提到的评分表 + 极大极小值搜索 + Alpha-Beta 剪枝。

还有其它的 AI 算法,比如蒙特卡洛树搜索,等等

文件存储

要求将文件存储在本地以实现数据的持久化

数据在内存中的存储

显然,每次都存储整个棋盘的数据是不合理的,我们只需要记录每一回合,棋子的落点与类型即可。

十分自然的,我们用一个链式表来存储这些数据,每一回合的数据都是一个节点,节点中包含了落点的坐标与类型。

数据在文件中的存储

这里我们使用的是 CSV 文件。

CSV 文件是一种通用的、相对简单的文件格式,被广泛应用于数据交换。CSV 文件由任意数目的记录组成,记录间以某种换行符分隔;每条记录由字段组成,字段间的分隔符是其它字符或字符串,最常见的是逗号或制表符。CSV 文件的文件名通常以 .csv 为后缀。

由于 CSV 文件的简单性,它的解析也相对简单,这里我们使用的是 Qt 的 QIODevice 类,它是一个抽象类,用于访问 I/O 设备。QIODevice 类是许多 Qt 类的基类,包括 QFile、QTcpSocket、QNetworkReply、QBuffer、QProcess、QUdpSocket 和 QTemporaryFile。

QIODevice 类提供了一些基本的函数,用于读写数据,比如 read()、write()、readLine()、readAll()、peek() 等等

我们可以使用 QFile 类来打开文件,然后使用 QIODevice 类的函数来读写文件。

存储的数据类似于这样:

1
2
3
4
5
6
round,x,y,type,step,gamemode
1,1,1,1,1,1
1,2,2,2,2,1
1,3,3,1,3,1
2,1,1,1,4,2
2,2,2,2,5,2

其它的选择

我们还可以选择使用 JSON 文件或者 XML 文件来存储数据。

JSON 文件是一种轻量级的数据交换格式,它基于 ECMAScript 的一个子集,采用完全独立于编程语言的文本格式来存储和表示数据。简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

JSON 文件的解析也相对简单,我们使用 Qt 的 QJsonDocument 类,它是一个 JSON 文档类,用于解析 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
{
"data": [
{
"round": 1,
"moves": [
{
"x": 1,
"y": 1,
"type": 1
},
{
"x": 2,
"y": 2,
"type": 2
},
{
"x": 3,
"y": 3,
"type": 1
}
],
"gamemode": "PVP",
"winner": "player1"
}
]
}

deploy && release

finally! 是时候把我们的游戏打包成一个可执行文件了!

只需要在 Qt Creator 中点击左下角的构建按钮,然后选择 release 模式,然后点击构建按钮,就可以构建出一个可执行文件。

然而,只是构建出一个可执行文件是不够的,可执行文件还需要依赖一些动态链接库才能正确的运行。

动态链接库

在 Windows 平台下,一般使用 windeployqt 工具来自动的将依赖的动态链接库复制到可执行文件所在的目录下,打开 安装 Qt 后自动创建的 Qt 命令行工具,然后切换到可执行文件所在的目录,然后执行 windeployqt 命令即可。

1
windeployqt.exe ./

我们还可以手动对可执行文件进行裁切,删除一些不必要的动态链接库,比如对于这个单机游戏,我们可以删除网络相关的动态链接库。

打包成安装包

我们还可以将可执行文件打包成一个安装包,这样用户就可以直接安装游戏了。

比较常用的打包工具有 NSIS、Inno Setup、WiX Toolset 等等。

这里使用 Inno Setup 来打包,Inno Setup 是一个免费的安装程序制作工具,它可以将可执行文件打包成一个安装包。

二进制压缩

upx 是一个免费的、开源的、便携式、可扩展的、高性能的可执行文件压缩工具,它可以将可执行文件压缩成更小的体积。

生成单文件可执行文件

如果不用动态链接而是静态链接,就可以生成一个单文件可执行文件,这样就不需要依赖动态链接库了,这里不做介绍。(其实我也不会)

利用虚拟文件系统生成单文件可执行文件

我们可以通过一款 Enigma Virtual Box 工具来将依赖的动态链接库打包到可执行文件中,这样就可以生成一个单文件可执行文件了。

这个工具的介绍可以看这里:Enigma Virtual Box

Enigma Virtual Box 是用于文件和注册表系统虚拟化的工具,它允许您将应用程序使用的所有文件和注册表合并到单个可执行文件中,而无需将虚拟文件提取到磁盘。使用 Enigma Virtual Box,您可以虚拟化任何类型的文件,动态库(*.dll),ActiveX/COM 对象(*.dll*.ocx),视频和音乐文件(*.avi*.mp3),文本文件(*.txt*.doc)等。Enigma Virtual Box 不会将临时文件提取到硬盘;文件仿真仅在进程内存中执行。

利用自解压缩文件生成单文件可执行文件

实际上在上面的介绍中我们可以了解到,Enigma Virtual Box 不会将临时文件提取到硬盘,文件仿真仅在进程内存中执行。

相应的,自然也就有一种将临时文件提取到硬盘的生成单文件可执行文件的方法了,那就是自解压缩文件。

运用这个方法的有一个著名的例子,那就是 PyQt 常用的打包工具 pyinstaller。

Release to GitHub

在 Github 上发布一个 Release 吧!hooray!