news 2026/10/21 14:44:38

C语言课设实战:基于EasyX的图形五子棋与人机AI实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言课设实战:基于EasyX的图形五子棋与人机AI实现

简介:这是一份基于C语言和EasyX图形库实现的五子棋游戏项目,适合C语言初学者、图形编程爱好者以及想了解游戏AI和网络编程的开发者学习参考。项目实现了人机对战、人人对战和联机对战三种模式,人机模式涉及Minimax或Alpha-Beta剪枝等经典搜索算法,网络部分则涉及socket通信,代码结构清晰,便于对照学习。压缩包共145个文件,包含6个C++源文件和5个头文件,以及工程配置、编译记录、图片素材、音频资源和可直接运行的程序,整体约57.36MB。已有1687人学习下载。通过这份资源,可以系统掌握EasyX图形库的棋盘绘制与交互处理,理解五子棋核心逻辑、AI决策思路,并接触联机对战中的网络编程实现,是巩固C语言并进阶图形编程的实用素材。

1. 项目定位:C语言课设里最值得跑起来的交互图形项目

帮某位同学调一个C语言大作业时,我见过最典型的翻车现场:代码在控制台里写得好好的,黑窗口里输出个棋盘、用坐标输入棋子,看起来没毛病,可一旦要求加上鼠标点击和图形界面,很多人就卡住了。这个基于EasyX的C语言五子棋项目,恰好就是「从控制台编程跨到图形交互编程」的绝佳跳板。它不是什么重型框架,就是一个纯C语言写的、能在Windows上直接编译运行的工程,覆盖了鼠标交互、绘图渲染、棋局逻辑和简单AI。适合刚学完C语言想做课程设计的人,也适合想搞明白「图形界面程序到底怎么转起来」的开发者。整个项目源码清晰,模块划分干净,改一改就能变成六子棋、黑白棋甚至联机版。

2. 架构设计与工程结构:一个五子棋窗口背后的分层

2.1 为什么选EasyX,以及编译环境的搭建

EasyX是针对Windows下Visual Studio的图形库,它的定位很直接:让C/C++初学者不用碰Windows API就能画点、画线、处理鼠标消息。比起OpenGL或SDL,EasyX最大的优势是没有复杂的初始化流程和资源管理,几分钟就能把窗口建起来。常见做法是直接去EasyX官网下载安装包,运行后它会自动识别本机安装的VS版本,一键安装到对应目录。装完以后,在代码里包含#include <easyx.h>或者老的#include <graphics.h>就行,不需要手动配置链接库——EasyX的安装程序会在VS的include和lib目录里自动写入对应文件。

工程配置上有个值得注意的细节:如果是用Visual Studio 2022,创建项目时选「空项目」即可,代码文件后缀保持.cpp也没关系——EasyX是C++接口的库,但内部函数都是C语言风格,C代码直接写进去也能编译。真正容易踩坑的是字符集设置,如果项目用了Unicode字符集,写字符串常量时要记得用宽字符写法,不然MessageBox这类API会报类型不匹配。我一般习惯在项目属性里把字符集改成「使用多字节字符集」,省去一堆L前缀的麻烦。

2.2 目录结构与模块职责

这个项目不是单文件堆到底,而是按职责拆成了四个核心部分。主文件负责窗口初始化和主循环,棋盘绘制模块只做图形渲染,逻辑模块管落子和胜负判断,AI模块单独拎出来处理人机对战。我见过很多人写C语言项目喜欢把所有函数堆在一个文件里,几百行勉强能看,上千行就完全没法维护。这个项目按功能拆分的做法,对课设答辩特别友好,因为老师问「这个模块是干什么的」时,你能直接指着一个文件说清楚。

典型的目录结构大概长这样,文件名可能略有不同:

gobang/ ├── main.cpp // 主循环,窗口初始化,消息处理 ├── chessboard.h // 棋盘数据结构和常量定义 ├── chessboard.c // 棋盘绘制,坐标换算 ├── game_logic.h // 落子、胜负判定接口 ├── game_logic.c // 五子连珠检测实现 ├── ai_player.h // AI接口声明 └── ai_player.c // 权值打分AI实现

头文件和源文件分离的做法值得多说一句:chessboard.h里只放结构体定义、宏定义和函数声明,具体实现全部放到.c文件里。这样当你想改AI逻辑时,只需要动ai_player.c,其它模块完全不受影响。编译的时候每个.c文件单独编译成.obj,最后再链接到一起——这也是老师很喜欢问的「头文件到底有什么用」的实际例子。另外,棋盘数据用的是二维数组int board[15][15],0表示空,1表示黑棋,2表示白棋,这个约定贯穿整个项目,记住它后面看代码就很顺。

2.3 主循环:消息驱动与重绘分离

EasyX程序的核心结构是一个while循环,每次循环做两件事:处理鼠标消息、重绘画面。这不是什么高深的事件驱动模型,而是很多图形程序的通用骨架。控制台程序是你输入一步它走一步,图形程序则是「不断刷新,有交互就响应」。

// main.cpp 核心主循环 #include <graphics.h> #include <conio.h> #include "chessboard.h" #include "game_logic.h" int main() { initgraph(640, 640, SHOWCONSOLE); // 创建640x640的游戏窗口 setbkcolor(WHITE); // 白色背景 cleardevice(); int board[15][15] = {0}; int current_player = 1; // 1黑棋先手,2白棋后手 int game_over = 0; while (1) { // 处理鼠标消息,有点击才执行落子逻辑 if (MouseHit()) { MOUSEMSG msg = GetMouseMsg(); if (msg.uMsg == WM_LBUTTONDOWN && !game_over) { int x = msg.x; int y = msg.y; // 拿到鼠标坐标后交给逻辑模块处理 handle_click(board, x, y, &current_player, &game_over); } } // 重绘整个棋盘,把所有棋子按数据画一遍 draw_chessboard(board); // 每帧间隔10ms,避免CPU占用过高 Sleep(10); } closegraph(); return 0; }

这段代码里最关键的设计是「数据与渲染分离」。board二维数组是唯一的数据源,落子只是改数组里的值,画图时每次都从board读取整个棋盘重绘。这样做的结果是,任何时候你想加悔棋、加回放,只需要操作数据再触发重绘就能实现,完全不用关心画面细节。另一个细节是MouseHit()和GetMouseMsg()的配合:先判断有没有鼠标事件,有才取出事件内容,否则窗口会阻塞在GetMouseMsg()等消息,界面就假死了。我一般习惯在帧尾加Sleep(10),主要目的是降低CPU占用率,不加也没问题,但加了以后程序跑起来更优雅。

3. 棋盘与棋子绘制:从像素坐标到二维数组索引

3.1 棋盘坐标系设计

15×15的五子棋盘有225个交叉点,但绘制时不能直接用交叉点坐标存数据——鼠标点击的位置是像素坐标,而棋子归属应该用「第几行第几列」来表示。这就是坐标换算要做的事情。整个项目里最核心的换算公式其实就两行:像素坐标转索引、索引转像素坐标。

常见的做法是设定棋盘左上角第一个交叉点为原点,每个格子间距固定。比如格子间距设为40像素,棋盘起点在 (30, 30),那么第row行第col列交叉点的屏幕坐标就是(30 + col * 40, 30 + row * 40)。反过来,鼠标点击位置(x, y)换算成索引就是col = (x - 30 + 20) / 40,row = (y - 30 + 20) / 40。加20是为了四舍五入,点击位置在格子中线上方时归到上一个交叉点,在中线下方时归到下一个交叉点。

3.2 绘制棋盘与落子标记

棋盘绘制分三层:画网格线、画交叉点标记(天元和星位)、画已落棋子。网格线用line函数画15条横线和15条竖线,交叉点的小圆点用circle函数画半径3像素的实心圆。棋子则是用solidcircle函数画实心圆,黑棋用黑色,白棋用白色,再画一个深色描边让白棋在白色背景下也能看清轮廓。

// chessboard.c 棋盘绘制核心代码 #include <graphics.h> #include "chessboard.h" #define GRID_SIZE 40 // 格子间距(像素) #define BOARD_ORIGIN 30 // 棋盘左上角第一个交叉点的坐标 void draw_chessboard(int board[15][15]) { // 清屏并重绘整个棋盘 setbkcolor(WHITE); cleardevice(); // 画网格线 setlinecolor(BLACK); for (int i = 0; i < 15; i++) { int start_x = BOARD_ORIGIN; int start_y = BOARD_ORIGIN + i * GRID_SIZE; line(start_x, start_y, start_x + 14 * GRID_SIZE, start_y); // 横线 line(start_x, start_y, start_x, start_y + 14 * GRID_SIZE); // 竖线 } // 画天元位置的小圆点,四个星位加中心 setfillcolor(BLACK); int stars[5][2] = {{3,3}, {3,11}, {7,7}, {11,3}, {11,11}}; for (int i = 0; i < 5; i++) { int sx = BOARD_ORIGIN + stars[i][1] * GRID_SIZE; int sy = BOARD_ORIGIN + stars[i][0] * GRID_SIZE; solidcircle(sx, sy, 3); } // 遍历二维数组,绘制所有棋子 for (int row = 0; row < 15; row++) { for (int col = 0; col < 15; col++) { if (board[row][col] == 0) continue; int cx = BOARD_ORIGIN + col * GRID_SIZE; int cy = BOARD_ORIGIN + row * GRID_SIZE; if (board[row][col] == 1) { setfillcolor(BLACK); solidcircle(cx, cy, 16); // 棋子半径16像素 } else if (board[row][col] == 2) { setfillcolor(WHITE); solidcircle(cx, cy, 16); setlinecolor(BLACK); circle(cx, cy, 16); // 白色棋子加黑色描边 } } } }

参数上要注意GRID_SIZE和棋子半径的比例。格子间距40像素时,棋子半径16像素是比较舒服的视觉比例,既不遮挡相邻交叉点,看起来也不再小气。如果你把窗口改成800×800,格子间距变大,棋子半径可以按比例提升到20或者22。判断和坐标换算里的BOARD_ORIGIN要永远保持一致,要是绘制和点击判定用的起点不同,就会出现「画在A点、点的时候却落在B点」的玄学问题,排查起来非常难受。

3.3 双缓冲绘图:为什么落子后画面会闪

初学者最常见的画面问题就是闪烁:每一次点击后,清屏和重绘之间有短暂的白屏瞬间,视觉上就是闪一下。EasyX提供了一套双缓冲机制,其实原理很简单——先在一块不可见的内存画布上完成所有绘制,一帧画完以后一次性刷新到屏幕上,用户看到的始终是完整的画面而不是绘制中途的状态。

// 双缓冲绘制示例 BeginBatchDraw(); // 开始批量绘制,后续绘图操作写入内存缓冲 for (int i = 0; i < 100; i++) // 假设要连续画100帧 { draw_chessboard(board); FlushBatchDraw(); // 将缓冲内容整体刷新到屏幕上 Sleep(10); } EndBatchDraw(); // 结束批量绘制,恢复到普通绘图模式

使用双缓冲的关键是把BeginBatchDraw()放在主循环之前或者循环体内,每次绘制结束后调用FlushBatchDraw()。很多教程会告诉你「绘图前加 BeginBatchDraw 就完事了」,但有个细节需要注意:如果BeginBatchDraw()和EndBatchDraw()之间的绘制量太大,或者循环里 Sleep 的时间太长,画面帧率会掉得厉害。五子棋这种低频交互程序完全不是问题,因为只有点击时才重绘一次。我一般把BeginBatchDraw()放在主循环外、cleardevice()之后,再把FlushBatchDraw()放在每次绘制函数末尾。这样做的好处是,循环内所有绘制操作都进缓冲,只有最后一帧画面真实呈现,完全消除闪烁。

4. 核心逻辑模块:落子判定、五连检测与人机AI

4.1 落子合法性:越界、重复与回合管理

鼠标点击后不能无脑落子,得先经过三道检查:坐标是否落在棋盘范围内、交叉点上是否已有棋子、当前是否轮到此玩家操作。这三步合在一起叫「落子合法性校验」,是整个棋局稳定性的基础。

// game_logic.c 落子逻辑 #include "game_logic.h" // 像素坐标转棋盘索引,返回0表示合法,返回-1表示越界 int convert_click_to_index(int x, int y, int *row, int *col) { // 检查是否在棋盘有效区域内 if (x < 10 || x > 610 || y < 10 || y > 610) // 30 + 14*40 = 590,留10像素余量 return -1; *col = (x - 30 + 20) / 40; *row = (y - 30 + 20) / 40; if (*row < 0 || *row >= 15 || *col < 0 || *col >= 15) return -1; return 0; } int handle_click(int board[15][15], int x, int y, int *current_player, int *game_over) { int row, col; // 第一步:像素坐标转索引 if (convert_click_to_index(x, y, &row, &col) != 0) return 0; // 点击了窗口边缘,忽略 // 第二步:检查交叉点是否已有棋子 if (board[row][col] != 0) return 0; // 已有棋子,不能重复落 // 第三步:落子,修改棋盘数据 board[row][col] = *current_player; // 第四步:检查是否五连 if (check_win(board, row, col) == 1) { *game_over = 1; // 这里可以弹窗或者绘制胜利提示 return 2; // 返回值2表示分出胜负 } // 第五步:交换回合 *current_player = (*current_player == 1) ? 2 : 1; return 1; // 返回值1表示正常落子 }

落子逻辑里最容易出错的是「检查胜负的时机」和「交换回合的时机」。正确顺序是先落子、再检测、最后换人,因为check_win需要判断的是刚落下这颗棋是否形成五连,如果你先把current_player交换了再去检测,黑棋赢了检测的却是白棋的棋子,整个判定就反了。这段逻辑里还有个隐藏约定:函数返回值设计成3种——0表示无效点击,1表示成功落子,2表示胜负产生。UI层拿到返回值后再决定是否停止响应鼠标事件。

4.2 胜负判定:四方向延伸计数

五子棋的胜负判定核心是「从当前落子点向四个方向(水平、垂直、两条对角线)延伸,数一数有几个连续的同类棋子,只要任意方向连续数达到5就赢」。实现思路很经典:从当前点往正反两个方向同时延伸,统计总数。

// game_logic.c 五连判定 // 四个方向的方向向量:右、下、右下、左下 static int dirs[4][2] = { {0, 1}, // 水平方向,列+1 {1, 0}, // 垂直方向,行+1 {1, 1}, // 对角线方向(\) {1, -1} // 反对角线方向(/) }; int check_win(int board[15][15], int row, int col) { int player = board[row][col]; for (int d = 0; d < 4; d++) { int count = 1; // 当前棋子自己先算一个 // 正向延伸:沿方向向量走 int nr = row + dirs[d][0]; int nc = col + dirs[d][1]; while (nr >= 0 && nr < 15 && nc >= 0 && nc < 15 && board[nr][nc] == player) { count++; nr += dirs[d][0]; nc += dirs[d][1]; } // 反向延伸:沿方向向量往回走 nr = row - dirs[d][0]; nc = col - dirs[d][1]; while (nr >= 0 && nr < 15 && nc >= 0 && nc < 15 && board[nr][nc] == player) { count++; nr -= dirs[d][0]; nc -= dirs[d][1]; } if (count >= 5) return 1; // 当前方向上形成五连 } return 0; }

这里有个容易翻车的细节:count初始值是1而不是0,因为当前落下的这颗棋本身就是连续的起点。如果初始化为0,两个方向都是同色棋的时候总数会少1,导致四连误判成没赢。dirs二维数组是方向向量表,{0,1}表示向右移动一行、列不变,{1,-1}表示行加一、列减一(斜向左下)。所有边界条件都靠nr >= 0 && nr < 15这套约束拦截,数组越界访问是C语言里最隐蔽的崩溃源,五子棋这种固定尺寸棋盘反而很容易忽略。我习惯先把边界检查写在while循环条件里,再访问board数组,顺序反了就会在最后一格越界读内存,结果不确定却查不出崩溃原因。

4.3 人机AI:权值打分法

这个项目里AI的实现用的是「权值打分」方案,不是深度搜索。思路非常符合课程设计的体量:遍历棋盘所有空位,对每个空位分别评估下黑棋和白棋的价值,选一个综合价值最高的位置落子。评估方法则是用一个查表的方式——看这个位置在四个方向上能形成什么样的棋形,比如活二、眠三、活三、冲四等,每种棋形对应一个分值,累加后就是该位置的得分。

// ai_player.c 权值AI核心逻辑 // 棋形权值表,数值越大说明这个棋形越有威胁 #define WIN_SCORE 100000 // 下一步能赢,优先级最高 #define FOUR_SCORE 10000 // 活四,基本锁定胜局 #define THREE_SCORE 5000 // 活三,要开始防守了 #define TWO_SCORE 1000 // 活二,有潜力 #define ONE_SCORE 100 // 单个棋子,聊胜于无 // 评估score[row][col]位置在四个方向上的棋形得分 int evaluate_point(int board[15][15], int row, int col, int player) { // 把AI自己的棋子设为player,对手设为other // 然后分别统计两个方向的连续棋子数量 int total_score = 0; for (int d = 0; d < 4; d++) { int count = 1; // 假设在这里下子 int block = 0; // 两端被封堵的个数 // 正向数连续同类棋子 int nr = row + dirs[d][0], nc = col + dirs[d][1]; while (nr >= 0 && nr < 15 && nc >= 0 && nc < 15 && board[nr][nc] == player) { count++; nr += dirs[d][0]; nc += dirs[d][1]; } // 如果遇到对手棋子或边界,算作被堵 if (nr < 0 || nr > 14 || nc < 0 || nc > 14 || (board[nr][nc] != 0 && board[nr][nc] != player)) block++; // 反向再来一次,逻辑同上 nr = row - dirs[d][0]; nc = col - dirs[d][1]; while (nr >= 0 && nr < 15 && nc >= 0 && nc < 15 && board[nr][nc] == player) { count++; nr -= dirs[d][0]; nc -= dirs[d][1]; } if (nr < 0 || nr > 14 || nc < 0 || nc > 14 || (board[nr][nc] != 0 && board[nr][nc] != player)) block++; // 根据连续数、封堵数查表给分 if (count >= 5) total_score += WIN_SCORE; else if (count == 4 && block == 0) total_score += FOUR_SCORE; else if (count == 4 && block == 1) total_score += THREE_SCORE; else if (count == 3 && block == 0) total_score += THREE_SCORE; else if (count == 3 && block == 1) total_score += TWO_SCORE; else if (count == 2 && block == 0) total_score += TWO_SCORE; else if (count == 2 && block == 1) total_score += ONE_SCORE; } return total_score; } // AI选择落子位置:遍历所有空位,计算攻防两端的得分,取最大值 void ai_move(int board[15][15], int ai_player, int *best_row, int *best_col) { int human_player = (ai_player == 1) ? 2 : 1; int max_score = -1; for (int row = 0; row < 15; row++) { for (int col = 0; col < 15; col++) { if (board[row][col] != 0) continue; // AI进攻价值:假设AI棋子下在这里的得分 int attack_score = evaluate_point(board, row, col, ai_player); // 对手威胁:假设对手棋子下在这里的威胁程度 int defend_score = evaluate_point(board, row, col, human_player); // 综合得分:进攻值 + 防守值 * 1.2,优先防守更稳妥 int total = attack_score + (int)(defend_score * 1.2); if (total > max_score) { max_score = total; *best_row = row; *best_col = col; } } } }

AI的评估逻辑里有个重要设计:每次评估一个空位时,假设AI自己在了子和假设对手在了子,分别算两种局势的价值。进攻分是AI下这里能形成的威胁,防守分是对手下这里会造成的威胁——两者相加后乘以防守权重,这样AI既不会一味进攻漏掉对手的冲四,也不会过于保守成了缩头乌龟。1.2这个系数就是我调参调出来的血泪经验,纯五五开权重时AI经常放着对手的三连不管只顾自己的四连,最后被人一招反杀。这个系数调高到1.5时AI会变得很怂,稍微有点威胁就跑;调到1.1时又浪得不行。项目默认给的1.2,我实测下来攻守最均衡。

5. 常见问题与避坑:从编译报错到界面卡顿的实战记录

5.1 编译通过后窗口一闪而过

现象:代码编译零错误,运行后窗口刚出现就立刻关闭,你甚至看不清棋盘长什么样。

原因:主函数里initgraph建了窗口,但程序没有进入循环就一路执行到closegraph()然后return了,窗口生命周期结束。新手最常犯的错误是觉得「画完棋盘就该退出」,完全没意识到图形界面程序必须持续运行时窗口才会停留。

解决:确保主函数最后进入while (1)死循环,循环里至少要有MouseHit()或getch()之类的阻塞等待。我习惯在循环里放鼠标检测加Sleep(10),这样没有操作时CPU占用也保持在很低水平。另外,网上有的教程让你system("pause")停住窗口,那样能看但没法交互,不是正经解法。

5.2 点击棋盘却没反应,或棋子画在错误位置

现象:鼠标点棋盘网格的正中间,落子画出来的位置偏到格子边缘甚至画到窗口边框上。

原因:像素坐标转索引的公式有问题,或者绘制和点击换算用了不同的起点。最常见的情况是绘制时BOARD_ORIGIN用了30,点击换算里却直接减了10,两个基准不一致,算出来的索引差了几格。

解决:把坐标换算收敛到一个函数里,绘制和点击都用同一个转换函数,不要各自写一套。检查转换结果可以在落子前临时printf("row=%d col=%d\n", row, col)输出到控制台(initgraph时带SHOWCONSOLE参数即可),对比实际点击位置是否一致。还有一个容易忽略的坑:鼠标消息里的msg.x和msg.y是窗口客户区坐标,不是屏幕坐标。如果你用了GetCursorPos之类的函数取到的会是屏幕坐标,没减窗口位置就会整体偏移,所以直接用GetMouseMsg()返回的x、y才靠谱。

5.3 画面闪烁严重,落子后棋盘像在抽风

现象:每次落子或者鼠标移动时,整个棋盘反复闪白,看着非常难受。

原因:典型的双缓冲没用好。鼠标每移动一次,程序就触发一次重绘,每一帧都要清屏、画网格、画全部棋子,中间任意时刻屏幕都可能处于空白或半绘制状态。

解决:在进入主循环前调用一次BeginBatchDraw(),在绘制函数末尾调用FlushBatchDraw(),程序退出前EndBatchDraw()。如果你发现加了双缓冲反而更卡,检查是不是在循环里重复调用了BeginBatchDraw()——它会清一次缓冲,导致画面先被清空再被画上,效果比不用还差。正确写法是Begin只调用一次,Flush每次绘制后调用一次。

5.4 已经四连了却判定没赢,再下一手才赢

现象:黑棋横着摆了四个子且两端都没堵,理论上已经是必胜局面,但程序就是不判断胜负,必须再点一手第五个棋子进去才提示获胜。

原因:胜负检测函数只检查了「当前落子位置是否为五连」,没有检查「全棋盘是否存在已经连成五子但你没发现」的情况。听着离谱但真会发生——如果你在落子逻辑里先交换了current_player再去检测,或者检测函数里统计连续棋子数时count初始值给错,就会漏判。

解决:check_win的count初始值必须是1,方向向量正反两个方向都要统计完整。另外,检测时机严格放在落子后、换人前,不要打乱顺序。如果还是漏判,在check_win函数里加一个调试输出:把四个方向统计到的连续数和该方向对应的坐标打印到控制台,一眼就能看出是不是某个边界条件把连续子截断了。

5.5 悔棋功能实现后棋盘错乱

现象:AI刚落完棋,你点悔棋,结果AI和玩家的棋子一起消失了,或者悔棋后下一手落子颜色全反了。

原因:悔棋只退了数据,没同步视角和回合状态。五子棋里黑棋先手,悔棋通常是撤销「当前玩家刚下的一手」,但人机对战时可能是AI落子你也撤销成AI落子前——你需要定义好悔棋精确定义是「撤销最后一步」还是「撤销双方各一步」。数据上就是栈弹出棋子坐标,把board[row][col]重新赋0,但current_player必须同时恢复成之前的状态,否则下一手落子用错了颜色。

解决:用一个结构体数组当历史栈,每个元素记录{row, col, player},落子时就压栈,悔棋时弹栈并恢复棋盘数据。弹栈后立即更新当前玩家,命令人类玩家继续下。注意悔棋按钮要控制点击频率,栈空了点悔棋就会越界。这是所有逻辑里最容易出bug的模块,我在课程设计阶段先后翻过三次车,最后总结出来的铁律是:数据、界面、状态三者永远同步修改,不允许只改其一。

6. 二次开发:给五子棋加一个能用的功能和改造顺序

拿到这个项目后想改造,我的建议是不要一上来就想着加「联机对战」这种大功能,应该从三个维度里挑一个:加深游戏逻辑(比如禁手规则)、加交互体验(比如落子音效)、加辅助系统(比如保存棋局)。我实操下来觉得性价比最高的是保存/加载棋局功能,它逼着你理解序列化和数据持久化,但代码量又不大。思路很简单:把board[15][15]这个数组写进一个文本文件,每行15个数字,0/1/2分别代表空、黑、白,再额外存一个当前轮到谁走的字段。加载时读文件重新填数组,再触发一次重绘就能恢复棋局。

// 保存棋局到文件 void save_game(int board[15][15], int current_player) { FILE *fp = fopen("gobang_save.txt", "w"); if (fp == NULL) return; fprintf(fp, "%d\n", current_player); // 第一行存当前玩家 for (int row = 0; row < 15; row++) { for (int col = 0; col < 15; col++) { fprintf(fp, "%d ", board[row][col]); } fprintf(fp, "\n"); } fclose(fp); }

加载函数就是反着读一遍文件,每行用fscanf按整数读入数组,读完后把current_player恢复成文件里记录的值,最后设game_over = 0让棋盘重新可交互。这里有一个细节:落子历史栈是否需要保存?如果你不保存悔棋栈,加载后的棋局就不能悔棋——但反过来想,这也可以当成一个「加载后禁止悔棋」的模式,算是取舍。我个人建议把历史栈也一起写进文件,文件结构改成「棋盘数据 + 历史栈长度 + 栈内容」,这样加载后能完整恢复棋局状态,连悔棋都好使。

改造完保存功能后,下一个顺手的改造是给AI加一个「难度分级」。做法是把AI综合得分里的防守系数做成可配置参数,简单模式设成0.8——AI更莽,喜欢进攻不防守;普通模式保持1.2;困难模式设成1.6——AI疯狂防守,你一有三连它就开始围堵。这个改动只需要改ai_player.c里的一个变量,UI层加三个按钮就行。

这个项目我拆过不止一遍,每次带新人看源码都会强调同一件事:先把board数组吃透,再去碰绘图和消息循环——数据模型是整个程序的定海神针。多少人在「鼠标坐标转索引」这个换算上栽过跟头,多少人把count初始值写错导致五连判定漏判,说实话我自己也犯过一模一样的错,最后是一步一步加调试输出才定位到问题。从那以后我每次拿到一个图形交互的C语言项目,都强制自己先画一张「数据流图」——从鼠标事件到函数调用再到数据变更,捋清楚再动手改代码。这次我把这些经验原原本本写出来,希望能帮到正要被五子棋课设折磨的你。

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

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

C# WinForms 接口测试工具:HttpClient 与 async/await 实战指南

简介&#xff1a;这份C#编写的接口测试工具以Windows Forms&#xff08;WinForm&#xff09;为图形界面&#xff0c;支持POST、GET、PUT、DELETE四种常见HTTP请求&#xff0c;适用于开发者在接口调试、联调或验收阶段快速验证请求参数、状态码与响应数据。资源为RAR压缩包&…

作者头像 李华
网站建设 2026/10/21 14:43:17

Wireshark网络抓包实战:Ubuntu下TCP/HTTP/DNS排障指南

1. 项目概述与准备工作1.1 为什么用 Wireshark 看网络流量网络问题是我日常排障里最头疼的一类。你盯着服务日志&#xff0c;看到的是应用层吐出来的状态&#xff1b;但真正在网络上发生了什么&#xff0c;往往被层层缓冲和重试机制遮住了。举一个我遇到过的场景&#xff1a;某…

作者头像 李华
网站建设 2026/10/21 14:40:35

n8n+Claude Skills:3分钟自动生成小红书美食手账图文流水线

上个月我在整理手机相册的时候发现&#xff0c;自己拍的美食图少说也有两三百张&#xff0c;真正发成小红书笔记的却不到十分之一。不是不想发&#xff0c;是每次做手账都太折磨人。打开设计工具、挑模板、调字体、费劲想文案、再一张张手动排版&#xff0c;快的时候二十多分钟…

作者头像 李华
网站建设 2026/10/21 14:39:15

企业级AI智能体安全体系实战:从提示注入防护到全链路审计

1. 为什么企业AI智能体需要一套独立的安全体系 先抛一个我在实际项目里反复遇到的场景&#xff1a;一个客服型AI智能体&#xff0c;接了公司CRM、订单系统和内部知识库&#xff0c;上线三个月表现一直很稳。某天有用户发现&#xff0c;只要在对话里输入一段精心构造的指令&…

作者头像 李华
网站建设 2026/10/21 14:39:09

从零搭建skills能力仓库:技能单元封装、注册与调用实践

1. 当“skills”成为一个项目标题&#xff1a;我看到的不是技能清单&#xff0c;而是一套能力封装系统第一次看到“skills”这个标题时&#xff0c;我脑子里冒出来的第一个念头是&#xff1a;这大概率不是一个教人“如何学习某项技能”的教程合集&#xff0c;而是一个把“技能”…

作者头像 李华
网站建设 2026/10/21 14:38:27

用Proxy打造轻量响应式状态管理:rea核心设计与实现

最近在做前端项目时&#xff0c;状态管理这块总是绕不过去。从早期的Redux&#xff0c;到后来的Mobx、Zustand&#xff0c;方案越出越多&#xff0c;但项目里的状态代码却越来越重。尤其在维护一个中大型后台系统时&#xff0c;跨组件共享数据、复杂异步更新、表单联动这些场景…

作者头像 李华