第一个C语言项目——AI辅助扫雷游戏实现
一、为什么选择扫雷作为第一个项目
前段时间刚学完分支循环、数组和函数,总觉得自己"学得会,用不出"。于是决定动手做一个控制台版的扫雷——把学到的东西真正用起来。
这篇博客记录的就是这个过程:从控制台版本的实现,到后来尝试用 AI 辅助做一个界面版本。 不敢说是什么教程,更像是一份学习路上的阶段性总结。
二、控制台版本扫雷游戏的实现
2.1 文件创建
创建main.c、game.c、game.h文件。将项目的实现逻辑放入main.c文件,游戏的功能放入game.c文件,利用game.h头文件将两个源文件联系起来。
2.2 菜单操作
首先打印菜单,我们可以使用一个函数封装打印菜单的操作。
void menu() {
printf("------------------\n");
printf("------1.start-----\n");
printf("------0.exit------\n");
printf("------------------\n");
}
在main函数中调用menu,运行结果如下
接着使用scanf函数和switch函数,根据用户输入的数字开始游戏或退出游戏。
int choose = 0;
scanf("%d", &choose);
switch (choose) {
case 1:
//game();
printf("游戏中");
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入无效,请重新输入");
}
game()函数还未定义,故把game函数注释,测试用户选择功能是否正常。运行结果如下:


用户可以正常选择。但我们发现:无效输入时程序会直接退出,而不是让用户重新输入;用户结束游戏时,程序也会直接退出,不能选择是否继续玩。
当程序开始运行时,至少打印一次菜单,所以使用do-while循环解决这个问题会是一个可行的办法。
int choose = 0;
do
{
menu();//打印菜单
//用户选择
scanf("%d", &choose);
switch (choose) {
case 1:
//game();
printf("游戏中\n");
printf("不过瘾可以再来一把哟\n");
break;
case 0:
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。
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
为什么要定义ROWS和COLS?
在创建数组时,不能简单地创建如show[ROW][COL] = … 假设数组长度这么设置,会给游戏留下隐患。举个例子,若玩家选择边缘处的坐标,程序在判断周围是否有雷时会越界访问数组。正因如此,可以这么创建数组:
char show[ROWS][COLS] = { 0 };
char play[ROWS][COLS] = { 0 };
这么创建数组,给数组留了缓冲区,解决了越界访问数组的问题。
2.3.2 初始化棋盘
当创建好了棋盘之后,就可以对棋盘进行初始化了。在show棋盘中,可以用*表示未被翻开的格子;在play棋盘中,用字符‘1’表示有雷,‘0’表示没雷。
初始化如下:
void init_board(char arr[ROWS][COLS],char s) {
//遍历棋盘并写入指定字符
for (int i = 0;i <ROWS ;i++) {
for (int j = 0;j < COLS;j++) {
arr[i][j] = s;
}
}
}
对所有数组元素初始化,这样,当边缘处格子被选择时,程序扫描缓冲区时与扫描’0’无异。
2.3.3 打印show棋盘
打印show棋盘主体只需要两个for循环,但棋盘展示不能只打印主体部分。为了让游戏更有可玩性,可以参考如下打印方式:
void print_board(char arr[ROWS][COLS], int r, int c) {
//打印顶部数字且使用|分隔每个坐标,方便查找坐标
for (int i = 0;i <= c;i++) {
printf("%d| ",i);
}
printf("\n");
//打印棋盘主体部分
for (int i = 1;i <= r;i++) {
//打印每行前先打印一行-|-,起到分隔作用,与顶部对齐
for (int k = 0;k <= c;k++) {
printf("-|-");
}
printf("\n");
printf("%d| ", i);//在每行前加上数字,方便查找坐标
for (int j = 1;j <= c;j++) {
printf("%c| ", arr[i][j]);
}
printf("\n");
}
}
将该打印方式封装成一个函数print_board。
运行结果如下:
已经成功打印棋盘,我们就可以进入下一步了。
2.3.4 布置雷
要想布置雷,首先确定雷的个数。定义雷数默认10。
#define MINES 10
雷的个数确定之后,我们不难想到,利用循环和随机数生成10个随机坐标并布置在play棋盘中。
void set_mine(char arr[ROWS][COLS]) {
int count = MINES;
while (count--) {
//生成两个1-9的随机数表示行列
int a = 1 + rand() % 9;//a表示所在行
int b = 1 + rand() % 9;//b表示所在列
//printf("%d ", a);
arr[a][b] = '1';//对随机坐标写入'1'布置雷
}
}
在main函数利用当前时间戳设置随机数种子
srand((unsigned int)time(NULL));
此时布置雷已经写好。要想知道是否布置成功可以打印play棋盘。运行结果如下:
不难发现随机雷已经布置成功。当然,也可以先布置雷后打印棋盘,且更有条理。
2.3.5 玩家扫雷过程框架
当菜单,初始化,布置雷功能可以正常运行,就可以开始处理游戏过程。游戏过程中,玩家不断输入坐标进行多次排雷操作,每次排雷成功立即执行下一次排雷,直到被炸死或排雷完成。
int count = 0;
printf("输入格式:行 列\n");
printf("示例:3 4\n");
while (1) {
//打印show棋盘
print_board(show, ROW, COL);
int r = 0;
int c = 0;
scanf("%d %d", &r, &c);//键盘输入坐标
//判断输入坐标是否合法
if (show[r][c] == '*' && r <= ROW && r > 0 && c <= COL && c > 0) {
//判断输入坐标是否为雷
if (play[r][c] == '0') {
printf("排雷成功\n");
}
else {
printf("你已被炸死\n");
break;
}
}
else {
printf("非法输入,排雷失败\n");
}
}
把打印棋盘的功能放入了循环,玩家每次排雷成功都能刷新棋盘。
外层if判断输入坐标是否为非法输入。外层if判断输入坐标是否有雷。
创建了一个count计数器,方便之后通过计算排雷数判定游戏胜利。
除游戏胜利以外,进行功能测试:


2.3.6 扫描
玩家输入坐标,程序立即计算周围雷数,并将“*”替换为周围雷个数。在替换之前,需要先进行搜索,计算。
可以将这个过程封装成一个返回值为char的函数,若将play数组以及玩家输入的坐标传入该函数,即可计算周围的’1’的个数并返回。
参考写法:
char search_mine(char arr[ROWS][COLS], int a, int b) {
//a,b表示传入行列
//sum变量存储附近总雷数
char sum = 0;
//计算附近3*3格子的雷数
for (int i = -1;i <= 1;i++) {
for (int j = -1;j <= 1;j++) {
sum += arr[i + a][j + b] - '0';
}
}
return sum;
}
2.3.7 替换
计算完整块区域的雷总数,就可以将原本的"*"替换为雷的总数。不妨把这个过程封装成一个void函数。
void sweep_mine(char show[ROWS][COLS],char play[ROWS][COLS], int a, int b) {
char sum = search_mine(play, a, b);
show[a][b] = '0' + sum;
}
将show和play数组分别传入该函数的参数1,参数2
用sum接收返回的雷数,并对show数组做修改。
通过运行测试修改是否成功:
2.3.8 胜利判定
前面已经创建了count计数器,但还没有使用。要想判断玩家是否把雷全部排完,此时就要用到计数器统计排雷次数,即每次排雷成功count+1,当count达到上限,游戏结束。
在game函数判断是否有雷的if分支中,可以新增一个if-else分支。
参考代码如下:
//判断输入坐标是否为雷
if (play[r][c] == '0') {
sweep_mine(show,play,r,c);
count++;
printf("排雷成功\n");
//判断是否排完雷
if (count == ROW * COL - MINES) {
printf("你赢了,游戏结束\n");
break;
}
将地雷数MINES改成80,测试一下胜利判定是否生效:
至此,扫雷游戏的基础功能已全部实现。
2.4 优化
2.4.1 炸开一片
实现了扫雷游戏的基础功能之后,就可以运行使用。然而,在使用的过程中,我们发现:每次游戏进行得太久。
由于每次排雷只能选择选择一个格子,且当某格子显示0时,其周围的3*3区域完全没有必要手动翻开。
所以,修改sweep_mine函数,当排雷位置周围无雷时,自动向外扩张。
void sweep_mine(char show[ROWS][COLS],char play[ROWS][COLS], int a, int b) {
if (a < 1 || a>9 || b < 1 || b>9 || show[a][b] != '*') return;
char sum = search_mine(play, a, b);
show[a][b] = '0' + sum;
count++;
if (show[a][b] == '0') {
for (int i = -1;i <= 1;i++) {
for (int j = -1;j <= 1;j++) {
sweep_mine(show, play, a + i, b + j);
}
}
}
}
在此基础上,把count设为全局变量,并删除在game函数中的count++。
修改后,每次选择到周围雷数为0的格子,对整块3*3区域重新调用sweep_mine函数。每次进入函数a,b被判断是否合法,是否已经被排。在show棋盘,每次"*"被修改为数字,count+1。
不妨将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_msvc16
ai实现:
运行:

四、结语
控制台版靠硬写,界面版靠 AI 搭骨架、自己填血肉。同样的功能,开发周期从"周"压缩到"天"——这不是偷懒,是把时间花在了真正该思考的地方。
更多推荐

所有评论(0)