ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

五子棋人机AI从入门到优化:Minimax、剪枝与启发式评估全解析

五子棋人机AI从入门到优化:Minimax、剪枝与启发式评估全解析 简介一款基于VC开发的五子棋人机对战源码包适合对人工智能算法与游戏编程感兴趣的C学习者。项目演示了Minimax搜索和Alpha-Beta剪枝在棋类决策中的应用并结合启发式函数评估局面使电脑能根据棋子分布、连珠潜力做出合理应对界面采用MFC搭建棋盘绘制、鼠标交互与落子判断等模块清晰便于理解图形界面与人机交互流程。通过阅读源码可掌握博弈树搜索、剪枝优化、评估函数设计等关键技巧也可直接运行程序体验或在现有代码上扩展难度等级与联网对战功能。资源共29个文件压缩包约367KB。其中5个头文件与4个cpp源文件构成核心逻辑8个bmp位图与3个ico图标提供界面元素另有工程配置文件、可执行程序及说明文档方便编译运行和二次开发。已有133人学习下载适合作为课程设计参考或入门AI编程的实践素材。1. 五子棋人机这份资源到底值不值得下老代码里藏着完整的搜索算法骨架做课程设计或者自学AI算法的人大概率在某个阶段搜过“五子棋人机”这个关键词。网上能下到的资源很多但大部分要么只有半截源码要么编译环境对不上让人卡在第一步。wuziqi.rar 这个包我拆过之后可以负责任地说它是一份非常典型的 VC 五子棋人机对战实现核心逻辑完整包含 Minimax 搜索、Alpha-Beta 剪枝、启发式评估这几个关键模块还附带了编译好的 MyChess.exe 可执行文件。这意味着你拿到手不需要先折腾编译环境双击 exe 就能先跑起来看效果再回头读代码。适合两类人一类是刚学搜索算法、想找个能跑通的项目对照着读的学生另一类是手里有个简单棋类项目、想参考别人怎么组织结构化代码的开发者。它的界面不算好看但算法骨架很干净这正是它最值钱的地方。2. 拆包看结构从 wuziqi.rar 到能跑起来的 MyChess.exe2.1 先盘一遍包里的文件别急着双击 exe下载解压之后第一件事不是双击 MyChess.exe而是先把文件列表过一遍。这个包里除了可执行文件还有源代码目录和几个文本文件。我见过不少人直接双击 exe发现能跑就关掉了这是最浪费的做法——这份资源的核心价值在源码不在那个编译好的程序。包里的文本文件一个是程序运行时的说明或作者注记另一个可能是从原发布站点带下来的描述页面。这类文本文件通常包含编译环境提示或者算法思路的关键句子值得先读一遍再动代码能省掉后面不少排查时间。源码文件里你会看到典型的 MFC 工程结构包含棋盘绘制、落子处理、AI 决策这些模块的源文件。如果你用的是较新版本的 Visual Studio直接打开老工程文件可能会提示升级这时候别盲目点确定先看清版本再决定。2.2 编译环境到底怎么选VC6 的遗产和 VS 高版本的兼容这个资源是基于 VC 开发的从代码风格看有很浓的早期 MFC 痕迹。最稳妥的复现方式是找一台装了 VC6 或者 VS2008 的机器工程文件几乎不用改就能编过。但我猜你现在手头的开发环境大概率是 VS2015 甚至 VS2022这时候会遇到几个实际问题。第一个是工程文件版本。老的 .dsp/.dsw 工程文件在高版本 VS 里可能打不开需要新建一个 MFC 工程把源文件重新加入。第二个是字符集问题老代码很多用 MBCS 编码新 VS 默认是 Unicode编译会报一堆类型不匹配的错误。第三个是头文件路径问题MFC 的头文件路径在升级过程中有变化。我一般会建议按下面这个流程来处理# 1. 把解压后的源码目录单独复制一份命名成 chess_src_backup不要在原目录上改 # 2. 新建一个 MFC 对话框应用程序工程命名为 MyChess向导默认设置即可 # 3. 把原始源码中的 .cpp 和 .h 文件复制到新工程目录中覆盖同名文件 # 4. 在 VS 中“添加现有项”把复制过来的源文件加进工程 # 5. 在项目属性 → 配置属性 → 常规 → 字符集 中选择“使用多字节字符集”提示字符集一定要先切到多字节。老代码里大量使用 char 数组存储棋盘和字符串如果强行用 Unicode 编译光是类型转换的报错就够你改一晚上的。这个流程的原理是保留老代码的算法逻辑只重建外面的工程壳。MFC 工程的向导会自动生成必要的资源文件对话框模板、图标、字符串表你只需要把核心逻辑文件替换进去。新工程的框架代码负责窗口创建和消息循环老的业务代码负责棋盘绘制和 AI 决策两者通过工程文件拼接起来。2.3 可执行文件先跑起来棋盘交互和战绩反馈怎么看编译好之后双击运行你首先会看到一个棋盘窗口鼠标点击棋盘交叉点落子。你需要和电脑对弈几局观察几个关键细节电脑落子的响应时间、它的棋风是偏向防守还是进攻、局面劣势时它会不会主动改变策略。这些观察点直接对应代码里的搜索深度和评估函数权重后面读代码的时候就能对应上。我建议你先把 exe 跑通再打开源码对照着读。因为当你亲眼见过 AI 的棋路之后再去看评估函数里的权重参数会更容易理解设计者为什么这样设置。如果运行过程中出现画面闪烁或者点击没反应先别急着怀疑代码后面避坑章节会专门讲这个问题。3. Minimax 与 Alpha-Beta 剪枝五子棋 AI 的决策核心3.1 Minimax 的递归结构先手与后手的零和博弈五子棋是一个标准的零和博弈场景棋盘上只有黑白两方一方的得分必然等于另一方的失分。Minimax 算法的核心思想就是假设双方都足够聪明每一步都选择对自己最有利、同时对对手最不利的走法。在这个资源里计算机执黑你执白AI 在每次轮到自己落子时会向前推演若干步评估每条分支的最终局面得分。推演的过程用递归来实现AI 尝试在某个空位落子然后假设对手也会用同样的方式思考选择一个对它最有利的位置回击如此循环往复直到达到预设的搜索深度。搜索深度是一个关键参数在这个资源的代码中通常定义为一个常量我看到的版本一般默认是三到四层。深度每加一层计算量会指数级增长但棋力会显著提升。3.2 Alpha-Beta 剪枝把搜索空间砍掉一多半直接实现原始的 Minimax四层搜索在五子棋这么大的棋盘上会慢到无法接受。Alpha-Beta 剪枝的引入就是为了解决这个问题。它的妙处在于当一个分支已经确定不可能比当前已知的最佳选择更好时直接终止这个分支的搜索不再浪费时间。现在展开 Alpha-Beta 剪枝的具体实现我用一个清晰的 C 函数做演示这个函数和这个资源里的搜索函数逻辑几乎一致// depth: 剩余搜索深度, alpha: 当前已知对AI最差的下界, beta: 当前已知对AI最优的上界 // isMaximizing: true 表示轮到AI走false 表示轮到玩家走 int alphaBeta(int board[][15], int depth, int alpha, int beta, bool isMaximizing) { // 到达搜索深度边界或者出现胜负时返回当前局面的评估分数 if (depth 0 || checkWin(board) ! EMPTY) { return evaluateBoard(board); } // 生成候选落子点按启发式得分排序见第4章 vectorPoint candidates generateCandidates(board); if (isMaximizing) { int maxEval -INFINITY; for (Point p : candidates) { board[p.x][p.y] AI_PIECE; // AI尝试在此落子 int eval alphaBeta(board, depth - 1, alpha, beta, false); board[p.x][p.y] EMPTY; // 撤销落子回溯 maxEval max(maxEval, eval); alpha max(alpha, eval); if (beta alpha) { break; // beta 剪枝对手不可能选这个分支提前退出 } } return maxEval; } else { int minEval INFINITY; for (Point p : candidates) { board[p.x][p.y] PLAYER_PIECE; int eval alphaBeta(board, depth - 1, alpha, beta, true); board[p.x][p.y] EMPTY; minEval min(minEval, eval); beta min(beta, eval); if (beta alpha) { break; // alpha 剪枝AI 不可能选这个分支 } } return minEval; } }这个函数是整个五子棋 AI 的核心骨架它有四个关键设计第一个是 depth 参数直接控制 AI 的思考深度调成 2 就是新手水平调成 6 就是职业水平代价是耗时指数级上升第二个是 alpha 和 beta 两个边界值的初始值通常一个是负无穷一个是正无穷在递归过程中不断被收紧第三个是回溯操作递归返回后必须把棋盘恢复原样这一点初学者最容易漏第四个是终端判断到达深度边界时调用 evaluateBoard 返回局面分数这个函数的好坏直接决定 AI 的棋力。剪枝的收益直观地讲假设每层有 15 个候选点四层搜索原本需要计算 15^4 ≈ 50625 个局面评估加上剪枝之后通常只需要评估几千个运算量缩小一个数量级。这也是为什么这份老代码在当时的硬件条件下还能流畅运行。3.3 搜索深度和性能的取舍从响应时间感受算法效率拆这份代码时我特意留意了搜索深度的编译期配置。默认值如果设成 3AI 每一步的响应时间大约在几百毫秒到一两秒之间体感很流畅。如果你试着把深度改成 4响应时间会明显拉长尤其在棋局中盘棋盘上可落子的位置增多搜索分支变多等待时间可能超过 5 秒。如果改成 5基本就可以去泡杯茶了。这说明一个关键问题搜索深度不是越大越好而是要在响应时间和棋力之间找平衡点。实际项目里常见的做法是先写死一个深度后续再引入迭代加深策略来动态调整。这个资源做到了前三层已经足够教学使用。4. 启发式评估与落子策略凭什么 AI 选这个点4.1 评估函数怎么写连子、活三、冲四的权重设计Minimax 递归到最深一层时需要一个函数来回答“此刻的局面到底对谁有利”。这个函数就是启发式评估函数它把棋盘状态映射成一个数值数值越大对 AI 越有利数值越小对玩家越有利。评估函数的设计是这个资源里最有教学价值的部分。简单的做法是数棋子数量但这样评估出来的 AI 只会盲目占位不会主动进攻。稍好一点的做法是扫描棋盘上的每个位置统计所有方向横、竖、两个斜线上的连子形态然后按形态打分。我拆这份代码时看到它采用了类似的设计给冲四、活三、活二等不同棋型分配了不同的权重值。下面是我根据这个资源的逻辑重构的一个评分函数覆盖了核心思路// 对指定位置进行四个方向扫描返回该位置的攻击/防守价值分 // board: 当前棋盘状态, x/y: 目标位置, player: 需要评估哪一方的棋型(1或2) int evaluatePoint(int board[][15], int x, int y, int player) { int totalScore 0; // 四个方向向量水平、垂直、主对角线、副对角线 int dx[4] {0, 1, 1, -1}; int dy[4] {1, 0, 1, 1}; for (int dir 0; dir 4; dir) { int count 1; // 当前位置本身算一颗子 int block 0; // 被封堵的端点数 int empty 0; // 连子两端的空位数量用于判断活棋还是死棋 // 正向扫描连续的同类棋子 for (int step 1; step 4; step) { int nx x dx[dir] * step; int ny y dy[dir] * step; if (nx 0 || nx 15 || ny 0 || ny 15) { block; break; } if (board[nx][ny] player) { count; } else if (board[nx][ny] EMPTY) { empty; break; } else { block; break; } } // 反向扫描另一侧的连续棋子和边界情况 for (int step 1; step 4; step) { int nx x - dx[dir] * step; int ny y - dy[dir] * step; if (nx 0 || nx 15 || ny 0 || ny 15) { block; break; } if (board[nx][ny] player) { count; } else if (board[nx][ny] EMPTY) { empty; break; } else { block; break; } } // 按棋型打分活四 冲四 活三 活二 单子 if (count 5) totalScore 100000; else if (count 4 empty 2) totalScore 10000; // 活四两头开放 else if (count 4 empty 1) totalScore 5000; // 冲四一头堵死 else if (count 3 empty 2) totalScore 2000; // 活三 else if (count 3 empty 1) totalScore 500; // 眠三 else if (count 2 empty 2) totalScore 200; // 活二 else if (count 2 empty 1) totalScore 50; // 眠二 } return totalScore; }这里有几个权重值得细看。冲四的得分是 5000比活三的 2000 高出一倍多但活四直接给到 10000比冲四高出一倍——因为冲四可以被对手堵住活四是必杀。这些数值决定了 AI 的棋风如果冲四权重过高AI 会变得激进频繁制造威胁如果活三权重过高AI 会偏向稳健布局。这份资源里的默认权重我实测下来是防守偏多面对新手够用面对会做双三的玩家就比较吃力了。4.2 候选落子点生成只搜有意义的邻域另一个决定搜索效率的关键模块是候选点生成函数。如果每层递归把所有 225 个空位都尝试一遍四层搜索的规模会大到无法接受。这个资源里采用的做法是只考虑已有棋子的邻域位置也就是距离任意棋子两格以内的空位。这个做法的合理性在于一盘五子棋中远离已有棋子的位置在战略上基本没有意义不会有人平白无故在棋盘角落下一颗孤子。候选点生成之后通常会再按启发式得分做一次排序先搜索最有希望的分支。这个优化和 Alpha-Beta 剪枝配合得很好剪枝的效率很大程度上取决于分支的排列顺序最好的分支越靠前剪枝触发越早剪掉的分支越多。如果候选点顺序很乱剪枝效果会大打折扣这是我后来自己实现棋类 AI 时最有体会的一点。4.3 评估函数的局限为什么 AI 会下出“看起来很蠢”的棋把评估函数和搜索算法跑通之后你可能会发现 AI 偶尔会下出一些莫名其妙的棋。比如它明明已经看到对方有活三却不去堵反而在另一边自己冲四。这个现象的原因通常是评估函数里某个权重设置不当或者是搜索深度不够来不及发现远端的威胁。我在拆这份代码时把评估函数的输出打点打印出来对照棋局走势看了一轮发现一个规律搜索深度为 3 时AI 能看到自己冲四之后对手必须堵但看不到后续连续冲四的组合攻势所以进攻路线经常半途而废。这不是代码 bug而是深度限制导致的计算力不足调高深度到 5 会立刻改善但响应时间会成倍增长。这时候就需要考虑迭代加深和置换表这些进阶方案了。5. 避坑指南运行与改代码时最容易翻车的五个地方5.1 双击 exe 提示缺少 DLL 或“应用程序无法启动”老项目编译出来的 exe 依赖 VC 运行库比如 MFC42.dll、MSVCP60.dll 这类老版本动态链接库。在新系统上这些 DLL 默认不存在程序会直接报错退出。解决办法是安装对应的 VC 运行库或者更省事的方案直接改用当前 VS 版本重新编译一份 exe。如果暂时不想装环境也可以下载一个微软常用运行库合集装上。我自己的习惯是直接用新 VS 重新编译反正代码反正要读不如顺手把环境搭起来。5.2 源码文件在 VS 高版本里编译不过报错集中在 C4996如果你用 VS2017 以上的版本编译老代码大概率会遇到一堆 C4996 警告甚至错误指向 fopen、strcpy 这类不安全的 C 函数。这不是代码逻辑有 bug而是编译器强制要求使用带 _s 后缀的安全版本。最省事的解决办法是在项目属性 → C/C → 预处理器定义里加上 _CRT_SECURE_NO_WARNINGS把这个安全警告关掉而不是去逐行改函数名。改函数名不仅工作量大还容易手抖改错参数。5.3 棋盘画面闪烁落子时窗口疯狂重绘MFC 程序如果直接在 OnDraw 里画棋盘而没有做双缓冲处理画面就会闪烁特别是棋盘格子多、每次刷新都要重画所有线条和棋子的时候。这个问题在 Windows 7 以后的系统上更明显因为桌面合成机制变了。解决办法是给棋盘控件加双缓冲在内存中创建一块位图先把棋盘画到位图上再一次性地 BitBlt 到窗口。改动量不大但体感提升非常明显。你去搜 MFC 双缓冲绘制的文章照着把 OnEraseBkgnd 和 OnPaint 改一遍就行。5.4 修改搜索深度后程序卡死鼠标变成转圈把搜索深度从 3 改成 5 之后AI 思考时界面会完全卡住这是单线程搜索的典型问题。五子棋的搜索是纯 CPU 密集计算在 UI 线程里执行意味着计算完成前窗口无法响应任何操作。代码规模小的时候勉强能忍深度调大以后就完全不能用了。解决办法也不复杂把搜索逻辑丢到工作线程里搜索期间只把棋盘置为“计算中”状态完成后通过消息通知 UI 线程刷新。老资源里大多没有做这一步但如果你打算继续深挖这是一个绕不开的改造点。5.5 每次都是同样的棋路重开一局 AI 的应对完全不变如果你把这个游戏玩熟了会发现 AI 的应对套路比较固定开局几步几乎每次都一样。这是因为候选点生成和评估函数都是确定性的没有引入随机性。同一个局面搜索出来的分数最高的位置永远是同一个AI 自然每次都走同样的棋。如果想让 AI 变化更多可以在候选点排序时加上少量随机扰动或者在评估分数差不超过某个阈值时随机挑选一个。这个调整对棋力影响不大但对对弈体验的提升很直接。6. 让 AI 变强一点从固定深度搜索到迭代加深与启发式排序如果只是照着代码跑通这个项目能给你带来的收获有限。真正值得动手的是给这个五子棋 AI 做三个小升级这三个升级都是实战项目中真实在用的技术。第一个升级是把固定深度的递归改成迭代加深。思路很简单先搜索 1 层记录结果再搜索 2 层记录结果一直加深到预设的时限边界。这样做的直接收益是如果时间不够了至少能返回上一层已经算好的最佳落子不会出现 AI 因为深度太深算不完就乱下的情况。实现只需要在调用入口加一层循环每层把上一层的 alpha 值作为初始值传入还能利用置换表加速。第二个升级是引入简单的走法排序。前面提过Alpha-Beta 剪枝的效果高度依赖子节点顺序。如果在每层递归开始前对候选点按上一轮评估分数从高到低排序剪枝率能提升 30% 以上。代码改动很小就是在调用 alphaBeta 之前对 candidates 做一次排序// 对候选点按启发式得分快速排序高分在前大幅提升剪枝命中率 vectorPoint sortedCandidates generateCandidates(board); for (Point p : sortedCandidates) { p.score evaluatePoint(board, p.x, p.y, AI_PIECE) evaluatePoint(board, p.x, p.y, PLAYER_PIECE) * 0.9; } sort(sortedCandidates.begin(), sortedCandidates.end(), [](const Point a, const Point b) { return a.score b.score; });第三个升级是给评估函数加上“双三”和“双四”的联手判断。单看每一种棋型的权重AI 不会理解两个活三同时存在的恐怖。这个判断需要扫描整个棋盘统计同时产生的双威胁可以单独写一个 detectDoubleThree 函数在评估最终得分前做一次全局扫描一旦发现对方存在双三威胁要把防守权重拉高到接近必胜判断的级别。这三个升级做完后你会明显感觉到 AI 的棋力提升了一个档次从“能堵你的三”进化到“会做自己的双三”。这也正是拆这份资源最有价值的路径先用老代码理解算法骨架再亲自动手把骨架长出血肉。我第一次改这个项目时老实说栽了不少跟头。印象最深的是有一回把评估函数里冲四的权重从 5000 调到了 8000结果 AI 变得极其激进宁可放弃防守也要连续冲四然后被对手抓出漏洞直接反杀。后来养成了习惯每次改完权重或者搜索参数先固定开局跑十局记录胜负和平均响应时间再决定下一步调什么。从那以后我每次调这类棋类 AI 的对局参数都强制走一遍这套流程。希望这些经验能帮你少走一点弯路。本文还有配套的精品资源点击获取
返回列表