五子棋AI源码实战:Alpha-Beta剪枝与评估函数调优指南

发布时间:2026/9/15 23:45:51
五子棋AI源码实战:Alpha-Beta剪枝与评估函数调优指南 简介这是一份基于VC开发的五子棋人机对战系统完整源码包面向对人工智能入门、棋类博弈算法及Windows图形界面编程感兴趣的开发者。代码实现了玩家与计算机的对弈流程核心采用Minimax搜索结合Alpha-Beta剪枝来模拟电脑决策并引入启发式函数评估棋盘威胁帮助学习者理解经典搜索策略的实际落地方式。资源共29个文件压缩包仅367KB其中包含bmp棋盘与棋子图像、h头文件、cpp源程序、ico图标、工程配置及可直接运行的exe演示程序代码结构清晰便于对照编译与二次修改。目前已有133人学习使用适合希望结合具体项目快速掌握人机博弈原理、MFC界面搭建和基础AI算法的读者。通过研读工程实现不仅可复现对战效果还能进一步优化评估逻辑或扩展难度等级是算法学习与课程设计的高性价比参考资料。1. 拿到 wuziqi.rar 先别点开五子棋人机工程该看哪几层“wuziqi.rar”这个名字一眼就能猜到内容一个以五子棋机器博弈为主题的 C 工程压缩包。这类包在课程设计、代码仓库分享里流传很广里面通常是一整套 VS 工程或 MinGW 可编译的控制台程序核心价值不在画棋盘和判胜负而在强度尚可的人机 AI评估函数、搜索深度、剪枝策略、先手后手差异。解压之后很多人第一反应是打开 main.cpp 从头读结果很容易陷进两百行坐标处理和绘制的代码里出不来。更高效的做法是倒着看先定位 AI 入口函数再顺着它捋数据结构和评分表最后把工程构建成本机可执行文件用自动对弈验证强度。这条路适合 C 基础尚可、想读懂课程设计代码或想拿现成 AI 做二开的从业者。2. 解压到编译把五子棋人机源码从 rar 里跑起来2.1 解压前先列清单rar 里到底有什么命令行下别急着双击先用工具看看包内清单。Windows 装了 7-Zip 之后用 PowerShell 调 7z 最直接7z l wuziqi.rar输出里能看到文件结构和每个文件的大小。见到.cpp、.h、.vcxproj基本可以确定是 Visual Studio 工程看到Makefile或CMakeLists.txt说明原作者用命令行构建跨平台可能性更高。还有一类包会直接带一个编译好的.exe和一个不算小的data目录这种多数是带图形界面的成品源码和运行文件混在一起。包内常见文件含义后续处理main.cpp / Board.cpp / AI.cpp工程源码决定用 Makefile 还是 VS 工程wuziqi.sln / .vcxprojVisual Studio 工程直接改平台工具集后编译README.txt规则与操作说明先读先手、禁手、棋盘尺寸wuziqi.exe已编译程序只做黑盒测试改不了 AI少数包加了密码。解压失败时先看文件名和说明里的备注很多课程设计包的密码就写在文件名后缀或说明页里。网上流传的“密码移除工具”尽量别碰合法且省事的做法是回到来源页面找分享者要密码。遇到只有密码没有来源的包我的习惯是直接放弃不值得为一个课程包冒险。提示如果7z l输出乱码多半是压缩包用了 GBK 编码的文件名。用7z l -scsGBK wuziqi.rar再试能避免解压后目录名变成乱码。2.2 用 g 构建控制台版五子棋包内没有工程文件时我一般先把所有 cpp 直接一起编译g main.cpp Board.cpp AI.cpp -o wuziqi.exe -O2 -stdc11 -static-O2打开优化Alpha-Beta 剪枝这类递归搜索对优化级别敏感Release 和 Debug 在同一深度下耗时能差一倍。-stdc11是兼容性底线老课程代码常依赖srand、time这类 C 风格接口用新标准编译反而容易碰到隐式转换告警。-static静态链接免得换台机器就需要加载libstdc-6.dll。如果源码拆得细也可以让 Shell 自动收集g $(find . -name *.cpp) -o wuziqi.exe -O2 -stdc11 -static这段是把当前目录下所有 cpp 找出来一起编进同一个可执行文件。遇到同名的main.cpp在子目录里重复出现时这条命令会报重复定义需要手动把重复文件从列表里去掉保留入口文件所在目录的一份。2.3 Visual Studio 用户的构建路径如果是.sln工程双击打开后最常见的问题是“工具集版本过旧”提示需要安装 VS2010 或 VS2013。不要为了它去装老版本 IDE直接在项目属性里把平台工具集改成当前版本比如Visual Studio 2022 (v143)大多数纯 C 代码能直接编过。编译时把警告级别开到/W4重点看 “unreferenced formal parameter” 和 “signed/unsigned mismatch”。前者说明某个函数参数没被用到AI 里的阈值可能没生效后者在搜索剪枝时会引起诡异的边界判断。编码问题也很好认源码注释全是乱码或者编译报C4819。这类文件通常保存为 GBK而 VS 默认按 UTF-8 解析。我在项目属性“命令行”里加一个/utf-8能解决大部分乱码反过来如果源码本来就是 UTF-8而在旧版 VC 上编译改成“使用 Unicode 字符集”即可。写到这里要补一句不要急着改逻辑先把工程完整编译通过并跑起一局再开始读 AI 代码——多数源码里藏着没有初始化的棋盘数组只有在实际对局里才会暴露。3. 五子棋AI的决策骨架落子点是怎么被算出来的3.1 先分清哪部分是人机哪部分是规则校验读代码前先画一条分界线棋盘坐标换算、胜负判定、输入输出属于“外围”真正的人机逻辑只在“给定局面返回下一步坐标”这一个函数里。常见名称有ai_move、computer_put、findBestMove。进入这个函数之前先确认三个事实棋盘是int board[15][15]还是动态二维数组当前执黑的是人是机空点集合是每次重扫还是维护了列表。这三个事实决定了后面评估函数怎么写也决定了你改参数时要连带的边界。比如有的工程用-1/0/1表示黑白/空有的用0/1/2。读胜负判定代码时最常看到这种片段int winCheck(int x, int y, int role) { int dirs[4][2] {{1,0},{0,1},{1,1},{1,-1}}; for (int k 0; k 4; k) { int cnt 1; for (int step 1; ; step) { int nx x dirs[k][0] * step; int ny y dirs[k][1] * step; if (nx 0 || ny 0 || nx N || ny N || board[nx][ny] ! role) break; cnt; } for (int step 1; ; step) { int nx x - dirs[k][0] * step; int ny y - dirs[k][1] * step; if (nx 0 || ny 0 || nx N || ny N || board[nx][ny] ! role) break; cnt; } if (cnt 5) return 1; } return 0; }这段代码里board[nx][ny] ! role同时承担了边界判断和“不是己方棋子就停止”两件事往两个方向各扫一次再合并计数。逻辑本身没错但注意cnt 5无禁手规则下超过五子的长连在多数网络对局平台判黑负而这段代码会把长连也当作胜利返回。要不要改成cnt 5看包内 README 怎么写的很多课程设计默认长连算赢改之前一定要把规则统一否则后面自动对弈统计会出偏差。3.2 最小最大搜索与 Alpha-Beta 剪枝在五子棋里的落法人机 AI 的骨架通常是负极大值搜索对一个局面递归往下推。五子棋棋盘大、空点密直接全盘搜索算不动所以多数课程设计代码里的搜索深度是 2 到 4配合“只看候选点”来裁剪分支。常见的递归函数长这样int alpha_beta(int depth, int alpha, int beta, int player) { if (depth 0) return evaluate(player); int best -INF; vectorPoint candidates genCandidates(); // 只取有棋子的周边点 for (Point p : candidates) { board[p.x][p.y] player; int val -alpha_beta(depth - 1, -beta, -alpha, -player); board[p.x][p.y] 0; if (val best) best val; if (best alpha) alpha best; if (alpha beta) break; // 剪枝 } return best; }注意这里用了-player交换攻防返回时对子节点取反这样一层函数就能同时处理“我下”和“对手下”不需要单独的 Max 层和 Min 层。alpha是当前节点已知的最优下界beta是父节点能接受的最差上界一旦alpha beta说明父节点已经不会选择这条分支继续搜索只会浪费时间。这个“负值取反”写法也是很多人后来改评估函数时出 bug 的地方——评估函数返回值必须始终站在当前player的立场取反交给搜索框架你如果在evaluate里自己也取了一次反就会左右互搏。genCandidates()在完整代码里通常被写成“遍历棋盘上所有空点、看周围两格内是否有棋子”。这个函数决定了每次递归的宽度。如果只生成周围一格有棋的点速度快但容易漏掉“对方有一串长连需要在空白处堵远点”的棋形放宽到两格搜索宽度明显变大。我一般用两格再配合 4.2 里的候选点评分截断来控宽度。3.3 评估函数一盘棋是怎么变成数字的搜索要打分关键是evaluate(player)怎么写。最粗暴但有效的做法是四个方向分别扫描数出黑方和白方的棋型数量套用下面的经验分值棋型分量级数值参考连五直接获胜100000活四对手无法同时堵两头10000冲四一端被堵只能堵着防1000活三能变成双活三或活四500眠三只能单向发展50活二早期布子基本单位10评分时常见做法是“当前玩家的总得分减去对手的总得分”。但注意减的时候要乘一个系数纯相减会让攻防权重相等实战中经常出现“赢了先手却没守住对方活三”的情况。关于权重的具体调法放到第 4 章展开。扫描四方向时一个容易错的地方是对“中间有洞”的棋型计数比如X_XXX这种形状拆成两段后每段都不到五连但组合起来是冲四。按五元组统计的方法能覆盖这个情况代价是棋盘上每个点要循环四方向乘若干窗口开销大但值得很多千行以内的课程设计在这里直接放弃评估质量差距也在这里拉开。4. 把评估权重和搜索参数调到能打赢人4.1 从改深度开始depth2 和 depth4 有何不同拿到能跑的工程后第一件事是打开搜索函数把DEPTH从 2 改成 4跑一局。如果反应慢说明候选点生成或评估函数有性能问题如果能跑完先记录落子时间再开一局看它会不会主动挡对方的活三。深度 2 的含义是“我下一步你下一步”它能挡住眼前那个活三但看不懂“你先冲四下一手变成活四”这串连续手段。深度 4 能看出两步棋的交换但代价是节点数指数增长分支因子按 15 算深度 2 是 225 个节点深度 4 是 225 的平方超过五万次评估单步几秒很正常。所以光改深度不够还要配合剪枝强度。我在代码里加了一行节点计数long long searchCount 0; int alpha_beta(int depth, int alpha, int beta, int player) { searchCount; // ... }搜索完成后打印searchCount用这个数做基线改任何参数前先记录改完再对比。比如把候选点从“周围一格”放宽到“周围两格”searchCount 可能翻几十倍把评估函数改成五元组统计searchCount 不变但单次耗时上升。两个指标分开看才不会把“评估慢了”误判成“搜索深了”。4.2 三个值得优先调的参数深度、候选点上限、算杀开关多数课程源码里搜索相关的魔数集中在文件头部#define SEARCH_DEPTH 4 #define CANDIDATE_LIMIT 10 #define VCF_SEARCH 1 // 终局算杀开关SEARCH_DEPTH控制递归层数建议 2 到 6 之间调。CANDIDATE_LIMIT表示每次生成候选点后只取评分最高的前 N 个进入递归。这个参数对速度影响极大我从 10 改到 20耗时常翻三四倍但把活三、活四周围的点挤进前 10棋力提升却不明显——因为真正决定胜负的关键落点往往在评分表的头部。VCF_SEARCH是“连冲胜利搜索”专门沿着冲四一路试能在几步内确认必杀比通用搜索提前结束局面。这个开关有时藏在编译宏里有时是运行时菜单选项。参数作用推荐起点调大代价SEARCH_DEPTH递归推演步数4节点指数增长CANDIDATE_LIMIT每层候选点数10时间线性上升棋力先升后钝VCF_SEARCH是否先做冲四算杀开尾局耗时可接受调整之后的验证不要靠感觉固定执黑/执白同一局面下分别用两套参数下完一整盘记录谁赢。连续换边各下十盘以上比“和我自己下了一局感觉变强”有说服力得多。第 5 章会给一个能跑一整夜的自动对弈脚本。4.3 评估权重怎么调守住比例比调绝对值更重要很多工程的评估函数写成return myScore - oppScore * 0.9防守权重 0.9 意味着我只要有一个活三对面连冲四都不太在乎这不对。正确思路是守住棋型之间的数量级比例活四要比活三高一个数量级冲四要比活三略高双活三的组合要能超过单个冲四这样搜索才会主动选择“先做双活三再冲四”的胜势走法。我常用来替换的起点值int shapeScore[6] {0, 10, 100, 1000, 100000, 1000000}; // 下标 0 空、1 活二、2 活三、3 冲四、4 活四、5 连五这组值的比例是 1:10:100:10000:100000活三和冲四之间只有一位的差距如果对手在另一边已经形成连五eval 无论如何都拦不住——所以更下层的逻辑是终局检测要优先于评估函数。在alpha_beta进入打分前先调一次胜负判定棋盘已经分出胜负时直接返回正负无穷不再进入候选点遍历。很多改参数的人漏掉这一步导致搜索还在把局面往已输的方向推。4.4 边界状态长连、禁手、双活三要按规则统一不同规则下同一局面的评价完全不同。常见课程包默认无禁手即黑棋长连也算赢如果要改成有禁手规则黑棋不能下出双活三和长连评估函数里的活三计算就必须把“这个活三能不能变成活四”再验证一次白棋则不受限。改动点集中在胜负判定和genCandidates里搜索框架不用动。我的建议是不要同时改规则和评估参数。规则改动牵涉到的边界状态多比如“四四禁手”需要判断两个方向的冲四是否都是真冲四空点是否被己方棋子占位后能直接连五。先把无禁手下调通记录胜率再考虑要不要加禁手分支。5. 自动对弈脚本用命令行和管道批量验证人机强度5.1 给控制台程序加一个“坐标输入”的测试接口如果程序原本是人机交互循环通常长这样while 循环里读玩家坐标调用 AI显示棋盘。要把它改成两个 AI 对弈常见做法是留一个宏开关让落子入口从键盘读数改成调用同一个 AI 函数。省事写法是在 main 里判断命令行参数if (argc 2 strcmp(argv[1], --self) 0) { while (winner 0) { Point p ai_move(BLACK); makeMove(p); if (winner) break; p ai_move(WHITE); makeMove(p); } }这段直接把“人下黑、AI 下白”的循环改成“AI 下黑、AI 下白”的自战模式。很多课程包没留这个口子那就另写一个 Python 脚本把两个可执行程序当黑盒用管道喂坐标属于不改源码也能验证强度的手段。5.2 用 Python 脚本驱动两个五子棋进程对局更通用的做法是程序本身保留cin x y输入坐标的代码另写一个驱动脚本通过管道交互。每个进程代表一方棋手收到对手坐标后落子并发回自己的坐标。脚本里最核心的循环import subprocess p1 subprocess.Popen([./wuziqi_black.exe], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, textTrue) p2 subprocess.Popen([./wuziqi_white.exe], stdinsubprocess.PIPE, stdoutsubprocess.PIPE, textTrue) def send(proc, x, y): proc.stdin.write(f{x} {y}\n) proc.stdin.flush() def recv(proc): line proc.stdout.readline().strip() x, y map(int, line.split()) return x, ysend负责把落子坐标写进子进程的标准输入recv从标准输出解析对手的回棋。注意两点子进程的交互协议必须严格约定行尾只放\n坐标用空格分开另外要设超时。如果某方在 10 秒内没输出就判超时负防止搜索死循环拖死整场比赛。这个脚本还可以加一个简单防呆每次收到坐标后主进程自己也维护一份棋盘发现子进程落点非法时输出“非法落子判负”比让程序自己崩掉好追踪。5.3 先手让先与胜率统计的坑批量对局最容易被忽略的是先手优势。五子棋黑方先手胜率明显更高同一个 AI 执黑能赢你执白可能被对面活三压死。脚本里必须做两件事一是每局结束后交换双方进程的角色让同一个二进制在下一局执黑二是记录下“黑胜、白胜、超时、非法落子”四类结果不要只记一二。胜率按“黑方胜场/总局数”和“白方胜场/总局数”分别统计差太多说明防守权重偏弱。跑 50 局以上再下结论。少于 20 局的结果受开局套路影响很大同一个程序如果固定走斜月开局先手方胜率很容易虚高。我在本地验证新参数时习惯把开局前四手固定在居中偏角的位置减少开局库影响然后才看中盘表现。6. 给 wuziqi 人机提速的三招置换表、候选点裁剪与 VCF 算杀验证强度之后如果发现单步耗时太长或者总在尾局搜不到关键冲四就要回到搜索本身提速。第一招是置换表把已经算过的“棋盘哈希 深度 分数”存起来用 Zobrist 哈希。每个落子点对应一个 64 位随机数棋盘状态等于所有已落子点的异或和。搜索进入重复局面时直接取缓存剪枝也要按缓存里的 bound 类型决定是否截断取到的分数是精确值就用否则只能当上下界参考。实现这个表通常要 1 到 2 百万项内存十几 MB对现代机器完全可接受。第二招是候选点裁剪。前面genCandidates返回周围两格全部空点数量多时直接限制了深度。实际做法是在生成点后先给每个空点算一个“如果在这里落子能形成多强的棋型”的粗略分按分排序后只保留前 8 到 12 个点进入 Alpha-Beta。这一招跟 4.2 的CANDIDATE_LIMIT不同那是直接砍掉生成结果这里会先把生成得分计算函数提出来单独跑一遍得分太低但位置在活二边缘的点多数情况下不值得保留因为冲四和活三的候选点一定在排序的头部。第三招是 VCF 算杀Victory by Continuous Four。尾局时双方都可能有连续冲四的固定杀法通用 Alpha-Beta 即使深度 8 也可能搜不出来因为中间有大量无关分支干扰而 VCF 只沿着“冲四 - 必须挡 - 再冲四”的路径走分支极少。实现上就是一个深度优先搜索只尝试能形成冲四的点检验对方挡完之后己方是否还能继续冲四几步内能连出五连则直接返回必胜分数。这套逻辑可以独立在搜索框架之外作为尾局前置检测。值得注意的反向用法是把同样的冲四检测套到对方身上判断自己是不是已经被 VCF 将死从而提前认输省时间。这三招里置换表和候选点裁剪是通用优化VCF 是五子棋特有的收敛方式。调完之后重新跑一遍第 5 章的自动对弈脚本重点看平均单步耗时和尾局胜率两个数值而不是只看谁赢。本文还有配套的精品资源点击获取