news 2026/10/1 10:49:31

C++智能五子棋大作业:从估值函数到α-β剪枝的AI博弈实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++智能五子棋大作业:从估值函数到α-β剪枝的AI博弈实现

简介:这是一份基于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玩家数据记录当前是黑方还是白方、手动或AI
ruler_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. 人机对战AI:pve_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_cast<chrono::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 里按 Ctrl+F5 运行,控制台窗口一闪而过

原因:main 函数正常跑完,程序退出后窗口自动关闭。大作业代码里可能用了getchar()或者system("pause"),但如果你改了入口逻辑,或者编译器优化把末尾读取吞了,就会闪退。

解决:在 main 函数 return 之前加一行

system("pause"); // 或者 getchar();

这样窗口会停在“请按任意键继续”,你才能截图给答辩老师看。注意system("pause")在有些环境会被杀毒软件拦截,用getchar()更通用。如果是图形界面版本,就把窗口关闭逻辑放在玩家点击“退出”之后,不要用system("pause")阻塞消息循环。

5.4 现象:鼠标点击棋盘落子位置总是偏,落子落在格线中间

原因:这份工程如果用图形界面,棋盘绘制坐标和鼠标事件坐标的换算没对齐。常见原因是棋盘左上角的偏移量(margin)没加进换算公式,或者格子宽度算错。比如你画格子时从 x=20 开始,每个格子宽 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 个点递归会爆炸 vector<pair<int, 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 或者加悔棋。这样既能快速建立代码信心,又能在答辩时说出“我改了什么、为什么这么改”。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 10:46:53

光伏板积灰四分类视觉检测实战

简介&#xff1a;本资源是一个面向计算机专业本科生及深度学习初学者的光伏运维实战项目&#xff0c;聚焦太阳能光伏板表面积灰状态的智能识别问题&#xff0c;适用于毕业设计、课程设计与算法实践训练。项目采用自建四分类灰尘图像数据集&#xff0c;集成普通数据增广、AutoAu…

作者头像 李华
网站建设 2026/10/1 10:46:07

AI浏览器的优势怎样通过任务比较

AI浏览器的优势&#xff0c;不适合用“更智能”概括。对需要整理网页信息的人而言&#xff0c;真正的差异是目标从提出到完成&#xff0c;中间还有多少动作必须亲自承担。比较传统浏览器与智能浏览器&#xff0c;可以选同一项工作&#xff0c;观察信息获取、处理和交付如何衔接…

作者头像 李华
网站建设 2026/10/1 10:45:16

CTF流量分析实战:从Wireshark过滤到TCP流重组与文件提取

CTF圈子里有个默认共识&#xff1a;Misc是新手最容易上手的方向&#xff0c;但同时也是最容易“一看就会、一做就废”的题型。尤其是流量分析&#xff0c;很多刚入门的同学拿着pcapng文件&#xff0c;打开Wireshark之后看着满屏花花绿绿的包&#xff0c;脑子一片空白&#xff0…

作者头像 李华
网站建设 2026/10/1 10:45:15

后端缓存实战:Redis穿透、击穿、雪崩与一致性方案

1. 缓存到底帮后端扛住了什么 做后端这些年&#xff0c;缓存数据应该是我最常打交道的技术之一了。不管你是刚入门准备面试&#xff0c;还是已经维护了几个老项目&#xff0c;只要系统一有性能问题&#xff0c;第一反应基本都是“加缓存”。这个思路本身没错&#xff0c;但缓存…

作者头像 李华
网站建设 2026/10/1 10:44:10

绿幕虚拟直播全攻略:抠像原理、设备布光与OBS实战指南

1. 绿幕虚拟直播到底在火什么1.1 先搞清楚什么是虚拟直播这两年直播行业变化太快&#xff0c;但“绿幕虚拟直播”并不是一个新鲜概念。说白了&#xff0c;就是用一块绿色背景布/墙把人拍下来&#xff0c;再用软件把绿色去掉&#xff0c;把主播“抠”出来&#xff0c;放到任意一…

作者头像 李华
网站建设 2026/10/1 10:43:24

Python+LangGraph构建可运维AI工作流实战指南

1. 这不是写代码&#xff0c;是给AI装上业务大脑的实操手册“Python Agent SDK&#xff1a;把业务场景转化为自动化工作流的完整流程”——这句话乍看像技术文档标题&#xff0c;但在我过去三年带团队落地27个AI Agent项目的过程中&#xff0c;它真正意味着&#xff1a;用Pyth…

作者头像 李华