基于EasyX的C++五子棋游戏开发实战:从环境搭建到AI对战
简介这款基于C与easyx库实现的五子棋游戏设计源码面向C初学者、图形编程爱好者与游戏开发入门者可帮助解决从控制台程序向图形界面游戏过渡的常见难题。项目完整覆盖棋盘绘制、鼠标交互、落子判定、人机AI对战等关键环节并保留了清晰的函数模块划分和注释便于二次开发。压缩包共36个文件大小约22.92MB核心包括3个cpp源文件和3个头文件承载游戏流程与AI逻辑14个jpg与3个png负责界面背景、棋盘及按钮素材4个mp3、2个wav与1个ogg提供背景音乐和音效另有Visual Studio解决方案、项目过滤文件、.gitignore和.gitattributes等工程配置可直接在VS中打开编译运行。该工程以easyx图形库绘图适合一边阅读源码、一边对照运行效果深入理解Windows下C图形编程的方法。已有203人学习下载值得作为课程设计或业余练手的参考项目。1. 一个能被点开、能下棋的五子棋源码为什么值得你从头写一遍当你拿到“基于easyx库的C五子棋游戏设计源码”这个题目时多半是大二的课程设计或者自学C后想做的人生第一个图形化小游戏。很多人以为它只是画个棋盘、点一下落颗棋子但真正实现起来会发现屏幕坐标怎么换算成格子、事件循环怎么不卡死、五连判定怎么写才不重复计数、AI 凭什么不走最蠢的那一步每一环都有让你翻车的小细节。这个标题指向的是一个能在 Visual Studio 下直接编译运行的小工程用 easyx 图形库画棋盘和棋子用 C 写回合制对局逻辑再配一个能陪你下棋的简单人机。它要解决的核心问题是让你从“会写 C 语法”跨到“能做完整交互程序”这一步这中间隔着的不是语法量而是图形编程、事件驱动和状态管理这一整套思路。适合 C 刚入门想找项目练手的人也适合拿它当游戏开发方向的第一块跳板。别小看它代码量不大但五脏俱全。2. 搭开发环境和窗口骨架easyx 在 Windows 下到底怎么跑起来2.1 为什么选 easyx它的定位和边界先说清楚网上 C 图形库五花八门但课程设计和入门练手这个场景里easyx 几乎是默认答案。它本质上是对 Windows GDI 的封装把那些写起来很啰嗦的 Windows 原生绘图 API 包装成了类似 Turbo C 时代的graphics.h风格头文件一引入就能画点、画线、画圆、接收鼠标消息学习成本极低。它的边界也很明确只支持 Windows不考虑跨平台。如果你以后要做 Linux 或 macOS 上的图形程序得换 Qt 或别的方案这不是 easyx 的职责。装好之后你在 Visual Studio 里新建一个空项目源文件里#include graphics.h然后直接调initgraph()就能弹出一个图形窗口不需要手动配置任何链接库安装包会自动把库文件放进 VS 的目录里。顺带一提dev-c 也能装 easyx但配置过程比 VS 折腾教学场景里我一般直接劝你用 Visual Studio Community 版本省事。提示如果安装 easyx 的时候没关掉 VS装完可能会出现库文件写不进去的情况。先关掉所有 VS 窗口再安装能省掉一次重装的步骤。2.2 新建空工程并跑通第一个图形窗口安装完成后打开 Visual Studio新建一个空项目添加一个main.cpp源文件然后敲下这一段代码。先别急着写五子棋能弹出一个画了黑点的窗口就说明环境没问题。#include graphics.h #include conio.h int main() { // 初始化一个 720x720 像素的图形窗口 initgraph(720, 720); // 设置背景色木纹黄模拟棋盘底色 setbkcolor(RGB(222, 184, 135)); cleardevice(); // 在窗口中心画一个半径为 16 的黑色实心圆当作测试棋子 setfillcolor(BLACK); solidcircle(360, 360, 16); // _getch() 阻塞等待用户按键按任意键后继续执行 _getch(); // 关闭图形窗口释放资源 closegraph(); return 0; }这段代码的逻辑很直白initgraph的两个参数是窗口的宽和高单位是像素setbkcolor设置背景色注意设置之后要调用cleardevice()清一次屏背景色才会真正生效solidcircle的前两个参数是圆心坐标第三个是半径。最后_getch()是万能调试手段它让程序暂停在这里不然窗口一开一关你什么都看不见。这里有个参数细节容易忽略initgraph里如果不传第三个参数默认创建的窗口是可以最小化和关闭的普通窗口。如果你看到某些老代码传了SHOWCONSOLE之类的样式那是为了同时显示控制台黑框调试时方便打印日志游戏里一般用不到。2.3 事件循环用消息驱动游戏而不是 sleep 轮询画一个静态窗口离游戏还差关键一步怎么接收鼠标点击。easyx 从 2021 版开始推荐用ExMessage配合消息循环处理输入这也是我们现在写代码的标准姿势。它的核心思路是程序在一个while循环里不断询问“有没有新消息”有就处理没有就继续往下跑。#include graphics.h int main() { initgraph(720, 720); setbkcolor(RGB(222, 184, 135)); cleardevice(); bool running true; while (running) { ExMessage msg; // 非阻塞地取一条鼠标消息如果没有消息则跳过本次循环 if (peekmessage(msg, EX_MOUSE)) { if (msg.message WM_LBUTTONDOWN) { // 鼠标左键按下msg.x 和 msg.y 是屏幕像素坐标 // 后续在这里处理落子逻辑 } } // 在这里执行每帧的绘制刷新 } closegraph(); return 0; }这里有两个容易弄混的 APIpeekmessage是非阻塞的窗口没有消息时它立刻返回主循环可以继续做绘制和逻辑运算适合游戏这种需要持续刷新的场景getmessage是阻塞的没有消息就挂起等待适合纯菜单或者点一下动一下的交互程序。五子棋虽然交互频率不高但因为有绘制刷新和后续动画的可能我习惯用peekmessage把主循环跑满。这段代码里ExMessage的msg.x和msg.y是鼠标事件发生时的屏幕坐标坐标原点在窗口左上角x 向右增大y 向下增大。这和数学坐标系不一样第一次写图形程序的人经常会在这里转不过弯记不住就遵守一条铁律往下是 y 的正方向数组第一维对应 y 方向的行。3. 把棋盘画出来坐标换算与鼠标落子的门道3.1 棋盘几何参数用常量代替魔法数字标准五子棋棋盘是 15 条横线、15 条竖线交叉出 225 个落点。注意一个常见误区线有 15 条但格子只有 14 个所以画线长度 格子间距 × 14而不是 × 15。这一点算错整个棋盘就会多出一条边。设计参数时我把所有尺寸定义为常量后面改格子大小就只动一处。常量名值说明BOARD_SIZE15棋盘线数落点也是 15×15CELL_SIZE40相邻交叉点的像素间距MARGIN40棋盘边缘到窗口边缘的留白RADIUS16棋子半径WINDOW_SIZE680窗口边长等于 MARGIN×2 (BOARD_SIZE-1)×CELL_SIZE(BOARD_SIZE-1) × CELL_SIZE是棋盘线的总长度两头各留 40 像素边距窗口就是 680×680。很多新手把窗口设成 640×640结果最外圈棋子的位置和棋盘线对不上原因就是没给棋子留边距。写代码时这些常量全部用宏或const int定义绝对不要到处写裸数字后面调尺寸时你就知道这有多重要。3.2 绘制棋盘与星位一次全量重绘的模板有了常量绘制函数会变得非常清晰。这个函数的目标是“无论棋盘变成什么样调用它就能画出一个全新的、干净的棋盘”后面悔棋、重开、重新对局都会复用它。void drawBoard() { // 每次重绘先重设背景再清屏 setbkcolor(RGB(222, 184, 135)); cleardevice(); // 画棋盘线实线线宽 2 setlinecolor(BLACK); setlinestyle(PS_SOLID, 2); for (int i 0; i BOARD_SIZE; i) { // 横线y 固定x 从左边距到右边距 int y MARGIN i * CELL_SIZE; line(MARGIN, y, MARGIN (BOARD_SIZE - 1) * CELL_SIZE, y); // 竖线x 固定y 从上边距到下边距 int x MARGIN i * CELL_SIZE; line(x, MARGIN, x, MARGIN (BOARD_SIZE - 1) * CELL_SIZE); } // 画五个星位标准五子棋棋盘的标记点 int star[5][2] { {3, 3}, {3, 11}, {7, 7}, {11, 3}, {11, 11} }; for (int i 0; i 5; i) { int sx MARGIN star[i][1] * CELL_SIZE; int sy MARGIN star[i][0] * CELL_SIZE; setfillcolor(BLACK); solidcircle(sx, sy, 4); } }这段代码的核心规律是“第 i 条线的坐标 边距 i × 格子间距”。横线和竖线共用同一个循环一次循环画两条边代码量省一半。星位数组里star[i][0]对应行也就是 y 方向star[i][1]对应列也就是 x 方向写的时候最容易把这两个下标搞反建议在注释里明确标出来。setlinestyle(PS_SOLID, 2)的前一个参数是线型PS_SOLID表示实线后一个是线宽单位是像素2 像素的线在 40 像素的格子里比例刚好。如果你发现棋盘线粗得离谱或者细到看不清优先检查这里的数值。3.3 鼠标坐标转棋盘坐标加半格再整除鼠标点下去拿到的msg.x和msg.y是屏幕像素坐标但棋盘数据的索引是 0 到 14 的行列号。中间需要一个换算函数这也是整个项目里最容易出差错的地方。核心思路是用“加半格后整除”做四舍五入比如鼠标点在第 3 列和第 4 列之间偏右的位置加上半格 20 像素后再除以 40结果就会落到 4 而不是 3。bool screenToBoard(int mx, int my, int row, int col) { // 允许点击区域比棋盘略大一圈降低误触边缘的概率 int minV MARGIN - 10; int maxV MARGIN (BOARD_SIZE - 1) * CELL_SIZE 10; if (mx minV || mx maxV || my minV || my maxV) return false; // 先减边距得到相对于棋盘左上角的偏移 // 加半格实现四舍五入再整除得到格子索引 int colTemp (mx - MARGIN CELL_SIZE / 2) / CELL_SIZE; int rowTemp (my - MARGIN CELL_SIZE / 2) / CELL_SIZE; // 钳制越界值边界点击可能算出 -1 或 15 if (colTemp 0) colTemp 0; if (colTemp BOARD_SIZE) colTemp BOARD_SIZE - 1; if (rowTemp 0) rowTemp 0; if (rowTemp BOARD_SIZE) rowTemp BOARD_SIZE - 1; row rowTemp; col colTemp; return true; }这里的整数运算看起来像玄学但原理很简单mx - MARGIN算出点击位置离棋盘原点的像素距离/ CELL_SIZE得到整除的列号。直接整除有一个问题——点击落在第 3 格偏右的位置时它还算第 3 格手感和视觉对不上。加上CELL_SIZE / 2也就是 20 像素之后超过半格就自动算下一列了。鼠标落点如果在最外的交叉点附近减法可能算出-1或15这就是越界钳制存在的意义。以后你调试时会发现不写这四行钳制有时候棋子会莫名其妙跑到棋盘外面去。3.4 棋子绘制与主循环落子双缓冲解决闪屏问题棋盘画好了接下来要把棋子画上去并接入主循环。棋子本质上就是一个在交叉点坐标上的实心圆颜色根据玩家不同切换。这里直接写一个drawPiece函数落子时调用它重绘时也能复用。void drawPiece(int row, int col, int player) { int x MARGIN col * CELL_SIZE; int y MARGIN row * CELL_SIZE; if (player 1) // 1 代表黑棋先手 { setfillcolor(BLACK); solidcircle(x, y, RADIUS); } else // 2 代表白棋 { setfillcolor(WHITE); solidcircle(x, y, RADIUS); } }坐标换算用了和棋盘绘制完全一致的公式x MARGIN col × CELL_SIZEy MARGIN row × CELL_SIZE。只要你棋盘线的绘制和棋子的绘制共用这套公式就不会出现“棋子偏在线的边上”这种问题。如果你发现棋子偏移别怀疑绘制函数先检查screenToBoard返回的row和col是不是和预期一致。主循环里接入落子逻辑后需要加上 easyx 的双缓冲机制不然每落一子整个窗口都会闪得厉害。原理是先在一张看不见的画布上把所有内容画完再一次性地显示到屏幕上而不是画一条线就往外刷一次。int main() { initgraph(WINDOW_SIZE, WINDOW_SIZE); BeginBatchDraw(); // 开启批量绘制双缓冲 bool running true; while (running) { ExMessage msg; if (peekmessage(msg, EX_MOUSE)) { if (msg.message WM_LBUTTONDOWN) { int row, col; // 点击有效且该位置还没有棋子时才落子 if (screenToBoard(msg.x, msg.y, row, col) board[row][col] 0) { board[row][col] currentPlayer; drawPiece(row, col, currentPlayer); // 后续在这里加胜负判断和回合切换 } } } FlushBatchDraw(); // 把当前绘制内容一次性呈现在窗口上 } EndBatchDraw(); // 关闭批量绘制 closegraph(); return 0; }board[row][col]是一个 15×15 的二维数组0 表示空格1 和 2 分别表示黑棋和白棋。currentPlayer是全局变量落子前判断board[row][col] 0是为了防止在同一位置重复落子这是五子棋交互里最基本的合法性校验。BeginBatchDraw和FlushBatchDraw的配对使用是 easyx 的标准姿势开局开启一次每帧提交一次EndBatchDraw最后收尾。开启双缓冲之后哪怕你每帧重绘整个棋盘也不会闪屏。4. 胜负判定与简单人机五子棋最核心的两段逻辑4.1 落点五连检测从棋盘扫描改成局部扫描胜负判定是第一段核心逻辑。最容易想到的方案是每走一步就全盘遍历看有没有五个连子但那样做又慢又容易重复计数。更好的方式是从刚落下的那颗棋子出发沿四个方向检查连子数水平、垂直、主对角线、副对角线。只要有一个方向连续同色棋子达到 5 个就判胜利。这个函数只需要传入落子的行列和当前玩家颜色。// 方向向量水平、垂直、主对角线、副对角线 int dirs[4][2] { {0, 1}, // 水平向右 {1, 0}, // 垂直向下 {1, 1}, // 主对角线 {1, -1} // 副对角线 }; bool checkWin(int row, int col, int player) { for (int d 0; d 4; d) { int count 1; // 当前落下的这颗棋子先算一个 // 向正方向延伸遇到同色棋子就计数加一 for (int step 1; ; step) { int nx row dirs[d][0] * step; int ny col dirs[d][1] * step; if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE || board[nx][ny] ! player) break; count; } // 向反方向延伸同样计数 for (int step 1; ; step) { int nx row - dirs[d][0] * step; int ny col - dirs[d][1] * step; if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE || board[nx][ny] ! player) break; count; } // 只要有一个方向达到五连就赢 if (count 5) return true; } return false; }这段代码的细节在于count从 1 开始因为落点自身就是一颗同色棋子每个方向都从落点出发向正负两边延伸中间的格子不会被重复统计碰到边界、空格或对方棋子就停止。count 5而不是 5是因为六连、七连在无禁手规则里同样算胜利写成等于 5 会漏判长连的情况。有个容易踩坑的地方是方向向量的坐标写法dirs[d][0]对应行方向的增量dirs[d][1]对应列方向的增量。特别是副对角线{1, -1}行增加、列减少方向是从右上到左下。画个图核对一遍再写代码能省下大量排错时间。4.2 演示级 AI遍历所有空位打分的贪心策略第二段核心逻辑是让电脑陪你下棋。我写的是最朴素也最好理解的评分法遍历棋盘上每一个空位分别假设“我下在这里”和“对手下在这里”给这个位置打分最后选总分最高的位置落子。这种方式不搜索未来步数属于贪心策略打不过高手但足够应付课程设计和日常和人对练的场景。评估一个位置的分数时还是基于四个方向连子长度和两端被堵的状态来做启发式打分。下表是我常用的基础权重数值是调出来的经验值不是数学推导的结果连子数两端开放情况分数对应的棋型大于等于 5不限1000000直接获胜4两端都开放100000活四必胜4一端开放10000冲四3两端都开放5000活三3一端开放1000眠三2两端都开放500活二2一端开放100眠二1至少一端开放10单子把权重表变成代码还需要一个模拟落子的过程计算某个位置放上某种颜色后各个方向的连子数和堵头数。// 评估在 (row, col) 位置放上 player 颜色的棋子后能得到多少分 int evaluatePoint(int row, int col, int player) { int total 0; for (int d 0; d 4; d) { int count 1; int blocked 0; // 正向延伸 int nx row dirs[d][0]; int ny col dirs[d][1]; while (nx 0 nx BOARD_SIZE ny 0 ny BOARD_SIZE board[nx][ny] player) { count; nx dirs[d][0]; ny dirs[d][1]; } // 延伸停止后如果是边界或对方棋子就算被堵一端 if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE || board[nx][ny] ! 0) blocked; // 反向延伸 nx row - dirs[d][0]; ny col - dirs[d][1]; while (nx 0 nx BOARD_SIZE ny 0 ny BOARD_SIZE board[nx][ny] player) { count; nx - dirs[d][0]; ny - dirs[d][1]; } if (nx 0 || nx BOARD_SIZE || ny 0 || ny BOARD_SIZE || board[nx][ny] ! 0) blocked; // 根据连子数和被堵端数折算分数 if (count 5) total 1000000; else if (count 4) { if (blocked 0) total 100000; else if (blocked 1) total 10000; } else if (count 3) { if (blocked 0) total 5000; else if (blocked 1) total 1000; } else if (count 2) { if (blocked 0) total 500; else if (blocked 1) total 100; } else if (count 1 blocked 1) { total 10; } } return total; }这个函数做的事情等于把一颗虚拟的棋子放进空位再沿着四个方向数一数能连出多长的链。blocked记录的是两端被边界或敌方棋子堵死的数量两端都开放就是最有威胁的活棋堵一端价值掉一个量级两端都堵死则基本没有发展空间。有了评估函数AI 下棋就变成了一个双层循环加一次比较void aiMove() { int bestRow -1; int bestCol -1; int bestScore -1; for (int i 0; i BOARD_SIZE; i) { for (int j 0; j BOARD_SIZE; j) { if (board[i][j] ! 0) continue; // 进攻分AI 必须考虑自己放在这里的价值 int attack evaluatePoint(i, j, AI_PLAYER); // 防守分AI 还得考虑玩家下这里的威胁 int defend evaluatePoint(i, j, HUMAN_PLAYER); // 攻防加权防守权重略高因为人类是先手 int score attack defend * 11 / 10; if (score bestScore) { bestScore score; bestRow i; bestCol j; } } } // 选完位置直接落子并绘制 board[bestRow][bestCol] AI_PLAYER; drawPiece(bestRow, bestCol, AI_PLAYER); }这里最有价值的参数是defend * 11 / 10这个防守权重。因为人是先手AI 后手情况下同等棋型的威胁程度比进攻更高所以我给防守分额外加了 10% 的权重。如果你发现 AI 总是光顾着堵人不会自己进攻就把这个系数调低到 1 或者 0.9反过来如果 AI 太贪进攻被人一招活三带走就调高到 1.2 左右。这是整个 AI 里最值得你手动改的参数。提示这个 AI 是演示级别的贪心策略没有做 α-β 剪枝搜索也不会看后面几步的棋。它最大的价值是逻辑清晰、能跑、方便你在此基础上做增强。别拿它和正规 AI 对战会输得很惨。4.3 评分法的两个边界问题评分法看着简单实际用起来有两个容易出错的地方。第一个是“禁手规则”的问题。国内一些比赛规则里黑棋有禁手限制不能下双三、双四和长连。但这个项目默认是普通对局规则不需要实现禁手判断。如果硬把禁手逻辑塞进来反而会破坏基础判定——在半路加一段不完整的禁手检查很可能会出现“明明五连了却判不了胜”这种尴尬局面。第二个是权重表里连子数链的统计口径。evaluatePoint是假设某个空位已经落子后再去数连子而落子后棋盘上是多了一颗棋子的。所以当你用evaluatePoint同时评估进攻和防守时同一个人工评分函数会被调用两次中间没有任何棋盘状态的修改不会造成数据污染。如果你在别的实现里看到有人先改board[row][col]再调评估函数、评估完再改回来逻辑等价但更容易因为异常流程忘记恢复状态。更推荐我给的方式只读棋盘不改棋盘。5. 常见问题排查easyx 五子棋最容易踩的五个坑5.1 窗口一闪而过根本看不到棋盘现象编译运行后图形窗口闪一下就消失了黑框控制台里的程序直接退出。原因最常见的是initgraph执行完之后没有任何阻塞操作的代码程序立刻跑到closegraph然后结束。新手容易顺手把书上的示例代码抄成“初始化 → 画图 → 关闭”中间忘了加事件循环。另一个原因是工程配置问题入口函数写到了某个退出分支里找不到循环体。解决在initgraph和closegraph之间加一个永真的while循环作为主循环或者用_getch()临时停住窗口。正式写五子棋时主循环必然存在因为你要不断接收鼠标消息所以只要把peekmessage那段循环体补上这个问题天然消解。调试期间如果只想看静态画面用_getch()应急没问题但记得最后删掉。5.2 点击位置和落子位置对不上或者左右镜像现象鼠标点在第 5 列棋子却出现在第 5 行点棋盘上方棋子落在左侧点交叉点附近棋子跑到四条线中间。原因这是 row、col 和屏幕 x、y 的映射关系搞混了。二维数组board[row][col]中第一维 row 是行对应屏幕坐标的 y第二维 col 是列对应屏幕坐标的 x。很多人写screenToBoard时返回rowTemp和colTemp的顺序写反或者画棋子时把col传到了 y 参数上都会导致这种错位。解决先在代码注释里固定一个约定“row y 方向col x 方向”然后在所有使用 row 和 col 的地方检查一遍。screenToBoard里rowTemp (my - MARGIN CELL_SIZE / 2) / CELL_SIZEcolTemp (mx - MARGIN CELL_SIZE / 2) / CELL_SIZE。绘制时x MARGIN col * CELL_SIZEy MARGIN row * CELL_SIZE。如果镜像了优先查dirs方向向量和star星位数组的下标顺序。实在排查不出来在screenToBoard里加一行输出打印鼠标坐标和计算出的 row、col用实际数据对照最快。5.3 每落一子窗口就闪烁白棋闪黑棋也闪现象画面绘制不是平滑的棋子落下去的一瞬间整个窗口像被“闪”了一下尤其是棋盘线密集的时候特别明显。原因绘制流程是逐条执行的先画一条线屏幕显示一条再画另一条屏幕又更新一次。每执行一次绘图 API窗口就会刷新一次这种“画一半显一半”的方式在视觉上就表现为闪烁。这是 easyx 初学者最典型的表现也是血泪经验里最常被问到的。解决使用批量绘制模式。initgraph之后立刻调用BeginBatchDraw()在整个主循环里每帧绘制完成后调用一次FlushBatchDraw()把整幅画布一次性推到屏幕上程序退场时再EndBatchDraw()。注意FlushBatchDraw必须放在所有绘制操作之后、下一帧绘制之前不然批量绘制没生效闪烁依旧。5.4 中文显示成一串方块或者乱码现象outtextxy输出“黑棋下”的时候窗口里显示的是空心方块或者根本看不见文字。原因easyx 默认的字体名、字符集和当前工程配置不匹配。VS 工程默认使用 Unicode 字符集而代码里如果直接写字符串字面量在窄字符和宽字符之间切换时就会出现编码错乱。另一部分原因是settextstyle没设置中文字体名默认字体不支持中文显示。解决统一用一个可靠的字体设置settextstyle(20, 0, _T(微软雅黑))第一个参数是文字高度像素值第二个参数为 0 表示宽度自适应第三个是字体名。所有中文字符串前加上_T()宏包裹例如_T(黑棋下)。_T()在 Unicode 工程里自动变成宽字符版本在多字节工程里保持窄字符这样能两头通吃。如果文字位置偏了检查outtextxy的坐标参数easyx 里文字坐标是左上角对齐。5.5 换台电脑跑不起来提示缺运行库现象编译好的 exe 拷贝到另一台 Windows 电脑上双击运行直接报错说缺少VCRUNTIME140.dll或MSVCP140.dll有时候错误信息里甚至出现microsoft visual c 14.0 is required这样的提示。原因这是典型的 VC 运行库缺失。VS 编译出的程序默认动态链接到系统的 C 运行库而目标电脑上没有装对应的 Visual C Redistributable 就会报错。这不是 easyx 特有的问题是所有用 VS 写的 C 程序都会遇到的部署坑。解决最简单的办法是给目标电脑装对应的运行库注意区分 x86 还是 x64程序是 32 位就装 x86 版本64 位就装 x64 版本32 位程序在 64 位系统上也能跑但缺库时优先补 x86。另一个办法是在 VS 里把运行库改成静态链接项目属性 → C/C → 代码生成 → 运行库选“多线程 (/MT)”这样 exe 会变大但不再依赖外部的 DLL拷贝到别的电脑上直接能跑。做课程设计交作业时这是最省心的方式。6. 让这个项目变成“能打完一局”的游戏悔棋、重开与调试习惯6.1 悔棋和重开给玩家一个后悔药基础对局流程跑通之后下一步不是加特效而是补上两个最影响体验的功能悔棋和重新开局。实现思路非常简单用一个容器把每一步的落点记录下来悔棋时就回退到上一步。#include vector std::vectorstd::pairint, int history; // 记录整局棋的落子顺序 void undoMove() { if (history.empty()) return; // 取出最后一步清掉棋盘上的棋子和绘制 auto last history.back(); history.pop_back(); board[last.first][last.second] 0; drawBoard(); FlushBatchDraw(); // 全量重绘后刷新 // 回合要回退给上一手玩家 currentPlayer (currentPlayer BLACK ? WHITE : BLACK); } void resetGame() { memset(board, 0, sizeof(board)); history.clear(); currentPlayer BLACK; drawBoard(); FlushBatchDraw(); }悔棋时要注意回合切换悔掉的这一手是谁下的回退后就应该轮到谁下。drawBoard()整盘重绘是这里最稳的选择因为你不确定悔棋位置附近有没有其他子需要擦除全量重绘免去了逐颗棋子擦除的麻烦。6.2 给窗口加一个信息栏当前执子和胜负提示游戏玩到一半经常忘掉轮到谁尤其是人机对战时人和 AI 各下一手只要一方没走棋界面根本看不出当前状态。在窗口底部预留一块区域用settextstyle设置字体后用outtextxy实时绘制当前执子方和胜负状态。settextstyle(20, 0, _T(微软雅黑)); setbkmode(TRANSPARENT); TCHAR msg[64]; _stprintf_s(msg, _T(当前执子%s), currentPlayer BLACK ? _T(黑棋) : _T(白棋)); outtextxy(20, WINDOW_SIZE - 30, msg);每次落子或切换回合后调用一次这段代码界面就会跟随状态更新。setbkmode(TRANSPARENT)会让文字背景透明否则文字后方会盖住一个不透明的色块很难看。如果你后面加上了“黑棋胜”的提示用一个全局变量存游戏状态绘制时根据状态输出不同的文本就行。6.3 一个能救命的调试习惯给 AI 留日志开关我对这个项目最大的一个习惯是给 AI 的评分逻辑留一个可开关的打印if (debugMode) { printf(AI 候选位置: (%d, %d), 攻: %d, 防: %d, 总分: %d\n, i, j, attack, defend, score); }AI 一旦走得不对你看着棋盘永远像在看黑匣子不知道它为什么选了那个位置。把每个空位的进攻分、防守分和最终总分打出来一眼就能看出是权重配比不对还是评估函数数错了连子数。很多教程不会提这个调试技巧但如果没有日志你调 AI 的每一步都像在瞎猜。我自己的习惯是写完代码先枰面测试半小时把先手、后手、长连、悔棋、重开各种边界情况点一遍再交给别人玩。这个项目真正给你的其实不是课设分而是你第一次亲手完成一个“能自然交互的程序”的节奏感。希望帮到你。本文还有配套的精品资源点击获取