五子棋AI源码拆解:C++/Qt + Alpha-Beta剪枝实战
2026/9/23 15:14:07 网站建设 项目流程

简介:基于C++与Qt实现的五子棋博弈游戏完整项目,核心采用极大极小搜索与Alpha-Beta剪枝算法,并附带设计报告。适合高校人工智能、计算机、自动化等专业学生用于课程设计或毕业设计,也适合对棋类AI感兴趣的开发者学习博弈树搜索与界面编程。压缩包共145个文件,包含18个cpp源码、9个h头文件、4个ui界面文件、4个qml文件、14个png图片、2个exe可运行程序及docx设计文档等,整体仅7.26MB,结构清晰便于查阅。目前已有65人学习下载。通过该资源可掌握五子棋局面评估、走法生成、搜索线程设计及Qt界面交互等关键实现,源码经测试可正常运行,可直接运行示例或在此基础上扩展双人对战、难度调节等功能,设计报告则为论文和答辩提供参考。

1. 五子棋AI,代码和设计报告都在这个包里

这份资源是一个用C++和Qt写的五子棋博弈游戏,AI核心不是暴力枚举,而是经典的极大极小搜索加Alpha-Beta剪枝,文件夹里还带一份可以直接改的课程设计报告。拿到包之后我先把源码翻了一遍,发现它不是那种只画个棋盘、双人对战的玩具,而是把AI走棋、走法生成、评估函数、攻击判断和UI完整拆开了——适合做课设或者毕设初版,也适合想搞懂博弈树搜索的同学对着源码一行行看。对这个包能解决什么问题做个概括:它给你的是一个能跑、能下、能打还能讲清楚原理的完整项目,不是网上那种缺头文件、少设计文档的半成品。下面我按自己的拆解顺序,把核心算法、代码结构、编译方式和踩过的坑从头过一遍。

2. 核心博弈逻辑:极大极小搜索与Alpha-Beta剪枝

2.1 从走法生成到搜索树,AI每一步在想什么

五子棋AI的核心问题很简单:给定当前棋盘,怎么找到最优落子点。暴力做法是把所有空位都试一遍,然后对每个落子后的局面再做同样的尝试,直到达到搜索深度上限。这个递归过程天然形成一棵博弈树,树的每一层代表一方落子。极大极小搜索要处理的就是这样一个二人零和博弈:我方回合取子节点分数的最大值,对方回合取最小值,最后回到根节点的分数对应的那步棋就是当前最优解。

int alphaBeta(int depth, int alpha, int beta, bool maximizingPlayer) { if (depth == 0 || gameOver()) { return evaluate(); // 到达叶子节点,用评估函数打分 } QVector<Move> moves = WalkingGenerator::generateMoves(); // 注意:moves 需要按启发式排序,剪枝效率才高 if (maximizingPlayer) { int best = INT_MIN; for (const Move &m : moves) { makeMove(m); best = max(best, alphaBeta(depth - 1, alpha, beta, false)); undoMove(m); alpha = max(alpha, best); if (beta <= alpha) break; // beta截断,剪枝 } return best; } else { int best = INT_MAX; for (const Move &m : moves) { makeMove(m); best = min(best, alphaBeta(depth - 1, alpha, beta, true)); undoMove(m); beta = min(beta, best); if (beta <= alpha) break; // alpha截断,剪枝 } return best; } }

这段代码有几个关键点要说清楚。第一,generateMoves不是把所有空位都丢进搜索,只在已有棋子周围一定范围内生成候选点,否则第一手棋要遍历225个位置,到第4层就是天文数字。第二,走法顺序直接决定剪枝效果:如果第一步就搜到最优解,后面大量分支直接截断;如果排序很差,Alpha-Beta就退化成了纯极大极小。第三,makeMove和undoMove的成对使用是搜索正确性的保证,漏一个undo,棋盘状态就错了,AI会走出所谓的"空气棋"。

2.2 Evaluate打分模型:棋子形状如何变成数值

叶子节点需要返回一个分数,这个分数来自Evaluate.cpp,它做的事情本质上是对当前局面做静态评估。先看横向、纵向、两条对角线方向,把连续同色棋子以及空位组成的不同形状映射成分数:五连是最高分,活四其次,冲四、活三、眠三、活二、眠二依次递减。典型权重表大概是这样的:

棋子形状分数范围说明
五连1000000直接获胜
活四100000两端都开放,必胜
冲四10000一端被封,另一端口子
活三5000再一手可成活四
眠三1000有潜力但受限
活二500基本棋形发展单位
眠二100弱棋形,比单子略好

这里有个现实工程问题:评估函数不能只看当前层自己的棋,还要考虑对手的威胁。常见做法是分别计算我方所有形状得分总和,再计算对方得分总和,最终得分等于"我方总分减去一个加权后的对方总分"。SearchAi.cpp里就用了这个思路,攻击时用VcAttack模块做局部强攻,防守时靠评估函数盯住对方的四连和三连。

一个容易理解偏差的地方是:评估分数本身不是绝对大小有意义,而是相对差有意义。两个局面分数差多少不重要,重要的是谁大谁小。所以调整权重时不要纠结"活三应该给5000还是6000",只要保持形状之间的相对等级关系合理就行。

2.3 Alpha-Beta剪枝:把搜索量从指数级压下来

没有剪枝的极大极小搜索复杂度是O(b^d),b是分支因子,d是搜索深度。五子棋棋盘大,手工设计的走法生成器能把b压到10到20,但深度到6层时仍然有上千万个节点。Alpha-Beta的价值在于保持结果不变的前提下,把搜索量砍掉一大截。

// 核心剪枝条件:当某一分支已经比另一侧的最优选项更差,直接终止该分支 if (beta <= alpha) { break; // 当前节点已经不可能影响根节点的决定 }

alpha代表我方目前能保证的最低分,beta代表对方能保证的最高分。MAX节点在更新alpha,MIN节点在更新beta,一旦beta不大于alpha,说明这个节点的剩余分支无论怎么走,都不会被双方选择,于是整段砍掉。代码里break触发的时机和走法排序强相关,排序越好,剪枝越早,效率越高。

这个包里的SearchAi.cpp还叠加了一个实用策略:前几步不启动全盘搜索,直接读OpenFive.cpp里的开局库,把AI的前三到五手固定在经过验证的常见开局上。这样既省搜索时间,又避免了AI开局乱下。算杀部分由calculate.cpp负责,它专门搜索"连续冲四最终必赢"的强制线路,这种VCF搜索在残局阶段比通用搜索精确得多。

3. 代码结构与运行流程:从OpenFive.cpp到SearchAi.cpp

3.1 文件职责与调用关系

把整个包里的核心文件逐个拆开看,角色分配很清楚。我整理了一张表,方便你对照源码找重点:

文件职责关键点
OpenFive.cpp主窗口与界面控制负责棋盘绘制、鼠标事件、菜单逻辑
ChessPlay.cpp棋局管理记录落子历史、判断胜负、撤销走棋
WalkingGenerator.cpp候选走法生成只在已有棋子邻域生成候选点
Evaluate.cpp静态评估把棋形转成数值分数
SearchAi.cppAI搜索主体极大极小搜索与Alpha-Beta剪枝入口
VcAttack.cpp攻击搜索冲四、活四攻击序列判断
calculate.cpp算杀模块VCF强制胜利路径搜索
GameThread.cpp博弈线程把AI计算放到后台线程,避免界面假死
moc_predefs.h.cbtQt元对象预编译文件编译辅助产物,不要手动改

调用链从上往下走:界面点击落子后,OpenFive.cpp把走棋事件交给ChessPlay.cpp更新棋局状态,然后启动GameThread里的AI计算。AI线程先调用WalkingGenerator生成候选走法,再进入SearchAi做递归搜索,搜索过程中不断调用Evaluate打分,遇到攻击局面就临时切到VcAttack或calculate.cpp做局部精确搜索。搜索完成后通过信号把最佳走法传回主线程,由ChessPlay执行落子。

3.2 博弈线程与界面分离:GameThread为什么不能省

这个点值得单独拿出来讲。很多课设项目把AI搜索直接写在鼠标事件回调里,结果AI想一步棋,整个窗口就卡住不能点、不能动,Windows甚至会弹出"程序无响应"的警告。原因很简单:搜索过程占用了UI线程,消息循环被阻塞。这个包里专门写了GameThread.cpp来处理这个问题,核心套路是:

class GameThread : public QThread { Q_OBJECT protected: void run() override { int bestMove = SearchAi::findBestMove(board, searchDepth); emit moveFound(bestMove); } signals: void moveFound(int move); };

主线程里创建线程对象,连接moveFound信号到主窗口的落子槽函数,AI计算期间界面可以自由缩放、拖动,计算完成后通过信号槽回到主线程执行更新。这里有两个细节:一是QThread对象不能直接delete,要等finished信号后再清理;二是搜索期间不能让用户重复落子或者同时开多个线程,否则棋局状态和线程结果会互相干扰。

我见过不少人在这一块翻车,明明AI能走棋但界面一卡一卡的,第一反应是去改算法,实际把线程加上就好了。GameThread这个文件的职责优先级很高,如果要改代码,先把线程模型搞懂再动SearchAi的逻辑。

3.3 开局库与算杀模块:OpenFive和calculate在做什么

OpenFive不单是主窗口,它还兼任开局库的角色。程序初始化时加载一些预先定义的常见开局走法,AI在前几手直接从库里取棋,不走搜索。

// OpenFive.cpp 中开局库的典型逻辑 bool tryOpeningBook(int step, int &x, int &y) { QVector<OpeningMove> book = loadDefaultOpenings(); for (const OpeningMove &item : book) { if (item.step == step && item.side == currentSide) { x = item.x; y = item.y; return true; } } return false; // 不在开局库中,进入正常搜索流程 }

这个设计很务实,因为前几步棋的搜索分支非常多但收益很低,直接用常见开局可以显著减少前几手的搜索开销。calculate.cpp的算杀逻辑则是另一个方向的优化:当检测到存在连续冲四可能获胜时,不走通用搜索,而是用受限的VCF搜索精确判断这条线路是否成立。这类搜索深度可以更深、节点更少,因为每一步候选走法都被限制在冲四范围内。

4. 编译部署:从Qt环境到跑起来的完整流程

4.1 Windows下用Qt Creator直接打开

这份代码是用Qt工程管理的,文件里有.pro项目文件,直接用Qt Creator打开最省事。打开之前确认三件事:Qt版本建议用5.15.2 LTS这个常见稳定版,编译器Kit选择MinGW 64-bit而不是MSVC,构建目录最好用默认的build-xxx文件夹,别把构建产物和源码混在一起。

Qt Creator打开.pro后会自动读取源文件列表,但有时会遇到Qt版本切换导致的缓存问题。我习惯在打开工程后做一步强制清理:在项目面板里右键选择"清理",然后重新构建。这一步能解决大量"明明代码没问题但编译报错"的玄学问题。

编译通过后直接运行,窗口中会先执黑或执白。如果需要修改AI的搜索深度,去SearchAi.cpp里找searchDepth这样的变量,把默认值从4改成5或6重新编译即可。

4.2 Linux下用qmake命令行编译

服务器或Linux桌面上没有Qt Creator也一样能编。包装里有.pro文件,用qmake和make两步就够了:

cd FiveChess # 先确认qmake在PATH里,如果没在就写Qt安装目录下的完整路径 qmake FiveChess.pro make -j4 ./FiveChess

qmake会把.pro里的配置展开成Makefile,-j4让CPU多核并行编译。如果在运行时报缺少libQt5Widgets.so之类的动态库错误,多半是系统没有安装Qt5的开发包,Ubuntu/Debian上用以下命令装上再重新编译:

sudo apt install qtbase5-dev libqt5widgets5

注意Linux上程序能不能显示窗口取决于有没有图形环境,纯SSH终端里跑Qt程序需要加offscreen参数:./FiveChess -platform offscreen,这是调试用的,不代表游戏能正常玩。

4.3 发布与换机运行

编译好的exe不能直接拷到别人电脑上。Qt程序运行时依赖Qt框架的DLL和平台插件,需要先用发布工具把依赖收集齐。Windows上在Qt命令行环境里执行:

cd build-FiveChess-Desktop_Qt_5_15_2_MinGW_64_bit-Release windeployqt FiveChess.exe

windeployqt会把Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll、platforms目录等全部复制到exe所在目录。然后把整个文件夹打包发给对方就能直接运行。用MSVC编译的项目要记得把对应的VC运行库也放进去,MinGW版本多半不用额外处理。

Linux发布相对简单,把编译好的可执行文件和设计报告放一起,在相同发型版或更新环境上跑起来问题不大;如果目标是跨发型版分发,可以用AppImage工具打包,这个不在本包范围内,只在需要的时候延伸了解即可。

5. 避坑记录:Qt版本冲突、搜索卡死与评估失衡

5.1 fatal: cannot mix incompatible Qt library (version ex50601) with this library

现象:编译能通过,但运行时程序直接崩溃,控制台打出类似fatal: cannot mix incompatible Qt library (version ex50601) with this library的报错。

原因:ex50601拆开看就是5.6.1。出现这个报错的本质是头文件版本和动态库版本不一致。最常见的情形是电脑上装了多个Qt版本,编译时用的是5.6.1的头文件,运行时却加载了另一个版本目录下的Qt5Core.dll;或者Qt Creator的构建配置和系统PATH指定的Qt库不是同一个。

解决:检查Qt Creator里构建套件的Qt版本路径,确认.pro文件没手动加过无关的INCLUDEPATH;在系统环境变量PATH里检查是否存在多个Qt的bin目录,把不需要的删掉;最后清理构建目录重新qmake生成Makefile。我遇到过一次是Anaconda环境里带了旧版Qt库,PATH里正好排在前面,把这条路径从PATH去掉后问题消失。

5.2 qt.qpa.plugin: Could not find the Qt platform plugin "linuxfb"

现象:在Linux开发板上或某些嵌入式环境运行程序,报qt.qpa.plugin: Could not find the Qt platform plugin "linuxfb" in...,程序退出。

原因:Qt平台插件缺失,或者平台插件搜索路径设置错误。linuxfb是嵌入式Qt平台插件,但桌面版Qt通常不默认编译这个插件;还有一种情况是程序找不到插件的加载目录。

解决:桌面Linux上改用xcb而不是linuxfb:./FiveChess -platform xcb,或者在代码main函数里加QApplication::setPlatformName("xcb")。如果确实需要linuxfb,要下载对应Qt版本的platforms插件放到可执行文件旁边的platforms目录下。更常见的是开发板场景,直接把可执行文件拷过去但没带插件目录,把Qt库目录下plugins/platforms的整个文件夹复制到程序运行目录即可。

5.3 LNK2019/LNK2001:MSVC与MinGW的链接差异

现象:新建Qt工程重新编译时,报一堆LNK2019、LNK2001无法解析的外部符号,指向的都是Q_OBJECT类里的信号和槽。

原因:这些符号是moc元对象编译器生成的。用MSVC编译器时,Qt Creator有时没有正确执行moc,或者.pro文件里缺失QT += widgets等模块声明。还有一种是我在支持热词里看到的常见事:工程文件是用MSVC配置的,实际却用MinGW试编,两者的导入库格式根本不兼容。

解决:先确认编译器类型,整个工程统一用一种。检查.pro文件里有没有QT += core gui widgets,源文件里包含Q_OBJECT的类头文件必须出现在头文件列表里,让moc能扫描到。然后删除构建目录重新qmake,再构建。我自己的习惯是拿到不熟悉的Qt工程先看.pro文件,通过它判断编码者预设的编译器,不要一上来就自己换Kit。

5.4 搜索深度过深导致界面假死

现象:AI在游戏过程中思考时间越来越长,界面卡顿,极端情况下几秒到十几秒才落子,甚至程序无响应。

原因:这是最常见的设计缺陷,搜索深度设成6甚至8,但没走后台线程,也没有限制搜索时间。每加一层深度,搜索时间不是翻倍而是以分支因子的量级增长。测试时觉得4层弱,直接改到8层,结果第一手棋就要算半天。

解决:首先把AI计算放到GameThread线程里,保证UI不阻塞;其次给搜索一个时间上限,比如设置计时器,超过2秒就返回当前最优着法;最后才是调深度。搜索深度不是越大越好,五子棋在可控时间内6层已经很强。还可以配合开局库和算杀模块,大部分棋局根本不需要走到满深度。

5.5 评估函数失衡:AI只会防守不会进攻

现象:AI能挡住玩家的棋,但自己从来造不成威胁,经常在防守过程中把优势一点点耗掉,最后输棋。

原因:评估函数只考虑了防守,或者进攻得分权重太低。具体到Evaluate.cpp的实现层面,可能是我方形状得分和对方形状得分的权重差设置不合理,导致AI宁可去堵对方一个眠三,而不顾自己这边已经形成的活三甚至冲四机会。

解决:把评估函数改成"我方得分减去对方得分乘以一个防守系数"的双向模式,防守系数通常可以在1.0到1.2之间调。更重要的是做一步简单自测:摆一个必胜局面,看AI是否会选择进攻获胜而不是防守。调整权重后反复用不同棋形测试,而不是上来就深搜。

6. 进阶调优:把AI棋力往上提的三个抓手

6.1 调评估权重而不是盲目加搜索深度

想让AI变强,优先调评估函数,而不是把搜索深度从6改到8。搜索深度的提升带来的效果呈指数级的计算开销增长,而权重调整几乎没有成本。我会把Evaluate.cpp里的评分函数单独抽出来,用一个参数文件或者宏定义管理,然后镜像测试:让AI自己和自己下,一次只动一个权重,记录输赢比和平均手数。正常课设阶段,保证形状等级关系正确、搜索深度4到6层、配合开局库,已经可以赢过大多数随手写的人类玩家。这个包里的VcAttack模块之所以重要,是因为攻击序列搜索比泛化搜索在局部攻杀上更精确,实际对局中很多决定性因素藏在算杀里。

6.2 走法排序与迭代加深

Alpha-Beta剪枝的效率和走法排序强相关,排序好可能只需要排序差十分之一的节点。我用过的最实用的排序手段是:搜索前先对候选走法做一次快速评估,把分数高的走法排在前面。

// WalkingGenerator.cpp 中走法排序的思路,关键是评估越好的走法越先搜索 QVector<Move> moves = generateMoves(board); std::sort(moves.begin(), moves.end(), [](const Move &a, const Move &b) { return quickEvaluate(a) > quickEvaluate(b); });

迭代加深的套路更实用:不是直接搜到6层,而是先搜2层,保存最佳着法;再搜3层,用上一层结果作为走法排序的起点。每一层的开销只是总时间的一小部分,但换来的排序效果能让Alpha-Beta剪枝效率大幅提升。本包SearchAi里没有明确写明这个策略,改造起来也不复杂,核心就是for循环包一层深度控制。

6.3 把调试日志加上,观察AI每一步的思考结果

最后分享一个我调博弈类程序必做的习惯:在SearchAi.cpp搜索函数里临时加一行调试输出,把当前搜索深度、候选走法数、剪枝次数、最佳走法以及评估分数打出来。把这些信息落地成日志后有三个好处:一是能判断是不是走法生成器生成了大量无意义的白痴棋;二是能发现剪枝率过低的情况,说明走法排序或alpha-beta边界更新有问题;三是能快速定位评估函数的异常分数来源,比如某一局分数特别高或特别低,直接对应到具体棋形去查。

深度:4 候选点数:18 剪枝数:26 最佳着法:(12,9) 分数:3400 深度:4 候选点数:17 剪枝数:31 最佳着法:(8,12) 分数:-1500

我印象最深的一次调试经历就是靠日志发现AI在评估时把对手的活四漏掉了,导致它总在应该堵四的时候去走自己的三。加一行日志前后,整个排查效率完全不同。从那以后,我每次拿到新的博弈类项目源码,第一件事不是直接跑,而是先在搜索入口加日志,构建好完整的观察路径再动手。这个小习惯帮我省了很多排查时间,也推荐你试试。希望这份五子棋源码拆解能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询