简介一套基于C实现的五子棋游戏源码核心采用极大极小值搜索与AlphaBeta剪枝算法并同时提供前端交互界面与后端服务逻辑适合计算机专业学生用于课程设计、毕业设计也可作为C博弈算法项目实战的参考。压缩包共66个文件、大小仅1.35MB结构上覆盖16个头文件与14个C源文件同时包含JSON配置、Markdown说明、GIF演示图和前端页面等便于按模块阅读、编译和调试。项目采用游戏界面、服务端与AI组件分层设计清晰地展示了经典博弈树搜索从原理到工程落地的完整流程可帮助学习者理解评估函数设计、搜索深度调整以及剪枝效率之间的关系也能在此基础上扩展棋力或改造界面。目前已有359人浏览学习附带的演示动画和项目文档能够帮助快速了解运行效果与代码结构是一款结构完整、适合学习借鉴的五子棋项目。1. 五子棋项目拆解极大极小值算法和AlphaBeta剪枝怎么落地用 C 写一个能下五子棋的 AI很多人第一反应是暴力搜索所有落子位置——棋盘 15×15每层 225 个分支搜索深度到 4 层就是 25 亿个节点电脑直接卡死。这个毕业设计项目把传统搜索算法真正跑通了极大极小值算法负责决策框架AlphaBeta 剪枝把无效分支砍掉配合评估函数让 AI 在毫秒级完成落子。它不是那种调库调出来的玩具而是从头实现了博弈树搜索、棋盘状态管理、前后端分离通信的完整工程适合正在做课程设计或毕设的计算机专业学生也适合想搞清楚传统搜索算法怎么应用的人。拿到源码后能直接编译运行也能基于它改搜索深度、调评估权重、换前端界面下面我把这套代码的算法逻辑、模块划分和运行过程拆开讲清楚。2. 核心AI算法极大极小值与AlphaBeta剪枝的实现机制2.1 极大极小值算法的决策框架五子棋本质上是一个双人零和博弈一方得分就是另一方失分不存在双赢。极大极小值算法就是在这种前提下建立的决策树假设自己是 MAX 方目标是让评估值最大化对手是 MIN 方目标是让评估值最小化。AI 在搜索时轮流站在双方视角评估局面最后往上回溯选出对自己最有利的一步。这个项目里的 AIComponent 类实现了整套搜索流程。先看核心搜索函数的简化代码// ai/AIComponent.cpp 核心搜索逻辑简化 int AIComponent::minimax(int depth, bool isMaximizing) { // 到达搜索深度或游戏结束返回局面评估值 if (depth 0 || gameBoard-isGameOver()) { return evaluateBoard(); } if (isMaximizing) { int maxEval -INFINITY; auto validMoves gameBoard-getValidMoves(); for (auto move : validMoves) { gameBoard-placePiece(move, COMPUTER_PIECE); int eval minimax(depth - 1, false); gameBoard-undoMove(move); maxEval std::max(maxEval, eval); } return maxEval; } else { int minEval INFINITY; auto validMoves gameBoard-getValidMoves(); for (auto move : validMoves) { gameBoard-placePiece(move, HUMAN_PIECE); int eval minimax(depth - 1, true); gameBoard-undoMove(move); minEval std::min(minEval, eval); } return minEval; } }这段代码的逻辑是递归构建博弈树depth控制搜索深度isMaximizing标记当前层是 AI 落子还是玩家落子。每一个合法落子都会在棋盘上临时放一颗棋子递归搜索下一层返回后立刻撤销undoMove确保棋盘状态被正确还原。placePiece和undoMove这两个操作配合默契是算法不污染棋盘状态的关键。参数上INFINITY通常取一个大数如 100000代表必胜或必败的极端评估值。搜索深度depth在这个项目里默认设为 4这是性能与智能程度的折中——深度太浅 AI 只能看到局部深度太深单步思考时间会超过 10 秒。如果机器性能好可以直接把深度改成 6AI 的棋力会明显上升但每步等待时间也会拉长到 30 秒以上需要做好取舍。2.2 AlphaBeta剪枝的代码实现极大极小值算法有个致命问题它会把所有分支都搜索完哪怕某些分支已经明显不可能影响最终决策了。AlphaBeta 剪枝就是干这个的——维护一个 Alpha 值MAX 方能保证的最低分和一个 Beta 值MIN 方能保证的最高分当某个分支的评估值超出了父节点的上下界时直接截断后续搜索。项目里的剪枝版本长这样// ai/AIComponent.cpp AlphaBeta剪枝搜索简化 int AIComponent::alphabeta(int depth, int alpha, int beta) { if (depth 0 || gameBoard-isGameOver()) { return evaluateBoard(); } if (isMaximizingTurn) { int maxEval -INFINITY; auto validMoves gameBoard-getValidMoves(); for (auto move : validMoves) { gameBoard-placePiece(move, COMPUTER_PIECE); int eval alphabeta(depth - 1, alpha, beta); gameBoard-undoMove(move); maxEval std::max(maxEval, eval); alpha std::max(alpha, eval); // 剪枝当前分支已经不可能被父节点选中 if (beta alpha) break; } return maxEval; } else { int minEval INFINITY; auto validMoves gameBoard-getValidMoves(); for (auto move : validMoves) { gameBoard-placePiece(move, HUMAN_PIECE); int eval alphabeta(depth - 1, alpha, beta); gameBoard-undoMove(move); minEval std::min(minEval, eval); beta std::min(beta, eval); if (beta alpha) break; } return minEval; } }和纯极大极小值算法相比这个版本多传了alpha和beta两个参数作用相当于给搜索树安了一对阈值MAX 层只关心能不能找到比当前alpha更大的值MIN 层只关心能不能找到比当前beta更小的值。一旦beta alpha说明这条分支无论再怎么搜都不会影响最终决策直接 break 跳出循环。剪枝效率的差异非常直观。搜索深度设为 4 时无剪枝的极大极小值算法要评估的节点数以亿计程序卡顿明显加剪枝后节点数能砍到二十分之一甚至更少AI 基本能做到 1~2 秒内落子。需要留意的是剪枝效率高度依赖走法顺序——如果每次优先搜最好的走法剪枝率会非常高如果从最差的走法开始搜那剪枝几乎不生效。所以这个项目里getValidMoves()返回的落子序列排了序把靠近已有棋子的位置放在前面这算是个隐性的性能优化点。2.3 评估函数的设计与权重评估函数决定了 AI 下棋的“品味”。同样的搜索深度评估函数设计得好AI 就知道冲四要比活三重要设计得差AI 可能连明显的双三都看不见。这个项目的evaluateBoard()分两个维度打分一是横向、纵向、两条对角线四个方向的棋形识别二是对已方棋型和对方棋型分别加权计算。合法落子的判断也直接绑在评估层面——如果某个位置周围没有任何棋子它在绝大多数情况下不会被搜索覆盖// gameboard/GameBoard.cpp 获取优先搜索的走法简化 std::vectorPosition GameBoard::getValidMoves() { std::vectorPosition moves; for (int row 0; row BOARD_SIZE; row) { for (int col 0; col BOARD_SIZE; col) { // 只搜距离已有棋子两格以内的空位否则搜索空间太大 if (isEmpty(row, col) hasNeighborInRange(row, col, 2)) { moves.push_back({row, col}); } } } // 按位置与中心的距离排序越靠近中心越优先能提升剪枝率 std::sort(moves.begin(), moves.end(), [](const Position a, const Position b) { return distanceToCenter(a) distanceToCenter(b); }); return moves; }这是一个工程味很足的设计。五子棋棋盘 225 个点如果全盘搜索每一层的分支数都是 225AlphaBeta 再强也扛不住。限制只搜索已有棋子周边的空位后分支数从 225 降到 20~30搜索树规模瞬间小了一个数量级。排序规则是让靠中心的位置优先搜索因为五子棋的胜负手往往在中心区域附近中心优先的走法更容易触发剪枝条件。评估函数的权重在这个项目里没有做得很复杂——活四给一个很高的分数冲四略低活三和眠三依次递减。想调 AI 风格的话代码里对应权重常量直接改数值就行比如想让 AI 更激进就把活三的权重往上调让它在搜索时更早地尝试连成活三。这个黑匣子打开后其实全是直白的数值比较改起来没有心理负担。3. 工程结构与前后端通信的实现3.1 核心类的职责划分这个项目的代码组织很清晰每个类都对应一个独立模块适合作为课程设计的参考规范。顶层入口是limi_gomoku.cpp负责初始化游戏环境、拉起各个模块GameController是控制中枢协调下棋流程状态流转GameBoard管理 15×15 的棋盘状态和胜负判定AIComponent负责 AI 决策逻辑也就是前面说的搜索算法Server和GameView管前后端交互和界面渲染。几个模块之间的依赖关系是单向的。看主流程代码// limi_gomoku.cpp 主流程简化 int main() { GameController controller; controller.initializeGame(); // 游戏主循环等待用户从界面点击落子 while (controller.isGameRunning()) { Position humanMove controller.getHumanMove(); if (!controller.placeHumanPiece(humanMove)) { // 非法落子提示并重新等待 continue; } Position aiMove controller.getAIMove(); controller.placeAIPiece(aiMove); } return 0; }游戏循环的逻辑是串行交替落子先阻塞等待界面传入玩家点击的坐标然后让 AIComponent 通过搜索算法算出 AI 的落子位置再落子并把画面更新推到前端。GameController在中间做状态机管理维护“当前轮到谁走棋、游戏是否结束、落子合不合法”这些信息。3.2 前端界面与后端逻辑如何连接项目带了前端目录limi_gomoku_front从 package.json 和 websocket.h 这两个文件可以看出来前端跑在浏览器里和后端 C 程序通过 WebSocket 通信。这是个加分设计——C 只负责棋力引擎和游戏逻辑界面渲染全部交给前端框架代码层面把“逻辑”和“展示”彻底解耦了。通信协议的大致行为是前端把玩家点击的棋盘坐标格式化成 JSON 后通过 WebSocket 发给 C 客户端C 解析坐标、调用 GameController 完成落子再把 AI 的落子坐标回传给前端渲染。由于 WebSocket 是全双工通道来回消息不需要轮询交互延迟很低。如果你打算自己重写界面只需要保证发消息的格式不变比如{type:move,x:7,y:9}这样的结构后端不用动。这种前后端分离的设计还有个好处AI 引擎完全独立于界面运行方便单独做 AI 联调测试。我在验证算法时直接把AIComponent接到一个命令行测试程序上模拟两个 AI 对弈不需要打开浏览器就能和搜索逻辑打交道。4. 编译运行从源码到人机对战全流程4.1 环境要求与 CMake 构建步骤项目根目录有CMakeLists.txt和CMakePresets.json说明它设计给 CMake 构建系统使用。建议直接用 VSCode 搭配 CMake 插件打开项目根目录自动生成构建目录后按 F7 编译。命令行构建的方式更直观# 在项目根目录执行 cmake -B build -DCMAKE_BUILD_TYPERelease cmake --build build -j4这里-DCMAKE_BUILD_TYPERelease很关键。我之前图省事直接用 Debug 模式编译AI 搜索函数满是递归Debug 模式跑起来比 Release 慢好几倍AI 单步思考时间直接飙到 10 秒以上还以为代码有性能 bug。后来切到 Release 模式开优化开关-O2后速度才恢复正常。做算法验证时 Release 模式是标配Debug 模式只适合排查数组越界和指针问题。编译产物会出现在build目录下和源码目录是分离的不会污染源码。如果你用的是 CLion 或 Visual Studio直接导入 CMakeLists.txt 也能识别整个工程结构不需要手动维护项目文件。前端部分也需要启动。前端是一个标准的 Node.js 项目cd limi_gomoku_front npm install npm run servenpm install会按照 package-lock.json 安装锁定版本的依赖不建议手动改依赖版本避免出现依赖冲突。启动后浏览器打开本地地址就能看到棋盘界面然后运行 C 后端程序两者建立 WebSocket 连接后就可以开始对局。4.2 首次对局的验证流程编译通过后第一次跑通程序建议按这个顺序检查先确认 C 程序正常启动日志输出“等待前端连接”接着确认前端页面加载成功能看到 15×15 的棋盘最后点击棋盘落子观察 AI 是否在规定时间内回应落子。如果 AI 长时间不回应优先检查前端页面浏览器控制台的 WebSocket 连接状态看是不是端口配错了。常见的端口配置在Server.cpp里我拿到源码时默认端口是 8080和我本地的其他服务冲突改个 9000 就避开了。项目里还带了运行截图limi_gomoku_gif.gif和Snipaste_2022-02-12_19-17-21.png打开能看到预期运行效果是什么样的——棋盘渲染正常、AI 落子位置合理、UI 上没有明显的布局错位。以这些截图作为对照基线当界面表现和截图不一致时就可以判断是不是代码改动破坏了原有功能。5. 避坑指南这套源码中最容易翻车的六个点5.1 落子后棋盘数据错乱现象AI 走完一步后棋盘点位上出现了多颗棋子或者 AI 的落子位置和实际显示不一致。原因最常见的翻车原因是在minimax递归搜索过程中底层的placePiece和undoMove没有成对出现。比如某个分支里落子之后在递归返回前直接 return 了评估值没有执行撤销操作导致棋盘上残留了搜索时临时放的棋子。解决把 AIComponent 的搜索代码重新过一遍确保每层递归里的placePiece后面紧跟着undoMove。可以写一个断言函数在每次搜完一步之后统计棋盘上的棋子总数正常情况应该是初始棋子数加 1如果多了就说明有落子没撤销。我一般在GameBoard的undoMove里加日志把每次撤销的位置打出来对照落子日志一眼就能看出哪对不齐。5.2 改搜索深度后卡死现象把alphabeta的深度从 3 改成 5 后AI 每步思考超过 30 秒体验完全没法接受。原因深度对搜索树规模的影响是指数级的。深度 4 的剪枝后节点数可能只有几千深度 5 就直接变成几万甚至几十万分支因子大约 20每加深一层耗时大致翻 20 倍。解决不要盲目调深度优先检查剪枝是否生效。在关键位置打印被剪枝的分支数确认beta alpha的判断是不是经常触发。如果剪枝触发率很低多半是走法排序没起作用——getValidMoves返回的序列质量不好。另外可以参考网上常见的评估函数权重做调整有些棋形权重配比能显著提升剪枝效率。5.3 编译时 WebSocket 依赖找不到现象报错提示websocket.h头文件路径不存在或者链接时找不到 WebSocket 库。原因这个项目用的是自己实现的 WebSocket 封装不是第三方库它的头文件和源文件在项目目录的include和src下。如果直接编译limi_gomoku.cpp而不是通过 CMake编译器不知道去哪找这些头文件。解决使用项目自带的 CMakeLists.txt 构建不要自己手写 g 命令。include目录已经在 CMake 配置里通过target_include_directories声明好了websocket.h在 include 文件夹里包含路径是#include websocket.h而不是websocket.h。如果一定要手动编译需要加上-I include参数并且把 src 下所有.cpp文件都传给编译器。5.4 前端页面打不开现象npm run serve启动成功但浏览器访问地址显示空白或连接拒绝。原因第一是端口占用默认端口被其他程序占了换成 3000 或 9000 就能解决第二是前端代码染上了 ES6 特性老版本浏览器不兼容页面直接报错渲染不出来。解决先确认 C 后端程序已经启动因为前端页面如果连接不到 WebSocket 服务端可能卡在初始化逻辑上再按 F12 打开浏览器开发者工具看 Console 报错有日志就直接按日志排查最后确认 C 端监听端口和前端连接端口一致不一致的话在Server.cpp里改端口号后重新编译。5.5 递归层数过深导致栈溢出现象搜索深度设置到 8 以上时程序中途崩溃栈溢出的报错出现在 AIComponent.cpp。原因每个搜索帧会压入一部分栈空间深度 8 的递归调用链加上函数内部变量和临时对象栈用量轻松超过默认栈大小。项目默认深度只有 4 不会触发改深度时容易踩这个坑。解决这个工程是基于传统搜索算法展示用的不建议把深度调超过 6。如果非要更深的搜索可以在编译时链接大栈选项比如 Linux 下用ulimit -s unlimited或者编译时加-Wl,--stack,16777216指定 16MB 栈但这只是缓兵之计评估函数的效率才是棋力的根本。5.6 评估函数只守不攻现象AI 只会堵玩家的棋完全不会主动进攻肉眼可见地“怂”。原因评估函数里把对方棋形的分数权重设得过高导致 AI 搜索时不管自己的双三、活四能得多少分只要对方有任何威胁性走法就直接去堵。防守权重压制了进攻权重。解决把评估函数的权重参数调平衡进攻权重和防守权重不应差太多。我的习惯是活三进攻权重设为 500、防守权重设为 450这样 AI 在自己快要赢的时候会果断出手。找到权重的节奏感是个斗感受的过程多跑几局人机对弈观察落子风格逐步微调就行。6. 进阶改造迭代加深与走法排序的联合调优这套源码最值得动手改的地方是把固定深度搜索升级为迭代加深搜索。现在的alphabeta是固定搜索 4 层不管局面多紧张收益都是固定的。改成迭代加深后AI 会从深度 1 开始逐层加深搜索每次都复用上一层的结果排序走法直到超出了单步思考时间预算才返回当前最优解。// AIComponent.h 新增的迭代加深入口示意 Position AIComponent::iterativeDeepeningSearch(int maxDepth, int timeLimitMs) { Position bestMove; auto startTime std::chrono::steady_clock::now(); for (int depth 1; depth maxDepth; depth) { int alpha -INFINITY, beta INFINITY; auto moves gameBoard-getValidMoves(); // 每次加深前给走法排序让剪枝更高效 std::sort(moves.begin(), moves.end(), compareByScore); for (auto move : moves) { gameBoard-placePiece(move, COMPUTER_PIECE); int score alphabeta(depth - 1, alpha, beta); gameBoard-undoMove(move); if (score alpha) { alpha score; bestMove move; } auto elapsed std::chrono::steady_clock::now() - startTime; if (elapsed std::chrono::milliseconds(timeLimitMs)) { return bestMove; } } } return bestMove; }这套改造有两个收益点。第一是可用性——固定 4 层在不紧张的局面浪费算力在关键攻防时又不够用时间预算模型让 AI 平均思考 1 秒、最多不超过 3 秒就把深度走到 5 或 6体验反而更好。第二是让走法排序信息和深度更新链路通了浅层搜完最佳的走法排到最前面深层搜索时 AlphaBeta 就能更快触发剪枝搜索效率比直接冷启动跳到一个深度高很多。走法排序方面我在验证时用了一个很简单的打分法确认它对剪枝效率的实际影响把评估函数对每个候选空位单独算一个局部分数再按分数从高到低排列。这个改进完全不动搜索算法本身只调整搜索顺序同等深度下搜索耗时能再减少约三成。想验证效果做法是分别在排序前后打印alphabeta的剪枝计数和总节点数数值缩小多少一眼就能看到。迭代加深改造好之后我每次拿到新权重的评估函数都习惯跑一个双 AI 对弈的自动化回归测试——让旧权重和新权重各执黑白下满一场统计胜负和每步耗时用来判断权重修改是否真的让棋力上升了。从那以后每次改完代码我都强制走一遍“编译 → 人机对局一局 → 双 AI 对弈自动测试 → 观察剪枝日志”这条流程再也没有出现过改完评估函数 AI 突然不会下棋的情况。希望这份拆解能帮你在毕设或课设中少走几步弯路快速把这份源码跑起来、再改成自己想要的样子。本文还有配套的精品资源点击获取
