news 2026/10/4 1:19:54

C++五子棋人机对战:控制台项目完整实现与AI评分法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++五子棋人机对战:控制台项目完整实现与AI评分法

“C++五子棋人机对战”是我一直推荐给C++初学者的第一个完整小项目。原因很直接:它不需要图形库、不需要网络、不需要第三方依赖,一个控制台窗口加一个编译器就能跑,但做下来你会发现,二维数组、函数拆分、循环控制、输入输出处理、甚至最基础的算法思想全都被你亲手用了一遍。我会从环境搭建讲到AI评分法,再给出一份完整的控制台版代码,最后把我自己踩过的几个坑一并列出来。适合已经学完C++基础语法、打算做第一个完整项目的人,也适合想搞明白“五子棋AI算法到底是怎么让电脑落子”的人。

1. 五子棋项目到底练了什么:先把整盘棋拆成五个模块

1.1 控制台里的棋局状态:棋盘、坐标和轮流顺序

任何一个回合制游戏,本质上都在反复处理三件事:状态、坐标、轮流。五子棋也不例外。状态就是当前棋盘上哪些位置有子、哪些位置为空,再加上“对局还在进行中”还是“已经分出胜负”。坐标则是玩家输入的第几行第几列和程序内部数组下标的对应关系。轮流则是一个循环:人类走一步、判断是否结束、AI走一步、判断是否结束、再回到人类。

这三件事看起来简单,实际写代码时几乎每个新手都会在其中一件事上出bug。尤其是坐标,属于那种“以为对了但总差一行”的经典问题。玩家习惯说“第8行第8列”,但C++数组下标从0开始,board[8][8]其实是第9行第9列。我第一次写的时候,在输入和AI落子两处都忘了做“减1”和“加1”的转换,结果玩家明明点的是正中央,AI却以为他点的是边角,棋局从第一步就歪了。解决方法很简单:在读取输入后统一减1,在输出落子时统一加1,并且全程只用一套内部坐标。

轮流用while(true)就能写:每轮先让玩家输入,输入合法后落子并判断胜负;如果没结束,再让AI落子并判断。退出条件除了胜负,还要有平局,也就是棋盘全满且没人赢。这个平局判断很容易漏,但一旦漏了,玩家和AI下满整张棋盘后程序会不知道怎么结束,陷入一种“都赢了又都没赢”的尴尬状态。把这三件事理清楚,后面加悔棋、加AI、加图形界面都不会慌,因为所有改动都是在原有状态上做增量。

1.2 项目模块拆解:先做能玩的双人对战,再谈AI

把整个项目拆开来,大概是五个模块:

  • 棋盘模块:负责棋盘的初始化、打印显示;
  • 输入模块:读取玩家的坐标,做合法性校验;
  • 规则模块:落子后判断是否五子连珠、是否平局;
  • AI模块:计算电脑该下在哪里;
  • 主控模块:在主循环里把前四个模块串起来。

这个拆分不是一开始就有的,而是我写了第二版才自然形成的。第一版我把所有逻辑全塞在main()里,两百多行下来自己想改点东西都要翻半天。后面拆成函数之后才意识到:写游戏本质上是“拆函数”而不是“堆代码”。建议你拿到这个项目后也先按这个模块划分写双人对战版本,让两个玩家轮流在控制台输入坐标。等双人版稳定了,再把“AI模块”从“玩家输入”那里替换掉。这个顺序能帮你把“规则”和“策略”分开,后面调试AI时会轻松很多。

2. 开发环境与编译器选型:VSCode+MinGW和“Visual C++ 14.0”报错

2.1 10分钟跑通VSCode下的C++编译

如果你以前只用在线编译器写几行语法练习,这次建议在本地搭一个环境,因为五子棋要跑交互式循环,在线编译器体验很差。另一个原因是,交互式程序需要多次输入输出,在线环境往往要频繁点击运行,一次输入错了又要重来,心态很容易崩。本地环境虽然第一次配置时有点门槛,但一劳永逸。

最轻量的组合是VSCode加MinGW-w64。步骤不复杂:

  1. 安装VSCode,然后在扩展市场搜索并安装C/C++扩展,作者是Microsoft的那个。
  2. 安装MinGW-w64,核心是把g++编译器装到机器上。Windows下推荐通过MSYS2来装,装完把.../mingw64/bin目录加到系统PATH。
  3. 打开终端执行g++ --version,能看到版本号说明编译器已经可用。
  4. 在VSCode里新建一个test.cpp,写个Hello World,按Ctrl+Shift+B配置编译任务。

编译任务其实就是让VSCode帮你执行一条g++命令。我在tasks.json里用的配置是这样的:

{ "version": "2.0.0", "tasks": [ { "label": "C++ 编译", "type": "cppbuild", "command": "g++", "args": [ "-fdiagnostics-color=always", "-g", "${file}", "-o", "${fileDirname}/${fileBasenameNoExtension}.exe" ], "group": "build", "problemMatcher": ["$gcc"] } ] }

这样按Ctrl+Shift+B编译,再在终端里运行生成的exe就能开始交互。调试的话再配一个launch.json,用gdb即可。

2.2 “error: microsoft visual c++ 14.0 or greater is required”是怎么来的

很多人在配环境的过程中遇到过这样一条完整报错:

error: microsoft visual c++ 14.0 or greater is required. get it with "microsoft c++ build tools"

我第一次看到时也很懵:我明明装了MinGW,为什么还找不到C++编译器?后来才明白,这条报错找的不是g++,而是微软的MSVC编译器,也就是Visual Studio Build Tools里那套工具链。常见触发场景是:用pip安装某些需要编译C++扩展的Python包、npm安装需要原生模块的依赖、或者某些安装脚本在检测cl.exe。

解决方式很简单:去微软官网下载“Microsoft C++ Build Tools”,在安装界面勾选“使用C++的桌面开发”工作负载,把MSVC编译器装好。它与MinGW不会冲突,两者可以共存。想确认有没有装成功,打开“开发者命令提示符”或普通终端执行cl,能看到编译器信息就说明环境OK。

2.3 选MinGW还是MSVC:这决定你以后能不能用EasyX

对纯控制台五子棋来说,MinGW的g++完全够用,代码不用改,编译命令简单。但如果你后面想把棋子画在窗口里、用鼠标点击落子,大概率会考虑EasyX这个图形库。EasyX有一个硬性要求:只支持MSVC,不支持MinGW。也就是说,你在VSCode+MinGW的环境里写好的逻辑代码,要用EasyX做图形壳,必须换到Visual Studio里编译。

所以建议按自己的方向选:

  • 只做控制台版、想轻量快速跑起来,选VSCode+MinGW;
  • 已经预料到要加图形界面、想做窗口化小游戏,直接装Visual Studio Community版,选MSVC,最省事。

环境这块不需要纠结太久。本项目第一个能跑的版本用哪种组合都行,核心逻辑代码是跨编译器一致的。

3. 棋盘、落子和胜负判定:五子棋最底层的三个逻辑

3.1 15×15棋盘用一个数组搞定

棋盘存储我用了一个全局二维数组,这是整个项目最朴素也最关键的数据结构:

const int BOARD_SIZE = 15; const int EMPTY = 0; const int HUMAN = 1; const int AI = 2; int board[BOARD_SIZE][BOARD_SIZE] = {0};

每个格子只有三种状态:0表示空、1表示玩家、2表示AI。为什么不用char数组或者bool加一个“谁下的”标志?int数组在控制台项目里最直白,打印和判断都很方便,性能完全不是瓶颈,而且以后想扩展成19路棋盘,只需要改一个常量。

打印棋盘的函数里要注意对齐。列号、行号都要用固定宽度输出,否则棋盘会歪。我习惯用iomanip里的setw函数:

#include <iomanip> void printBoard() { cout << " "; for (int j = 0; j < BOARD_SIZE; j++) cout << setw(2) << j + 1 << " "; cout << endl; for (int i = 0; i < BOARD_SIZE; i++) { cout << setw(2) << i + 1 << " "; for (int j = 0; j < BOARD_SIZE; j++) { if (board[i][j] == EMPTY) cout << " . "; else if (board[i][j] == HUMAN) cout << " X "; else cout << " O "; } cout << endl; } }

这里我用X和O来表示玩家和电脑,而不是围棋的实心空心圆。原因会在踩坑章节展开:Windows控制台对特殊符号的处理很容易变成乱码,对初学阶段不友好。先用X/O跑通逻辑,想美化再换符号不迟。

3.2 玩家输入必须过三道校验

玩家的输入看起来简单,但一个稳的程序至少要校验三件事:坐标能不能转成数组下标、是否越界、这个位置是否已经有棋子。我的写法是把校验放在一个循环里,不合法就continue让玩家重新输入:

int row, col; cout << "你的落子:"; if (!(cin >> row >> col)) { cin.clear(); cin.ignore(1024, '\n'); cout << "输入无效,请输入两个数字" << endl; continue; } row--; col--; if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { cout << "坐标越界,请重新输入" << endl; continue; } if (board[row][col] != EMPTY) { cout << "这里已经有棋子了" << endl; continue; }

其中cin.clear和cin.ignore是处理“玩家输入了字母”这种情况的,后面避坑章节会细讲。这一层校验的主要意义是:你在学语法时习惯了“输入永远合法”,真正写交互程序时,用户永远会输入你没想到的内容。越早建立这个意识,后面写任何程序都受益。

3.3 胜负判定只围绕最后一手棋展开

五子棋判断胜负最不容易出错的写法,不是每次下完扫描整张棋盘,而是从刚刚落下的那一颗棋子出发,沿着横、竖、两条斜线四个方向,分别往两边数一数连续同色棋子有多少颗。只要其中某个方向连续同色数大于等于5,就赢了。

方向可以用一个二维数组表示:

int dirs[4][2] = {{0, 1}, {1, 0}, {1, 1}, {1, -1}};

分别代表横向、竖向、主对角线、副对角线。checkWin的核心逻辑是这样的:

int checkWin(int row, int col, int player) { int dirs[4][2] = {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int d = 0; d < 4; d++) { int count = 1; for (int s = 1; ; s++) { int r = row + dirs[d][0] * s; int c = col + dirs[d][1] * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || board[r][c] != player) break; count++; } for (int s = 1; ; s++) { int r = row - dirs[d][0] * s; int c = col - dirs[d][1] * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || board[r][c] != player) break; count++; } if (count >= 5) return 1; } return 0; }

这个写法有个好处:每次都从最后一手棋开始,不需要遍历整块棋盘。五子棋的胜负只可能因为新落下的棋子而改变,旧局面没赢的话,新棋子附近没形成五连就不会出现别处的五连。所以“局部检查”在逻辑上是完备的,效率也高很多。

4. 人机AI核心:评分法原理与完整实现

4.1 为什么初版AI不用博弈树搜索

网上讲五子棋AI动不动就提极大极小搜索、α-β剪枝、蒙特卡洛树搜索。这些听起来很酷,但对第一次写人机对战的初学者来说,博弈树搜索第一关就过不去:五子棋棋盘有225个落点,假设只搜索两步,根节点有约225个分支,每个分支又有约224个子分支,节点数就是五万多;搜索深度加到5层,节点数会膨胀到亿级以上,不做剪枝根本跑不完。就算做剪枝,评估函数不够好的话,搜得深也不一定下得准。

所以我建议第一个版本用评分法。评分法的核心是贪心:逐个检查每一个空位,假装把棋子放下去,看看这个位置对“进攻”和“防守”分别能贡献多少分,然后把两个分数合并,选总分最高的位置落子。它不向后看几步,但已经能让电脑拥有“堵你活三”“自己凑活四”这些基本棋感。对一个小型控制台项目来说,这已经是很合格的对手了。

4.2 棋形评分表:给活三、冲四定个“价格”

评分法最关键的一步是先定规则:什么样的棋形值多少分。我采用的方法是,对某个空位在某一个方向上,假设放上己方棋子后,沿着这个方向能连成多少个连续同色棋子,再观察两端是否开放。所谓“开放”,是指紧挨着连续棋子的下一格为空,还有发展空间;如果遇到对方棋子或者棋盘边界,就算封闭。

我把分值和棋形的关系整理成了一张表:

落子后形成的棋形两端情况分值实际含义
五连及以上任意100000直接赢,最高优先级
活四两端都开50000无论对手怎么堵都能成五
冲四一端开10000对手必须立刻堵,否则下一步赢
活三两端都开5000下一步能发展成活四或冲四
眠三一端开1000威胁比活三小,但还是要注意
活二两端都开500潜在发展成三的能力
眠二一端开100很弱的棋形
活一两端都开50几乎没什么威胁
眠一一端开10单子,基本可以忽略

这张表不是唯一的,网上能搜到很多大同小异的版本,甚至有人把“双活三”单独定价更高。做项目时不需要追求绝对最优,只要能拉开“四、三、二、一”之间的量级差距,AI的行为模式就会正确。量级拉开的意义在于:一个冲四的分值,要高于很多个活二加起来的分值,这样AI在“自己要做四”和“发育一堆没用的二”之间,会坚定选择前者。

4.3 进攻与防守分数的合并策略

有了评分表,就能写一个评估函数,计算“如果某个玩家在某个空位落子,这个位置值多少分”。我在实现里对四个方向分别统计连子数和开放端数量,再查上面那张表加分:

int evaluatePosition(int row, int col, int player) { int score = 0; int dirs[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int d = 0; d < 4; d++) { int dx = dirs[d][0], dy = dirs[d][1]; int count = 1; int openEnds = 0; for (int s = 1; ; s++) { int r = row + dx * s, c = col + dy * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (board[r][c] == player) count++; else { if (board[r][c] == EMPTY) openEnds++; break; } } for (int s = 1; ; s++) { int r = row - dx * s, c = col - dy * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (board[r][c] == player) count++; else { if (board[r][c] == EMPTY) openEnds++; break; } } if (count >= 5) score += 100000; else if (count == 4) score += (openEnds == 2 ? 50000 : (openEnds == 1 ? 10000 : 0)); else if (count == 3) score += (openEnds == 2 ? 5000 : (openEnds == 1 ? 1000 : 0)); else if (count == 2) score += (openEnds == 2 ? 500 : (openEnds == 1 ? 100 : 0)); else if (count == 1) score += (openEnds == 2 ? 50 : (openEnds == 1 ? 10 : 0)); } return score; }

这里有个细节值得解释:我在两个方向各数一次连续棋子,如果遇到空格就增加一次openEnds。所以一个两端完全开放的活三,openEnds会是2;只有一端开放的眠三,openEnds是1;两端都被堵死则openEnds为0,棋形价值归零。

AI选点的时候,对每个空位要算两种分数:把AI自己的棋子放下去,是进攻分;把玩家的棋子放下去,是防守分。合并规则很简单:

int total = attack + defense;

这样AI既会主动发展自己的棋,也会在玩家快赢时去堵。想让AI更偏防守,就把防守分乘以一个大于1的系数;想让它更激进,反过来处理。

4.4 调参实测:让电脑忽强忽弱

这个版本跑起来后,我对局测试的第一感觉是:AI知道堵我的活三,也知道自己有三连时会去冲四。原因是评分表里活三和冲四的分值都很高,它计算防守分时会捕捉到“这个地方放玩家棋子能形成活三”的高分,从而赶来堵。

不过它也有明显的弱点:面对跳子形成的形状(比如“X_XX”这种中间隔了一格的棋形),它往往看不太懂。因为我的evaluatePosition只统计了简单连续的同色棋子,没有对“跳过一格”做额外处理。这是简化评分法的正常局限。想改善,可以在评分逻辑里增加对间隔棋形的识别,但代码会复杂不少。

调难度也很简单。想给新手一个温柔点的AI,可以这么做:计算出每个空位的总分之后,不直接选最高分,而是在分数排名前10个位置里随机选一个。这样AI会有“失误”,下棋节奏也更接近真人。我实测下来,一个带随机性的AI对刚接触五子棋规则的朋友来说,比一个计算精确的AI有趣得多。想让AI变强就把随机范围缩小,甚至完全关闭随机。

5. 控制台版完整代码:复制就能跑,跑完就能改

5.1 完整代码

下面这份代码就是把前面几个模块组合起来的完整控制台版。为了不让你一上来就被编码问题困扰,棋子符号我用X表示玩家、O表示电脑。整个程序只有一个源文件,直接复制到你的test.cpp里编译运行即可。

#include <iostream> #include <iomanip> #include <utility> using namespace std; const int BOARD_SIZE = 15; const int EMPTY = 0; const int HUMAN = 1; const int AI = 2; int board[BOARD_SIZE][BOARD_SIZE] = {0}; void printBoard() { cout << " "; for (int j = 0; j < BOARD_SIZE; j++) cout << setw(2) << j + 1 << " "; cout << endl; for (int i = 0; i < BOARD_SIZE; i++) { cout << setw(2) << i + 1 << " "; for (int j = 0; j < BOARD_SIZE; j++) { if (board[i][j] == EMPTY) cout << " . "; else if (board[i][j] == HUMAN) cout << " X "; else cout << " O "; } cout << endl; } } int checkWin(int row, int col, int player) { int dirs[4][2] = {{0, 1}, {1, 0}, {1, 1}, {1, -1}}; for (int d = 0; d < 4; d++) { int count = 1; for (int s = 1; ; s++) { int r = row + dirs[d][0] * s; int c = col + dirs[d][1] * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || board[r][c] != player) break; count++; } for (int s = 1; ; s++) { int r = row - dirs[d][0] * s; int c = col - dirs[d][1] * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE || board[r][c] != player) break; count++; } if (count >= 5) return 1; } return 0; } bool isFull() { for (int i = 0; i < BOARD_SIZE; i++) for (int j = 0; j < BOARD_SIZE; j++) if (board[i][j] == EMPTY) return false; return true; } int evaluatePosition(int row, int col, int player) { int score = 0; int dirs[4][2] = {{1, 0}, {0, 1}, {1, 1}, {1, -1}}; for (int d = 0; d < 4; d++) { int dx = dirs[d][0], dy = dirs[d][1]; int count = 1; int openEnds = 0; for (int s = 1; ; s++) { int r = row + dx * s, c = col + dy * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (board[r][c] == player) count++; else { if (board[r][c] == EMPTY) openEnds++; break; } } for (int s = 1; ; s++) { int r = row - dx * s, c = col - dy * s; if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break; if (board[r][c] == player) count++; else { if (board[r][c] == EMPTY) openEnds++; break; } } if (count >= 5) score += 100000; else if (count == 4) score += (openEnds == 2 ? 50000 : (openEnds == 1 ? 10000 : 0)); else if (count == 3) score += (openEnds == 2 ? 5000 : (openEnds == 1 ? 1000 : 0)); else if (count == 2) score += (openEnds == 2 ? 500 : (openEnds == 1 ? 100 : 0)); else if (count == 1) score += (openEnds == 2 ? 50 : (openEnds == 1 ? 10 : 0)); } return score; } pair<int, int> aiMove() { int emptyCount = 0; for (int i = 0; i < BOARD_SIZE; i++) for (int j = 0; j < BOARD_SIZE; j++) if (board[i][j] == EMPTY) emptyCount++; if (emptyCount == BOARD_SIZE * BOARD_SIZE) { return {BOARD_SIZE / 2, BOARD_SIZE / 2}; } int bestScore = -1; int bestRow = BOARD_SIZE / 2, bestCol = BOARD_SIZE / 2; for (int i = 0; i < BOARD_SIZE; i++) { for (int j = 0; j < BOARD_SIZE; j++) { if (board[i][j] != EMPTY) continue; int attack = evaluatePosition(i, j, AI); int defense = evaluatePosition(i, j, HUMAN); int total = attack + defense; if (total > bestScore) { bestScore = total; bestRow = i; bestCol = j; } } } return {bestRow, bestCol}; } int main() { cout << "C++五子棋人机对战" << endl; cout << "玩家执黑(X),电脑执白(O)" << endl; cout << "输入格式:行 列,范围 1~15,例如:8 8" << endl; while (true) { printBoard(); int row, col; cout << "你的落子:"; if (!(cin >> row >> col)) { cin.clear(); cin.ignore(1024, '\n'); cout << "输入无效,请输入两个数字" << endl; continue; } row--; col--; if (row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE) { cout << "坐标越界,请重新输入" << endl; continue; } if (board[row][col] != EMPTY) { cout << "这里已经有棋子了" << endl; continue; } board[row][col] = HUMAN; if (checkWin(row, col, HUMAN)) { printBoard(); cout << "玩家获胜!" << endl; break; } if (isFull()) { cout << "平局!" << endl; break; } pair<int, int> ai = aiMove(); board[ai.first][ai.second] = AI; cout << "电脑落子:" << ai.first + 1 << " " << ai.second + 1 << endl; if (checkWin(ai.first, ai.second, AI)) { printBoard(); cout << "电脑获胜!" << endl; break; } if (isFull()) { cout << "平局!" << endl; break; } } cout << "对局结束" << endl; return 0; }

5.2 主循环的执行顺序

代码的主循环值得一行行看一遍,因为它是整个游戏的“心脏”。运行顺序是:

  1. 打印当前棋盘;
  2. 等待玩家输入行和列;
  3. 校验输入,不合法就让玩家重新输入;
  4. 玩家落子,检查是否胜利、是否平局;
  5. 如果游戏没结束,调用aiMove算出电脑落点;
  6. 电脑落子,再次检查是否胜利、是否平局;
  7. 回到第1步。

这个循环里,AI选点函数中我加了一个小处理:如果棋盘上还没有任何棋子,AI第一步直接下正中心。因为第一手棋所有位置分值都相同,都是0,评分法容易选到(0,0)这种边角,而下中心是公认最合理的开局,这个特殊分支能避免AI开局走得太蠢。实际测试时会感受到这一行代码的作用。

5.3 上手后建议立刻试的三个小改动

如果你把完整代码跑通了,建议先做三个小改动练手,每一个都能加深对代码的理解:

  • 把打印棋盘的符号从X/O改成●/○。注意这一步会暴露Windows控制台编码问题,你得额外处理代码页和源文件编码。
  • 调整AI权重。把aiMove里的int total = attack + defense;改成int total = attack + defense * 2;,下一局棋你会明显感觉到电脑变得更爱堵你的棋。
  • 给AI加一个“随机失误”开关。在选出一个总分后,通过rand计算是否换成随机位置,这样就能做出简单、中等、困难三档难度。

6. 实际踩过的坑:编译、乱码、输入崩溃和窗口闪退

6.1 中文和棋子符号在控制台里乱码

我在控制台版里特意用了X/O而不是●/○,不是偷懒,是真的被乱码教训过。Windows控制台默认代码页通常是GBK,而现代编辑器新建的源文件默认编码是UTF-8。当一个UTF-8编码的源文件里写的中文字符串直接cout输出到GBK控制台时,多数情况会变成一堆看不懂的符号。至于●和○这些特殊符号,在部分控制台字体下显示成问号或者乱格,也很常见。

几个处理办法:一是像我的代码一样,干脆用英文和X/O这类基础符号,最省心;二是调用system("chcp 65001")把控制台代码页切到UTF-8;三是把源文件改成GBK编码保存,但那样在VSCode里可能又反过来显示乱码。我的建议是:先跑通逻辑,最后再考虑美化输出。很多新手在这个问题上耗掉的时间,足够把整个双人对战版本写完。

6.2 cin读到字母后死循环

如果你输入坐标时手滑输了一个字母,比如“8 a”,程序可能会陷入无限循环,不停打印“你的落子:”却不接受输入。原因是cin在遇到a时进入了fail状态,后续所有读取操作都会立刻失败,而变量row、col的值保持不变,程序就一直走continue分支。

解决方式就是主循环里那段代码:

if (!(cin >> row >> col)) { cin.clear(); cin.ignore(1024, '\n'); cout << "输入无效,请输入两个数字" << endl; continue; }

cin.clear()负责把cin从fail状态恢复为正常,cin.ignore(1024, '\n')把输入缓冲区里残留的坏字符丢弃。这个技巧不只五子棋用得上,任何控制台交互程序都可以直接套。

6.3 段错误多半是数组越界,不是空指针

新手写这个项目时遇到“段错误”往往第一反应是空指针,其实在五子棋里更常见的是数组越界。比如方向数组有4个方向,循环时写成for(int d = 0; d <= 4; d++),第5次访问dirs[4]就越界了;比如检查棋盘某个方向时,没有先判断新坐标是否在0到14之间,直接访问board[r][c],一旦r或c变成负数,就会访问到非法内存。

定位段错误的方式很简单:在VSCode里打断点调试,程序崩掉时会停在出问题的代码行,然后看“局部变量”窗口就能发现是哪个坐标出了问题。我的经验是,凡是出现段错误,先检查所有数组下标边界,90%的bug都能定位到。别空想,直接调试器跑一遍最快。

6.4 双击可执行文件时窗口一闪而过

在VSCode里按F5跑程序时终端会保留,很多人没注意到一个问题:直接双击生成的exe文件时,程序运行完控制台窗口会立刻关闭,看起来就像“闪退”。五子棋对局结束后main返回,exe退出,窗口就没了。这不代表程序错了,只是你没有一个机会看最后的输出。

解决方式是在main返回前加一句system("pause"),或者更通用一点,在结束前调用cin.get()等待一个回车。注意如果前面用了cin读数字,回车符可能还残留在缓冲区,cin.get()会被立刻消费掉,所以有时需要连写两个cin.get(),或者先用cin.ignore清一下。这些细节不致命,但遇到时别以为是编译器问题。

7. 从控制台走向图形界面:EasyX改版与更强的AI方向

7.1 如果改用EasyX,只需要换掉“显示”和“输入”

把控制台版改成图形版,最开心的一件事是AI模块、胜负判断这些核心逻辑不用重写。你只需要替换两样东西:把打印棋盘换成在窗口上画线、画棋子;把cin输入换成鼠标点击事件。EasyX的鼠标消息处理,本质上是获取点击坐标,然后换算成行列号。这和我前面说的“坐标转换”是同一个问题,只是从“用户输入的数字”变成了“鼠标像素坐标”。

一个可行的改造路线是:initgraph创建800×800的窗口,15路棋盘每条线间隔约50像素;用MOUSEMSG类型处理鼠标左键按下消息;根据点击位置的x、y除以格子间距得到行列下标;落子后用solidcircle画棋子。其他逻辑全部复用控制台版的函数。这一步做完,你的项目就从一个“黑白文字棋盘”变成一个像样的桌面小游戏了。

7.2 想让AI更强,下一步该做什么

如果你把评分法跑熟之后想继续提升棋力,我建议按这个顺序进阶:

  • 第一步,加入必赢/必堵检测。在aiMove之前,先检查AI是否有直接成五的点,有就立刻下;再检查玩家是否有下一手就成五的点,有就立刻堵。这个改动只有几十行,但能明显减少AI“见死不救”的蠢操作。
  • 第二步,引入两步搜索。在评分法选出的前几个候选点基础上,模拟双方各下一手后再评估,相当于让AI多长了一只眼睛。
  • 第三步,学α-β剪枝和评估函数优化。这时候再回来看博弈树,你已经有足够的代码积累去理解剪枝为什么高效。
  • 第四步,收集常见开局,做一个小开局库,解决评分法开局容易走出平庸棋形的问题。

我自己在实际项目中体会最深的一点是:不要一开始就追最强AI,先让程序“能陪你把一局下完”,再慢慢往里面加深度。评分法版本虽然连“跳活三”都识别不好,但它已经让我第一次感受到“电脑在跟我过招”。这种正反馈对学习C++来说,比任何练习题都管用。后面想扩展成图形界面还是加强AI,都只是在这个基础上加盖楼层而已。

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

MBIST原理与PATR2实战:芯片内建自测试核心技术解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:18:32

MNE-python源定位环境配置全指南:从零搭建EEG/MEG分析环境

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/4 1:17:53

NeRF三维重建实战:从手机拍摄到模型导出的全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

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

FDTD Solutions自学笔记:网格、边界、光源与材料拟合的避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

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

Python+OpenCV车牌识别实战:从图像处理到GUI界面完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华