目录
前言:
这篇文章是讲解如何用c语言编写扫雷游戏,对之前的知识灵活运用加以巩固,加深自己的理解。
1.游戏概述
扫雷是一款由微软在1992年发布的游戏,目标是在9*9的棋盘里找出所有没有雷的格子,即为胜利棋盘大小和雷的数量是可以随便设置的。本篇文章来讲一下如何在VS2022里编写9*9基础扫雷游戏。链接放在下面,各位可以亲自体验一下。
2.游戏规则
1.棋盘里有些地方布置这有雷,有些地方是空白。
2.游戏开始时,排查的地方如果没有雷,会显示周边雷的个数。
3.通过点击格子,如果没雷且周边也没有雷会显示空白,有雷被炸死,游戏结束.
4.如果打开的格子周围的地雷数量与数字相符,则该格子周围的其他格子也会被打开。
5.游戏的目标是在不踩到地雷的情况下,找出所有没有地雷的格子。
扫雷游戏需要玩家运用逻辑推理和数学计算来推断哪些格子可能有地雷,哪些格子是安全的。通过不断尝试和推理,最终完成游戏。
3.游戏实现思路
第一步:设置一个游戏界面函数,来打印游戏界面。
第二步:我们需要创建两个二维数组,一个存放棋盘本身的数据,一个对外展示。
第三步:对棋盘进行初始化。
第四步:将棋盘以9*9的格式打印出来。
第五步:布置雷,如果是雷就放1,不是雷就放0‘。
第六步:输入坐标,进行排查,循环,直到炸死或者游戏结束。
为了使游戏更加灵活我们可以在头文件里运用#define设置行列,雷的个数。
游戏实现
1. 文件结构
通过三个文件来实现(为了方便后期的修改和调试)
这里
我们使用tean.h来存放函数的声明
使用tean.c来实现函数完整的定义
使用sl.c包含main函数和函数的调用
我们创建三个文件,有一些头文件要调用多次,太麻烦了,所以我们可以直接把头文件放tean.h里面,其余两个.c文件直接#include "tean.h"就行了,如图
2.构建主函数main()
在sl.c文件里定义一个变量a来作为是否开始游戏的选项,但是如果要想要多次游戏的话,就需要将switch分支语句和sand()函数放在一个循环里实现,但是第一次进入程序的时候,我们必须保证能至少进行一次判断,这里使用do while循环就比较合适,把变量a作为do while循环的表达式。使用sand()函数打印菜单界面,每次循环结束到回到这个界面。运用switch语句进行判断是否进行游戏,如果输入1则进入arr()开始游戏,输入0结束循环,程序结束,输入其它数字,显示输入错误,重新输入。并循环执行switch语句,直至玩家主动退出。
关于switch语句这篇文章有更详细的介绍:(C语言)选择语句详解-CSDN博客
int main() { int a = 0;//定义一个变量 do { //打印界面 sand();//菜单界面 scanf("%d",&a); switch(a) { case 1: arr();//开始游戏 break; case 0: printf("退出游戏\n"); break; default: printf("输入错误,请重新输入\n"); } } while (a);//a作为循环是否结束的表达式 return 0; }
3.游戏菜单sand()
void sand()
{
printf("***************************\n");
printf("***************************\n");
printf("******* 1.开始游戏 ********\n");
printf("******* 0.退出游戏 ********\n");
printf("***************************\n");
printf("***************************\n");
}
这里我们就可以先运行一下,看有没有说明问题,包括后面的代码也可以写一点测试一点,防止出问题了,还要一个一个排查。
4.创建游戏函数arr()
扫雷游戏我们首先要创建一个数组放雷,而棋盘我们用二维数组zxc[x][y]来表示,x,y表示棋盘的行,列。创建棋盘时,要看一下这款游戏是怎么排雷的,当我们排除一个没有雷的地方时,他会显示周围九宫格里雷的个数,如下图:
但是我们排查雷的时候会发现,如果排查棋盘最边上的地方时,它搜查的地方会超过我们所设置的9*9的棋盘里,但是它仍然会显示周边雷的个数,如下图,以2那个坐标为中心的周边八个地方有两个雷,但它左边的地方没有在我们所设置的9*9的棋盘的范围内,超出棋盘范围,造成越界问题
所以我们可以在设置棋盘的时候把行和列设置成11*11,但我们只用其中的9*9,这样就不会造成越界现象,如下图,中间蓝色范围是用来布置雷的棋盘,而周边多出来的一圈绿色便是用来防止越界问题的,那么在这个棋盘上我们输入坐标排查的的范围都是在棋盘的范围之内,就不存在越界问题了,所以在设置棋盘的时候我们将存放数据的数组可以设置成char[11][11]
5.给棋盘初始化
假如我们已经布好了雷,有雷的用‘1’显示,没有雷的用‘0’显示,当我们搜查一个坐标的时候,如果这个坐标周围有一个雷,那么我们需要将这个雷的数量信息记录并存储下来,同时打印给玩家,但是我们存放雷的数组里面已经有数据了,如果再存放在里面就会可能产生混淆从而导致棋盘混乱打印困难,所以需要创建两个棋盘,一个雷的棋盘asd,一个显示雷的信息的棋盘zxc。当然,你们想起一个什么样的名字都可以,看个人习惯。
char asd[Rons][Eons]; //雷的分布 ,无雷是‘0’,有雷是‘1’ char zxc[Rons][Eons]; //棋盘初始布局 ‘*’
雷的棋盘用‘0’,给用户看的棋盘用‘*’显示,因为这些都是字符,所以用char类型。这边因为我是已经把雷布置好的,所以有第二张图里有10个‘1’,正常情况是全‘0’的,至于棋盘周边的修饰我等一下会讲到。运用for循环,0~11,把两个棋盘全部初始化我们使用同一个函数对这两个数组进行初始化处理,如图:
注意:因为创建了三个文件,tean.h里是存放函数的声明的,使用函数前要把它放在tean.h里面声明一下,而那些函数的使用则放在tean.c里面,
void Acp(char wsg[Rons][Eons], int able, int xwf, char sdf) { int i = 0; int n = 0; for (i = 0; i < Rons; i++) { for (n = 0; n < Eons;n++) { wsg[i][n] = sdf;//初始化数组 } } }
6.打印棋盘
我们把棋盘全初始化之后,就可以来打印棋盘了,为了输入1,开始游戏的时候我们可以在棋盘的上方和左边加上数字,使棋盘看上去更规范。我在添加数字之后还加了横杠,怎么舒服怎么来。
设置一个Awf函数,先定义一个x变量代表行,一个y变量代表列,,打印一行‘*’点缀一下,运用for循环在棋盘上面打印0~9的数字,循环结束后别忘了加个\n,换行。在打印一行——把数字和棋盘分割开。然后运用for循环语句,因为asd函数和 zxc函数全部到传给了wsg,所以它可以同时打印两个棋盘,进入第一个for循环时先把每行的序号1~9打印出来,而且你仔细看,这是不是和二维数组的下标一样,后面我们就是通过下标来排查的,至于那个‘|’打不打印都可以,第二层循环就可以开始打印棋盘了,因为是字符,所以要用%c来打印,第一行结束打印完后加一个换行。结尾和前面一样
void Awf(char wsg[Rons][Eons], int able, int xwf)
{
int i = 0;
int j = 0;
printf("******** 扫雷 *******\n");
for (int s = 0; s <= able; s++)
{
printf("%d ", s);
}
printf("\n");
printf("————————————————————\n");
for (i = 1; i <= able; i++)
{
printf("%d|", i);
for (j = 1; j <= xwf; j++)
{
printf("%c ", wsg[i][j]); //打印asd[i][j]中的符号
}
printf("\n");
}
printf("******** 扫雷 *******\n");
}
7.布置雷
现在我们棋盘初始化完成了,但是asd里面全是‘0’,没有雷的信息,所以现在我们需要往数组asd里面放入雷的信息,那么我们该如何放置呢?在游戏里面雷的出现是随机的,不固定的,这就需要
1.rand() 函数
c语言里面的 rand() 函数,使用这个函数需要包含一个头文件 stdlib.h,我们把它写在tean.h里面就行,但使用rand()函数生成的随机数可能不是完全随机的,而是一种伪随机数,玩游戏不可能玩一次就行,那怎么办呢?在一些固定的模式下以它的种子为基准值生通过某种算法根据成的可以算的伪随机数。而rand函数的种子一般是默认为一个固定值,那么现在我们可以知道,如果想让rand函数生成一个真正的随机数就需要时刻改变它的种子就可以了。
2.srand()函数
可以使用srand()函数设置随机数种子,如果没有设置随机数种子,rand()函数在调用时,自动设计随机数种子为1。随机种子相同,每次产生的随机数也会相同。但如何让种子每次发生变化呢?
3.time函数
在自然界中,时间是无时无刻不在变化的,我们可以用time函数来作为种子,此函数会返回从公元1970年1月1日的UTC时间从0时0分0秒算起到现在所经过的秒数
有了这三个函数,我们知道了一个可以生成真正随机数的方法。把这个代码写在main()函数里面。
但注意:一次程序运行只需要使用srand函数确定一次入口就行,如果每次随机数的生成都依赖srand则可能会因为生成时间相近而导致生成的随机数相近甚至相同。
srand((unsigned int)time(NULL)); //给随机数的种子随机化。
而为了在数组里面布置雷,我们就需要限制rand的范围。因为我们设置的是9*9的棋盘,所以把生成的数%棋盘的长宽然后+1,就能得出1~9了,代码如下:
int x = rand() % able + 1; //1~9行
int y = rand() % xwf + 1; //1~9列
现在雷的坐标我们已经布置好了,接下来就是确定雷的个数,我们先前设的雷的个数是10,如果想的话也可以多设几个。
#define Shr 10 //雷的个数
先定义一个变量jsq表示雷的个数,然后用while循环,把jsq作为while循环是否结束的开关,然后,把雷出现的范围规定在1~9行列里面,用if语句判断wsg[x][y]是不是雷,不是,就把这个位置强行变成雷,布置一个雷就jsq--,等雷布完之后,jsq=0;循环结束。
//布置雷 void Aqw(char wsg[Rons][Eons], int able, int xwf) { int jsq = Shr;//布置10个雷 while (jsq) { int x = rand() % able + 1; //1~9行 int y = rand() % xwf + 1; //1~9列 if (wsg[x][y] != '1') //判断[x][y]是不是雷 { wsg[x][y] = '1'; //强行将[x][y]变成雷 jsq--; //雷的个数减一 } } }
别忘了,在布置完雷之后,再调用一下Awf(asd, Ron, Eon);把雷打印出来,看一下有没有问题,不看雷的话,就加//把它注释,不然,每次循环都会把这个打印出来。
Awf(zxc, Ron, Eon);把用户界面在打印出来,
8.雷的排查
雷都布置好了之后,就可以进行排查了,设置一个排雷函数Wdlxs()把两个棋盘传过去,设变量x,y为横纵坐标,此处的lei是可排查的次数,当我们如果将无雷区域全部排查出来之后我们该如何结束呢,我们使用while循环,条件是lei < Ron*Eon- Shr即lei<行*列-雷的数量,每次成功排雷后,lei++,当lei< Ron*Eon- Shr不成立时while循环结束,排雷成功。
//排查雷 void Wdlxs(char asd[Rons][Eons], char zxc[Rons][Eons], int able, int xwf) { int x = 0; int y = 0; int lei = 0; while (lei<=Ron*Eon- Shr) { } if(lei==Ron*Eon- Shr) { printf(“游戏胜利\n"); } }
但是我们输入坐标的时候,会出现三种情况:
1,排查的地方不是雷。
2,排查的地方是雷。
3,超出范围了。
我们可以先用一个if判断语句,当x,y都在指定范围内时进入,在进行判断排查的是不是雷,
不在范围内就进入else语句,提醒输入错误,请重新输入。
if (x > 0 && x <= able && y > 0 && y <= xwf) { } else { printf("坐标错误,请重新输入:\n"); }
当排查的范围正确是,就可以再设一个if语句,如果踩到雷了,就直接提醒炸死了,然后把雷的分布棋盘打印出来,break跳出当前while语句。
如果排查的不是雷,那么就需要排查这个坐标周围的一个九宫格共八个坐标,看有多少个雷,把数字打印到[x][y]上面,
if (asd[x][y] == '1') { printf("你踩到雷了,游戏失败,你个SB\n"); Awf(asd, Ron, Eon); break; } else { Awf(asd, Ron, Eon); int c = Fget(asd, x, y); zxc[x][y] = c + '0'; Awf(zxc, Ron, Eon); }
排查周围的雷需要用到Fget(asd, x, y);
大家可以看这张图,这样排查就一幕了然了。我们可以设置一个函数,返回值就是雷的数量,在这里我们将char类型转换为int,方便计算。
int Fget(char asd[Rons][Eons], int x, int y) { return (asd[x - 1][ y - 1] + asd[x - 1][ y] + asd[x - 1][ y + 1] + asd[x][ y - 1] + asd[x][ y + 1] + asd[x + 1][ y - 1] + asd[x + 1][ y] + asd[x + 1][ y + 1] - 8 * '0'); }
以上基本上所有的扫雷代码就完成了。但还是有瑕疵的,无法向网页一样,如果这个坐标周围没有雷会展开一大片空白。
9.无雷区域的扩展
此处无雷区域的展开我们使用递归函数实现,如果当前排查的地方没有雷,那么就进入Pduan_zbian(asd,zxc, x, y);函数里面
if (asd[x][y] == '1') { printf("你踩到雷了,游戏失败,你个SB\n"); Awf(asd, Ron, Eon); break; } else { Pduan_zbian(asd,zxc, x, y); Awf(zxc, Ron, Eon); if (Zbydsgl(zxc, Ron, Eon)) { printf("恭喜你通关了,你真是个天才\n"); Aqw(asd, Ron, Eon); break; } }
和刚才一样,先排查(x,y)周边有几个雷,然后返回到zxc[x][y] = c + '0';,但如过周边一个雷倒没有,那么c=0,if不执行,else if执行,如果zxc[x][y] = ‘*’,就把‘ ’空格赋给zxc[x][y] ,然后定义变量i,j,两层for循环,进行函数调用
//递归,打开这边没有雷的地区 void Pduan_zbian(char asd[Rons][Eons], char zxc[Rons][Eons],int x, int y) { int c = Fget(asd, x, y); if (c) { zxc[x][y] = c + '0'; } else if(zxc[x][y]=='*') { zxc[x][y] = ' '; int i = 0; int j = 0; for (i =x-1; i <=x+1; i++) { for (j =y-1; j <=y+1; j++) { Pduan_zbian(asd, zxc,i, j); } } } }
10.获胜条件
无雷区域扩展完后,我们的获胜条件判断也需要更改,在这里我们可以设变量lei=0,计算棋盘里的‘*’还剩几个,当棋盘中剩余的为排查 * 数量为十个 (雷的数量) 时,即为胜利,则返回1,否则返回0。
//判断还有多少个无雷区
int Zbydsgl(char zxc[Rons][Eons], int able, int xwf)
{
int i = 0;
int j = 0;
int lei = 0;
for (i = 1; i <= able; i++)
{
for (j = 1; j <= xwf; j++)
{
if (zxc[i][j] == '*')
{
lei++;
}
}
}
printf("你还需要排查%d\n",lei);
if (lei == Shr) //没有被排查的地方等于雷的数量
{
return 1;
}
else
{
return 0;
}
}
返回的是真时,if语句执行,游戏胜利,打印显示雷的棋盘,break跳出循环。
游戏源代码
tean.h
#define _CRT_SECURE_NO_WARNINGS
#define Ron 9 //定义一个符号Ron为9 表示行
#define Eon 9 //定义一个符号Eon为9 表示列
#define Rons Ron+2 //同上 表示行
#define Eons Eon+2 //同上 表示列
#define Shr 10 //雷的个数
#include <stdio.h>
#include <stdlib.h> //rand,srand的头文件
#include <time.h> //time的头文件
//初始化棋盘,把棋盘全部初始化成对应的符号
void Acp(char wsg[Rons][Eons], int able, int xwf,char sdf);
//打印棋盘
void Awf(char wsg[Rons][Eons],int able,int xwf);
//布置雷
void Aqw(char wsg[Rons][Eons], int able, int xwf);
//排查雷
void Wdlxs(char asd[Rons][Eons],char zxc[Rons][Eons] , int able, int xwf);
//判断周边有多少个雷
int Fget(char asd[Rons][Eons], int x, int y);
//递归,打开这边没有雷的地区
void Pduan_zbian(char asd[Rons][Eons], char zxc[Rons][Eons], int x, int y);
//判断还有多少个无雷区
int Zbydsgl(char zxc[Rons][Eons], int able, int xwf);
tean.c
#define _CRT_SECURE_NO_WARNINGS
#include "tean.h"
//初始化棋盘,把棋盘全部初始化成对应的符号
void Acp(char wsg[Rons][Eons], int able, int xwf, char sdf)
{
int i = 0;
int n = 0;
for (i = 0; i < Rons; i++)//行
{
for (n = 0; n < Eons; n++)//列
{
wsg[i][n] = sdf;
}
}
}
//打印棋盘
void Awf(char wsg[Rons][Eons], int able, int xwf)
{
int i = 0;
int j = 0;
printf("******** 扫雷 *******\n");
for (int s = 0; s <= able; s++)
{
printf("%d ", s);
}
printf("\n");
printf("————————————————————\n");
for (i = 1; i <= able; i++)
{
printf("%d|", i);
for (j = 1; j <= xwf; j++)
{
printf("%c ", wsg[i][j]); //打印asd[i][j]中的符号
}
printf("\n");
}
printf("******** 扫雷 *******\n");
}
//布置雷
void Aqw(char wsg[Rons][Eons], int able, int xwf)
{
int jsq = Shr;//布置10个雷
while (jsq)
{
int x = rand() % able + 1; //1~9行
int y = rand() % xwf + 1; //1~9列
if (wsg[x][y] != '1') //判断[x][y]是不是雷
{
wsg[x][y] = '1'; //强行将[x][y]变成雷
jsq--; //雷的个数减一
}
}
}
//判断周边有多少个雷
int Fget(char asd[Rons][Eons], int x, int y)
{
return (asd[x - 1][ y - 1] + asd[x - 1][ y] + asd[x - 1][ y + 1] + asd[x][ y - 1] +
asd[x][ y + 1] + asd[x + 1][ y - 1] + asd[x + 1][ y] + asd[x + 1][ y + 1] - 8 * '0');
}
//递归,打开这边没有雷的地区
void Pduan_zbian(char asd[Rons][Eons], char zxc[Rons][Eons],int x, int y)
{
int c = Fget(asd, x, y);
if (c)
{
zxc[x][y] = c + '0';
}
else if(zxc[x][y]=='*')
{
zxc[x][y] = ' ';
int i = 0;
int j = 0;
for (i =x-1; i <=x+1; i++)
{
for (j =y-1; j <=y+1; j++)
{
Pduan_zbian(asd, zxc,i, j);
}
}
}
}
//判断还有多少个无雷区
int Zbydsgl(char zxc[Rons][Eons], int able, int xwf)
{
int i = 0;
int j = 0;
int lei = 0;
for (i = 1; i <= able; i++)
{
for (j = 1; j <= xwf; j++)
{
if (zxc[i][j] == '*')
{
lei++;
}
}
}
printf("你还需要排查%d\n",lei);
if (lei == Shr) //没有被排查的地方等于雷的数量
{
return 1;
}
else
{
return 0;
}
}
//排查雷
void Wdlxs(char asd[Rons][Eons], char zxc[Rons][Eons], int able, int xwf)
{
int x = 0;
int y = 0;
int lei = 0;
while (lei<=Ron*Eon- Shr)
{
printf("请输入坐标:");
scanf("%d %d", &x, &y);
if (x > 0 && x <= able && y > 0 && y <= xwf)
{
if (asd[x][y] == '1')
{
printf("你踩到雷了,游戏失败,你个SB\n");
Awf(asd, Ron, Eon);
break;
}
else
{
Pduan_zbian(asd,zxc, x, y);
Awf(zxc, Ron, Eon);
if (Zbydsgl(zxc, Ron, Eon))
{
printf("恭喜你通关了,你真是个天才\n");
Aqw(asd, Ron, Eon);
break;
}
}
}
else
{
printf("坐标错误,请重新输入:\n");
}
}
}
sl.c
#define _CRT_SECURE_NO_WARNINGS
#include "tean.h"
void sand()
{
printf("***************************\n");
printf("***************************\n");
printf("******* 1.开始游戏 ********\n");
printf("******* 0.退出游戏 ********\n");
printf("***************************\n");
printf("***************************\n");
}
//扫雷的基本代码
void arr()
{
char asd[Rons][Eons]; //雷的分布 ,无雷是‘0’,有雷是‘1’
char zxc[Rons][Eons]; //棋盘初始布局 ‘*’
//初始化棋盘,把棋盘全部初始化成对应的符号
Acp(asd, Rons, Eons, '0');
Acp(zxc, Rons, Eons, '*');
//打印棋盘
// Awf(asd, Ron, Eon);
// Awf(zxc, Ron, Eon);
//布置雷
Aqw(asd, Ron, Eon);
// Awf(asd, Ron, Eon); //布置雷之后在打印一遍。
Awf(zxc, Ron, Eon);
//排查雷
Wdlxs(asd, zxc, Rons, Eons);
}
int main()
{
srand((unsigned int)time(NULL)); //给随机数的种子随机化。
int a = 0;
do
{
//打印界面
sand();
scanf("%d", &a);
switch(a)
{
case 1:
arr();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
}
} while (a);
return 0;
}