
“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. 开发环境与编译器选型VSCodeMinGW和“Visual C 14.0”报错2.1 10分钟跑通VSCode下的C编译如果你以前只用在线编译器写几行语法练习这次建议在本地搭一个环境因为五子棋要跑交互式循环在线编译器体验很差。另一个原因是交互式程序需要多次输入输出在线环境往往要频繁点击运行一次输入错了又要重来心态很容易崩。本地环境虽然第一次配置时有点门槛但一劳永逸。最轻量的组合是VSCode加MinGW-w64。步骤不复杂安装VSCode然后在扩展市场搜索并安装C/C扩展作者是Microsoft的那个。安装MinGW-w64核心是把g编译器装到机器上。Windows下推荐通过MSYS2来装装完把.../mingw64/bin目录加到系统PATH。打开终端执行g --version能看到版本号说明编译器已经可用。在VSCode里新建一个test.cpp写个Hello World按CtrlShiftB配置编译任务。编译任务其实就是让VSCode帮你执行一条g命令。我在tasks.json里用的配置是这样的{ version: 2.0.0, tasks: [ { label: C 编译, type: cppbuild, command: g, args: [ -fdiagnostics-coloralways, -g, ${file}, -o, ${fileDirname}/${fileBasenameNoExtension}.exe ], group: build, problemMatcher: [$gcc] } ] }这样按CtrlShiftB编译再在终端里运行生成的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。也就是说你在VSCodeMinGW的环境里写好的逻辑代码要用EasyX做图形壳必须换到Visual Studio里编译。所以建议按自己的方向选只做控制台版、想轻量快速跑起来选VSCodeMinGW已经预料到要加图形界面、想做窗口化小游戏直接装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; } pairint, 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; } pairint, 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 主循环的执行顺序代码的主循环值得一行行看一遍因为它是整个游戏的“心脏”。运行顺序是打印当前棋盘等待玩家输入行和列校验输入不合法就让玩家重新输入玩家落子检查是否胜利、是否平局如果游戏没结束调用aiMove算出电脑落点电脑落子再次检查是否胜利、是否平局回到第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都只是在这个基础上加盖楼层而已。