
简介基于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(); // 到达叶子节点用评估函数打分 } QVectorMove 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节点在更新alphaMIN节点在更新beta一旦beta不大于alpha说明这个节点的剩余分支无论怎么走都不会被双方选择于是整段砍掉。代码里break触发的时机和走法排序强相关排序越好剪枝越早效率越高。这个包里的SearchAi.cpp还叠加了一个实用策略前几步不启动全盘搜索直接读OpenFive.cpp里的开局库把AI的前三到五手固定在经过验证的常见开局上。这样既省搜索时间又避免了AI开局乱下。算杀部分由calculate.cpp负责它专门搜索连续冲四最终必赢的强制线路这种VCF搜索在残局阶段比通用搜索精确得多。3. 代码结构与运行流程从OpenFive.cpp到SearchAi.cpp3.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) { QVectorOpeningMove 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 ./FiveChessqmake会把.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.exewindeployqt会把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/LNK2001MSVC与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 中走法排序的思路关键是评估越好的走法越先搜索 QVectorMove 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在评估时把对手的活四漏掉了导致它总在应该堵四的时候去走自己的三。加一行日志前后整个排查效率完全不同。从那以后我每次拿到新的博弈类项目源码第一件事不是直接跑而是先在搜索入口加日志构建好完整的观察路径再动手。这个小习惯帮我省了很多排查时间也推荐你试试。希望这份五子棋源码拆解能帮到你。本文还有配套的精品资源点击获取