扫雷,相信大家在上小学的时候都有玩过,本篇博客小编将以9*9的棋盘为例,来带你实现扫雷小游戏,重温儿时的记忆~
目录
在写代码之前,我们要先把扫雷实现的逻辑理清楚,逻辑理清楚就会发现扫雷的实现其实并不难。
一.扫雷的分析与设计
1.1扫雷游戏功能说明
1.首先,游戏一定会有菜单,我们可以通过菜单来控制是 ‘ 开始游戏 ’ 还是 ‘ 结束游戏 ’。
2.扫雷的棋盘是9*9的格子(可以自行修改)
3.要设定雷的个数,本篇博客以10个雷为例(可以自行修改)
4.玩家在排查雷时
- 选择的位置如果不是雷,则显示周围有几个雷
- 选择的位置是雷,玩家被炸死,游戏结束
- 把除了10个雷的非雷位置都找出来,则排雷成功,游戏结束
游戏界面
【网页版】
【我们模拟实现的】
菜单:
扫雷界面:
排查雷界面:
1.2游戏数据分析
从上面的游戏界面可以看出来,我们布置雷的信息和排查雷的信息都需要在这个9*9的格子中储存,所以,我们首先能想到的就是定义一个9*9的二维数组来储存这两组信息。
现在,我们来布置雷,是雷我们就存放1,没有雷我们就存放0
假设我们现在要排查(2,5)这个位置,它不是雷,我们就统计这个位置周围一圈8个位置有几个雷,从图片中可以看到,周围只有一个雷。
假设我们现在要排查(8,6)这个位置,它不是雷,我们统计它周围一圈雷的个数时,这个位置的下面一排的三个坐标则会越界,为了防止越界,我们可以将数组扩大一圈,雷还是只在中间的9*9坐标上,周围一圈不去布置雷就行,这样就解决了越界的问题。所以我们将储存数据的二维数组创建成11*11比较合适。
我们再继续分析,当我们排查出的这个坐标不是雷的时候,我们会统计周围雷的个数并打印出来
但是这个统计出来的数据如果放到排查雷的这个数组是不是就会混淆,假设周围有一个雷,我们设置这个位置为1,那这个1究竟是一个雷,还是周围雷的个数呢?
有的同学可能会说,布置雷的符号不用1表示,让他们有个区分不就好了,理论上是可以的,但我们把雷的符号也设置成1,在后面写代码统计非雷位置周围的雷的个数的时候是有帮助的。我们先保留这个问题,看到后面就可以理解了。而且,如果这样做了,这么一个棋盘上既有雷的信息,也有非雷的信息,也有排查出雷的歌数的信息,比较混杂,不够方便。
那如果不替换雷的符号,我们还可以怎么办呢?
其实,这里我们可以通过定义两个相同规格的11*11的二维数组就可以解决喽,一个数组mine用来储存雷,一个是用来排查雷show(给玩家看的数组),这样就互不干扰了。
同时,为了保持神秘,我们可以把最开始给玩家看的show数组初始化成 ‘ * ’ ,而布置雷的数组初始化成 ‘ 0 ’ ,后面布置雷的时候再改成 ‘ 1 ’ 就行了,这两个可以用一套函数来初始化。
看到这里,可能会有人疑问,show数组因为要存放字符 ‘ * ’ ,所以是char类型的,那左边的mine数组不直接存放整形0、1,而是存放的字符‘ 0 ’、‘ 1 ’ ?其实这里就是为了方便后面写代码来操作两个数组,比如刚刚说的初始化数组,两个用一套函数就行,既然可以用一套函数,那我们就保证他们的数据类型是一样的就可以了
对应数组:
char mine[11][11] = {0} ;//用来存放布置好雷的信息
char show[11][11] = {0} ;//用来存放排查出来的雷的个数信息
1.3文件结构的设计
与之前的三子棋相同,我们还是创建三个文件,分别是game.c game.h test.c
- test.c //写游戏的测试逻辑
- game.c //写游戏中的函数实现
- game.h //写游戏中需要的数据类型和函数声明
二.游戏代码的具体实现
2.1 test.c—游戏逻辑
这里,和我们三子棋一样的结构,先创建简单的游戏结构,打印菜单,让玩家选择开始游戏还是结束游戏。
我们分装成四个小函数
1)menu函数用来打印菜单界面
2)test函数用来执行让玩家选择开始游戏还是结束游戏
这里还是使用do-while 结构,因为我们菜单至少执行一次,下一次是玩家选择。
根据上面菜单,选择1则开始游戏,选择0则结束游戏,输入其他数字则选择错误,需要重新输入,根据需求,我们选用swith结构,当玩家选择1,则进入游戏(游戏内部逻辑分装成第3个函数game)。
而玩家输入的数据,我们创建一个整形变量input来接收即可。
具体可以看代码再理解一下:
3)game函数来实现游戏的具体逻辑
在写游戏逻辑之前,小编要先说一个点。因为这篇博客是以9*9的棋盘为例的,如果直接把数组定义成这样,就是直接写死了,比如说你后面想改成15*15的棋盘,你就要把代码中所有9和11一个个的修改成15和17,这样就会特别麻烦,所以我们在game.h中可以直接定义宏来存放9和11这两个数字,如下:
这样子会更方便,如果后期要更换棋盘,直接改ROW和COL这两个宏定义的数即可。
还有,我们是以布置10个雷的信息为例的,后面如果要修改,为了方便,我们这里也在game.h中直接定义宏来规定雷的个数,如下:
好了,我们继续实现游戏内部逻辑
创建好了数组以后,我们最能想到的就是要先初始化两个棋盘,我们写一个InitBoard函数来实现,这里我们使用同一套函数实现初始化的,但是初始化的内容不一样,我们就可以把要初始化的字符也同数组名,行,列一起传进去即可。
初始完了以后我们是不是要打印出来看一下,所以我们要再写一个DisBoard函数来实现,这里我们要注意的是,传递数组名以后,我们还要传递的参数不是ROWS(11),COLS(11)了,而是ROW(9),COL(9),因为我们要展示出来给玩家看的也只是中间的9*9的格子。
注意:展示完以后,我们在最后调试的时候,要把展示雷的数组注释掉,因为我们只给玩家看第二个满是“*”的数组。
展示完了以后,就可以开始布置雷的信息了,我们用SetMine函数来实现。注意这个函数传参的时候,除了把mine这个数组传递过去,还要传ROW和COL这两个参数,因为布置雷也是在中间9*9的格子中。(布置好了雷的信息,我们打印出来给自己看一下,当然在游戏最后调试的时候,也要隐藏起来,不然就给玩家看到雷在哪里了)
等布置完雷的信息以后,我们就可以开始让玩家排查雷了,这里我们分装一个FindBoard函数来实现。注意这里要传递的参数,要把mine数组和show数组都传进去(这里的逻辑,我们放到后面game.c文件里面的对应部分写),以及我们要操作的中间9*9的格子的行和列,ROW与COL。
4) 主函数main
这个就没什么好说的了
2.2 game.h—所需数据类型与函数的声明
这里比较简单,我们就把刚刚分装的函数都在这里声明一下即可,以及刚刚定义的5个宏
这里注意分装的函数的类型都写成是void类型就可以了。
上面test.c文件中的game.c中的传递的函数参数,ROWS,COLS,ROW,COL,我们都用小写的形式接收即可,后面方便我们看,不会弄混。
还有我们传过去的数组mine与show数组都是11*11的,所以我们在接收的时候也要注意用char类型的11*11的数组结构接收即可。
最开是初始化数组里面,我们传递了两个字符'0'和'1',所以我们InitBoard函数接收的时候用char类型的变量set接收即可。
2.3 game.c —游戏函数的具体实现
这里开始,我们就一个个的详细实现刚才分装的函数。
1. InitBoard
这个比较简单,我们直接遍历二维数组,然后赋值成set就可以了,大家可以自己看一下
2. DisBoard
这个函数是用来展示棋盘的,我们先打印两条线,把棋盘信息框住
具体棋盘代码就打印在这两条线中间就好,比较美观
在展示中间9*9的格子之前,我们可以先单独打印第一行的列号(0 1 2 3 4 5 6 7 8 )
打印完列号之后记得换行。
而我们的行号就可以跟在遍历数组打印棋盘信息的时候一起了。
如图,在进入第一层变量i控制的行中直接先打印行号(从0开始,i每次变换都打印),再进入变量j控制的列中,打印二维数组的每个位置的信息,结束每一行的打印后,要换行。
3. SetMine
很明显,设置雷的信息要用到while的循环结构,并且这个函数就要用到我们在三子棋中相同的生成随机数的方式了,生成1~9的数字,然后判断,如果这个位置还没有被设置成雷,就把这个位置设置成 '1' ,同时我们用来记录雷的个数的变量count--,直到十个雷全部都布置完再结束循环。
这里我们注意在前面test.c的文件中的test函数中写下生成随机数有关的srand函数(也是用时间来当做种子):
以及,在game.h文件中包含一下这些库函数的头文件:
SetMine函数的代码如下:
4. FindBoard
这个函数应该是这里面比较难得部分了,我们仔细来讲一下:
我们先来捋一下思路,排查雷的信息的时候,会决定我们玩家的输赢,玩家输的时候,是因为选择的坐标有雷,直接被炸死了,由此才结束游戏。而玩家赢得游戏则是因为布置的10个雷的位置没有被玩家排查,即玩家找出了其他非雷的全部位置。这两种情况下游戏会结束。
那我们玩家排查雷肯定要用到while的循环结构,我们先再while循环的外面定义count,这里的count是用来记录玩家找的非雷的个数。
而while括号里的条件,就是玩家找出的非雷个数是小于我们定义的非雷个数(我们定义的非雷个数可以用ROW*COL-Easy_Count表示),即while(count < ROW*COL-Easy_Count)当玩家找出的非雷的数等于定义的非雷的数时,循环自然结束,玩家不用再排查。
循环结束以后,我们再判断循环结束是因为,玩家被炸死才提前结束循环的,还是,玩家找全了非雷的个数才结束的。前者的情况判定玩家输掉游戏,后者的情况判定玩家赢得游戏。这个判定用if-else的结构判断就行。(如果count的数等于定义的非雷数,玩家获胜,否则玩家失败)
那我们再来看while循环里面玩家选择的具体情况。
我们定义2个变量x,y,用来接收玩家选择的行和列。
接下来就是判断,如果玩家选择的位置在mine数组中发现是' 1 ' ,则说明这个位置有雷,玩家被炸死,同时把雷的信息再打印一下,让玩家死的瞑目,再用break跳出循环即可。
如果玩家选择的位置是' 0 ',就又分两种情况了,一种情况就是玩家之前筛查过这个位置了,要提示一下,另一种就是第一次筛查这个位置,要统计这个位置周围的雷的个数,并打印在show数组上。同时打印给玩家看一下show数组。(我们统计这个位置周围雷的个数的函数用get_mine再分装一下,用它的返回值ret表示周围雷的个数,并把show数组中(x,y)这个位置赋值成ret)。
我们来具体看一下代码:
get_mine函数
这个函数的参数,我们传mine数组和玩家排查的这个坐标就好。
它的返回值我们定义成整形。
当我们统计周围雷的个数的时候,我们前面设定雷为' 1 '则发挥了大用处,把玩家筛选的坐标周围的8个位置都加在一起,是几,雷就有几个,是不是很巧妙~
当然,我们前面说了这个函数是int类型的,那我们可以把字符' 0 '和' 1 '转化成数字 0 和 1 ,这个转化就使用到ASCII码表(观察一下数字0和字符0中间差48,也就是一个字符0)
因为是这个位置周围是8个坐标,所以我们要减去8个 ' 0 ' (减去8个字符0)。
而我们要把ret直接复制到对应的show数组的位置的时候,要注意这个返回值ret是整形类型的,而show数组是字符类型的二维数组,所以要在ret后面加上一个字符 ' 0 ' 。
三.代码展示
到这里我们就结束喽,小编把三个文件的代码完整的放出来:
test.c
#include "game.h"
void game()
{
char mine[ROWS][COLS];//布置好雷的信息
char show[ROWS][COLS];//排查出雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');//展示棋盘
DisBoard(mine, ROW, COL);
DisBoard(show, ROW, COL);//设置雷的信息
SetMine(mine, ROW, COL);
DisBoard(mine, ROW, COL);//排查雷的信息
FindBoard(mine, show, ROW, COL);}
void menu()
{
printf("********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("********************\n");
printf("请输入数字:\n");
}void test()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();//打印菜单
scanf("%d",&input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}} while (input);
}
int main()
{
test();
return 0;
}
game.h
#include<stdio.h>
#include<stdlib.h>
#include<time.h>#define ROWS ROW+2
#define COLS COL+2
#define ROW 9
#define COL 9#define Easy_Count 10
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//展示棋盘
void DisBoard(char board[ROWS][COLS], int row,int col);//设置雷的信息
void SetMine(char mine[ROWS][COLS], int row, int col);//排查雷的信息
void FindBoard(char mine[ROWS][COLS], char show[ROWS][COLS],int row, int col);
game.c
#include "game.h"
void InitBoard(char board[ROWS][COLS], int rows, int cols,char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}void DisBoard(char board[ROWS][COLS],int row,int col)
{
printf("-------扫雷-------\n");
int j = 0;
for (j = 0; j < col; j++)
{
printf("%d ", j);//打印列号
}
printf("\n");
int i = 0;
for (i = 1; i < row; i++)
{
printf("%d ", i);
for (j = 1; j < col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("-------扫雷-------\n");
}void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = Easy_Count;
while (count)
{
int x = rand() % ROW + 1;
int y = rand() % COL + 1;if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
int get_mine(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') ;
}void FindBoard(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = 0;
while (count<ROW*COL - Easy_Count)
{
printf("请输入你要排查的坐标\n");
scanf("%d%d", &x, &y);
//是雷
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisBoard(mine, ROW, COL);
break;//游戏结束
}
//不是雷
else
{
if (show[x][y] != '*')//已经排查过
{
printf("该坐标已经被排查过,请再次输入\n");
}
else//统计这个坐标周围雷的个数放到show数组里面
{
printf("该坐标不是雷\n");
int ret = get_mine(mine, x, y);
show[x][y] = ret + '0';
DisBoard(show, ROW, COL);
count++;
}
}
}
if (count == ROW * COL - Easy_Count)
{
printf("恭喜你,游戏成功!\n");
}
else
{
printf("很遗憾,游戏失败\n");
}
}
到这里,就要说再见啦,小编会继续努力更新的~