1.游戏《C语言》—— 【扫雷游戏】

前言 

        Justin向大家问好,那么今天所讲的是扫雷游戏简单版本。扫雷大家都玩过吧 , 如果是简单版本,就是对一片 9*9 的区域进行排雷,那么如果雷为10个,就需要排查71个,只要排查71个完,并且没有踩到雷,就为成功。 

一.大致思路

1.扫雷的初始化,需要创建二维数组 

2.初始化棋盘  

3.打印棋盘 

4.布置雷 

5.对雷进行排查

 1.建立2个源文件,1个头文件,如图。

二.把游戏的大致框架做出来

1.首先在test.c里面写,咱们把菜单打印出来 ,这里使用了函数的创建和调用以及do while循环和switch分支结构,使用函数方便了我们的操作,而使用do while循环是因为这是一个循环过程,我们如果选择1就进游戏,选择0就退出游戏,如果不是1也不是0就要重新的提供选择,而且我们一开始就要打印菜单,那么do while循环是好的选择。 

2.switch分支结构提供了选项的选择里面的选项内容为case 1以及case 0,不是case 1和case 0就走default选项。

代码如下:



#include<stdio.h>
void menu() // 不需要返回所以是void , 菜单的内容 1 为 玩游戏 , 0 为 退出游戏
{
	printf("*********************\n"); 
	printf("**** 1.PLAY GAME ****\n"); 
	printf("**** 0.EXIT GAME ****\n"); 
	printf("*********************\n"); 
}

void game()
{



}
void test() //  调用函数,我们返回值是void ,什么也不返回
{
	int	input = 0;  //初始化
	// 因为是一个循环过程所以要用do while
	do 
	{
		menu(); // 创建菜单的函数
		printf("请选择是否开启Justin扫雷游戏:> "); // 提供选择 
		scanf_s("%d", &input);   // 输入选择 
		switch (input) // 选择的内容
		{
		case 1:
			game();   // game 里面写游戏的内容 
			break; 
		case 0:
			printf("你退出游戏 ,Justin 觉得很可惜\n"); // 退出游戏 
			break; 
		default:
			printf("警告,你输入错误,请重新选择\n"); // 不是选择1也不是0,那就是错误的,所以提示重新选择
			break;
		}

	} while (input); 
}

int main()
{
	test(); // 创建函数
	return 0; 
}

运行情况如下:

1.因为我们还没有写game里面的游戏内容,所以选择1是没有游戏内容的,那么我们输入了2和3,也就是没有选择1和0,那么就会走default选项,输出了警告,最后我们输入了0,游戏退出,程序结束,因为这是一个循环的过程,所以使用do while是特别好的。

三.理解棋盘的布局

1.发现问题

1. 首先我们先来定义一下 ,0 不是雷 ,1是雷,因为扫雷游戏嘛。那么我们先来理解一下扫雷游戏的布局以及运行,我们今天要做的是9*9布局的棋盘,如果我们直接就设置9*9的布局。那么它会存在一种问题,因为大家都知道我们在扫雷的时候,选择一个坐标后,如果它不是雷,那么它就会统计,自身周围有多少个雷。以下面的图举例子,比如我输入坐标(2,5)那么它不是雷,它就会统计自身周围有多少个雷,那么(2,5)坐标周围有1个雷,所以最后统计为1个雷,并且会显示出来。

2.那么如果,我选择的是(8,6)坐标呢,那是不是下面的部分就统计不了了,就越界了。

2.解决问题

1.那么我们已经知道了问题所在,就来解决它,其实很简单,只要给布局扩展为11*11,就可以了,如下图。那么可能就会有好兄弟问了,Justin Justin 你前边不是说了我们要做9*9的布局吗,为什么变成11*11了呢??哈哈 ,我的好兄弟,我们的初心是不会忘的,我们扩展了布局就不会存在越界的情况了,已经解决问题,那么我们只要对11*11里面的9*9进行操作就可以了,对不对,而且,我们最后呈现的也是9*9的布局,我的好兄弟。

3.另一个问题所在

那么我们已经解决了布局的问题了,难过的是又出现了另一个问题。

我们前边说了0不是雷,1是雷,那么我们需要将排查出的雷的数量信息记录存储,并打印出来,作为排雷的重要参考信息的。那这个雷的个数信息存放在哪⾥呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难。

解决方法:

1.雷和⾮雷的信息不要使⽤数字,使⽤某些字符就⾏,这样就避免冲 突了,但是这样做棋盘上有雷和⾮雷的信息,还有排查出的雷的个数信息,就⽐较混杂,不够⽅便。 这⾥我们采⽤另外⼀种⽅案,我们专⻔给⼀个棋盘(对应⼀个数组mine)存放布置好的雷的信息,再 给另外⼀个棋盘(对应另外⼀个数组show)存放排查出的雷的信息。这样就互不⼲扰了,把雷布置到 mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考。

2.保持神秘,show数组开始时初始化为字符 '*',为了保持两个数组的类型⼀致,可以使⽤同⼀ 套函数处理,mine数组最开始也初始化为字符'0',布置雷改成'1'。

3.这样一个mine数组来设置,show数组来展现

如下图:

 

 四.使用代码来实现game游戏的内容

1.棋盘的初始化

咱们前边已经创立好了2个源文件,一个头文件

2.我们要在头文件game.h去定义ROWS,COLS

#include<stdio.h>


#define ROW 9  //9*9的布局,所以ROW和COL为9
#define COL 9 

#define ROWS ROW + 2   //11*11的布局,所以ROWS和COLS为11
#define COLS COL + 2   

3.那么我们在test.c里面写,找到void game(),在里面写上2个数组,关于为什么创建我们上面已经说过啦,我们需要引用头文件到test.c ,因为我们在game.h里面定义了。代码如下

引用头文件为:

#include"game.h"
void game()
{
	//创建二维数组
	char mine[ROWS][COLS] = { 0 }; //用来存放布置好雷的信息 
	char show[ROWS][COLS] = { 0 }; //用来存放排查出来雷的个数信息

}

有些好兄弟,可能会说 Justin Justin 你test.c的#include<stdio.h>头文件出哪里了,其实我把它放在了game.h里面了,那么为什么没有报错误,因为我们在test.c里面包含了game.h头文件#include"game.h" , 可以直接引用。注意使用自己创建的头文件是要用" " ,而不是< > . 如图:

 2.创建棋盘

在test.c里面写,代码如下:

//棋盘的创建,我们要对9*9布局进行操作所以ROW和COL要传

InitBoard(mine, ROWS, COLS, '0');//mine数组全部为'0'  
InitBoard(show, ROWS, COLS, '*');//show数组全部为'*'

然后对函数进行声明,在game.h里面 ,代码如下:

 //这里很巧妙我用了 int set 来接受'0'和'*' 这样我就可以对这2个进行设置而不用多去建立一个数组
void InitBoard(char arr[ROWS][COLS] ,int rows, int cols, char set);

最后在game.c里面写代码,进行棋盘的创建 ,一样的要包含game.h的头文件

#include"game.h"
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
	for (int i = 0; i < rows; i++) // 遍历行
	{
		for (int j = 0; j < cols; j++) // 遍历列
		{
			arr[i][j] = set;   
		}
	}

}

好了,这一小步骤就完成了。 

3.棋盘的打印

在test.c里面写代码,因为我们只想打印出9*9,所以传参为ROW ,COL,如图

	//棋盘的打印
	DisplayBoard(mine, ROW, COL);  
	DisplayBoard(show, ROW, COL);  

然后在game.h里面声明 ,如图 

//棋盘的打印
void DisplayBoard(char arr[ROWS][COLS], int row, int col);

最后,在game.c里面实现,代码如下

//打印棋盘
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
	for (int i = 1; i <= row; i++) // 遍历行
	{
		for (int j = 1; j <= col; j++) // 遍历列
		{
			printf("%c ", arr[i][j]);  
		}
		printf("\n");  // 换行,不然捏在一起
	}

}

打印结果:

但是!但是!但是!

这样子是不是不美观,所以我们改进一下 ,代码如下 

//打印棋盘
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
	printf("——Justin的扫雷游戏——\n");  // 这样子更加好看 
	for (int j = 0; j <= col; j++)//打印列的数字,这里的j是从0开始,如果是1的话,会出现不对齐
	{
		printf("%d ", j); 
	}
	printf("\n"); //换行

	for (int i = 1; i <= row; i++) // 遍历行
	{
		printf("%d ", i);   //打印行的数字
		for (int j = 1; j <= col; j++) // 遍历列
		{
			printf("%c ", arr[i][j]);  
		}
		printf("\n");  // 换行,不然捏在一起
	}

}

效果如下

OK,这一小步骤完成了,兄弟们,快了快了! 

4.在mine 数组里面设置雷 

我们要在game.h 里面定义雷,并且设置数量

#define Boom 10  //定义炸弹10个

兄弟们,不知道你们了解过srand , rand , time函数吗 。如果不了解可以看我之前的文章有大致讲解实现猜数字游戏——详细版-CSDN博客

里面有讲解这些函数,那么我们在这里就简单讲解一下,因为会用到  。 

1.我们要生成随机数就需要用到rand函数,但是生成的是伪随机数,什么意思呢,你可以这样理解是计算机计算生成的随机数,不是真正意义上的随机数 。

2.那么怎么样可以让它生成真正的随机数呢,就需要用到srand函数,让rand函数做为srand函数的种子,理解起来可能有点抽象,但是这样生成的随机数就是真正的随机数,但是它每次生成的随机数都是一样的。

3.那么就需要用到time函数,time函数是一个时间戳,它的时间是不断的变化的,从1970年开始到现在,那么随机数就会随着时间的变化不断变化。

4.那么我们把3个函数一起使用,就可以生成出真正并且不断变化的随机数。

 这3个函数都是需要包含头文件的,我们把它写在game.h里面,而且另外2个源文件都包含了game.h的头文件,所以可以直接使用。如下图:

#include<stdlib.h>
#include<time.h>

然后在test.c里面的void test()里面写生成真正的随机数,因为不需要返回时间所以是NULL

srand((unsigned int)time(NULL));

 最后在game.c里面写设置雷的代码,如图: 

因为game.c也是包含game.h的头文件所以说test.c里面的生成随机数是可以直接用的

//在mine数组里面设置雷 
void SetMine(char mine[ROWS][COLS], int row, int col)
{
	int count = Boom;  //初始化count为Boom
	while (count) 
	{	
		//这里我们要用到rand函数,咱们前边定义了ROW为9 ,那么row来接收
		int x = rand() % row + 1; //row%一个数是0-8 ,+1就为1-9; 
		int y = rand() % col + 1; //col%一个数是0-8 ,+1就为1-9; 
		if (mine[x][y] == '0') //mine数组里面的这个坐标,如果是'0'
		{
			mine[x][y] = '1';//那么就设置为雷'1'
			count--; //每一次设置成功就减减
		}
		
	}
}

 按道理,循环会为10次或者10次以上,因为如果不是'0' ,那么它就会跳过,再来循环一次,直到10个雷全部设置完

生成2次,2个图,效果如下:

好的兄弟们,那么这一步骤也完成了

5.排查雷

好的,兄弟们。首先在test.c文件里的game写排查雷的函数,如下图

//排查雷
FindMine(mine, show, ROW, COL);  

然后在game.h里面对该函数进行声明

//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row , int col);

 

最后在game.c里面对函数进行实现。 

那么我们先把整体的逻辑整出来,首先我们需要输入值对吧来扫雷,然后是一个需要不断的 输入来进行排雷,那么肯定是需要while循环的。明白这一逻辑,输入的值要有一个范围的,我们布局的是9*9的规模,超出这个范围要报出警告,所以,我们要限制输入值的范围。

1.输入值的范围

if (x >= 1 && x <= row && y >= 1 && y <= col) //判断有效的范围
{
	
}
else // 不是该范围的提示信息
{
	printf("Justin提示你输入错误,不在这范围\n"); 
}

整体框架代码

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0; // 初始化
	int y = 0; 

	// 因为排雷是多次的,所以肯定是用while循环
	while (1) //暂时先放1,没有什么原因
	{
		printf("Justin请你排雷,请输入:>");
		scanf_s("%d %d", &x, &y); 
		if (x >= 1 && x <= row && y >= 1 && y <= col) //判断有效的范围
		{
			
		}
		else // 不是该范围的提示信息
		{
			printf("Justin提示你输入错误,不在这范围\n"); 
		}
		
	}
		
}

 运行结果

 2.对雷进行判断

如果你输入的值,正巧是雷的话,那很遗憾,就被炸死了。否则呢?不是雷,就要对该坐标自身的周围进行统计有多少个雷,代码实现如下

if (mine[x][y] == '1') // 如果输入的值,正巧是炸弹的话,就被炸死
{
				printf("你被炸死啦,我的好兄弟啊\n");
				DisplayBoard(mine, ROW, COL); //把布置雷的棋盘打印出来,死得明白点
				break; //炸死了就退出结束了
}
else //输入的值,不是炸弹那就需要统计周围炸弹的个数
{
				int count = GetMine(mine, x, y); //用函数来实现,把得到的值放入count
				//因为是要在show数组显示,所以用show来接收,count得到的数+'0',可以转变为字符型
				show[x][y] = count + '0'; 
				DisplayBoard(show, ROW, COL); //打印给玩家呈现的棋盘
}

让我们来解剖一下,想必关于雷的if语句执行的过程大家都已经明白了,那么我们来解析else所执行的语句。那么想要统计坐标周围雷的个数,首先先写个函数来实现雷个数的统计。然后需要一个count值来接收函数实现返回的值,那么为什么要+'0' ,因为可以把得到的数字转换成字符,我们之前创建的也是字符,然后把它放到show数组来显示。  

3.如何实现雷个数的统计

那么实现这一个函数有2种方法

第一种方法:

我们先解释一下,如果输入的坐标不是雷也就是不是'1' , 那么就统计该坐标周围雷的个数,这是第一个思路。第二个思路,也就是我们前边设置的时候’0‘不是炸弹,’1‘是炸弹,这就是为什么要这样设计的原因,方便了我们后期统计雷的个数,下面我们用一个图来讲解,如何统计 

 

那么在统计完后,我们还需要减掉8 * '0' , 因为我们周围是有8个数,如果全部是0也就是没有雷,那么显示的就是0个,如果有一个是雷,那么就会显示1个雷,我们要减去的是字符0,而不是数字0,字符0对应的是48,如果周围有一个雷,就是49-48=1,有2个就是50-48=2。

这就体现了我们前边使用字符0和1来做为坐标的好处,如果是其他的数,是不是就不好统计了

代码实现如下

//实现统计周围雷的个数,为什么加static ,因为我不想让别人看到和使用这个函数
//所以加上了static让它只能在这个内部使用,而且我把它写在这里也是不想暴露出去
static int GetMine(char mine[ROWS][COLS], int x, int  y)
{

	return  mine[x - 1][y] 
		+ mine[x - 1][y - 1]
		+ mine[x][y - 1] 
		+ mine[x + 1][y - 1]
		+ mine[x + 1][y]
		+ mine[x + 1][y + 1]
		+ mine[x][y + 1] 
		+ mine[x - 1][y + 1] - 8 * '0';  

}

方法二

其实了解了前边,那么这个方法也一样,只不过是用for循环

static int GetMine(char mine[ROWS][COLS], int x, int  y)
{
	int count = 0;  
	for (int i=x-1; i<=x+1; i++)
	{
		for (int j = y - 1; j <= y + 1; j++)
		{
			count += (mine[i][j] - '0'); 
		}
	}
	return count; 
}

那么雷的统计就完成了,来看下整体的代码 

4.进行优化

虽然说整体是做出来了,但是你得让它结束,除了被雷炸死的情况。

那么我们先初始化一个win ,然后在while循环的表达式里面,写入代码,如图

每次输入一次,并且不是雷,就在else里面win++ 

那么什么意思呢?因为排雷,除了10个雷还有剩余的71个不是雷。那么就是win<9*9-10 ,就等于win<71,排除了71个并且不被雷炸死,就胜利,结束循环。那么我们可以在下面写一个if语句,如

想要验证也可以,我们把雷的数量设置成80个,那么我们只用排查一个就可以了,运行结果 

 5.还有一个优化,就是解决可能会出现输入重复的坐标,那么解决方法如下图

如果是‘*’,那么没有被排查过,否则else 被排查过

当我输入重复的坐标后,会报出提示

五.完整代码 

test.c的代码

#include"game.h"
void menu() // 不需要返回所以是void , 菜单的内容 1 为 玩游戏 , 0 为 退出游戏
{
	printf("*********************\n"); 
	printf("**** 1.PLAY GAME ****\n"); 
	printf("**** 0.EXIT GAME ****\n"); 
	printf("*********************\n"); 
}

void game()
{
	//创建二维数组
	char mine[ROWS][COLS] = { 0 }; //用来存放布置好雷的信息 
	char show[ROWS][COLS] = { 0 }; //用来存放排查出来雷的个数信息

	//棋盘的创建,我们要对9*9布局进行操作所以ROW和COL要传

	InitBoard(mine, ROWS, COLS, '0');//mine数组全部为'0'  
	InitBoard(show, ROWS, COLS, '*');//show数组全部为'*'

	//棋盘的打印

	DisplayBoard(show, ROW, COL);

	//在mine数组设置雷 
	SetMine(mine, ROW, COL);  
	DisplayBoard(mine, ROW, COL);

	


	//排查雷
	FindMine(mine, show, ROW, COL);  

}
void test() //  调用函数,我们返回值是void ,什么也不返回
{
	srand((unsigned int)time(NULL));
	int	input = 0;  //初始化
	// 因为是一个循环过程所以要用do while
	do 
	{
		menu(); // 创建菜单的函数
		printf("请选择是否开启Justin扫雷游戏:> "); // 提供选择 
		scanf_s("%d", &input);   // 输入选择 
		switch (input) // 选择的内容
		{
		case 1:
			game();   // game 里面写游戏的内容 
			break; 
		case 0:
			printf("你退出游戏 ,Justin 觉得很可惜\n"); // 退出游戏 
			break; 
		default:
			printf("警告,你输入错误,请重新选择\n"); // 不是选择1也不是0,那就是错误的,所以提示重新选择
			break;
		}

	} while (input); 
}

int main()
{
	test(); // 创建函数
	return 0; 
}


game.c的代码

#include"game.h"
void InitBoard(char arr[ROWS][COLS], int rows, int cols, char set)
{
	for (int i = 0; i < rows; i++) // 遍历行
	{
		for (int j = 0; j < cols; j++) // 遍历列
		{
			arr[i][j] = set;   
		}
	}

}

//打印棋盘
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
	printf("——Justin的扫雷游戏——\n");  // 这样子更加好看 
	for (int j = 0; j <= col; j++)//打印列的数字,这里的j是从0开始,如果是1的话,会出现不对齐
	{
		printf("%d ", j); 
	}
	printf("\n"); //换行

	for (int i = 1; i <= row; i++) // 遍历行
	{
		printf("%d ", i);   //打印行的数字
		for (int j = 1; j <= col; j++) // 遍历列
		{
			printf("%c ", arr[i][j]);  
		}
		printf("\n");  // 换行,不然捏在一起
	}

}

//在mine数组里面设置雷 
void SetMine(char mine[ROWS][COLS], int row, int col)
{
	int count = Boom;  //初始化count为Boom
	while (count) 
	{	
		//这里我们要用到rand函数,咱们前边定义了ROW为9 ,那么row来接收
		int x = rand() % row + 1; //row%一个数是0-8 ,+1就为1-9; 
		int y = rand() % col + 1; //col%一个数是0-8 ,+1就为1-9; 
		if (mine[x][y] == '0') //mine数组里面的这个坐标,如果是'0'
		{
			mine[x][y] = '1';//那么就设置为雷'1'
			count--; //每一次设置成功就减减
		}
		
	}
}


//实现统计周围雷的个数,为什么加static ,因为我不想让别人看到和使用这个函数
//所以加上了static让它只能在这个内部使用,而且我把它写在这里也是不想暴露出去
static int GetMine(char mine[ROWS][COLS], int x, int  y)
{

	return  mine[x - 1][y] 
		+ mine[x - 1][y - 1]
		+ mine[x][y - 1] 
		+ mine[x + 1][y - 1]
		+ mine[x + 1][y]
		+ mine[x + 1][y + 1]
		+ mine[x][y + 1] 
		+ mine[x - 1][y + 1] - 8 * '0';  

}
//
//static int GetMine(char mine[ROWS][COLS], int x, int  y)
//{
//	int count = 0;  
//	for (int i=x-1; i<=x+1; i++)
//	{
//		for (int j = y - 1; j <= y + 1; j++)
//		{
//			count += (mine[i][j] - '0'); 
//		}
//	}
//	return count; 
//}
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0; // 初始化
	int y = 0; 
	int win = 0; 
	// 因为排雷是多次的,所以肯定是用while循环
	while (win < row * col - Boom) //win判断的是扫雷的结束,当满足这个条件就结束循环了
	{
		printf("Justin请你排雷,请输入:>");
		scanf_s("%d %d", &x, &y); 
		if (x >= 1 && x <= row && y >= 1 && y <= col) //判断有效的范围
		{
			if (show[x][y] == '*')
			{
				if (mine[x][y] == '1') // 如果输入的值,正巧是炸弹的话,就被炸死
				{
					printf("你被炸死啦,我的好兄弟啊\n");
					DisplayBoard(mine, ROW, COL); //把布置雷的棋盘打印出来,死得明白点
					break; //炸死了就退出结束了
				}
				else //输入的值,不是炸弹那就需要统计周围炸弹的个数
				{
					int count = GetMine(mine, x, y); //用函数来实现,把得到的值放入count
					//因为是要在show数组显示,所以用show来接收,count得到的数+'0',可以转变为字符型
					show[x][y] = count + '0';
					DisplayBoard(show, ROW, COL); //打印给玩家呈现的棋盘
					win++;
				}
			}
			else//否则被排查过
			{
				printf("改坐标被排除过啦\n");
			}
		}
		else // 不是该范围的提示信息
		{
			printf("Justin提示你输入错误,不在这范围\n"); 
		}
		
	}
	if (win == row * col - Boom) // 满足这个条件胜利
	{
		printf("恭喜你,胜利啦,Justin 觉得你太厉害了\n");
		DisplayBoard(mine, ROW, COL);// 打印雷的布置
		
	}

}



game.h的代码

#include<stdio.h>
#include<stdlib.h>
#include<time.h>



#define ROW 9  //9*9的布局,所以ROW和COL为9
#define COL 9 

#define ROWS ROW + 2   //11*11的布局,所以ROWS和COLS为11
#define COLS COL + 2   

#define Boom 10  //定义炸弹10个


// 函数的声明 

 //这里很巧妙我用了 int set 来接受'0'和'*' 这样我就可以对这2个进行设置而不用多去建立一个数组
void InitBoard(char arr[ROWS][COLS] ,int rows, int cols, char set);

//棋盘的打印
void DisplayBoard(char arr[ROWS][COLS], int row, int col);

//在mine数组里面设置雷
void SetMine(char mine[ROWS][COLS], int row , int col);

//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row , int col);

结尾

文章里的扫雷布局图片为了方便理解我使用了鹏哥C语言的课件,也经过了同意使用!

感谢大家的阅读,希望能帮助到大家! 

  • 33
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值