C++智能五子棋大作业:从估值函数到α-β剪枝的AI博弈实现
发布时间:2026/10/1 10:49:34来源:尧图网络
简介这是一份基于C实现的智能五子棋程序定位为计算机专业期末大作业或课程设计参考项目。程序支持人机对战与双人对战两种模式内置简易AI决策逻辑并提供简洁直观的控制台交互适合正在备战大作业、需要项目实战的初学者下载学习。压缩包共含18个文件其中8个cpp源文件与7个头文件按功能划分涵盖棋盘、棋子、输赢判定、人机/双人对战模式以及AI落子等模块另附1个可直接运行的exe、1份README说明和1张界面预览图便于快速体验、对照效果与阅读说明整体大小仅118KB。源码经过本地编译和严格调试确保可直接运行该作品曾获导师认可并给出98分高分。当前已有107人学习下载对想借鉴五子棋AI决策、学习C模块化编程、理解人机对战实现或直接提交高分作业的学生都具有参考价值。1. 基于C的智能五子棋先搞清楚这份高分大作业能拿来做什么如果你正在找C的五子棋人机对战与双人对战源码做课程设计这份包含8个头文件和6个cpp的工程是一个可以直接编译运行的起点。它不算复杂但把棋盘、规则、AI决策、双人对战模式都覆盖到了评审分98分说明至少过了导师这关。适合两类人一是要交期末大作业的计算机专业学生想找一个能跑、能答辩、结构完整的项目二是想弄明白估值函数和简单游戏AI怎么落地的初级开发者。它不是什么商业级引擎而是一份“看得完、改得动”的作业源码后续你要加悔棋、加禁手、换AI算法都有清晰的切入点。2. 程序结构拆解从8个头文件和6个cpp看模块怎么分工拿到压缩包第一件事不是急着编译而是先扫一遍文件清单。这份工程的文件命名风格很“大作业”ruler_fuction.cpp、base_fuction.cpp、pve_fuction.cpp 里把 function 拼成了 fuction还有 c-main- ruler_fuction.cpp 这种带连字符的名字。我的经验是越是这种命名越说明作者把精力放在逻辑实现而非工程规范上反而是好事——模块拆得细代码量不会爆炸。2.1 文件清单里那些“fuction”其实暴露了模块划分思路抛开拼写问题这组文件分工非常清晰。我把主要文件列成一张表方便你对照着源码找位置。文件职责定位对应功能base_borad.h / wuzi_borad.h棋盘的数据结构和绘制初始化棋盘、显示棋盘、判断坐标是否越界chess.h棋子类型定义定义空格、黑棋、白棋等状态player.h玩家数据记录当前是黑方还是白方、手动或AIruler_of_wuzi.h五子棋规则胜负判定、落子合法性pvemode.h / pvpmode.h游戏模式入口人机对战、双人对战pve_computer_decision.cppAI核心计算机选点落子base_fuction.cpp / action_fuction.cpp通用动作接收输入、转换坐标、交换回合source.cpp主程序菜单选择、整体流程控制看到这里你应该能明白为什么这类项目“换个人也看得懂”board 只管棋盘rule 只管规则AI 单独一个文件模式之间不交叉。答辩时老师问“这个项目是不是抄的”如果你能说出每个文件的职责以及为什么 AI 决策要单独放一个 cpp会很有说服力。我一般会用箭头把调用链顺一遍source.cpp 接收菜单输入 → 创建 Board → 进入 pve/pvp 模式 → 每次落子先走合法性检查 → 然后调 AI 或人工输入 → 落子后调 checkWin。这样整个程序就是一个标准的状态机后续加悔棋、加复盘都是在状态之间插逻辑。2.2 棋盘与棋子的数据表示二维数组就是最稳的选型这份工程里的棋盘类最可能的实现方式是二维整型数组。常见做法是定义一个 15×15 的全局数组或类成员数组每个格子用 0、1、2 表示空、黑棋、白棋。代码大概长这样// board.h 中棋盘类的核心结构 enum ChessType { EMPTY 0, BLACK 1, WHITE 2 }; class Board { public: static const int SIZE 15; // 标准五子棋棋盘 15x15 int grid[SIZE][SIZE]; // 0 空1 黑2 白 Board() { for (int i 0; i SIZE; i) for (int j 0; j SIZE; j) grid[i][j] EMPTY; // 初始化全空 } bool isValid(int x, int y) { // 判断落子位置是否合法 if (x 0 || x SIZE || y 0 || y SIZE) return false; // 越界直接拒绝 return grid[x][y] EMPTY; // 已有棋子也不能下 } void place(int x, int y, ChessType c) { grid[x][y] c; // 落子后写入数组 } };这里的enum ChessType是关键用整数枚举而不是字符后面做 AI 打分时可以直接把枚举值当数值参与运算。isValid把越界和重复落子两个检查合二为一很多新手会漏掉越界判断后面第五章会讲到这里翻车的后果。place方法是整个游戏的唯一写入入口只要所有模式都通过它落子就不会出现棋盘数据错乱的问题。为什么选二维数组而不是图结构因为五子棋的棋盘点位天然是二维网格数组下标可以直接对应坐标四个方向扫描时用偏移量计算即可不用维护邻接表。数组的缺点是固定尺寸但作业里通常写死 15×15完全够用。如果你以后想支持 19 路围棋只需要把SIZE改成 19再统一检查所有用SIZE的地方。下面这个 Player 类也是工程里少不了的它决定了某一步棋是键盘输入还是 AI 决策// player.h 中的玩家类 class Player { public: ChessType color; // 黑或白 bool isManual; // 是否人工 Player(ChessType c, bool manual) : color(c), isManual(manual) {} };棋盘显示则依赖wuzi_borad.cpp。控制台版本通常用字符画图形版则是把grid值映射到图片资源p1.png上。p1.png在工程里大概率是背景图或棋子素材说明作者可能做了基础图形界面。如果你用控制台运行不要因为 p1.png 缺失而慌张源码逻辑不依赖图片也能跑。控制台绘制的要点是列号对齐我一般用setw(2)控制宽度// wuzi_borad.cpp 中的棋盘绘制控制台版 #include iomanip void drawBoard(const Board board) { cout ; for (int i 0; i Board::SIZE; i) cout setw(2) i; cout \n; for (int i 0; i Board::SIZE; i) { cout setw(2) i; for (int j 0; j Board::SIZE; j) { if (board.grid[i][j] BLACK) cout ●; else if (board.grid[i][j] WHITE) cout ○; else cout ·; } cout \n; } }setw(2)让两位数的行列号右对齐棋盘看起来成直角。这一步对体验影响很大很多同学直接cout i棋盘会歪虽然不影响功能但答辩演示很掉价。2.3 玩家与模式分发pvp 和 pve 共用一套棋盘看文件里有 pvemode.h 和 pvpmode.h说明作者是把两个模式分开写的但底层复用了同一个 Board 和规则函数。主程序 source.cpp 里通常是一个 while 循环 菜单选择类似下面的结构// source.cpp 中主程序的模式分发骨架 int main() { cout 1. 人机对战 2. 双人对战 0. 退出\n; int mode; cin mode; Board board; Player black(BLACK, true); // 黑方手动 Player white(WHITE, true); // 白方手动pvp 时两人都手动 while (mode ! 0) { if (mode 1) { white.setManual(false); // 人机模式下让白方变成 AI } else if (mode 2) { black.setManual(true); white.setManual(true); // 双人模式两边都是人工 } startGame(board, black, white, mode); break; // 示意实际会循环到用户选择退出 } return 0; }这里的 Player 类一般包含isManual标志和color属性。看到setManual(false)就能明白人机对战的本质只是把某一方的输入源从键盘换成 AI 决策函数。这个设计很聪明双人对战和人机对战不需要两套棋盘交互逻辑区别只在白色棋子的来源。接下来是回合循环这部分把所有模块串起来也是你改代码时最容易出 bug 的地方// source.cpp 中一次落子的完整流程 void startGame(Board board, Player black, Player white, int mode) { Player* current black; while (true) { drawBoard(board); // 先把当前局面画出来 int x, y; if (current-isManual) { cout (current-color BLACK ? 黑方落子 : 白方落子) ; cin x y; x--; y--; // 用户习惯从 1 开始数数组从 0 开始要减 1 } else { computerDecide(board, current-color, x, y); // AI 决策 cout AI 落子 x 1 , y 1 \n; } if (!isValidMove(board, x, y)) { cout 不能下这里重新输入\n; continue; // 坐标非法时不允许切换回合 } board.place(x, y, current-color); if (checkWin(board, current-color)) { drawBoard(board); cout (current-color BLACK ? 黑方胜 : 白方胜) \n; break; } if (isDraw(board)) { cout 和棋\n; break; } current (current black) ? white : black; // 交换回合 } }x--; y--;这条注释是重点如果你在别的程序里看到有人忘了减 1就会造成 15×15 棋盘最后一行的非法越界。current (current black) ? white : black;用指针切换回合比交换两个对象更安全。AI 落子时不需要手动判断合法性因为computerDecide内部已经保证只会返回合法位置但双人模式人工输入必须做校验。你会发现isManual这个标志一天用五次都不夸张菜单、回合切换、逻辑分支、显示当前方全靠它。3. 人机对战AIpve_computer_decision.cpp的落子评分逻辑人机对战是这个项目的核心卖点。你下黑棋AI 下白棋它看起来会堵你也会自己找活三冲四。它背后没有深度学习也没有复杂搜索树很大概率是一个“评分函数 遍历空位”的贪心算法。这个算法做课程设计完全够用做出来的棋力大概是“能堵、会连五、但不会做多步杀”。3.1 为什么AI能“堵”住你评估函数的基本盘AI 能下棋的逻辑很简单枚举棋盘上所有空位计算每个位置落子之后对当前局面的“得分”选最高分的落子。所谓得分是在这个位置下白棋后白棋在四个方向上的潜在连五数量。将常见棋型映射到分数表是我自己调 AI 时最常用的参考棋型两端都没堵一端被堵连五100000100000四子50000活四10000冲四三子5000活三1000眠三二子几百一二十这个表告诉 AI 什么局面优先。连五的分数必须设成绝对值最高的 100000因为一旦有连五直接胜利其他任何棋型都比不上。活四给 50000是因为活四两端都没堵对方只能干瞪眼下一手必成五。冲四给 10000比活三高因为冲四有强制威胁。活三给 5000是进攻组织的基本盘。比分可以微调但优先级不要破坏连五 活四 冲四 活三 活二。打开pve_computer_decision.cpp你大概率能看到类似下面这样的打分函数。我把它简化了一下但核心逻辑是一致的// pve_computer_decision.cpp 中简化的棋型打分思路 #include vector using namespace std; // 用方向向量把四个维度统一成“从一个点出发向两边延伸” const int dx[4] { 1, 0, 1, 1 }; const int dy[4] { 0, 1, 1, -1 }; int evaluatePoint(Board board, int x, int y, ChessType me, ChessType enemy) { if (!board.isValid(x, y)) return -1; // 非法位置直接淘汰 int totalScore 0; for (int dir 0; dir 4; dir) { // 四个方向横、竖、主对角、副对角 int count 1; // 当前点算一颗 int block 0; // 两端被堵的情况 // 向正方向延伸 for (int step 1; step 5; step) { int nx x dx[dir] * step; int ny y dy[dir] * step; if (nx 0 || nx Board::SIZE || ny 0 || ny Board::SIZE) { block; break; } if (board.grid[nx][ny] me) count; else if (board.grid[nx][ny] enemy) { block; break; } else break; // 遇到空位就不再延伸 } // 向负方向延伸规则同上 for (int step 1; step 5; step) { int nx x - dx[dir] * step; int ny y - dy[dir] * step; if (nx 0 || nx Board::SIZE || ny 0 || ny Board::SIZE) { block; break; } if (board.grid[nx][ny] me) count; else if (board.grid[nx][ny] enemy) { block; break; } else break; } if (count 5) totalScore 100000; // 直接连五最高优先级 else if (count 4 block 0) totalScore 50000; // 活四几乎必杀 else if (count 4 block 1) totalScore 10000; // 冲四只能堵一头 else if (count 3 block 0) totalScore 5000; // 活三形成威胁 else if (count 3 block 1) totalScore 1000; // 眠三仍有价值 else totalScore count * 10; // 其他散子给基础分 } return totalScore; }这里的dx/dy数组是四方向扫描的利器。每个方向分成正负两头处理遇到对方棋子或棋盘边界就停止延伸同时用block统计封堵次数。边界条件千万不能漏如果你不判断nx 0数组越界读出来的值是垃圾AI 会把边界当活四。我一般会在步进循环开头就检查边界和判断敌人棋子放一起少写一个分支还能避免逻辑遗漏。3.2 攻击与防守权重AI会堵你是因为它在替你想如果 AI 只按上面的函数打分它只会疯狂下自己的棋不会堵你。但实际项目里你下三子出去AI 大概率会来堵。原因是在决策函数里它会同时计算“我下这里的进攻分”和“你如果下这里的进攻分”然后相加或取最大值代码大概是这样的// 决策函数遍历所有空位综合攻防得分 void computerDecide(Board board, ChessType aiColor, int bestX, int bestY) { ChessType humanColor (aiColor BLACK) ? WHITE : BLACK; int maxScore -1; bestX Board::SIZE / 2; // 默认落天元 bestY Board::SIZE / 2; for (int i 0; i Board::SIZE; i) { for (int j 0; j Board::SIZE; j) { if (!board.isValid(i, j)) continue; int attackScore evaluatePoint(board, i, j, aiColor, humanColor); // 进攻分 int defendScore evaluatePoint(board, i, j, humanColor, aiColor); // 防守分 int total attackScore defendScore; // 线性叠加 // 防守分可以加权例如 AI 偏保守时用 1.2 倍防守权重 if (total maxScore) { maxScore total; bestX i; bestY j; } } } }attackScore是假设 AI 落这个点后AI 能形成的棋型defendScore是假设对手落这个点后对手能形成的棋型。把两者相加AI 就会主动占领“双方都危险”的位置——这就是你会看到它堵你的原因。我一般会在defendScore前乘一个 1.1 到 1.3 的系数让 AI 偏向防守这样人机对局时新手会觉得 AI“更聪明”不容易被一波带走。调权重时给你个参照如果 AI 总是不堵你说明权重太低或者根本没加如果 AI 只防守不进攻说明权重超过 2.0 了。我从不会一上来就乘 3而是从 1.1 开始每下一盘看一次效果。权重调优没有银弹只有多对局。我遇到过一种翻车AI 总是下在被动防守但防守位置总是选错原因是evaluatePoint传参时把me和enemy写反了AI 以对手视角评估自己的落子结果总是选择对自己没用、但对对手有利的位置。所以关键时候建议在循环里加一个调试输出打印每个候选点的进攻分和防守分对照真实局面找规律。3.3 贪心落子的性能边界225个空位的扫描量到底多大这个算法本质是“逐点评估”复杂度是 O(空位数 × 方向数 × 延伸步数)。15×15 棋盘最多 225 个空位四方向各延伸 5 步算下来每次决策最多 225 × 4 × 10 9000 次计数操作。在 C 里执行时间不到 1 毫秒所以即使没有优化人机对战也完全无感。但你要注意它的边界如果棋盘改成 19×19空位变成 361计算量翻倍但依然可接受真正的瓶颈是“只看一步”导致棋力上限低不能看出活三变冲四的多步组合。解决方案就是后面第六章讲的搜索树。改代码时你只需要替换computerDecide的内部实现不需要动棋盘和规则接口稳定是这份作业给的底气。如果你想量化性能可以用chrono测一下每次决策耗时#include chrono auto begin chrono::high_resolution_clock::now(); computerDecide(board, WHITE, x, y); auto end chrono::high_resolution_clock::now(); cout AI 决策耗时 chrono::duration_castchrono::microseconds(end - begin).count() us\n;Debug 模式下这个数字可能高达几十毫秒Release 模式通常在 1 毫秒以内。课程设计建议用 Release 提交否则答辩现场卡顿很尴尬。4. 双人对战与规则判定胜负检查如何做到不重不漏双人对战比人机简单但胜负判定是所有人机对战共享的基础。如果判定函数有缺陷AI 连五了都不知道自己赢了整份代码就废了。所以这一章把 ruler_of_wuzi.h 里最核心的检查逻辑讲透。4.1 四个方向向量用最笨的方法检查五连判断胜负的标准是“有没有任意一个位置向横、竖、主对角、副对角四个方向中的任一方向连续走出 5 颗同色棋子”。常规做法是遍历所有格子以当前格子为起点检查它右边、下边、右下、左下四个方向。这里有个容易踩的坑如果只检查右边和下边会漏掉斜向所以必须用方向数组把四组偏移一次性覆盖。// ruler_of_wuzi.h 中的胜负判定核心 bool checkWin(const Board board, ChessType color) { // 四个方向右、下、右下、左下 const int dirs[4][2] { {0,1}, {1,0}, {1,1}, {1,-1} }; for (int x 0; x Board::SIZE; x) { for (int y 0; y Board::SIZE; y) { if (board.grid[x][y] ! color) continue; // 不是当前颜色就跳过 for (int d 0; d 4; d) { int count 1; int nx x dirs[d][0]; int ny y dirs[d][1]; // 沿着方向连续数 while (nx 0 nx Board::SIZE ny 0 ny Board::SIZE board.grid[nx][ny] color) { count; nx dirs[d][0]; ny dirs[d][1]; } if (count 5) return true; // 达到五连直接判胜 } } } return false; }这里只往一个方向延伸从每个点开始数后面连续同色。因为遍历是从左到右、从上到下当前点的前方已经在前面的格子数过了不会漏。dirs数组的顺序无所谓关键是四个向量要覆盖需要检查的四个方向。如果你想更稳妥也可以写成双向延伸但那样每条五连会被正反各数一次逻辑没错但多了一倍循环。我一般只查这 4 个方向省下不必要的判断。循环里的if (board.grid[x][y] ! color) continue;是性能优化如果当前格子不是本方棋子直接跳过整块方向遍历。大作业里棋盘小看不出来但如果以后做成 19 路这个continue能省一半时间。4.2 坐标合法性、重复落子与平局判定规则模块不只有胜负判定。落子前必须检查坐标是否在 0~14 范围内以及该位置是否为空。这个逻辑在第二章的Board::isValid里做过但要注意规则层和棋盘层职责要分开——棋盘类只提供“能不能放”规则类负责“该不该判负”。双人对战里最常见的需求是平局。棋盘填满时所有人下完如果checkWin返回 false就是平局。判断平局可以实时数棋子的总数也可以每次落子后检查是否还有空位。大作业里通常用前者因为简单bool isDraw(const Board board) { for (int i 0; i Board::SIZE; i) for (int j 0; j Board::SIZE; j) if (board.grid[i][j] EMPTY) return false; // 还有空位没下完 return true; // 没有空位平局 }注意isDraw必须在checkWin之后调用因为即便最后一个空位导致连五也要先判胜而不是判平。顺序错了你会遇到“明明五连了还提示平局”的诡异 bug。这里再提一个优化思路如果你要做 19 路棋盘或者想让代码更规整可以在 Board 类里加一个emptyCount成员落子成功时递减。这样isDraw变成return board.emptyCount 0;不用每次扫描 225 个格子。大作业里这个优化意义不大但面试官最吃这一套因为你展示出了“我知道棋盘填满是渐进发生的”这个意识。4.3 规则模块的接口设计ruler_of_wuzi.h 里该怎么组织一份好的规则头文件应该只暴露几个函数给人调用比如isValidMove、checkWin、isDraw。其中isValidMove可以简单转调Board::isValid但也可以在将来扩展禁手规则时留一个口子。比如你要做三三禁手、四四禁手只需要在isValidMove里加一段判断主程序完全不用改。// ruler_of_wuzi.h 对外接口示例 bool isValidMove(const Board board, int x, int y); // 落子合法性 bool checkWin(const Board board, ChessType color); // 当前玩家是否胜利 bool isDraw(const Board board); // 是否平局五子棋有黑棋禁手规则三三、四四、长连有的作业要求实现有的不要求。这份工程从文件名看叫 ruler_of_wuzi.h但大概率只实现了基础判定因为人机对战和双人对战都没有禁手提示。如果你要扩展禁手需要在isValidMove里对黑棋额外调用checkForbidden函数伪代码如下bool isValidMove(const Board board, int x, int y) { if (!board.isValid(x, y)) return false; // 如果是黑棋且当前落子会构成三三/四四/长连则禁止 // 注意禁手只约束黑棋白棋没有禁手 return !isForbidden(board, x, y); }禁手判断的思路是“临时落子 → 检查黑色棋型组合 → 恢复棋盘”。这里的恢复一定要用board.place(x, y, EMPTY)不能直接改数组否则会绕过棋盘类的状态管理。我只提醒一句禁手逻辑里长连判定是“连续六颗及以上”不是“刚好六颗”别写成count 6否则七连和八连会漏判。5. 编译与运行避坑Visual C环境下的5个翻车现场我自己接过不少大作业源码十份里有三份第一轮编译就挂。这份工程整体质量不错但运行环境不对照样白搭。下面 5 个坑是我实际踩过或最常见的按“现象→原因→解决”写你可以直接对号入座。5.1 现象双击 exe 提示缺少 MSVCP140.dll 或 VCRUNTIME140.dll原因这个项目是用 Visual Studio 编译出来的运行时依赖 Microsoft Visual C Redistributable。目标电脑上没有安装对应版本的运行时库就会弹出这个报错。不是源码问题也不是病毒。解决去微软官网下载 Visual C Redistributable 安装包x64 和 x86 都建议装上装完后再运行 exe。如果提示的是更老的 2010/2013 版本那就搜对应年份的 Redistributable 包。装完之后一般就得救。注意别从第三方下载站乱下载容易带全家桶。5.2 现象用 VS 打开源码后中文注释变成乱码编译报“常量中有换行符”原因源文件用 GBK/GB2312 编码保存而新版 Visual Studio 默认按 UTF-8 解析。中文字符被错误解码后字符串里可能出现奇怪的字符导致编译器崩溃。解决用“文件→高级保存选项”把源码重新保存为“UTF-8 with BOM”编码BOM 能让 VS 正确识别。更稳妥的做法是把代码里的中文提示信息改成英文或者统一用 VS 的“保存所有”时选择编码。我一般直接把注释和 UI 文字全部改成英文省得后面发给别人又在别的电脑上报编码错。5.3 现象在 VS 里按 CtrlF5 运行控制台窗口一闪而过原因main 函数正常跑完程序退出后窗口自动关闭。大作业代码里可能用了getchar()或者system(pause)但如果你改了入口逻辑或者编译器优化把末尾读取吞了就会闪退。解决在 main 函数 return 之前加一行system(pause); // 或者 getchar();这样窗口会停在“请按任意键继续”你才能截图给答辩老师看。注意system(pause)在有些环境会被杀毒软件拦截用getchar()更通用。如果是图形界面版本就把窗口关闭逻辑放在玩家点击“退出”之后不要用system(pause)阻塞消息循环。5.4 现象鼠标点击棋盘落子位置总是偏落子落在格线中间原因这份工程如果用图形界面棋盘绘制坐标和鼠标事件坐标的换算没对齐。常见原因是棋盘左上角的偏移量margin没加进换算公式或者格子宽度算错。比如你画格子时从 x20 开始每个格子宽 30那么鼠标 x 坐标应先减 20 再除以 30很多人直接除 30 导致偏移。解决找到一个已知位置的格子比如第一行第一列中心坐标手动往鼠标消息里输出换算前后的值和像素值对比。如果项目是控制台下的字母坐标输入那么问题不在鼠标而是你输入的行列号从 1 开始但数组下标从 0 开始需要在Board::isValid之前把输入值减一。这一条对两份 UI 不同的工程都适用。5.5 现象AI 从来不堵你只会自己乱下原因AI 决策函数里没有计算防守分或者防守分权重设成了 0。另一种可能是evaluatePoint传参时把me和enemy写反了AI 以对手视角评估自己的落子结果总是选择对自己没用、但对对手有利的位置。解决打断点在computerDecide里看下一步选择的位置再打印attackScore和defendScore。正常情况下你下出一个活三时AI 的最佳位置应该离你的活三很近。如果defendScore为 0就回去查看evaluatePoint的形参传入顺序。权重建议先设defendScore * 1.2再慢慢调不要一上来就给 3否则 AI 只顾防守自己的棋形完全打不开。还有一个容易忽略的点如果你修改了棋盘大小但没有同步修改checkWin里的Board::SIZE会导致越界读内存这种情况在 Debug 下不崩Release 下偶发崩溃。建议所有循环都统一用Board::SIZE常量不要单独写死 15。6. 从大作业到可扩展项目把AI改成α-β剪枝搜索当你把上面的代码都跑通了你会觉得 AI 还是有点“一根筋”它能堵住当前的活三但如果你做两步连续冲四它往往顾此失彼。这是因为当前算法是贪心只看一步。想提升棋力最直接的办法是把决策升级为搜索树用极小极大minimax加 α-β 剪枝让 AI 向前看两到四步。贪心在每一步选最大分但五子棋里很多杀棋是两步以上的连续冲四单步分数看不清。极小极大的思路是假设 AI 下一步会挑最优对手下一步也会挑最优那么 AI 应该选择“在对手最优应对下局面分依然最高”的那一步。递归深度到 2 或 4棋力会有质的提升。以下代码放在pve_computer_decision.cpp里替换原来的computerDecide主逻辑。核心是alphaBeta函数返回当前局面对当前走棋方的分值。// 使用 α-β 剪枝的极小极大搜索深度 2~4 int alphaBeta(Board board, int depth, int alpha, int beta, ChessType turn) { if (depth 0) { // 到叶子节点用原有 evaluatePoint 对整个棋盘打分 return evaluateBoard(board, turn); } if (checkWin(board, turn)) return INT_MAX / 2; // 当前方已经连五 if (checkWin(board, (turn BLACK) ? WHITE : BLACK)) return -INT_MAX / 2; int best -INT_MAX; // 只枚举分数最高的前 8 个空位否则 225 个点递归会爆炸 vectorpairint, int candidates getCandidates(board, turn); for (auto pos : candidates) { board.place(pos.first, pos.second, turn); int val -alphaBeta(board, depth - 1, -beta, -alpha, (turn BLACK) ? WHITE : BLACK); board.place(pos.first, pos.second, EMPTY); // 悔棋恢复局面 if (val best) best val; if (best alpha) alpha best; if (alpha beta) break; // 剪枝 } return best; }这里getCandidates不是随便返回所有空位而是只返回有相邻棋子的位置并且按evaluatePoint预排分取前 8 到 10 个。这样深度为 4 时最多搜索 10^4 量级节点C 可以秒算。关键点有两个一是每次递归落子后要立刻把棋盘恢复成 EMPTY否则棋盘状态会被污染二是用INT_MAX / 2而不是INT_MAX避免加减溢出。α-β 剪枝的细节是alpha是当前节点已知的下界beta是上界一旦alpha beta就说明当前分支不可能贡献更优解可以直接剪掉。这里用-alphaBeta来实现上下界翻转是常见的 negamax 写法比单独写两个函数更简洁。如果你之前没接触过建议先画一棵 3 层搜索树手推一遍再回头看代码会豁然开朗。改完后先在双人对战模式下用黑白两方都走 AI 测试观察 AI 是否会在三步前主动做“活三冲四”的组合杀。再把depth调成 2 和调成 4 各下一盘感受棋力变化。如果发现搜索耗时超过 3 秒优先检查getCandidates是否真的限制了候选数量或者候选排序是否合理。我一般会把候选数量设为 8深度设为 4赢下普通贪心 AI 轻松。从那以后我每次拿到这类大作业源码都会强制自己先编译一遍、跑通双人对战再进去改 AI 或者加悔棋。这样既能快速建立代码信心又能在答辩时说出“我改了什么、为什么这么改”。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网