
简介一份基于VC与MFC实现的五子棋人机对战系统源码面向人工智能入门学习者、算法爱好者以及需要完成相关课程设计或毕业设计的高校学生。该项目以Minimax搜索算法和Alpha-Beta剪枝技术作为计算机落子的决策依据同时引入启发式评估函数从棋子分布、潜在连珠威胁等维度评价棋盘局面完整展示了棋类游戏AI从评估到搜索再到落子的典型流程也演示了MFC环境下界面绘制、鼠标交互与游戏状态管理的方法。压缩包共包含29个文件核心为4个cpp源文件与5个h头文件另有8个bmp图片素材、3个ico图标、2个txt说明、1个doc文档以及一个可直接运行的可执行程序并附带dsw/dsp工程文件方便用VC系列编译器打开重建压缩包整体只有367KB非常轻量适合快速下载学习目前已有133人学习或下载。通过阅读和运行这套代码可以直观看到Alpha-Beta剪枝如何减少搜索分支、启发式函数如何评估棋型也可在现有结构上替换算法、调整难度或复用界面模块是兼顾理论教学与上机实践的小型完整项目。1. wuziqi.rar 解压后先搞清楚这是一个“能下赢新手的 AI”你从网盘或课程设计里拿到 wuziqi.rar解压后多半会看到三样东西一个能直接双击运行的 exe、一份 C/C 源码、一份什么都写了又什么都没说的说明。多数人第一局赢了电脑就把它删了这其实是个误判。这个“五子棋人机”程序的价值不在棋盘画得多好看而在于 AI 怎么选点。五子棋棋形能被拆成有限几个模式落子选择又能被搜索树压缩到几层递归里天然适合理解评估函数、极小化极大与剪枝。对进入机器博弈的人来说它是门槛最低的项目对看代码的人来说它是能拉出来改参数调强度的基准。下面把最可靠的那套实现拆开从开局到编译的细节都过一遍。2. 五子棋ai算法评估函数与 α-β 剪枝决定它能下多强2.1 人机强度分三档随机落子、贪心打分、搜索剪枝机器博弈五子棋代码大全里最常见的实现分三档。第一档是随机落子空位里随便挑适合演示棋盘渲染第二档是贪心打分遍历所有空位给每个点按周围棋形算一个分取最大第三档是搜索在当前局面基础上往前推演几层再对后续局面综合打分。绝大多数 wuziqi.rar 里能赢过新手的版本都落在第二档和第三档之间。差别就在评估函数和剪枝写得多完整。强度档位落子依据典型对局表现代码量参考随机等概率选空位基本必输十几行贪心当前局面评分最高能成四但不防守几十行搜索剪枝推演 4~6 层后取最优会连攻也会补防二百行左右选择搜索方案而不是深度学习路线原因很实际五子棋虽然是零和博弈但 15×15 棋盘加候选点限制后分支数能压到几十个传统 α-β 剪枝在个人电脑上就能跑出可用效果不需要训练集、显卡和部署框架。你拿到一个五子棋人机包先看它有没有搜索函数基本就知道它属于哪一档。2.2 评估函数把棋形变成可比较的数值评估函数决定 AI“怎么看局面”。常见做法是沿四个方向扫描每个长度 5 的窗口统计窗口里玩家棋子的数量、空位数量以及是否被对手阻断再折算成分数。下面这段 C 代码是一个够用的骨架棋盘用 15×15 二维数组表示0 为空1 和 2 分别代表两方。/* 统计 player 在棋盘上的棋形分 */ int evaluate_side(int board[15][15], int player) { int score 0; int dirs[4][2] {{1,0},{0,1},{1,1},{1,-1}}; int x, y, d, i, dx, dy, nx, ny; for (x 0; x 15; x) { for (y 0; y 15; y) { for (d 0; d 4; d) { dx dirs[d][0]; dy dirs[d][1]; int count 0, empty 0; for (i 0; i 5; i) { nx x i * dx; ny y i * dy; if (nx 0 || nx 15 || ny 0 || ny 15) { count -1; break; } if (board[nx][ny] player) count; else if (board[nx][ny] 0) empty; else { count -1; break; } } if (count 0) { if (empty 0 count 5) score 100000; else if (empty 1 count 4) score 10000; else score count * count * 10; } } } } return score; } /* 总评估值我方分值减去对方分值 */ int evaluate(int board[15][15]) { return evaluate_side(board, AI_PLAYER) - evaluate_side(board, HUMAN_PLAYER); }逻辑说明evaluate_side只负责把一个玩家的棋形量化evaluate再把两个玩家分差作为局面得分正数表示 AI 占优。每个方向扫描时窗口里出现对手棋子就把count置为 -1表示这条线不算数。参数说明empty 0 count 5对应成五给最高分 100000empty 1 count 4对应活四给 10000其余情况用count * count * 10表达“连子越多收益越高”的递增关系。这个分值表不是唯一答案你可以把活三提到 5000、冲四提到 8000AI 的进攻风格立刻变化。记住评估函数的粒度直接决定搜索出来的“最优手”是否真的合理。2.3 极小化极大与 α-β 剪枝五子棋ai算法的主循环有了局面分下一步就是往前推演。五子棋是轮流落子的零和游戏一方得分高另一方得分就低所以用负极大值Negamax写法最简洁。它等价于极小化极大只是把“一方最小化对手分”统一成“每层都取负的最大值”。/* 返回当前局面对 player 的分数越大越有利 */ int negamax(int board[15][15], int depth, int alpha, int beta, int player) { int winner check_win(board); if (winner player) return 100000 depth; if (winner OTHER(player)) return -100000 - depth; if (depth 0) return evaluate(board); MoveList cand generate_candidates(board, 2); int best -100000000; for (int i 0; i cand.count; i) { apply_move(board, cand.list[i], player); int val -negamax(board, depth - 1, -beta, -alpha, OTHER(player)); undo_move(board, cand.list[i]); if (val best) best val; if (val alpha) alpha val; if (alpha beta) break; /* 剪枝 */ } return best; }逻辑说明落子后递归调用对对手视角取负值实现双方立场互换。alpha是当前方能接受的最低分beta是对方能接受的最低分一旦alpha beta说明这条分支已经不可能影响最终选择直接中断这就是剪枝。参数说明depth一般取 4 到 6深度每加 1落子耗时大约按候选点数倍增。winner player时返回100000 depth加depth是为了让 AI 在必胜局面里偏向更快赢棋反过来失败局面减depth则是让它尽量晚输。check_win需要在每层递归开头执行否则 AI 可能忽略已经成五的局面。2.4 候选点生成不在空棋盘上瞎搜2.4.1 为什么全盘搜索不现实15×15 棋盘有 225 个空位如果 depth 为 4全盘展开理论上有 225^4 个节点任何个人电脑都扛不住。五子棋的棋理是“新棋必须挨着旧棋”所以常见做法是只把已有棋子周边半径 r 内的空位作为候选点。半径 2 时候选点通常只有几十个搜索规模瞬间降到几千万节点配合 α-β 剪枝后实际计算的节点还会再少一到两个数量级。2.4.2 候选半径、节点量与耗时的关系下面这段代码生成候选点遍历所有空位检查它周围半径 r 内是否已经有棋子。void generate_candidates(int board[15][15], int radius, MoveList *out) { for (int x 0; x 15; x) { for (int y 0; y 15; y) { if (board[x][y] ! 0) continue; int near 0; for (int dx -radius; dx radius !near; dx) { for (int dy -radius; dy radius; dy) { int nx x dx, ny y dy; if (nx 0 nx 15 ny 0 ny 15 board[nx][ny]) { near 1; break; } } } if (near) add_move(out, x, y); } } }逻辑说明先跳过已占用点再对空位的邻域做两层循环。board[nx][ny]非零即表示邻域有棋子这个空位进入候选列表。radius取 1 时只覆盖相邻点取 2 时覆盖跳一格的位置能抓到手四和跳活三的防守点。半径候选点数量约depth4 节点量级参考110~2010^4230~6010^6~10^7380~12010^8 以上参数说明通用配置是radius 2速度快且不会漏掉常见棋形。如果 AI 在残局阶段明显漏掉远端做杀再把半径调到 3并配合时间限制兜底。候选点生成是整个五子棋人机实现里最容易被忽略的性能瓶颈很多“AI 想半天”的包问题不在深度而在候选集太大。3. rar 解压后的工程结构、界面选型与对局流程3.1 wuziqi.rar 解压后先找四个东西rar 作为一种打包格式里面装什么完全看作者习惯。常见做法是把课设或开源项目完整打包解压后优先找这四类内容可执行文件、源码文件、说明文档、资源文件。它们决定你是“双击就能玩”还是“得先编译”。文件/目录作用需要做什么wuziqi.exe 或 debug/release 下的 exe可直接运行的程序双击前先确认来源可信wuziqi.cpp / main.cpp / ChessBoard.cpp源码主体用编辑器打开看语言和工程类型readme.txt / 说明.docx编译方式、操作规则先读节省排错时间res / image / font 目录棋盘贴图、字体、音效必须和 exe 保持相对路径拿到包后先看有没有源码。只有 exe 的 rar 包虽然能玩但从机器博弈角度看基本没有学习价值真正值得研究的是那个带搜索函数的源文件。提示解压后先杀毒再运行尤其是直接双击 exe 的场景。老课设压缩包里混入额外文件的例子并不少见。3.2 控制台版与图形界面先跑通再说好不好看很多五子棋人机程序是控制台版原因不是作者不会做界面而是控制台版把注意力集中在 AI 逻辑上。棋盘用二维字符数组打印玩家输入行列坐标AI 计算出坐标再落子。相比 MFC、Qt 或网页版控制台版的编译依赖最少适合验证算法。while (!game_over) { print_board(board); if (turn HUMAN) { printf(输入行和列(1-15): ); scanf(%d %d, row, col); while (!is_valid_move(board, row, col)) { printf(该点已占用或越界重新输入: ); scanf(%d %d, row, col); } } else { Move mv search_best_move(board, SEARCH_DEPTH); row mv.x; col mv.y; printf(AI 落子: %d %d\n, row, col); } apply_move(board, row, col, turn); if (check_win(board, row, col)) { print_board(board); printf(%s 胜\n, turn AI ? AI : 玩家); break; } turn 3 - turn; }逻辑说明主循环里唯一的状态切换动作是turn 3 - turn因为两方标识是 1 和 23 减当前值正好得到对方。is_valid_move负责校验越界和重复落子必须在玩家输入后立刻执行。参数说明SEARCH_DEPTH就是第 2 章里的搜索深度。控制台版的坐标从 1 开始还是从 0 开始取决于print_board怎么打印行号和列号很多包的坐标混乱问题不是逻辑错而是打印循环从 1 开始但数组下标从 0 开始看代码时先确认这一点。3.3 一局对局的状态流转与悔棋图形界面版在此基础上增加状态机和历史栈。状态可以拆成四个等待玩家输入、AI 计算中、判定胜负、对局结束。悔棋则是把历史落子栈回退两步让玩家和 AI 都回到上一手之前。typedef enum { STATE_WAIT_INPUT, STATE_AI_THINK, STATE_CHECK_END, STATE_OVER } GameState; struct Move history[225]; int history_len 0; /* 悔棋弹出玩家和 AI 各一手 */ void undo_last_two() { if (history_len 2) return; board[history[--history_len].x][history[history_len].y] 0; board[history[--history_len].x][history[history_len].y] 0; }逻辑说明history按落子顺序压栈AI 作为当前回合落子后栈顶是 AI 的手再往下是玩家的手所以回退两步就是完整悔棋。执行悔棋后必须将状态切回STATE_WAIT_INPUT否则界面会卡在 AI 计算中。参数说明栈大小建议取 225因为五子棋最多下满棋盘。完善一点的做法是把棋盘快照一并压栈这样未来做“回放”功能不用重新落子。状态机的好处是 AI 计算耗时较长时主线程可以先刷新界面显示“思考中”再在计算完成后切换状态避免界面假死。4. 编译运行与路径依赖wuziqi.rar 最容易踩的坑4.1 rar 解压与中文目录名是跑不起来的第一个坑拿到 wuziqi.rar第一件事不是打开源码而是解压到合适的位置。老式 C 工程里常见相对路径加载资源比如./res/board.bmp如果解压到“五子棋人机\最终版(1)\”这种包含中文和括号的目录部分老旧文件读取代码会直接失败。推荐解压到一个纯英文无空格路径例如D:\wuziqi\。C:\Program Files\7-Zip\7z.exe x wuziqi.rar -oD:\wuziqi\ -y逻辑说明x是解压并保留目录结构-o指定输出目录且目录后要带反斜杠-y表示跳过所有确认提示。解压完成后检查D:\wuziqi\wuziqi\wuziqi.exe或是直接看根目录有没有 exe路径层级不同是常见差异。参数说明如果系统只有 WinRAR直接右键解压到“wuziqi”文件夹即可。真正要留意的是解压后不要手动把 exe 单独拷到桌面运行这样会丢失与资源文件的相对路径关系。4.2 缺少 DLL 与运行库wuziqi.rar 常见报错怎么解老课设 exe 常见的报错是“缺少 MSVCP140.dll”或“缺少 VCRUNTIME140.dll”这代表你的机器缺少对应版本的 Visual C Redistributable。DLL 是程序运行库不是软件本体缺了它双击就弹错误框。报错文本原因处理方式缺少 MSVCP140.dll缺少 VS2015~2022 运行库安装 Microsoft Visual C Redistributable x86缺少 VCRUNTIME140.dll同上通常装 x86 解决0xc000007b 应用无法启动exe 位数和 DLL 位数不匹配分别装 x86 和 x64 两个版本参数说明很多 32 位课设 exe 需要 x86 版本运行库光装 x64 解决不了所以始终把 x86 和 x64 都装上。装完运行库仍然报错时用“依赖”工具Dependency Walker 等打开 exe 看具体缺哪个 DLL不要盲目重装系统。提示遇到 DLL 报错先查系统日志路径是“事件查看器 - Windows 日志 - 应用程序”错误模块和异常代码会指向真正原因。4.3 源码打开乱码GBK 与 UTF-8 的一行命令转换源码打开后满屏“锟斤拷”或“铪铪”是编码问题而不是文件损坏。老 C 工程按 GBK 保存现代编辑器默认按 UTF-8 打开中文注释和字符串全部错位。最有代表性的字符就是“锟斤拷”它本质是 UTF-8 解码 GBK 字节流时的替换符。iconv -f GBK -t UTF-8 main.cpp main_utf8.cpp逻辑说明-f指定源编码-t指定目标编码转换后另存为新文件避免破坏原始工程。如果源码很大且分散在多个文件可以用循环批量处理例如for %f in (*.cpp *.h) do iconv -f GBK -t UTF-8 %f %f.utf8。参数说明转换完成后用新版 Visual Studio 打开会有概率因为 BOM 差异导致编译警告普遍做法是让编辑器统一存成“UTF-8 with BOM”或直接改回 GB2312 保存。判断源码是 GBK 还是 UTF-8 最快的方法是看字符串有没有夹杂中文中文正常就不用转只有出现替换符再转。5. 把“人机”调得更顺手三个参数与一段验证脚本5.1 深度与时间预算怎么配搜索深度是第一个要调的参数。depth 设为 2 到 3AI 落子极快但只会看眼前两步新手多练几局就能找到漏洞depth 设为 4攻防基本正常depth 设为 5 或 6中盘计算量明显增加但能做出连续冲四和活三的配合。按候选点半径 2 估算depth4 时单手耗时通常在 0.5 到 3 秒之间depth6 就可能超过 15 秒必须靠剪枝质量和置换表兜底。参数建议值效果SEARCH_DEPTH4攻防均衡单手秒级SEARCH_DEPTH5~6中盘做杀更强需要剪枝优化候选半径2标准配置性能与不漏棋兼顾5.2 棋形分值表与防守倾向很多五子棋人机玩起来像“愣头青”是因为评估函数里进攻分压过防守分。机器博弈五子棋代码大全里的标准做法是把棋形拆开显式打分而不是只数棋子。一张能用的分值是成五 100000、活四 10000、冲四 5000、活三 1000、眠三 200、活二 100。想让它防守更稳把对方棋形分乘 1.2 再参与评估AI 会在攻防之间明显偏向补位。int threat_score evaluate_side(board, HUMAN_PLAYER); int attack_score evaluate_side(board, AI_PLAYER); return attack_score - (int)(threat_score * 1.2);逻辑说明威胁系数1.2表示把对手的局面分放大两成AI 看到对方活三会比自己的活三更紧张。这个倍率调成 1.0 时 AI 进攻积极调成 1.5 时偏向龟缩防守适合用来做不同难度档位。5.3 用自动对局脚本验证调参效果手玩十局很难量化改动好坏最直接的方法是写一个自动对局脚本让 AI 和随机落子对手打统计胜率。下面这段 Python 骨架可以直接运行ai_move的部分替换成你项目里封装的搜索接口。import random EMPTY 0 def check_win(board, x, y): player board[x][y] for dx, dy in ((1,0),(0,1),(1,1),(1,-1)): cnt 1 for sign in (1, -1): nx, ny x, y while True: nx sign * dx ny sign * dy if not (0 nx 15 and 0 ny 15) or board[nx][ny] ! player: break cnt 1 if cnt 5: return True return False def ai_move(board, depth): # 这里是 AI 的接口可以替换成 C 程序输出也可以调 Python 版搜索 for x in range(15): for y in range(15): if board[x][y] EMPTY: return x, y def play_one(depth): board [[EMPTY] * 15 for _ in range(15)] for turn in range(225): if turn % 2 0: x, y ai_move(board, depth) else: empty [(x, y) for x in range(15) for y in range(15) if board[x][y] EMPTY] x, y random.choice(empty) board[x][y] 1 if turn % 2 0 else 2 if check_win(board, x, y): return turn % 2 return 2 # 跑 50 局统计 AI 执黑胜率 wins sum(1 for _ in range(50) if play_one(4) 0) print(fAI 执黑胜率: {wins / 50:.2f})逻辑说明turn % 2 0代表 AI 执黑先行每局最多 225 手有人成五立即返回胜负。check_win是标准四方向连子检测排除越界后累加计数。这个脚本把play_one(4)中的深度换成 5 或把威胁系数换成 1.2就能对比不同配置的胜率变化。参数说明随机对手是基线如果 AI 对随机落子都不能稳定全胜说明搜索或评估逻辑有明显漏洞。把随机对手换成上一版 AI还能做同门对战进一步验证改动方向是否正确。本文还有配套的精品资源点击获取