C语言——扫雷(含递归展开)

目录

1.菜单栏的建立

2.雷信息的存储

3.初始化

4.建立雷区

5.布雷

6.扫雷

7.扫雷之递归展开

代码小白の结语


大家好啊!今天给大家带来的是扫雷游戏的代码实现

首先,我们来看一下我们为实现扫雷的几个主要步骤

1 . 菜单栏的建立

2 . 信息储存

3 . 初始化

4 . 建立雷区

5 . 布雷

6 . 扫雷

7 . 递归展开

在正式开始之前,我们先定义出两个源文件和一个头文件,分别为test.c 、game.c和game.h

而在使用时将定义类的全部放进game.h里,用#include"game.h"进行引用就行了

为什么要这样作呢?因为我们分开写的话,

在test.c里可以清晰地看见自己写代码的逻辑,如下

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
	//建立菜单供用户选择
	printf("****************************\n");
	printf("****   1.play   0.exit  ****\n");
	printf("****************************\n");
}
void game()
{
	//1.信息建立
	char mine[ROWS][COLS] = { 0 };
	char show[ROWS][COLS] = { 0 };
	//2.初始化(创建初始化函数Initboard)
	Initboard(mine, ROWS, COLS, '0');
	Initboard(show, ROWS, COLS, '*');
	//3.建立格子,创建函数Displayboard
	//此处传ROW,COL是因为让用户只看到这个
	//ROWS,COLS是用于防止计算雷的数量时越界用的
	//Displayboard(mine, ROW, COL);
	Displayboard(show, ROW, COL);
	//4.布置雷
	Setmine(mine, ROW, COL);
	//Displayboard(mine, ROW, COL);
	//5.扫雷
	mine_sweeping(mine, show, ROW, COL);
	//若踩到无雷处则自动扩散
	//dilatation(mine, show, ROW, COL);
}
int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏");
			break;
		default:
			printf("输入错误,请重新输入");
			break;
		}
	} while (input);
	return 0;
}

而将代码的定义放进game.h里,如果我要改变我的变量如”雷的个数“时,就会方便很多,如下

#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define Count 10

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

void Initboard(char board[ROWS][COLS], int rows, int cols);
void Displayboard(char board[ROWS][COLS], int row, int col);
void Setmine(char mine[ROWS][COLS], int row, int col);
void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//void dilatation(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y);

(注意!注意!注意!本文中的ROW和COL代表的是行和列,具体看上方代码!!!)

最后在game.c中实现代码,这样的话,我们打代码时无论是检查还是改变变量,效率都会大大提升,如下

#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//1.初始化实现
void Initboard(char board[ROWS][COLS], int rows, int cols, int set)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			//此处若为这样的话,就不知道要放‘0’还是‘1’
			//若放set就可以将mine->'0',show->'*'
			//board[i][j] = '0';
			board[i][j] = set;
		}
	}
}
//
void Displayboard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i <= row; i++)
	{
		//此处打印各自上方的数字序列(用以方便用户寻找坐标)
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		//此处用以打印左侧的数字序列,每一行循环之前都将打印一个数字
		printf("%d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

void Setmine(char mine[ROWS][COLS], int row, int col)
{
	int count = Count;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}
int mine_count(char mine[ROWS][COLS], int x, int y)
{
	return mine[x - 1][y + 1] + mine[x][y + 1] +
		mine[x + 1][y + 1] + mine[x - 1][y] +
		mine[x + 1][y] + mine[x - 1][y - 1] +
		mine[x][y - 1] + mine[x + 1][y - 1] - 8 * '0';
}
void dilatation(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	int i = 0;
	int j = 0;
	int win = 0;
	if (mine_count(mine, x, y) == 0)
	{
		show[x][y] = ' ';
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if (show[i][j] == '*' && i > 0 && i <= row && j > 0 && j <= col)
				{
					dilatation(mine, show, row, col, i, j);
				}
			}
		}
	}
	else
	{
		show[x][y] = mine_count(mine, x, y) + '0';
	}
}
int Win(char show[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	int WIN = 0;
	for (int i = 1; i <= row; i++)
	{
		for (int j = 1; j <= col; j++)
		{
			if (show[i][j] != '*')
			{
				WIN++;
			}
		}
	}
	return WIN;
}
void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	//int number = 0;
	int win = 0;
	//判断坐标合法性
	while (win < ROW * COL - Count)
	{
		printf("请输入坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//1.踩到雷
			if (mine[x][y] == '1')
			{
				printf("踩到雷了,游戏失败\n");
				Displayboard(mine, ROW, COL);
				break;
			}
			//2.没踩到雷
			else
			{
				dilatation(mine, show, row, col, x, y);
				Displayboard(show, row, col);
				//此处统计一下格子上空格的个数,建立函数
				win = Win(show, row, col);
				//printf("%d", win);
			}

		}
		else
			printf("输入坐标非法,请重新输入:");
	}
	if (win == row * col - Count)
	{
		printf("恭喜你扫雷成功\n");
	}
}

接下来我们话不多说,进入正题!!

相信扫雷对各位来说肯定不陌生,于我而言,这个游戏也是我初中电脑课时必不可少的一个环节(哭笑),那接下来,就让我们看看这个游戏的实现吧!

1.菜单栏的建立

正如上图所示,扫雷游戏是由一个个格子组成,而固定数目的雷随机分布在格子上,随着雷被我们一个个找到,游戏也慢慢走向了结尾。

这样子看的话,如若我们要实现扫雷的话,那我们就先建立一个菜单栏,代码如下

#include"game.h"
void menu()
{
	//建立菜单供用户选择
	printf("****************************\n");
	printf("****   1.play   0.exit  ****\n");
	printf("****************************\n");
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请输入你的选择:>");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("退出游戏");
			break;
		default:
			printf("输入错误,请重新输入");
			break;
		}
	} while (input);
	return 0;
}

这是一个老生常谈的部分了,我们先将menu打出来,告诉用户如若输入 1 则进入game函数(游戏的主要部分),如若输入 0 则退出游戏。但,说不定有些玩家一身反骨,就是要输入一些这两个数字以外的数,那我们如果遇见这种情况,就default提醒一下玩家:输入错误就OK了!

这一部分较为简单,我们简短地总结一遍,就直接进入我们今天的主要部分吧!

2.雷信息的存储

在开始讲解之前,我们先来预想一下吧:我们先假设我们雷区的大小为9*9,那我们建立了雷区之后,就应该到布雷的环节了吧,为了区分有雷与无雷,我们是不是应该将有雷的地方标记为‘1’,而无雷的地方1标记为‘0’啊,不然我们就不知道我们的雷布置下去没有。那接下来重点来了:

假设我点了一个位置,而他周围一圈共有一个雷,那如果我这时将‘1’打印在屏幕上时,我在调试代码时,又怎么知道这个‘1’代表的是雷,还是我判断出来的雷的个数呢?

那这时就有人要问了:我直接用其他符号比如‘#’来表示雷不就行了,各位看官莫要着急,细细往下看便明白了

此时有一个方法——建立另一个雷区,而在另一个雷区上只有‘ * ’如下图

那纵观我们建立的两个数组,我们是不是让其中一个在开始时全部都时‘ 0 ’方便调试,而另一个则是玩家角度即全打印‘ * ’,如若我们仅仅只是用其他符号来表示雷的话,后期调试时就没有现在来得方便,且在后文(第六步 扫雷 中没法进行),而这两个数组一个叫mine,一个叫show

理论讨论至此,那我们就知道了我们应该建立两个数组,如下

//game.h
#define ROW 9
#define COL 9

//test.c
char mine[ROW][COL] = { 0 };
char show[ROW][COL] = { 0 };

可是,真的时建立一个9*9的数组吗?

各位别急,这个问题将在第三点里被解决。各位往下看便是了!

3.初始化

可能有些不清楚的人会问:初始化是干啥用的?这里假设我想让棋盘上刚出现时全是空格,如下

亦或是想让他全是‘*’

但是如果不初始化的话打印出来就是这个样子的(你无法确定)

既然如此,我们就先初始化一下我们的棋盘吧,那我们是不是应该初始化一个9*9的雷区如下呢?

//game.h
#define ROW 9
#define COL 9

//test.c
Initboard(mine, ROW, COL, '0');
Initboard(show, ROW, COL, '*');

但是,真的是初始化一个9*9的雷区吗?

我们来预想一下,我们把雷区初始化完且将雷布置上去之后,就要开始扫雷了对吧!一个格子被翻开,我们是不是应该知道他周围一圈有多少个雷啊。那好,假设这颗雷在雷区中心,我们只需要判断他所在位置周围一圈有多少颗雷就可以了。但,问题来了:

如果这颗雷在雷区的边缘位置,那我们再去判断周围的8个格子时,是不是就越界了呀!

有人可能会说:那我在点开格子时判断一下,如果在边边,那我就计算没越界那个方向(如左边)的5个格子,如果在角落,那我就计算一下没越界那个方向(如左上方)的3个格子里的雷的数量不就好了?

这个方法可行,但是,太麻烦了!

不妨这样子想,我们要的是一个9*9大小的雷区,那我们就建立一个11*11的雷区如下图

这时如果我还需要判断是否越界了吗?在mine数组里,我们直接将整个11*11的雷区初始化成‘0’,即0个雷,而我们打印时只让玩家看到红色的部分。这样一来,当玩家选择了边界的坐标时,在红色方框外的坐标自动判定为没有雷,就不必去判断我该往哪个方向去计算雷的数量了

又因为是两个数组(mine,show),未来我们在game.h里接收时如果将mine和show的分开来接收就需要写两行,但我更希望直接拿一个board来接收两个数组,如下

void Initboard(char board[ROWS][COLS], int rows, int cols);

所以我们的show数组也可以建立成11*11的大小,如下

//game.h
#define ROW 11
#define COL 11
#define ROWS ROW+2
#define COLS COL+2

//test.c
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };

 而我们要初始化的话,可以在test.c里定义一个函数Initboard,而在game.h里声明一下这个函数,最后在game.c里实现,如下

//test.c
Initboard(mine, ROWS, COLS, '0');
Initboard(show, ROWS, COLS, '*');

//game.h
void Initboard(char board[ROWS][COLS], int rows, int cols);

//game.c
void Initboard(char board[ROWS][COLS], int rows, int cols, int set)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			//此处不另放一个set的话,就不知道要放‘0’还是‘1’
			//若放set就可以将mine->'0',show->'*'
			board[i][j] = set;
		}
	}
}

因为只需要函数Initboard帮忙初始化而不需要返回值,所以用的就是void

而初始化的过程也是较为简单的,只需要定义出随即两个数(此处的是 i 和 j ),加上两个for循环(代表整个雷区),最后将board[i][j]定义为想要的就行了(board同时代表了mine和show) 

但有一个小的点需要注意一下,如果我们在test.c里传参时只传了mine,show,rows,cols的话就会发现一个问题,我在初始化board时,board代表的是mine和show两个,此时我们根本不知道是要初始化成‘ 0 ’还是‘ * ’。而如果我们传参时将各自需要初始化的符号一起传过去,而用set统一接收起来,这是我们就将这个问题解决啦!(具体请参考上一个代码)

4.建立雷区

初始化完,我们就该建立雷区了!先将代码奉上!!

//test.c
//4.建立格子,创建函数Displayboard
//此处传ROW,COL是因为让用户只看到这个
//ROWS,COLS是用于防止计算雷的数量时越界用的
Displayboard(mine, ROW, COL);
Displayboard(show, ROW, COL);

//game.h
void Displayboard(char board[ROWS][COLS], int row, int col);

//game.c
void Displayboard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i <= row; i++)
	{
		//此处打印各自上方的数字序列(用以方便用户寻找坐标)
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		//此处用以打印左侧的数字序列,每一行循环之前都将打印一个数字
		printf("%d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

和初始化一样,我们可以在test.c里建立一个函数 ,我们将其取名为:Displayboard,接下来就是在game.c里实现他了

因为我们的雷区是一个二维平面,如果我们想建立的雷区并不是那么复杂的话,那么只需要两个for循环就可以表示整个雷区了,如下

void Displayboard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	
	for (i = 1; i <= row; i++)
	{
		
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
	}
}

因为我们创建的是一个11*11的数组,而我们只想让玩家看到中间那个9*9的雷区,所以我们的 i 和 j 都需要从1开始,并让其<=row或<=col(此处放row和col的原因是:我们未来想改变雷区的大小时这里就会自动改变,而我们如果在此处放上数字的话就达不到这种效果),这样就能将其打印出来啦!

但当我们想让代码跑起来时,却发现出了问题

我们会发现我们忘记了要换行,这是一个容易疏忽的点,就在这里提上一嘴

当我们加上printf("\n");后,如下

void Displayboard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	
	for (i = 1; i <= row; i++)
	{
		
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
        printf("\n");
	}
}

此时的结果就是我们想要的啦!

但,其实还有一点小小的不足,这不足之处在于:如果此时让玩家输入坐标的话,那玩家想知道那是第几行第几个的话就需要一个一个去数,游戏体验感就会变差,那我们能不能再将代码优化成下图所示的模样呢?

答案是肯定的,那我们来设想一下,最上面的那一行0~9我们是不是只需要建立一个for循环,从0开始,让其循环10次,也就是让其<=row就可以啦,此处放一张图促进理解

而最左边那一列数字的话我们这样看,每一行开始之前都打印一个数字,而这个数字在跟着第二层for循环循环过一遍了之后又需要++一下,那我们发现, j 是不是相当符合这个条件啊!那我们就可以直接将最左边一列放进第二层for循环里

综上所述,如下就是代码最终的样子啦!

void Displayboard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	for (i = 0; i <= row; i++)
	{
		//此处打印各自上方的数字序列(用以方便用户寻找坐标)
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		//此处用以打印左侧的数字序列,每一行循环之前都将打印一个数字
		printf("%d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

5.布雷

建立完雷区之后我们就应该进行雷的布置了,简单点理解,就是埋雷!话不多说,先把代码奉上

//test.c
//5.布置雷
Setmine(mine, ROW, COL);

//int main()内部
srand((unsigned int)time(NULL));

//game.h
void Setmine(char mine[ROWS][COLS], int row, int col);

//game.c
void Setmine(char mine[ROWS][COLS], int row, int col)
{
	int count = Count;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

好,接下来我们就来具体谈谈布雷这个环节吧!

我们先来理一理我们的逻辑吧。我们在玩扫雷游戏时,应该要保证每一局游戏玩到的雷的位置是不一样的,如果雷的位置一直不变的话,这个游戏也就没有什么价值了。

那么既然要让其随机分布,那么我们就得用到rand函数(生成随机数列),此外,我们还需要srand函数(随机数起点)与时间戳time(注:这是一个库函数,需要引头文件#include<time.h>,而rand与srand的头文件都是#include<stdlib.h>),具体如上

随机分布的问题解决了之后,我们就这样子做:我们先定义两个 x 与 y ,如果想让其在1~9之间随机分布可怎么做呢?各位且看,我如果让(此处先拿 x 举例)x = rand()模上一个row,也就是x = rand()%row,那我们是不是就可以让其产生0~8之间的随机数啦,那我们再令其整体+1,是不是就可以产生1~9之间的数字了呢,如下

int x = rand() % row + 1;
int y = rand() % col + 1;

 而后,我们就可以让mine[x][y]代表雷区中的随即坐标了!

但我们所要布置的雷可以根据我们的想法来定义数量,而数量是一个还好,如果不止一个呢?那我们就会发现此处需要一个while循环,那while循环什么时候停止呢?在我们的雷布置完了之后就该停止了对吧,那我们可以在game.h里设置一个#define Count 10来定义雷的数量吧 ( 注:此处的10代表的是10个雷,且定义在game.h里更加便捷,想改就能改,不用再在冗长的代码里找 ) ,而整个Setmine函数里就可以用int count = Count来接收,而while旁的()内就放上count就行了

目前代码如下

//test.c
//int main()内
srand((unsigned int)time(NULL));

//game.h
#define Count 5

//game.c
void Setmine(char mine[ROWS][COLS], int row, int col)
{
	int count = Count;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
	}
}

但我们再来想一想,如果这么做的话,那电脑会不会定义到同一个地方去呢?就是电脑在一个地方埋完了雷之后,会不会又随机定义到此处再埋一颗呢?这是完全有可能的对吧!既然如此,我们可以用 if 判断一下,如果此处已经有雷了,那我们就不管他 ( 电脑就会重新定义一个随机数 ) ,如果没有雷,那我们就让将该处的mine数组由字符‘ 0 ’改为字符‘ 1 ’代表埋了一颗雷,而后再count--一下,当雷埋完了之后,循环也就停止了。如下:

if (mine[x][y] == '0')
{
	mine[x][y] = '1';
	count--;
}

至此,我们埋雷这一步骤也就讲完了,接下来我们进入下一步,扫雷!

6.扫雷

 进行到第六步,我们也就进行到今天最主要的一部分了,话不多说,上代码!

//test.c
mine_sweeping(mine, show, ROW, COL);

//game.h
void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

//game.c
int mine_count(char mine [ROWS][COLS], int x, int y)
{
	return mine[x - 1][y + 1] + mine[x][y + 1] +
		mine[x + 1][y + 1] + mine[x - 1][y] +
		mine[x + 1][y] + mine[x - 1][y - 1] +
		mine[x][y - 1] + mine[x + 1][y - 1] - 8 * '0';
}
void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int number = 0;
	//判断坐标合法性
	while (number<ROW*COL-Count)
	{
        printf("请输入坐标:>");
	    scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//1.踩到雷
			if (mine[x][y] == '1')
			{
				printf("踩到雷了,游戏失败\n");
				Displayboard(mine, ROW, COL);
				break;
			}
			//2.没踩到雷
			else
			{
				int count = mine_count(mine, x, y);
				show[x][y] = count + '0';
                Displayboard(show, ROW, COL);
				number++;
			}
		}
	}
	if (number == ROW * COL - Count)
	{
		printf("恭喜你扫雷成功\n");
	}
}

 在正式开始之前,我们先来理一下扫雷这一步骤的逻辑。

我们想让用户扫雷,就需要让用户输入坐标,而这个坐标就需要我们用scanf来接收,即

scanf("%d %d",x,y);

在此之前或许我们可以用 printf 提醒一下玩家:这个时候该输坐标啦。

但是总有玩家不按游戏规则来,9*9的雷区非要输入一个不在这个区间内的坐标,那这时我们就可以用 if 语句来判断一下:如果坐标在给定区间内,则进行扫雷游戏的下一步,else,则用 printf 提示输入错误,随后再让玩家输入一次坐标,那我们发现,这是一个循环的过程,所以我们需要一个while语句,但条件是什么呢?我们暂时还不知道,那我们就先放个 1 进去,上述代码如下

void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	//判断坐标合法性
	while (1)
	{
        printf("请输入坐标:>");
	    scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//1.踩到雷
			
			//2.没踩到雷
		}
	}

而玩家输入了一个坐标之后,会遇见两种情况:1.直接踩到雷,然后游戏结束。2.玩家没有踩到雷,游戏继续。进行到这里我们就发现,我们需要一个 if  else语句进行判断:

如果踩到雷,则printf 告诉玩家:你踩到了雷,然后被炸死了。随后用Didplayboard将雷的分布打印出来,最后break跳出循环。 

如果没踩到雷,则计算一遍该坐标周围8个格子里有多少颗雷,并将结果以字符的形式打印在show[x][y]上(将show[x][y]处的‘ * ’换掉),最后调用Displayboard将输出坐标后的结果打印出来。

可是问题来了,我该怎么计算其周围八个格子内雷的个数呢?

设想一下,我们先前将有雷的地方设置为‘1’,无雷的地方设置为‘0’,我们如果想知道周围8个格子里有多少个雷,仅需要将一个一个列出来相加就行了!

但,真的能直接相加吗?答案是否定的!

我们放在上面的是字符‘1’,而不是数字1’,但解决方法也很简单

我们仔细看一看这张ASCII码表就会发现,字符‘0’的大小为48,而其他字符的值减去字符‘0’的值都等于我们想要的数字,比如字符‘1’的值为49,‘1’ - ‘0’ == 49 - 48 = 1。由此我们可以想到,我们将玩家输入的坐标附近的八个坐标的字符加在一起,最后统一减去8个 ‘0’ ,就能得到我们想要的结果啦!!

但这么看的话我们的代码好像有点长,那我们就将其放进一个自定义函数mine_count里吧!

代码如下:

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

而返回值我们用int count接收,而接收的值再加上‘0’就又能变成对应的字符,最后将其放在show[x][y]上就可以了,代码如下

//1.踩到雷
if (mine[x][y] == '1')
{
	printf("踩到雷了,游戏失败\n");
	Displayboard(mine, ROW, COL);
	break;
}
//2.没踩到雷
else
{
	int count = mine_count(mine, x, y);
	show[x][y] = count + '0';
	number++;
}

代码敲完了,但我们忽略了一个问题:我们现在的while循环里的条件还是(1),这意味着我们除非点到雷我们才会退出游戏,且我们现在写的这个游戏必定以失败结尾,所以我们需要一个变量,且这个变量要么一直在减少,等到其减小为0时便终止循环。要么这个变量就有一个条件设置(比如a<3(下文有a++)),当这个变量不满足这个条件时,循环终止。

而这里我们选择第二种明显更好一点。你想啊,我们如果能将这个变量(这里就叫这个变量为number吧)设置为 雷区总数 - 雷的个数,而我们每扫完一个格子就让number++一下,那我们不就完成了吗!!!

代码如下:

void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int number = 0;
	//判断坐标合法性
	while (number<ROW*COL-Count)
	{
        printf("请输入坐标:>");
	    scanf("%d%d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//1.踩到雷
			if (mine[x][y] == '1')
			{
				printf("踩到雷了,游戏失败\n");
				Displayboard(mine, ROW, COL);
				break;
			}
			//2.没踩到雷
			else
			{
				int count = mine_count(mine, x, y);
				show[x][y] = count + '0';
                Displayboard(show, ROW, COL);
				number++;
			}
		}
	}
	if (number == ROW * COL - Count)
	{
		printf("恭喜你扫雷成功\n");
	}
}

有人会问:最后为什么要用 if 先判断一下才printf提示玩家扫雷成功呢?

因为 到哪个位置的不只是因为扫雷成功了,也有可能是因为扫雷失败了!当玩家踩到雷的时候会break跳出语句,而这时如果不判断的话,出现的问题就会很尴尬,因为电脑先提示你扫雷失败了,接着break跳出语句后有提示你扫雷成功了。由此可见,这里判断一下还是相当必要的!

接下来我要讲的是扫雷游戏里最难的一个部分

7.扫雷之递归展开

在正式开始讲之前,先上代码!

void dilatation(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	int i = 0;
	int j = 0;
	int win = 0;
	if (mine_count(mine, x, y) == 0)
	{
		show[x][y] = ' ';
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if (show[i][j] == '*' && i > 0 && i <= row && j > 0 && j <= col)
				{
					dilatation(mine, show, row, col, i, j);
				}
			}
		}
	}
	else
	{
		show[x][y] = mine_count(mine, x, y) + '0';
	}
}
int Win(char show[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	int WIN = 0;
	for (int i = 1; i <= row; i++)
	{
		for (int j = 1; j <= col; j++)
		{
			if (show[i][j] != '*')
			{
				WIN++;
			}
		}
	}
	return WIN;
}
void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	//int number = 0;
	int win = 0;
	//判断坐标合法性
	while (win < ROW * COL - Count)
	{
		printf("请输入坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//1.踩到雷
			if (mine[x][y] == '1')
			{
				printf("踩到雷了,游戏失败\n");
				Displayboard(mine, ROW, COL);
				break;
			}
			//2.没踩到雷
			else
			{
				dilatation(mine, show, row, col, x, y);
				Displayboard(show, row, col);
				//此处统计一下格子上空格的个数,建立函数
				win = Win(show, row, col);
				//printf("%d", win);
			}

		}
		else
			printf("输入坐标非法,请重新输入:");
	}
	if (win == row * col - Count)
	{
		printf("恭喜你扫雷成功\n");
	}
}

为什么要有这一个部分呢?我们先来看两张图(递归前与递归后)

图一只能一个一个输,即使那一片都是‘0’也只会显示输入坐标位置的变化

而图二则能将输入坐标周围为‘0’的地方全部变成空格

那这是怎么做的呢?我们往下看吧!

先想一想,我们要达成的效果是:当我们输入一个坐标时,它会自动检测周围是否有雷。如若有雷,则将该坐标周围雷的数目以字符的形式打印在show上。如果没雷,则会自动向四周扩展并再一次检测四周有没有雷,如此往复。这么看来,这个问题用函数递归解决倒也是相当符合。

那问题来了,如果我要用函数递归来解决的话,那我总需要一个函数吧,那这个函数的作用是什么呢?仔细想想,这个函数的作用是不是就是扩张并检测啊,而dilatation有扩张的意思,我们就拿这个作为我们自定义函数的名字吧!

我们先做一个假设:假设我们想让他往八个方向扩张,每向八个方向扩张完之后,再让扩张后的坐标向八个方向进行再扩张。那我们想啊,如果要这样扩张的话,那他会不会往回扩张呢?很有可能吧!那我们应该怎么做才能让他往回扩张时不会扩张到相同的地方呢?

这里我们是不是可以让他每扩张到一个坐标时,就将其用另一种符号标记一下呀,比如空格('   ')。当其检测到空格时,就会知道这个地方我来过,就不会再来了,这样就有效避免了死循环。

但,你会发现,如果要按照这个思路去实现代码的话,麻不麻烦暂且不谈,写出来的代码的效率是不是很低啊,我要让他每一次都分八个方向去行动,如下

分完后再让每一个分出去的小部分再分一遍,听得都怪绕的,那有没有什么好一点的办法呢?

答案是肯定的,如下

(模型有点简陋,各位看客多多担待一下哈)

如果我们不让他分开行动了,我让这个函数的功能变成一次向外扩散一圈(图中黑点表示玩家选中的没有雷的坐标,第一次扩张成黄色那一圈,第二次扩张成红色那一圈......),而原本的想法也有可取之处,我们每让其扩散一层,就将周围一圈之内没有雷的格子全部变成空格,如若有雷的话,就将周围一圈内的雷的个数以字符的形式打印在show数组上

接下来我们来一步步拆解

进入函数前我们需要判断,该坐标周围一圈有没有雷,如果有,统计个数;如果没有,进入函数

void dilatation(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	if (mine_count(mine, x, y) == 0)
	{
		//函数主体
	}
	else
	{
		show[x][y] = mine_count(mine, x, y) + '0';
	}
}

那进入函数主体之后,我们先让该坐标在show上变成空格(能进入函数主体的周围一圈都没有雷),而后我们发现,我们需要让这个函数做的,是让其扩张一圈,那我们总该需要知道其周围一圈各自的坐标吧,随后再用双层 for 循环代表这一圈里的内容,易得 x 的坐标是从x-1到x+1的,y的坐标则是从y-1到y+1

接下来问题来了,如果要递归的话,我是应该把x-1传过去呢还是把x+1传过去呢?是应该把y-1传过去呢还是把y+1传过去呢?你发现都不对,那我们换一种思路,如果我把 i 传上去会怎么样?

如果我把 i 传上去的话,i 在for循环里出来后,其代表的是扩张出去那一圈每个点的 x 坐标,同理,j 代表的就是扩张出去那一圈每个点的 y 坐标。如此一来,我们将 i 和 j 传过去就刚好解决了如何让其越扩越大的问题

理论成立,上代码!

void dilatation(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	int i = 0;
	int j = 0;
	int win = 0;
	if (mine_count(mine, x, y) == 0)
	{
		show[x][y] = ' ';
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if (show[i][j] == '*' && i > 0 && i <= row && j > 0 && j <= col)
				{
					dilatation(mine, show, row, col, i, j);
				}
			}
		}
	}
	else
	{
		show[x][y] = mine_count(mine, x, y) + '0';
	}
}

多提一嘴,show[x][y] = '    ';是为了不让其递归时反复判断已经判断过的,而再次递归时就只需要用 if 语句判断一下是否为星号就行了(如果是星号则必定未被判断过)。且为了不让其判断到超出格子的地方,则我们需要限定一个范围,如上。

最后的最后,我们的代码终于是,敲完了!!!

吗?

如果此时我们如果尝试去让代码跑起来的话,你会发现代码跑不动了!

如图,我的雷全都扫完了,但电脑还是提示你让你继续下,这不存心想让我输吗?

但当我们会去检查代码时就会发现我们的number并没有把dilatation函数消除的方块给计算在内,那这可如何是好啊?

我们仔细想想,我们当时让while循环停止的条件是

while (number < ROW * COL - Count)

而number计算的又是玩家点开的格子数,那我们不妨这样子写:

定义一个新的函数名叫Win,代表我们即将将代码敲完,走向胜利!!而这个函数的作用是计算递归完之后雷区内剩余的不是‘ * ’的数量。我们同样可以用双层 for 循环代表整个雷区,从 1 开始,<=row / col,再定义一个变量WIN,当我们找到一个不是‘ * ’的格子时就让WIN++一下,至此,我们才算是真正地完结了!!!

上代码!!!

int Win(char show[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	int WIN = 0;
	for (int i = 1; i <= row; i++)
	{
		for (int j = 1; j <= col; j++)
		{
			if (show[i][j] != '*')
			{
				WIN++;
			}
		}
	}
	return WIN;
}
void mine_sweeping(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	//int number = 0;
	int win = 0;
	//判断坐标合法性
	while (win < ROW * COL - Count)
	{
		printf("请输入坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			//1.踩到雷
			if (mine[x][y] == '1')
			{
				printf("踩到雷了,游戏失败\n");
				Displayboard(mine, ROW, COL);
				break;
			}
			//2.没踩到雷
			else
			{
				dilatation(mine, show, row, col, x, y);
				Displayboard(show, row, col);
				//此处统计一下格子上空格的个数,建立函数
				win = Win(show, row, col);
				//printf("%d", win);
			}

		}
		else
			printf("输入坐标非法,请重新输入:");
	}
	if (win == row * col - Count)
	{
		printf("恭喜你扫雷成功\n");
	}
}

完结,撒花*★,°*:.☆( ̄▽ ̄)/$:*.°★* 。

代码小白の结语

这篇博客篇幅达到了17006个字,同时也是我的处女作。在此处我想感谢一下一位在51CTO上颇负盛名的大佬——蒙奇D索隆,没有他的帮助,我的第一篇blogger也不会像现在这般圆满。

最后,如果大家喜欢这篇博客的话,希望大家可以多多关注喔!!!

                                                                                                                      ————清晨朝暮

                                                                                                                                    2023.10.2  

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值