ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

第一个C语言项目——AI辅助扫雷游戏实现

2026/8/26 15:39:16 拓冰建站 浏览量
第一个C语言项目——AI辅助扫雷游戏实现 一、为什么选择扫雷作为第一个项目前段时间刚学完分支循环、数组和函数总觉得自己学得会用不出。于是决定动手做一个控制台版的扫雷——把学到的东西真正用起来。这篇博客记录的就是这个过程从控制台版本的实现到后来尝试用 AI 辅助做一个界面版本。不敢说是什么教程更像是一份学习路上的阶段性总结。二、控制台版本扫雷游戏的实现2.1 文件创建创建main.c、game.c、game.h文件。将项目的实现逻辑放入main.c文件游戏的功能放入game.c文件利用game.h头文件将两个源文件联系起来。2.2 菜单操作首先打印菜单我们可以使用一个函数封装打印菜单的操作。voidmenu(){printf(------------------\n);printf(------1.start-----\n);printf(------0.exit------\n);printf(------------------\n);}在main函数中调用menu运行结果如下接着使用scanf函数和switch函数根据用户输入的数字开始游戏或退出游戏。intchoose0;scanf(%d,choose);switch(choose){case1://game();printf(游戏中);break;case0:printf(退出游戏);break;default:printf(输入无效请重新输入);}game()函数还未定义故把game函数注释测试用户选择功能是否正常。运行结果如下用户可以正常选择。但我们发现无效输入时程序会直接退出而不是让用户重新输入用户结束游戏时程序也会直接退出不能选择是否继续玩。当程序开始运行时至少打印一次菜单所以使用do-while循环解决这个问题会是一个可行的办法。intchoose0;do{menu();//打印菜单//用户选择scanf(%d,choose);switch(choose){case1://game();printf(游戏中\n);printf(不过瘾可以再来一把哟\n);break;case0:printf(退出游戏\n);break;default:printf(输入无效请重新输入\n);}printf(\n);}while(choose);从上面的代码我们不难发现。输入的值不仅可以让程序进入相对应的分支还能控制循环条件。若选择0退出游戏的同时循环也会退出若选择1结束游戏时重新进入循环选择若无效输入choose为非零整数依然为真即可以重新进入循环。运行结果如下至此菜单操作功能完善。2.3 游戏功能实现接下来我们在game.c中实现游戏功能并在game.h中声明。2.3.1 棋盘创建在进行扫雷游戏时玩家不能看到雷的位置但游戏的进行需要雷的位置。因此我们需要创建两个棋盘。一个棋盘让玩家只能看见附近3*3的雷的个数另一个棋盘知道所有雷的位置。使用二维数组创建棋盘。棋盘棋盘的行列数默认为9且被定义为ROW和COL。并定义ROWS和COLS。#defineROW9#defineCOL9#defineROWSROW2#defineCOLSCOL2为什么要定义ROWS和COLS在创建数组时不能简单地创建如show[ROW][COL] … 假设数组长度这么设置会给游戏留下隐患。举个例子若玩家选择边缘处的坐标程序在判断周围是否有雷时会越界访问数组。正因如此可以这么创建数组:charshow[ROWS][COLS]{0};charplay[ROWS][COLS]{0};这么创建数组给数组留了缓冲区解决了越界访问数组的问题。2.3.2 初始化棋盘当创建好了棋盘之后就可以对棋盘进行初始化了。在show棋盘中可以用*表示未被翻开的格子在play棋盘中用字符‘1’表示有雷‘0’表示没雷。初始化如下voidinit_board(chararr[ROWS][COLS],chars){//遍历棋盘并写入指定字符for(inti0;iROWS;i){for(intj0;jCOLS;j){arr[i][j]s;}}}对所有数组元素初始化这样当边缘处格子被选择时程序扫描缓冲区时与扫描’0’无异。2.3.3 打印show棋盘打印show棋盘主体只需要两个for循环但棋盘展示不能只打印主体部分。为了让游戏更有可玩性可以参考如下打印方式voidprint_board(chararr[ROWS][COLS],intr,intc){//打印顶部数字且使用|分隔每个坐标方便查找坐标for(inti0;ic;i){printf(%d| ,i);}printf(\n);//打印棋盘主体部分for(inti1;ir;i){//打印每行前先打印一行-|-起到分隔作用与顶部对齐for(intk0;kc;k){printf(-|-);}printf(\n);printf(%d| ,i);//在每行前加上数字方便查找坐标for(intj1;jc;j){printf(%c| ,arr[i][j]);}printf(\n);}}将该打印方式封装成一个函数print_board。运行结果如下已经成功打印棋盘我们就可以进入下一步了。2.3.4 布置雷要想布置雷首先确定雷的个数。定义雷数默认10。#defineMINES10雷的个数确定之后我们不难想到利用循环和随机数生成10个随机坐标并布置在play棋盘中。voidset_mine(chararr[ROWS][COLS]){intcountMINES;while(count--){//生成两个1-9的随机数表示行列inta1rand()%9;//a表示所在行intb1rand()%9;//b表示所在列//printf(%d , a);arr[a][b]1;//对随机坐标写入1布置雷}}在main函数利用当前时间戳设置随机数种子srand((unsignedint)time(NULL));此时布置雷已经写好。要想知道是否布置成功可以打印play棋盘。运行结果如下:不难发现随机雷已经布置成功。当然也可以先布置雷后打印棋盘且更有条理。2.3.5 玩家扫雷过程框架当菜单初始化布置雷功能可以正常运行就可以开始处理游戏过程。游戏过程中玩家不断输入坐标进行多次排雷操作每次排雷成功立即执行下一次排雷直到被炸死或排雷完成。intcount0;printf(输入格式:行 列\n);printf(示例:3 4\n);while(1){//打印show棋盘print_board(show,ROW,COL);intr0;intc0;scanf(%d %d,r,c);//键盘输入坐标//判断输入坐标是否合法if(show[r][c]*rROWr0cCOLc0){//判断输入坐标是否为雷if(play[r][c]0){printf(排雷成功\n);}else{printf(你已被炸死\n);break;}}else{printf(非法输入排雷失败\n);}}把打印棋盘的功能放入了循环玩家每次排雷成功都能刷新棋盘。外层if判断输入坐标是否为非法输入。外层if判断输入坐标是否有雷。创建了一个count计数器方便之后通过计算排雷数判定游戏胜利。除游戏胜利以外进行功能测试2.3.6 扫描玩家输入坐标程序立即计算周围雷数并将“*”替换为周围雷个数。在替换之前需要先进行搜索计算。可以将这个过程封装成一个返回值为char的函数若将play数组以及玩家输入的坐标传入该函数即可计算周围的’1’的个数并返回。参考写法:charsearch_mine(chararr[ROWS][COLS],inta,intb){//a,b表示传入行列//sum变量存储附近总雷数charsum0;//计算附近3*3格子的雷数for(inti-1;i1;i){for(intj-1;j1;j){sumarr[ia][jb]-0;}}returnsum;}2.3.7 替换计算完整块区域的雷总数就可以将原本的*替换为雷的总数。不妨把这个过程封装成一个void函数。voidsweep_mine(charshow[ROWS][COLS],charplay[ROWS][COLS],inta,intb){charsumsearch_mine(play,a,b);show[a][b]0sum;}将show和play数组分别传入该函数的参数1参数2用sum接收返回的雷数并对show数组做修改。通过运行测试修改是否成功2.3.8 胜利判定前面已经创建了count计数器但还没有使用。要想判断玩家是否把雷全部排完此时就要用到计数器统计排雷次数即每次排雷成功count1当count达到上限游戏结束。在game函数判断是否有雷的if分支中可以新增一个if-else分支。参考代码如下//判断输入坐标是否为雷if(play[r][c]0){sweep_mine(show,play,r,c);count;printf(排雷成功\n);//判断是否排完雷if(countROW*COL-MINES){printf(你赢了游戏结束\n);break;}将地雷数MINES改成80测试一下胜利判定是否生效至此扫雷游戏的基础功能已全部实现。2.4 优化2.4.1 炸开一片实现了扫雷游戏的基础功能之后就可以运行使用。然而在使用的过程中我们发现每次游戏进行得太久。由于每次排雷只能选择选择一个格子且当某格子显示0时其周围的3*3区域完全没有必要手动翻开。所以修改sweep_mine函数当排雷位置周围无雷时自动向外扩张。voidsweep_mine(charshow[ROWS][COLS],charplay[ROWS][COLS],inta,intb){if(a1||a9||b1||b9||show[a][b]!*)return;charsumsearch_mine(play,a,b);show[a][b]0sum;count;if(show[a][b]0){for(inti-1;i1;i){for(intj-1;j1;j){sweep_mine(show,play,ai,bj);}}}}在此基础上把count设为全局变量,并删除在game函数中的count。修改后每次选择到周围雷数为0的格子对整块3*3区域重新调用sweep_mine函数。每次进入函数a,b被判断是否合法是否已经被排。在show棋盘每次*被修改为数字count1。不妨将MINES改为5运行测试炸开一片功能程序正常运行。2.4.2 细节优化每次排雷开始前屏幕会打印棋盘也就意味着每玩一次游戏会打印多个棋盘看着不够美观。所以可以每次扫完雷进行一次清屏操作。但随之而来的又是一个小问题清屏操作会把程序刚刚打印的“排雷成功”清除。所以我们可以在每次排雷结束时等待半秒进入清屏操作。只需在game函数的while循环末尾添加如下代码Sleep(500);system(cls);以上操作完成控制台版扫雷游戏彻底完成。三、界面版本扫雷游戏实现完成了控制台版本之后就可以尝试利用AI做出一个带界面扫雷游戏。以下操作使用deepseek最近发布的deepseek harness。选择deepseek V4 flash模型。发送ai提示词在尽量少改动代码的基础上使⽤raylib库将扫雷游戏改造成带界⾯的版本raylib我已经安装好了路径是C:\raylib\raylib-6.0_win64_msvc16ai实现运行四、结语控制台版靠硬写界面版靠 AI 搭骨架、自己填血肉。同样的功能开发周期从周压缩到天——这不是偷懒是把时间花在了真正该思考的地方。