1200字范文,内容丰富有趣,写作的好帮手!
1200字范文 > 【C语言】百玩不腻的扫雷小游戏(初阶到进阶)

【C语言】百玩不腻的扫雷小游戏(初阶到进阶)

时间:2023-11-18 17:42:33

相关推荐

【C语言】百玩不腻的扫雷小游戏(初阶到进阶)

文章目录

一、前言二、准备工作三、展示初始菜单四、核心内容:game函数的实现❗1.初始化扫雷地图(三个“各显神通”的地图)2.埋下地雷💣3.展示扫雷地图的实现4.重点!玩家扫雷的实现1️⃣ 初阶版2️⃣ 进阶版(含递归展开)(I)标记功能的实现(II)展开功能的实现 5.判断玩家扫雷成功6.总体代码展示 五、总结

一、前言

🎐🎐想必大家玩过一款经典的小游戏——扫雷吧!扫雷小游戏是一款大众类的益智小游戏,1992年微软发布的Windows3.1加入该游戏,从那时起便风靡全球。由于它既不用网络也不用下载,当年都是电脑自带的,成为的初高中生微机课的必备小游戏哈哈哈哈(俺也一样)。前面我们实现了三子棋小游戏,那么,在学习了C语言的知识以后,我们又是否能够自己实现一个扫雷小游戏呢?❗❗本文章将手把手教你如何用C语言实现扫雷小游戏!(文章很长,前后贯通性较强,请耐心品尝哦!)

二、准备工作

老规矩,因为工程量比较大,所以我们新建两个源文件和一个头文件来实现我们的工程。这一步是为了防止函数过多,越往后面写越混乱了,也是为了更好地完善和改进我们的工程

📌具体操作如下:

新建一个项目后,创建一个后缀为.c的test.c源文件,用于实现主函数和一些主要操作。

接着创建一个后缀为.c的game.c源文件,封装实现扫雷游戏中的各种函数。

最后创建一个game.h头文件,用于声明game.c中的所有函数。

准备工作已经完成啦!

下面让我们正式开始扫雷游戏的实现!

三、展示初始菜单

🎈游戏初始菜单是面向玩家的第一个面板,必须让玩家清晰的知道下一步要怎么做,有什么选择,因此我们要让玩家输入自己的选择,并根据他的选择,系统会进行相应的下一步。那么这里我们的思路是:先显示一个面板让玩家看到他应该如何进行自己的选择(如输入1就是开始游戏,输入0就是退出游戏),然后我们再利用选择结构,根据玩家作出的选择做出相应的回应。

📌具体操作如下:

void menu(){printf("******************************\n");printf("********* 1.play ************\n");printf("********* 0.exit ************\n");printf("******************************\n");}int main(){int input = 0;do{menu();printf("请输入:>");switch (input){case 1:game();//输入为1,进行game函数(函数内容后面实现)break;case 0:printf("退出游戏\n");//输入为0,退出游戏break;default:printf("输入错误,请重新输入\n");//输入为其他数字,重新输入break;}} while (input);return 0;}

这里利用了循环,给了玩家输入错误之后可以重新输入的机会。而利用输入的变量input作为循环条件表达式,巧妙地运用了C语言中0为假、非0为真的原理,当玩家输入0时循环结束,而输入其他数字时循环依然继续。

📌测试结果如下:

本以为到这里我们的第一步已经完成了,但当我测试过程中不小心输入了一个字母而不是数字时,出现了下面的情况📌

错误输入一个字符之后,直接退出游戏:

而输入一个非法数字之后再输入一个字符,会进入死循环:

为什么会这样子

这是因为,当我们输入了一个非数字的字符时,scanf函数无法从输入缓冲区中读取数据存进变量input中,直接跳过不执行。跳过之后input的值依然是初始值0,所以进入switch中便执行相应的case 0情况,游戏结束,跳出游戏。

而在(第一次)输入一个非法数字后,input改变了(比如动图中第一次输入的值为数字3,则input变为3),系统提示输入错误之后进入下一个循环(第二次),而我们第二次输入的则是一个非法的字符,scanf依然无法读取,系统直接进入switch判断,此时input的值依然为3,依然提示输错误。再进入下一次循环(第三次),由于输入缓冲区里依然有内容(既输入的非法字符),scanf依然无法读取,直接跳过,玩家无法重新输入,系统再次提示输入错误。如此循环往复,便造成了死循环。

如何修正这个bug

这里我们有一个思路,每次输入之后检测scanf的返回值是否为1(scanf的返回值为已成功读入的参数列表中的项数)。如果为1则正常执行switch语句;如果不为1,则需要将缓冲区中的字符先清空,确保玩家能够重新输入。

具体代码如下📌

while (1){if (scanf("%d", &input) != 1){printf("输入非法,请重新输入:");char ch = '0';while ((ch = getchar()) != '\n'){;}}elsebreak;}

运行效果演示⭕

🎈🎈这样,我们就很好地避免了玩家输入错误的情况,让玩家有更好的游戏体验~~

四、核心内容:game函数的实现❗

当玩家输入1之后,那么就进入我们激动人心的时刻——扫雷开始!!接下来是game函数整体游戏逻辑的

大体思路:

第一步:我们首先需要设置扫雷的地图,就像我们平时玩扫雷游戏那样有一个网格状的地图。

第二步:向地图中埋下地雷(由于埋下的地雷是不能给玩家看到的,所以我们可以设置两张地图分别用于隐藏地雷和展示给玩家操作)。

第三步:实现玩家扫雷的功能,并在每次扫雷之后判断玩家输赢。

第四步:在玩家每次进行操作后(扫描坐标、标记坐标等),展现出玩家操作改变后的地图。

有了这样的大体思路后,接下来我们就来看具体如何实现吧🎈

1.初始化扫雷地图(三个“各显神通”的地图)

我们先从9×9的地图下手,从易到难,实现之后我们就可以任意修改扫雷的难度

🧐这是一个9×9的扫雷地图,我们可以看到,它是由横9行纵9列组成的,根据学过的知识我们很容易能想到,我们可以创建一个二维数组的储存扫雷中的各种信息。而由于埋下的地雷是不能给玩家看到的,所以我们可以设置两张地图,也就是创建两个数组,分别用于储存隐藏地雷的信息和回馈给玩家他每一步操作后的信息。

创建完数组之后我们需要初始化我们的数组,这里我们可以规定,地雷是字符‘1‘,无地雷是字符‘0’,在初始化的时候我们只需把二维数组中的所有元素都赋为字符’0‘就可以了。

小细节💡:如果我们定义直接定义9×9的地图,那么后期在判断一个格子周围八个格子中有多少个地雷时,如果判断的格子处于地图最外圈(也就是边界),这时候会出现数组越界的问题,所以为了保险起见,我们不妨直接定义一个11×11的地图(既在9×9地图外围加一圈),而在埋雷的时候只在9×9的范围内执行,这样就能很好地防止出现数组越界的问题。

🎈由于这里的9和11在工程中会大量使用,我们不妨在头文件game.h中直接宏定义两个值为9的ROW和COL标识符常量,再宏定义两个ROWS和COLS(它们的值是分别在ROW和COL的基础上加上2),方便后期修改,增加代码可读性。

(注意:在源文件中想要引用头文件中宏定义的标识符常量,源文件中必须包含头文件)

🎉下面就可以愉快地敲代码啦!!

具体代码如下📌

//在game函数中定义了三个二维数组void game(){char mine[ROWS][COLS] = {'0' };//隐藏埋雷的信息的数组char show[ROWS][COLS] = {'0' };//展现给玩家看的数组char storage[ROWS][COLS] = {'0' };//这是哪冒出来的??不要急!往下看看!Initial(mine, storage,show, ROWS, COLS);//给三个数组初始化(11*11 初始化)}

//遍历数组初始化void Initial(char mine[ROWS][COLS], char storage[ROWS][COLS], char show[ROWS][COLS], int rows, int cols){int i = 0;int j = 0;for (i = 0;i < rows;i++)//初始化{for (j = 0;j < cols;j++){mine[i][j] = '0';storage[i][j] = '0';show[i][j] = '*';//show地图是给玩家看的,初始化为'*',增加神秘感}}}

来了嗷!!前面一直在说创建两个二维数组,而代码中突然冒出来一个新的数组storage,这个数组有什么用呢❓

🎐这里的storage起到存储玩家扫过位置的功能,在后面玩家扫雷时如果扫到了已经扫过的位置,这时storage便可以派上用场。在递归展开(后面介绍)的过程中storage也大有用途,这里我们先准备好,后面自有大用!

🎐 至此我们就很好地创建并初始化了三个地图,分别是:mine数组埋雷地图、show数组展示地图、storage储存已扫描坐标地图。呼应了小标题中三个“各显神通”的地图

2.埋下地雷💣

🎈有了最基础的地图之后,我们就要进行关键的一步!埋下地雷!扫雷扫雷,哪么扫的雷从哪里来的,放到哪里去,都需要我们来实现。那么如何埋下地雷呢?我们知道,每开一把扫雷游戏,地雷的位置都不一样,什么可以实现每次都不一样呢?

🎈没错!又是我们熟悉的随机数。利用随机数生成一组坐标,每次在9×9的范围内埋下不同位置的一组地雷,就可以很好地实现我们的埋雷功能了。(注意:这里的埋雷是在mine数组中埋,也就是我们前面创建的隐藏埋雷的信息的数组)

🎈如果想要调整不同难度,增加或减少雷的数量,我们可以宏定义一个标识符常量Mine_num,想要调整难度的时候修改它的值就ok了。如下图。(这里我们以10个雷为例)

具体代码如下📌

void game(){char mine[ROWS][COLS] = {'0' };//隐藏埋雷的信息的数组char show[ROWS][COLS] = {'0' };//展现给玩家看的数组char storage[ROWS][COLS] = {'0' };//存储玩家已点开的坐标的信息Initial(mine, storage,show, ROWS, COLS);//给show、mine和storage初始化(11*11 初始化)SetMine(mine, ROW, COL);//埋雷}

void SetMine(char mine[ROWS][COLS], int row, int col)//埋雷{int x = 0;int y = 0;srand((unsigned int)time(NULL));//时间作为随机数的种子,每一时刻都不同,保证了产生数的随机性int count = Mine_num;//可以理解为现在我们拥有的雷的数量(10个)while (count){x = rand() % row + 1;y = rand() % col + 1;if (mine[x][y] == '0'){mine[x][y] = '1';//地雷是字符'1'count--;}}//如果生成的随机坐标上没有雷,便埋雷,我们拥有的雷数减一;如果该坐标有雷,则跳过重新生成坐标//直到我们的雷埋完为止(既count为0,0为假,跳出循环)}

当然,使用srand和rand这对兄弟函数还有time函数别忘了包含头文件哦!它们的头文件分别是stdlib.h和time.h,如下图:

小细节💡:既然我们的源文件都需要包含头文件来引用我们前面宏定义的标识符常量,那我们不妨将库函数头文件全部在game.h函数中包含,这样我们在test.c和game.c两个源文件中只需要包含game.h头文件即可。(注意包含自定义头文件加的是双引号""而不是尖括号<>)。如下图:

game.h头文件包含的库函数头文件和宏定义的常量一览:

📍📍

3.展示扫雷地图的实现

🎈至此,我们已经有了三张“各显神通”的地图(其实就是三个相应数组构成的地图),并且在其中的mine地图中埋下了危险的地雷💣前面可以说都是扫雷游戏的准备工作,那么接下来才是真正玩家开始游玩的阶段!

💡玩家要想知道怎么扫雷,有哪里可以扫雷,那么就得像我们平时玩扫雷一样,一开始就能看到一个扫雷的地图,然后我们再通过自己的推理一步一步地在这个地图上操作,每一次操作后再呈现出一个对应操作后改变的地图。

就像这样:

如何用代码打印出一个类似的地图呢❓先放效果图🔻

玩家未操作的初始地图:

玩家操作后的地图:

💡这里可以看到,我们可以用字符’|‘和’-'来组成这么一个网格状的地图,而在每一个格子存放着我们前面创建的show数组中的元素,这样玩家扫雷时,就是通过改变show数组中的元素,使得每次操作后的地图都有所不同。当然,我们除了要在每一次玩家扫雷操作完成后呈现show数组展示地图,在玩家被地雷炸死的时候也要展示一下我们的mine数组埋雷地图,也就是让玩家看一下地雷的位置,让玩家“死得明白”😁/doge。有了这个思路,让我们来看一下具体如何实现吧!

🔎原理:用双重for循环遍历打印。

外循环每次打印一组,一共九组(视格子行和分割线行为一组,如图中红框部分)

内循环每次打印一格,一个九格(视一个小格子和它底下的小分割行为一格,如图中黄框部分)

具体代码如下📌

void Display(char arr[ROWS][COLS], int row, int col)//传给形参arr的实参可以是show数组也可以是mine数组{int i = 0;int j = 0;for (i = 1;i <= row;i++)//外循环{//内循环//打印变化的数组元素for (j = 1;j <= col;j++){printf(" %c ", arr[i][j]);//arr[i][j]中的字符每次都可以变化,保证了地图的可变性(关键)!!!!!if (j <= col - 1)//美观起见,控制最后一个小格子的竖杆不打印{printf("|");}}printf("\n");//不要忘记换行哦!//打印分割线if (i <= row - 1)//美观起见,控制最后一行下面的分割线不打印{for (j = 1;j <= col;j++){printf("---");if (j <= col - 1)//美观起见,控制最后一个小格子的竖杆不打印{printf("|");}}}printf("\n");//不要忘记换行哦!}}

✨为了玩家更容易判断自己想要扫描的坐标的具体位置,增加游戏体验,这里我们可以加上行号和列号。

void Display(char arr[ROWS][COLS], int row, int col){int i = 0;int j = 0;for (j = 0;j <= col;j++)//打印列序号{printf("%2d ", j);//这里设置为数字域宽为2,防止列数为两位数时影响总体观感if (j <= col - 1){printf(" ");//列序号后面不用打印竖杆,将竖杆替换为空格即可(注意这有一个空格,对应一个竖杆)}}printf("\n");//for (i = 1;i <= row;i++){for (j = 0;j <= col;j++)//这里将循环变量j的初值设为0,给列序号挪出一块位置。{if (j == 0)//打印行序号{printf("%2d ", i);//这里设置为数字域宽为2,防止行数为两位数时影响总体观感}else{printf(" %c ", arr[i][j]);if (j <= col - 1){printf("|");}}}printf("\n");//if (i <= row - 1){for (j = 0;j <= col;j++){if (j == 0){printf(" ");//行序号下面不用打印分割线,将分割线替换为空格即可(注意这有三个空格,对应"---")}else{printf("---");if (j <= col - 1){printf("|");}}}}printf("\n");}}

运行效果演示⭕

将地图修改为12×12,效果也是一样的

(注:本文后期以10×10的地图为例)

🆗这样之后,我们就可以在游戏中需要展示的地方加上我们的Display函数来展示我们的地图了。比如,在游戏开始之前展示一下初始化的地图,就像这样:

void game(){char mine[ROWS][COLS] = {'0' };//隐藏埋雷的信息的数组char show[ROWS][COLS] = {'0' };//展现给玩家看的数组char storage[ROWS][COLS] = {'0' };//存储玩家已点开的坐标的信息Initial(mine, storage,show, ROWS, COLS);//给show和mine初始化(11*11 初始化)SetMine(mine, ROW, COL);//埋雷Display(show, ROW, COL);//展现给玩家的扫雷地图}

运行效果演示⭕

一切就绪,接下来就是最最最最最最最最最最最最核心的模块了,也就是玩家开始扫雷❗❗❗

4.重点!玩家扫雷的实现

1️⃣ 初阶版

🎈玩家扫雷该如何实现呢?仔细想想其实不难,因为我们已经准备好了很多东西。下面我将说说我的思路:

玩家扫雷肯定不止一次吧?除非第一次扫雷就被炸死了/doge。那么我们就可以用循环来实现这个功能,只能当玩家被炸死或者扫完全部的雷获胜之后,才跳出我们的循环。这是大体的简单思路,接下来直接上代码!用代码说明方法!(还押上韵了/doge)

int is_lose=0;//先定义一个判断玩家是否扫雷失败的变量(0为假非0为真,在玩家被炸死的时候我们改变is_lose的值为非0的值即可)while (1){//进入循环,先让玩家扫一次,这里定义一个Player_SweepMine函数用于玩家扫雷。Player_SweepMine(show, mine, storage, ROW, COL, &is_lose);//注意!这里is_lose变量的传参方式是传址,传址调用才能真正改变变量的值if (is_lose==0)//如果is_lose为假则玩家还没输,进入判断{//判断玩家是否获胜(这里的Is_win函数在后面会将说明,他的功能就是判断是否获胜)if (Is_win(show, ROW, COL)){printf("恭喜你,扫雷成功!\n");//如果获胜则提示后直接跳出game函数return;}else//没有获胜,扫雷(循环)继续continue;}else//如果is_lose的值不为0,意味着玩家被炸死了{printf("很遗憾,你被炸死了\n");//如果失败则提示并展示地图后后直接跳出game函数Display(mine, ROW, COL);return;}}

🎉将它塞到我们的game函数里面吧

void game(){char mine[ROWS][COLS] = {'0' };//隐藏埋雷的信息的数组char show[ROWS][COLS] = {'0' };//展现给玩家看的数组char storage[ROWS][COLS] = {'0' };//存储玩家已点开的坐标的信息Initial(mine, storage,show, ROWS, COLS);//给show和mine初始化(11*11 初始化)SetMine(mine, ROW, COL);//埋雷Display(show, ROW, COL);//展现给玩家的扫雷地图int is_lose = 0;while (1){Player_SweepMine(show, mine, storage, ROW, COL, &is_lose);if (is_lose==0){if (Is_win(show, ROW, COL)){printf("恭喜你,扫雷成功!\n");return;}elsecontinue;}else{printf("很遗憾,你被炸死了\n");Display(mine, ROW, COL);return;}}}

🎐思路很简单吧!下面让我们来看看这里面的重点——Player_SweepMine函数的实现

🔎其实这里的Player_SweepMine函数(初阶版)也不难实现。让我来展开说说❗

第一步:提示玩家,让玩家输入一个想要扫雷的坐标;

第二步:判断该坐标是否在地图范围内,如果超出范围则提示玩家重新输入,在范围内便进行下一步;

第三步:判断该坐标是否已经被扫过,如果被扫过则提示玩家重新输入,直到玩家扫到一个新的坐标再进行下一步;

第四步:判断这个坐标中是否埋有雷,若有则改变is_lose为非0的值并跳出该函数,若没有,则判断周围一圈的雷数,并将这个数字展现在show地图上给玩家看,最后要标志一下这个坐标已经扫过了,再跳出函数。

📌具体代码如下:

void Player_SweepMine(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int* is_lose){int x = 0;int y = 0;printf("请输入坐标:>\n");scanf("%d%d", &x, &y);//玩家输入坐标if ((x >= 1 && x <= row) && (y >= 1 && y <= col))//判断该坐标是否在范围内{if (storage[x][y] == 'X')//判断该坐标是否已经被扫过了{printf("该坐标扫过了,请重新输入\n");}else{if (mine[x][y] == '1')//判断该坐标是否埋有地雷{(*is_lose) = 1;//修改is_lose的值为1,便于理解(既is_lose为真,玩家真的被“炸死了”)return;}else{//计算周围一圈的雷数并展示show地图char count = CountMine(mine, x, y);//这里定义了一个数地雷的函数(这里的雷数是数字字符,不是数字)show[x][y] = count;//将雷数储存到show数组对应的坐标中Display(show, ROW, COL, x, y);//打印展示出show数组构成的地图storage[x][y] = 'X';//标志该坐标已扫过return;}}}else{printf("坐标非法,请重新输入:>\n");//跳出该函数后,依然没法跳出外部循环,则继续进入该函数,玩家可重新输入坐标}}

🎈这里的思路也并不复杂(初阶版嘛,复杂就寄了),这里我们主要看一下完成数地雷功能的函数CountMine是如何实现的。

💡数地雷,就是计算一下这个坐标周围八个坐标中有几个里面是埋有地雷的,那么便要请出我们的mine数组(show数组里面可没有地雷哦)。这里有一种简单的思路,我们定义一个变量count为计数器,双层循环遍历一下这八个坐标,有地雷count加一,没有就跳到下一个坐标,最后,返回count的最终结果即可。

具体代码如下📌

char CountMine(char mine[ROWS][COLS], int x, int y){int count = 0;int i = 0;for (i = x - 1;i <= x + 1;i++){int j = 0;for (j = y - 1;j <= y + 1;j++){if (mine[i][j] == '1')count++;}}return count + 48;//注意这里返回的是字符,而我们的count是一个数字。由ASCII码表可知,字符'0'的ASCII码为48,因此我们在count的基础上加上48,返回的便是对应数字字符的ASCII码值。}

运行效果演示⭕

先打印一个mine地图看看地雷的位置,我们看到坐标(4,4)的周围有两个地雷,下面看看我们的CountMine函数能否成功数出地雷个数❓

🆗成功了!这样我们就实现了数地雷的功能。

💡下面再提供另外一种思路:既然前面我们在函数返回值那里用到了ACSII码值的运算,那我们是否可以直接利用ACSII码值的这种特点计算出坐标周围的雷数呢?答案是可以的。前面我们定义mine数组中无地雷为字符‘0’有地雷为字符‘1’正是为这里埋下伏笔!!

具体思路:(在mine数组中)我们可以将指定坐标周围所有坐标中的元素相加起来,但是字符是不能直接相加的,参与运算的实际上是字符的ACSII码值。我们知道,字符’0’的ACSII码是48,而字符’1’的ACSII码是49,那么我们可以将八个坐标中的字符全部加起来的总和减去8×48,计算出来的数就是我们想要的雷数(相当于每一个字符减48,字符‘1’减48是数字1,字符’0’减48是数字0,最后加起来的数就是雷的数量),最后别忘了在函数返回值的时候再加上数字48,得到相应的数字字符。🎈

具体代码如下📌

char CountMine(char mine[ROWS][COLS], int x, int y){return ((mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +mine[x][y - 1] + mine[x][y + 1] +mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1]) - 8 * '0') + 48;}

🎉相比遍历的方法,这种方法是不是巧妙许多呢~~

2️⃣ 进阶版(含递归展开)

🎈接下来到了本文章的精华所在!

上面初阶版只是简单地实现了扫雷的大致流程,我们知道,真正的扫雷游戏中,还有两个非常重要的功能——“标记功能”“展开功能”

⭐标记功能可以让玩家在他认为有雷的坐标标上记号,使玩家更好地整理自己的思路。

⭐展开功能更为关键,展开功能是当玩家扫到周围都没有雷的坐标时,系统会自动展开直到遇到周围有雷时停止。如果没有展开功能,就像我们的初阶版一样,玩家在扫到一个地雷数为“0”的坐标时,还要自己展开周围八个坐标,十分麻烦。

(两个功能的实现效果如图所示⭕)

有了这两个功能,会大大提升我们扫雷游戏的可玩性。

✨ 那么我们该如何模拟实现这两个功能呢?下面就来看看具体如何实现吧!!

(I)标记功能的实现

标记功能是一项可选功能,最好是在玩家需要的时候可以唤起,不需要了就关闭。那么该如何实现这种机制呢?

💡玩家在输入坐标的时候就是他进行操作的时候,那么如果此时玩家不想进行扫雷操作而想标记一下他推理出来的雷的位置,我们能否把扫雷功能切换为标记功能呢?答案是可以的。我们知道,(0,0)这个坐标是不在地图范围内的,正常来说输入这个坐标后系统会报错并让玩家重新输入。那么,我们可以利用(0,0)这个特殊的坐标,把他改造成来回切换扫雷和标记功能的开关,既方便我们实现,又便于玩家输入。

具体代码如下📌

void Player_SweepMine(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int* is_lose){//在玩家扫雷函数中加入标记坐标FlagMine函数Display(mine, ROW, COL);int x = 0;int y = 0;printf("请输入坐标(输入坐标0 0可进入标记功能):>\n");scanf("%d%d", &x, &y);if ((x >= 1 && x <= row) && (y >= 1 && y <= col)){if (storage[x][y] == 'X'){printf("该坐标扫过了,请重新输入\n");}else{if (mine[x][y] == '1'){*is_lose = 1;return;}else{char count = CountMine(mine, x, y);show[x][y] = count;Spread(show, mine, storage, ROW, COL, x, y);Display(show, ROW, COL, x, y);storage[x][y] = 'X';}}}else if (x == 0 && y == 0)//输入坐标为0 0时,开启标记功能{FlagMine(show, ROW, COL);//玩家标记地雷位置功能}else{printf("坐标非法,请重新输入:>\n");}}

void FlagMine(char show[ROWS][COLS], int row, int col)//FlagMine函数的具体实现{int x = 0;int y = 0;while (1)//为了让玩家可以多次标记,我们设置一个循环{printf("请输入您想要标记的坐标(再次输入坐标0 0退出标记功能):>");scanf("%d%d", &x, &y);//玩家输入想要标记的坐标if ((x >= 1 && x <= row) && (y >= 1 && y <= col))//在范围内时,则可正常标记{if (show[x][y] == '*'){show[x][y] = '!';//规定标记的记号为字符'!'Display(show, ROW, COL);}else{if (show[x][y] == '!'){printf("这里已经标记过了,请重新标记\n");}else{printf("该坐标已经扫过了,无需标记\n");}continue;//若玩家输入坐标标记过了或者扫过了,则跳回去重新输入}}else{if (x == 0 && y == 0)//若输入0 0坐标,则跳出标记功能{Display(show, ROW, COL);return;}printf("坐标越界,请重新标记\n");//不是0 0坐标但是超出范围,提示越界并让玩家重新输入continue;}}}

运行效果演示⭕

输入坐标0 0进入标记功能,输入坐标6 6标记成功;

再次标记,输入坐标5 5,标记成功;

输入坐标0 0退出标记功能,系统提示输入扫雷坐标。

🎉标记功能实现!!

(II)展开功能的实现

最后我们来介绍一下展开功能如何实现。展开功能可以说是最困难的一个模块了(小编当时也是想了很久😇)既然实现比较复杂,那我们就从简单的开始,先实现指定坐标周围无地雷时,如何展开周围八个坐标(下面称为展开一周)。

展开一周我们考虑在检测到指定坐标周围雷数为0时,将周围一周的元素都替换成字符空格’ '(在show数组中替换),并打印展示出来。

具体代码如下⭕

void Spread(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int x, int y){if (CountMine(mine, x, y) == '0'){show[x][y] = ' ';int i = 0;for (i = x - 1;i <= x + 1;i++){int k = 0;for (k = y - 1;k <= y + 1;k++){if (show[i][k] == '*'){show[i][k] = ' ';}}}}elsereturn;}

而我们在试运行之后,发现这个代码其实是有问题的。❌

总结前面的错误我们可以得出结论,展开不是全部换成空格,而是周围没雷的地方换成空格,而有雷的地方就需要换成相应雷数的数字字符。

💡从分析错误后我们发现,展开是这样的一个逻辑:当我们扫开一个坐标后,首先需要判断这个坐标周围是否埋有地雷,这用我们已有的CountMine函数就能很好地实现。如果没有地雷,则执行展开。周围八个坐标依次展开,每一个坐标的周围又有八个坐标,又需要判断它周围是否有雷,周围没雷就将其换成字符空格‘ ’并继续展开这个坐标周围的八个坐标,有雷就换成对应雷数的数字字符并停止展开。这样周而复始,直到无法展开了为止。那么这种重复展开应该如何实现呢?

没错,这就要请出我们的重量级选手——递归!!

接下来我将通过画图帮助你理解📌

💡以这副已经展开的图为例:

⭐ 假设我们第一次扫的坐标为8 3(称为坐标一),由于坐标一周围并没有雷,所以可以进行展开。由于坐标一周围八个都要再次判断是否能够展开以及执行展开(简称为判断),我们以其右上角的坐标7 4(称为坐标二)为例,其他坐标同理;

⭐ 坐标二周围也没有雷,所以又可以进行一次展开,但我们注意到,坐标二的判断范围(既黄色区域)有一部分包含在坐标一的判断范围(既红色区域)中,为了提高效率,我们可以在每个坐标判断之后给他将他标记为以扫过的坐标(利用已有的storage函数),下次扫到它时就不会重复判断了;

⭐ 同坐标一以坐标二为例,我们在坐标二的判断范围内以坐标6 5(称为坐标三)为例。坐标三周围依然没有雷,但是他上方坐标的周围有雷,雷数分别是 2 2 1,因此其上方的三个坐标换为数字字符,其他换为空格;

⭐ 最后我们看到,坐标5 6周围出现了雷,因此蓝色区域不再进行展开,该分支展开停止。

🎐当然,上述我们只分析了坐标一(红色坐标)扫开后进行展开过程中的一个分支,展开可以理解为往外扩散直到遇见雷就停止。

(注:图中白色叉号为每一次展开与上次展开扫描坐标的重叠部分)

具体代码如下📌

void Spread(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int x, int y){if (CountMine(mine, x, y) == '0')//递归条件1{show[x][y] = ' ';//扫开的坐标周围雷数为0必定换为空格int i = 0;for (i = x - 1;i <= x + 1;i++){int k = 0;for (k = y - 1;k <= y + 1;k++){if (CountMine(mine, i, k) >= '1' && CountMine(mine, i, k) <= '8' && storage[i][k] != 'X')//条件二{show[i][k] = CountMine(mine, i, k);storage[i][k] = 'X';}else if (show[i][k] == '*' && storage[i][k] != 'X')//递归条件2、3{show[i][k] = ' ';storage[i][k] = 'X';Spread(show, mine, storage, ROW, COL, i, k);}}}}elsereturn;}

运行效果演示⭕

这样,我们就很好地得到了我们想要的效果🎉

将其放到我们的game函数中(由于每一次成功扫开一个坐标后才能触发递归展开,因此我们把它放在计算雷数后面)

void Player_SweepMine(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int* is_lose)//玩家扫雷机制{Display(mine, ROW, COL);int x = 0;int y = 0;printf("请输入坐标(输入坐标0 0可进入标记功能):>\n");scanf("%d%d", &x, &y);if ((x >= 1 && x <= row) && (y >= 1 && y <= col)){if (storage[x][y] == 'X'){printf("该坐标扫过了,请重新输入\n");}else{if (mine[x][y] == '1'){*is_lose = 1;return;}else{char count = CountMine(mine, x, y);show[x][y] = count;Spread(show, mine, storage, ROW, COL, x, y);//展开Display(show, ROW, COL);storage[x][y] = 'X';}}}else if (x == 0 && y == 0){FlagMine(show, ROW, COL);}else{printf("坐标非法,请重新输入:>\n");}}

🎉这就是我们game函数的完整体啦

5.判断玩家扫雷成功

🎈最后补充一下前面落下的判断输赢机制,为什么要放到最后讲呢?因为它与标记功能也有一定的关系。

思路:定义一个win变量,初始化为0,通过双重循环遍历show数组,如果数组元素不为字符’*'和‘!’,则win加一。这样最后得出win的量,当玩家获胜时就是地图上除地雷外其他坐标的数量(如:在9×9坐标中就是71,这里我们用ROW*COL-Mine_num进行通式计算)

注意:不为’*‘和’!'的show数组元素只能为ROW*COL-Mine_num个,若标记错位置或标多标少都不能判断为扫雷成功

具体代码如下📌

int Is_win(char show[ROWS][COLS], int row, int col){int i = 0;int j = 0;int win = 0;for (i = 1;i <= row;i++){for (j = 1;j <= col;j++){if (show[i][j] != '*' && show[i][j] != '!')win++;}}if (win == ROW * COL - Mine_num)return 1;//win变量的值满足获胜条件时,返回1,既扫雷成功为真elsereturn 0;//win变量的值不满足获胜条件时,返回0,既扫雷成功为假}

6.总体代码展示

test.c源文件

#define _CRT_SECURE_NO_WARNINGS#include"game.h"void game(){char mine[ROWS][COLS] = {'0' };//隐藏埋雷的信息的数组char show[ROWS][COLS] = {'0' };//展现给玩家看的数组char storage[ROWS][COLS] = {'0' };//存储玩家已点开的坐标的信息Initial(mine, storage,show, ROWS, COLS);//给show和mine初始化(11*11 初始化)SetMine(mine, ROW, COL);//埋雷Display(show, ROW, COL);//展现给玩家的扫雷地图//玩家扫雷并判断输赢int is_lose = 0;while (1){Player_SweepMine(show, mine, storage, ROW, COL, &is_lose);if (is_lose == 0){if (Is_win(show, ROW, COL)){printf("恭喜你,扫雷成功!\n");return;}elsecontinue;}else{printf("很遗憾,你被炸死了\n");Display(mine, ROW, COL);return;}}}void menu(){printf("******************************\n");printf("********* 1.play ************\n");printf("********* 0.exit ************\n");printf("******************************\n");}int main(){int input = 0;do{menu();printf("请输入:>");//让玩家输入一个数字,确保玩家在输入其他非数字时能再来一次while (1){if (scanf("%d", &input) != 1){printf("输入非法,请重新输入:");char ch = '0';while ((ch = getchar()) != '\n'){;}}elsebreak;}/*判断玩家下一步如何进行*/switch (input){case 1:game();break;case 0:printf("退出游戏\n");break;default:printf("输入错误,请重新输入\n");break;}} while (input);return 0;}

game.h头文件

#define _CRT_SECURE_NO_WARNINGS 1#include <stdio.h>#include <time.h>#include <stdlib.h>#define ROW 10//显示行数#define COL 10//显示列数#define ROWS ROW+2//真实行数#define COLS COL+2//真实列数#define Mine_num 10//地雷数量void Initial(char mine[ROWS][COLS], char storage[ROWS][COLS], char show[ROWS][COLS], int rows, int cols);void SetMine(char mine[ROWS][COLS], int row, int col);void Spread(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int x, int y);void Display(char arr[ROWS][COLS], int row, int col);void Player_SweepMine(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int* is_lose);void FlagMine(char show[ROWS][COLS], int row, int col);int Is_win(char show[ROWS][COLS], int row, int col);

game.c源文件

#define _CRT_SECURE_NO_WARNINGS 1#include"game.h"int Is_win(char show[ROWS][COLS], int row, int col){int i = 0;int j = 0;int win = 0;for (i = 1;i <= row;i++){for (j = 1;j <= col;j++){if (show[i][j] != '*' && show[i][j] != '!')win++;}}if (win == ROW * COL - Mine_num)return 1;elsereturn 0;}//char CountMine(char mine[ROWS][COLS], int x, int y){return ((mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] +mine[x][y - 1] + mine[x][y + 1] +mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1]) - 8 * '0') + 48;//int count = 0;//int i = 0;//for (i = x - 1;i <= x + 1;i++)//{//int j = 0;//for (j = y - 1;j <= y + 1;j++)//{//if (mine[i][j] == '1')//count++;//}//}//return count + 48;}//void Spread(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int x, int y){if (CountMine(mine, x, y) == '0')//条件1{show[x][y] = ' ';int i = 0;for (i = x - 1;i <= x + 1;i++){int k = 0;for (k = y - 1;k <= y + 1;k++){if (CountMine(mine, i, k) >= '1' && CountMine(mine, i, k) <= '8' && storage[i][k] != 'X')//条件二{show[i][k] = CountMine(mine, i, k);storage[i][k] = 'X';}else if (show[i][k] == '*' && storage[i][k] != 'X')//条件三{show[i][k] = ' ';storage[i][k] = 'X';Spread(show, mine, storage, ROW, COL, i, k);}}}}elsereturn;}//void FlagMine(char show[ROWS][COLS], int row, int col){int x = 0;int y = 0;while (1){printf("请输入您想要标记的坐标(再次输入坐标0 0退出标记功能):>");scanf("%d%d", &x, &y);if ((x >= 1 && x <= row) && (y >= 1 && y <= col)){if (show[x][y] == '*'){show[x][y] = '!';Display(show, ROW, COL);}else{if (show[x][y] == '!'){printf("这里已经标记过了,请重新标记\n");}else{printf("该坐标已经扫过了,无需标记\n");}continue;}}else{if (x == 0 && y == 0){Display(show, ROW, COL);return;}printf("坐标越界,请重新标记\n");continue;}}}//void Player_SweepMine(char show[ROWS][COLS], char mine[ROWS][COLS], char storage[ROWS][COLS], int row, int col, int* is_lose)//玩家扫雷机制{int x = 0;int y = 0;printf("请输入坐标(输入坐标0 0可进入标记功能):>\n");scanf("%d%d", &x, &y);if ((x >= 1 && x <= row) && (y >= 1 && y <= col)){if (storage[x][y] == 'X'){printf("该坐标扫过了,请重新输入\n");}else{if (mine[x][y] == '1'){*is_lose = 1;return;}else{//计算周围一圈的雷数并展示show地图char count = CountMine(mine, x, y);show[x][y] = count;Spread(show, mine, storage, ROW, COL, x, y);//展开Display(show, ROW, COL);storage[x][y] = 'X';//标志该坐标已扫过//递归展开用到}}}else if (x == 0 && y == 0)//玩家标记地雷位置功能{FlagMine(show, ROW, COL);}else{printf("坐标非法,请重新输入:>\n");}}//void Display(char arr[ROWS][COLS], int row, int col){int i = 0;int j = 0;for (j = 0;j <= col;j++)//打印列序号{printf("%2d ", j);if (j <= col - 1)//'|'换成空格{printf(" ");}}printf("\n");//for (i = 1;i <= row;i++){for (j = 0;j <= col;j++)//数字组{if (j == 0)//打印列序号{printf("%2d ", i);}else{printf(" %c ", arr[i][j]);if (j <= col - 1){printf("|");}}}printf("\n");if (i <= row - 1){for (j = 0;j <= col;j++)//分割线组{if (j == 0){printf(" ");}else{printf("---");if (j <= col - 1){printf("|");}}}}printf("\n");}}//void SetMine(char mine[ROWS][COLS], int row, int col){//埋雷int x = 0;int y = 0;srand((unsigned int)time(NULL));int count = Mine_num;while (count){x = rand() % row + 1;y = rand() % col + 1;if (mine[x][y] == '0'){mine[x][y] = '1';count--;}}//for (int i = 0;i < Mine_num;i++)//另外一种埋雷方法//{//while (1)//{//x = rand() % row + 1;//y = rand() % col + 1;//if (arr[x][y] == '0')//{//arr[x][y] = '1';//break;//}//}//}}//void Initial(char mine[ROWS][COLS], char storage[ROWS][COLS], char show[ROWS][COLS], int rows, int cols){int i = 0;int j = 0;for (i = 0;i < rows;i++)//初始化{for (j = 0;j < cols;j++){mine[i][j] = '0';storage[i][j] = '0';show[i][j] = '*';}}}

五、总结

🎈断断续续写了好几天终于完成了这篇博客,心得就是函数命名要有意义,不然函数太多的时候,调用时很容易搞混了。创作不易,看完不妨给作者一个三连吧!!欢迎各位大佬指正!!

本文完。

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。