ARTICLE DETAIL

资讯详情

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

C++五子棋人机对战实战:从AI算法到踩坑记录

C++五子棋人机对战实战:从AI算法到踩坑记录 这段时间用C做了一个五子棋人机对战的小项目从棋盘数据结构、胜负判定到AI决策全流程自己实现了一遍。说实话做之前我以为核心难点也就是个AI算法实际动手才发现C里那些平时被忽略的细节——vector的边界、输入缓冲残留、类继承里覆盖和隐藏的坑全都会在项目里冒出来。如果你学完C基础想找一个能写完、能玩、能反复优化的练手项目这个五子棋人机对战是非常合适的选择难度适中又能把面向对象、STL、算法设计都串起来。这篇文章把整个项目的设计思路、核心实现和踩坑记录都整理出来希望能给正在做类似项目的朋友一些参考。1. 项目整体设计与模块拆解1.1 为什么用C做五子棋而不是Python或C#很多朋友会问五子棋这种小游戏用Python十几行就能跑为什么还要用C我的想法是练手项目的目的不是“用最少代码实现功能”而是让语言特性和数据结构在真实场景里“被用起来”。C在这类项目里有一个天然优势你可以非常清楚地看到数据在内存里怎么组织、函数之间怎么传递状态这和用脚本语言写出来的感觉完全不一样。另外如果对比游戏开发里常见的C和C#会更有意思。C#配合游戏引擎做棋牌类游戏确实开发效率高引擎把渲染、事件循环都封装好了你只需要写逻辑但C做控制台五子棋棋盘绘制、输入解析、循环控制全部亲力亲为这反而逼着你把游戏运行的本质搞明白。等你以后再接触C#或者其他语言会发现很多概念是相通的只是封装程度不同。说回项目本身这个五子棋项目我定位成一个控制台应用依赖只有标准库。不做图形界面有几个好处一是逻辑和界面完全解耦AI算法可以直接在纯逻辑层调试二是代码跨平台Windows和Linux都能编都能跑不用担心GUI库的兼容问题。实际做完之后我最大的体会是控制台版一点不“土”它把注意力集中在最核心的算法和数据结构上等核心逻辑稳定后再套个图形界面就是水到渠成的事。1.2 功能拆解棋盘、规则、AI和交互四层动手写代码之前我先把功能拆成了四个模块分别是棋盘表示、规则判定、AI决策和交互控制。这一步很关键如果一开始就直接在main函数里堆逻辑后面调试AI时会非常痛苦。棋盘表示负责存储当前棋局状态包括棋盘大小、每个位置是空、黑子还是白子以及坐标合法性判断。规则判定包括落子合法性检查和五子连珠判断。AI决策给定当前棋盘状态返回AI认为最优的落子位置。交互控制负责玩家输入坐标、显示棋盘、切换回合、判断游戏是否结束。这四个模块之间是单向依赖的交互控制调用规则判定和AI决策AI决策读取棋盘表示规则判定也只操作棋盘数据。我在实现时用类把每一层封装起来比如Board类管棋盘Player作为抽象基类HumanPlayer和AIPlayer分别继承实现输入和AI决策。这样以后想加一个网络对战或者新的AI策略只需要替换或扩展对应的类不用动其他部分。这种分层设计看起来绕但实际写下来代码更清晰每段代码的职责都很单一。等到写AI算法的时候我只需要关注棋盘状态不用关心玩家怎么输入调试起来非常舒服。1.3 控制台版本的核心优势快速验证AI逻辑有人可能会觉得控制台版没有图形界面不够“像游戏”。但从开发体验来说控制台版本的反馈速度是图形界面没法比的。我在调AI权重的时候经常要反复修改评分参数、重新编译运行、观察几百局对局结果控制台版本整个过程几秒钟就能完成而如果一开始就用图形界面光是处理绘制和事件就会消耗大量时间。而且控制台版还有一个好处可以很容易地打印调试信息。比如AI评估每一步棋的分数我直接把它显示出来就能直观看到AI为什么选择这个位置。这种“看到AI内心想法”的能力在优化算法时太重要了。我觉得做这类逻辑密集型的小项目第一版做成控制台是性价比最高的选择核心逻辑验证通了再做界面完全来得及。2. 核心数据结构与C关键语法实战2.1 棋盘表示二维vector和边界问题的处理五子棋棋盘我选了15×15的规格这是国内常见玩法。棋盘的存储方式我用了std::vectorstd::vectorint而不是C风格二维数组。原因很简单vector自带边界检查用at()时、可以方便地动态创建和复制而且代码可读性更好。const int BOARD_SIZE 15; using Board std::vectorstd::vectorint; Board createBoard() { return Board(BOARD_SIZE, std::vectorint(BOARD_SIZE, 0)); }这里用0表示空位1表示玩家棋子2表示AI棋子。为什么不直接用char或者枚举其实都可以但int在判断和调试时更直观打印出来还不用处理字符转换。我甚至建议用一个简单的enum来定义避免魔法数字enum Stone { EMPTY 0, PLAYER 1, AI 2 };棋盘数组建好后紧接着就要考虑边界问题。五子棋的胜负判断、AI扫描连子都离不开方向遍历而方向遍历最常见的bug就是数组越界。我的做法是每访问一个相邻位置之前都先判断目标位置是不是在0到14之间。刚开始写的时候很容易漏掉某个方向的边界检查一越界程序就崩溃后来我写了一个通用的inBoard函数所有遍历前先调用它问题就彻底解决了bool inBoard(int r, int c) { return r 0 r BOARD_SIZE c 0 c BOARD_SIZE; }这个函数看着简单但它是后续所有算法的基础千万别偷懒。2.2 坐标解析与输入处理字符串转数组的C实现交互这块最容易被新手忽略实际做起来坑不少。玩家输入坐标我设计成“字母数字”格式比如H8代表第8行第8列这样比单纯输入两个数字更友好。字母对应列数字对应行。解析的核心就是把字符转成数组下标// 输入形如 H8返回 {7, 7} bool parseInput(const std::string input, int row, int col) { if (input.size() 2 || input.size() 3) { return false; } char colChar std::toupper(input[0]); if (colChar A || colChar A BOARD_SIZE) { return false; } col colChar - A; row std::stoi(input.substr(1)) - 1; if (row 0 || row BOARD_SIZE) { return false; } return true; }这段代码里有两个细节值得注意。第一std::toupper处理的是单个字符如果你直接调std::toupper传char类型在有些平台上可能出问题建议先强转成unsigned char。第二std::stoi在遇到非数字字符时会直接抛异常所以输入校验一定要放在它前面否则玩家输个乱码程序就崩了。还有输入缓冲残留的问题这个特别容易踩。如果你用cin x读取坐标按回车后缓冲区里会留一个换行符下一次getline读到的是空字符串然后程序就像“卡住了”。处理办法是在每次读取前调用std::cin.ignore()或者统一用getline读整行再解析后者更稳。std::string line; std::getline(std::cin, line); // 读一行避免残留问题 if (!parseInput(line, row, col)) { std::cout 输入不合法请重新输入\n; continue; }2.3 胜负判定四方向扫描的细节与剪枝技巧判断五子连珠是游戏逻辑的核心。最直接的做法是遍历整个棋盘对每个有棋子的位置朝四个方向横向、竖向、两个对角线检查是否有连续五个同色棋子。方向可以用方向数组来表示const int dirs[4][2] { {0, 1}, // 水平 {1, 0}, // 垂直 {1, 1}, // 主对角线 {1, -1} // 副对角线 };判断时有个小优化只需要往一个方向数不需要两个方向都数。比如横向只需要从当前点往右数5个位置检查是否全为同类棋子即可。如果先从最左边的点开始往右数就不会漏掉任何五连。实现如下bool isWin(const Board board, int player) { for (int r 0; r BOARD_SIZE; r) { for (int c 0; c BOARD_SIZE; c) { if (board[r][c] ! player) continue; for (auto d : dirs) { int endR r d[0] * 4; int endC c d[1] * 4; if (!inBoard(endR, endC)) continue; bool win true; for (int k 0; k 5; k) { int nr r d[0] * k; int nc c d[1] * k; if (board[nr][nc] ! player) { win false; break; } } if (win) return true; } } } return false; }注意这个函数在每次落子后调用实际上只需要检查以刚落子位置为中心的相关方向就够了不用全盘扫描。但15×15的棋盘全盘扫描也就225次遍历性能完全无压力所以简化处理即可。这个“先算终点是否越界再判断里面内容”的思路值得记一下它能避免很多越界崩溃。2.4 类的组织继承、虚函数与覆盖隐藏的坑既然用C面向对象肯定要用上。我设计了Player作为抽象基类再派生出HumanPlayer和AIPlayerclass Player { public: explicit Player(int id) : playerId(id) {} virtual ~Player() default; virtual std::pairint, int getMove(const Board board) 0; protected: int playerId; }; class HumanPlayer : public Player { public: explicit HumanPlayer(int id) : Player(id) {} std::pairint, int getMove(const Board board) override; }; class AIPlayer : public Player { public: explicit AIPlayer(int id) : Player(id) {} std::pairint, int getMove(const Board board) override; };基类中getMove是纯虚函数强迫派生类实现。这样做的好处是游戏主循环完全不需要知道当前是人类玩家还是AI统一调用player-getMove(board)即可这就是多态的价值。不过这里有个C的经典坑——覆盖和隐藏的区别。如果你在派生类里写了同名函数但漏掉了override关键字或者参数列表和基类不完全一致那就不是覆盖而是隐藏。比如基类是virtual std::pairint,int getMove(const Board),你在派生类里写成std::pairint,int getMove(Board board)少了个const引用编译不会报错但调用时根本不会进入派生类版本而是直接调用基类的纯虚函数导致无法通过编译或者更隐蔽的是变成重载行为完全不符合预期。经验写派生类覆盖虚函数时务必加上override。编译器会帮你检查是不是真的覆盖成功这是代码质量的一个重要保障。另一个值得注意的点是析构函数。基类的析构函数要么是virtual要么是protected。否则通过基类指针删除派生类对象时行为是未定义的。虽然这个小项目里Player对象基本都是栈上创建的但养成这个习惯对以后写大型项目很重要。3. 五子棋AI算法从贪心评分到博弈搜索3.1 先写一个随机AI再逐步优化刚开始搭好框架时AI部分我直接用随机落子。这样做的目的不是应付而是先让整个游戏流程跑通。有句经验永远不要在一个没跑通的项目里优化细节。随机AI虽然“笨”但能很快验证棋盘、胜负判断、输入处理这些基础功能是否正常。随机AI几行就搞定std::pairint, int getMove(const Board board) override { while (true) { int r rand() % BOARD_SIZE; int c rand() % BOARD_SIZE; if (board[r][c] EMPTY) return {r, c}; } }但玩几局就会发现随机AI毫无还手之力因为对手随便连三个子它都不会挡。于是我开始构思更聪明的方案。3.2 评分函数的思路方向扫描与开放性判断五子棋AI的入门算法是“贪心评分”给每个空位打分选分最高的位置落子。打分的关键在于评估“如果在这里落子能形成多大的威胁”。评估不能只看一个方向要综合横、竖、两个对角线四个方向每个方向单独计算再累加。每个方向的评分逻辑是从落子点向两端延伸统计连续的同色棋子数量同时判断两端是否开放。所谓“开放”就是该方向延伸到头时是空位而不是被对方棋子或棋盘边界堵住。两端都开放和只一端开放的棋型价值完全不同。比如同样是四个连续棋子如果是两端都开放的就是“活四”对手怎么堵都堵不住如果一端被堵就是“冲四”对手必须立刻应战。这里的分数差异非常大我在实现里用了一张权重表来区分棋型描述代表情况建议分值五连已经达成五个连续1000000活四四个连续两端都开放100000冲四四个连续一端开放50000活三三个连续两端都开放10000眠三三个连续一端开放1000活二两个连续两端都开放500眠二两个连续一端开放100活一单个棋子两端都开放50眠一单个棋子一端开放10这里的分值差距为什么拉这么大因为五子棋的威胁是“指数级”的一个活四远比两个活二重要得多。如果分值差距不大AI很可能为了活二放弃了对手的冲四这局基本就输了。实测下来分值量级差100倍左右AI的落子选择才比较“合理”。3.3 攻守平衡进攻分与防守分的加权确定落子位置时不能只看自己能形成多大威胁还要看这个位置对对手来说有多大威胁。所以某个空位的最终评估分是两部分组成进攻分AI棋子落在这里能形成的最大威胁。防守分如果玩家棋子落在这里能形成的最大威胁。最终的评估分是进攻分和防守分的加权和。这里有个关键参数防守权重应该略高于进攻权重。原因是五子棋中一旦让对手形成必杀棋型你再怎么攻都来不及了所以AI在势均力敌时应该优先堵对手。我实测下来的一个合理比例是进攻权重1.1防守权重1.3。注意不要过于偏向防守否则AI会变得畏手畏脚总是在堵对方自己却没什么攻势。这个比例可以做成常量方便后续调整int score attackingScore * 11 / 10 defendingScore * 13 / 10;3.4 核心实现评估函数和选点函数下面是我最终使用的评估逻辑核心函数有两个evaluatePoint负责计算某个空位落子后的单方向威胁分findBestMove负责遍历所有空位找最优解。int evaluatePoint(const Board board, int r, int c, int player) { static const int dirs[4][2] {{0,1},{1,0},{1,1},{1,-1}}; int total 0; for (auto d : dirs) { int count 1; // 当前位置本身算一颗 bool leftOpen false, rightOpen false; int leftBlock 0, rightBlock 0; // 向右正向延伸 for (int step 1; step 5; step) { int nr r d[0] * step; int nc c d[1] * step; if (!inBoard(nr, nc) || board[nr][nc] 3 - player) { rightBlock 1; break; } if (board[nr][nc] EMPTY) { rightOpen true; break; } count; } // 向左反向延伸 for (int step 1; step 5; step) { int nr r - d[0] * step; int nc c - d[1] * step; if (!inBoard(nr, nc) || board[nr][nc] 3 - player) { leftBlock 1; break; } if (board[nr][nc] EMPTY) { leftOpen true; break; } count; } int blockCount leftBlock rightBlock; int openCount (leftOpen ? 1 : 0) (rightOpen ? 1 : 0); if (count 5) { total 1000000; } else if (count 4) { if (openCount 2) total 100000; else if (openCount 1) total 50000; else total 1000; } else if (count 3) { if (openCount 2) total 10000; else if (openCount 1) total 1000; else total 100; } else if (count 2) { if (openCount 2) total 500; else if (openCount 1) total 100; else total 10; } else if (count 1) { if (openCount 2) total 50; else if (openCount 1) total 10; else total 1; } } return total; } std::pairint, int findBestMove(const Board board) { int bestScore -1; std::pairint, int bestMove {-1, -1}; for (int r 0; r BOARD_SIZE; r) { for (int c 0; c BOARD_SIZE; c) { if (board[r][c] ! EMPTY) continue; int attack evaluatePoint(board, r, c, AI); int defend evaluatePoint(board, r, c, PLAYER); int score attack * 11 / 10 defend * 13 / 10; if (score bestScore) { bestScore score; bestMove {r, c}; } } } return bestMove; }这段代码有一些取舍。为了简单清晰扫描方向时最多延伸4步因为再长的连子已经没有额外价值。在判断边界时我用board[nr][nc] 3 - player来判断“对方棋子”因为这里玩家和AI分别用1和2表示3 - 1 23 - 2 1非常巧妙。3.5 进阶方向极大极小搜索与Alpha-Beta剪枝上面的贪心评分AI已经能打赢很多新手玩家了但它的局限也很明显只评估一步看不到更远的威胁。比如对手在某个位置已经形成“活三”下一步就要变“冲四”或“活四”贪心AI可能觉得“我先去连自己的活三”结果漏掉了对手的杀招。要解决这个问题就要用博弈搜索。核心思路是“极大极小搜索”模拟玩家和AI轮流落子若干步假设AI每一步都选对自己最有利的分支玩家每一步都选对AI最不利的分支。搜索的深度越大AI就越“深谋远虑”。但五子棋搜索的分支很多15×15有225个空位搜索两步就是225×224≈5万种局面三步就是上千万完全不可行。这时候就要配合Alpha-Beta剪枝把明显不可能的分支直接裁掉。再结合启发式搜索先评估所有落子点只在分数最高的前10到20个位置里搜索速度和棋力就能达到一个不错的平衡。我在第一版贪心AI稳定之后把搜索深度做到了4层配合评分裁剪单步耗时大概一两秒棋力已经能挡住不少业余玩家。不过这个复杂度超出了本文范围你可以作为下一步的优化方向来尝试。4. 实操记录从环境搭建到首版可玩4.1 开发环境与编译工具的选择开发环境这块我强烈推荐VS Code加MinGW-w64的组合轻量、免费、跨平台。很多初学者在Windows上装了VS Code之后发现一运行就报“找不到编译器”这通常是因为没有安装MinGW或者没有把编译器路径加入系统PATH。我用的配置流程简单整理一下下载MinGW-w64安装到一个没有空格的路径比如D:\mingw64。注意如果你把编译器装到C:\Program Files这种带空格的目录很多构建脚本处理起来会有问题建议避开。把D:\mingw64\bin加入系统PATH环境变量。在VS Code里安装C/C扩展然后通过CtrlShiftP运行“C/C: Edit Configurations (UI)”在编译器路径里指定g.exe的完整路径。编译直接用终端命令比如g -stdc17 main.cpp Player.cpp AIPlayer.cpp -o gomoku -Wall。启动编译选项里的-Wall一定要加上它会输出所有警告。我在这个项目里很多潜在问题比如未使用变量、类型转换不匹配都是靠-Wall提前发现的。把警告当成错误处理能少踩很多坑。如果你在Windows上遇到提示“已检测到匹配的 Visual C Redistributable跳过安装”这是在安装某些C运行库时的正常提示说明你系统里已经有对应运行库了不是错误。另外如果你在安装Python包时遇到“Microsoft Visual C 14.0 or greater is required”的报错那是因为那个包需要用MSVC编译。我们做C五子棋用的是MinGW不依赖MSVC两者别搞混了。4.2 主循环和代码结构整个程序的主循环并不复杂核心是“循环-判断-落子”三步走。我用伪代码来描述初始化棋盘 创建玩家对象和AI对象 当前回合设为玩家 while (游戏未结束) { 显示棋盘 if (当前回合是玩家) { 读取玩家输入解析坐标 校验工厂初始状态合法 在棋盘上落子 } else { 调用AI的getMove获取落子位置 在棋盘上落子 打印AI落子信息 } 判断是否有五连 如果有宣布胜者并退出 判断棋盘是否已满平局退出 切换当前回合 }代码结构上我分成了几个文件main.cpp放主循环Board.h放棋盘类Player.h放玩家抽象类HumanPlayer.cpp和AIPlayer.cpp分别放实现。分文件的组织方式在项目规模小的时候看起来是“杀鸡用牛刀”但它让你习惯“头文件声明源文件实现”的C工程模式这个习惯越早养成越好。4.3 调参过程AI是怎么变聪明的第一版AI用的是纯进攻分结果很糟糕。现象是AI只顾着连自己的子完全不管玩家已经快连成四个了。我加了防守分之后AI明显会堵人了但又有新问题它特别胆小基本不进攻全程在堵我。后来我把进攻权重和防守权重从1:1逐步调整最终定格在1.1比1.3AI才开始“该攻的攻该守的守”。调这组数据时我感觉就像在给一个学生“上课”。它表现不好你就得想明白是哪个环节出了问题。是权重的比例不对还是评估函数对某些棋型识别不准确这时候我强烈建议把每个落子点的分数打印出来看。我实测的调参技巧在findBestMove里加一个开关可以输出每个空位的进攻分和防守分。这样AI每走一步你都能清楚它是基于什么理由选这个位置。比如它选了一个分数不是“最合理”的位置你可以发现是某个方向扫描逻辑出错了而不是分数设计问题。4.4 画棋盘和优化交互体验棋盘显示这块我给每个格子加上了行列标记横向用A到O纵向用1到15这样玩家输入坐标时很方便。每次落子后程序会把整个棋盘重新打印一遍并把最新落子的位置用特殊标记标出来比如加个括号或者不同符号。void displayBoard(const Board board) { std::cout ; for (int c 0; c BOARD_SIZE; c) { std::cout static_castchar(A c) ; } std::cout \n; for (int r 0; r BOARD_SIZE; r) { std::cout (r 1) ; for (int c 0; c BOARD_SIZE; c) { if (board[r][c] PLAYER) std::cout ● ; else if (board[r][c] AI) std::cout ○ ; else std::cout ; } std::cout \n; } }这里有一个中文符号和终端宽度的问题。在Windows命令行里●和○是全角字符如果后面再跟一个空格列间距会不一致导致棋盘看起来歪歪扭扭。我后来调整成每个棋子后面只跟一个空格整体才对齐。如果你在Linux终端跑可能还需要根据字体微调。这种显示问题不会影响逻辑但要耐心调整否则玩家看到歪歪扭扭的棋盘会很烦躁。另外建议在代码里加一个system(cls)来清屏不过这个函数在Windows和Linux里行为不太一样Linux下应该用system(clear)。如果要跨平台可以封一层简单的函数。5. 常见问题与避坑指南5.1 编译和运行时的典型报错做项目时遇到报错是最正常的我整理了几个高频问题你可以直接对照排查。问题现象根本原因解决方法VS Code提示“找不到编译器”MinGW没有安装或没有配置PATH把MinGW的bin目录加入PATH重启VS Code编译报错“cannot open output file”上一次运行的程序还在占用exe关闭正在运行的黑窗口重新编译中文输出乱码源文件是UTF-8控制台默认GBK代码开头加system(chcp 65001)或统一用英文输出cin 之后getline读不到内容输入缓冲区残留换行符统一用getline读整行再解析数组越界程序崩溃方向扫描没做边界检查所有访问前先调inBoard函数程序运行一闪而过主函数结束后窗口自动关闭用std::cin.get()等待回车再退出virtual函数没有被调用派生类漏写override或签名不匹配在派生类方法后加override让编译器检查这里面最隐蔽的是缓冲区残留问题。比如你先用cin n读了一个数字按回车后换行符还在缓冲区里紧接着的getline把这个残留的换行符读走了然后你的程序就开始抽风。我建议从项目一开始就统一用getline读输入再手动解析能省掉很多莫名其妙的bug。5.2 逻辑类疑难杂症AI不防守和重复落子AI不防守的问题通常不是你“忘了写防守分”而是你写了防守分但分值太低了。比如进攻分里面的“活三”给了10000但防守分里对“对面有活三”识别成了1000那AI就会觉得连自己的活二更重要。解决办法就是把防守权重调上去或者检查评估函数里是不是对“对方棋子”扫描时把棋子类型搞反了。还有一个常见bug是AI把棋子落在已经有子的位置上。原因多半是findBestMove的循环里没有跳过非空位置或者evaluatePoint的某个分支没有考虑当前点本身。我在开发第二版时遇到过AI总是下在同一个位置后来一检查是评估分数时把已占用位置也算了进去。修复方法很简单评分循环第一行就判断if (board[r][c] ! EMPTY) continue;别漏。5.3 验证AI棋力的实用方法怎么知道AI到底变强了没不能靠眼睛看一盘棋判断。我写了一个“AI对AI”的自动对局模式让两个AI互相下每次下完统计胜率。比如A版本AI和B版本AI对战我连跑20局然后对比胜率。20局在控制台版本里大概几分钟就跑完非常快。这种自动对局模式还有一个隐藏好处可以批量测试代码改动是否引入了逻辑bug。每次改动算法后如果AI对AI突然出现大量平局或异常落子说明代码出问题了不用等玩家手动玩几局才发现。5.4 调试AI时的小工具心得我还给自己写了一个非常简陋的“调试后门”在AI选点前把前几个候选位置的分数打印出来。这样做的好处是AI如果有明显的“蠢棋”我能立刻定位是哪个位置分数算错了。有一次我发现AI特别喜欢下在靠近边角的位置排查后才发现横向扫描时数组越界让分数变成了一个很大的垃圾值。这种bug如果不打分数靠肉眼观察棋局真的很难发现。6. 写在最后的经验和扩展方向这个五子棋人机对战项目做完后我的一个很直接的感受是C的语法书看十遍不如自己动手写一个能跑的小项目。写的过程中vector怎么用、虚函数怎么覆盖、输入缓冲怎么回事、编译警告怎么处理全都有了真实的场景。以后再遇到类似问题脑子里会立刻浮现出当时调试的画面而不是去翻书查语法。如果你想让这个项目继续延伸我有几个具体的建议。第一把控制台界面换成图形界面比如用Qt或者SFML核心AI代码完全不用改只要把输入输出层替换掉就行。第二给AI增加难度等级比如在随机AI和贪心AI之间做个“等级切换”让你的游戏既能给新手玩也能给高手玩。第三增加悔棋功能用std::stack保存历史落子记录回退时把棋盘状态恢复即可这个扩展既实用又不难。第四如果你对算法感兴趣可以继续做极大极小搜索和Alpha-Beta剪枝让AI真正有“算计”能力。最后再分享一个小技巧写这类项目时一定要把“调试输出”当作一个正式功能来设计。我后来做其他项目也习惯性地给关键函数加调试开关这个习惯帮我节省了大量时间。希望这篇分享对你有所帮助如果你也在写五子棋人机对战欢迎交流你的AI策略和踩坑经历。
返回列表