七、数组和函数实践:扫雷游戏

1.1 扫雷游戏的功能说明

• 使⽤控制台实现经典的扫雷游戏

• 游戏可以通过菜单实现继续玩或者退出游戏

• 扫雷的棋盘是9*9的格⼦

• 默认随机布置10个雷

• 可以排查雷

​ ◦ 如果位置不是雷,就显示周围有几个雷

​ ◦ 如果位置是雷,就炸死游戏结束

​ ◦ 把除10个雷之外的所有雷都找出来,排雷成功,游戏结束

1.2 游戏文件结构的设计

为了养成良好的习惯,同时为以后在公司团队合作编程打下基础,该游戏采用多文件来进行编程。
test.c //⽂件中写游戏的测试逻辑 
game.c //⽂件中写游戏中函数的实现等,是整个游戏的核心代码,一般不会对用户展示。
game.h //⽂件中写游戏需要的数据类型和函数声明等

2 扫雷游戏的具体功能及代码实现

2.1 菜单

代码:

void menu()
{
	printf("**********************\n");
	printf("******  1.play  ******\n");
	printf("******  0.exit  ******\n");
	printf("**********************\n");
}

在游戏开始前首先打印菜单供用户选择,输入1则开始游戏,输入0则退出游戏。

所以在主函数中应该还存在一个分支结构,这个结构负责对输入的结果进行判断,如果是1则运行游戏相关的函数,是0就退出游戏。

int main()
{
	int input = 0;
    srand((unsigned int)time(NULL));//用来生成随机数的种子,不理解可以先不管
	do
	{
		menu();//打印菜单
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)//对用户输入的结果进行判断
		{
		case 1://结果为1,开始游戏
			game();
			break;
		case 0://结果为0,退出游戏
			printf("退出游戏\n");
			break;
		default://两个结果都不是,重新输入
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

注意:变量input要在do-while循环外定义,如果在内部定义的话while将无法识别input,导致编译错误。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5UfVBHOw-1691422964508)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807151920451.png)]

2.2 核心代码

2.2.1 游戏数据结构的分析

从扫雷游戏的基本玩法可以知道:

1.如果排查的位置是雷,就被炸死了,游戏结束。

2.如果排查的位置不是雷,就显示这个坐标周围有几个雷。

那么在扫雷前,布置的雷和排查出的雷的信息都需要存储,所以需要一定的数据结构来存储这些信息。因为需要在9*9的棋盘上布置雷的信息和排查雷,那么首先想到的就是创建⼀个9*9的数组来存放信息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rWy6zOEc-1691422964509)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230729105256899.png)]

需要布置雷的位置就存放1,不需要布置雷的位置就存放0。

假设排查(2,5)这个坐标时,访问周围的一圈8个黄色位置,统计周围雷的个数是1。

假设排查(8,6)这个坐标时,访问周围的⼀圈8个黄色位置,统计周围雷的个数时,最下面的三个坐标就会越界,为了防止越界,在设计的时候,给数组扩大一圈,雷还是布置在中间的9*9的坐标上,外围一圈不去布置雷即可,这样就解决了越界的问题。所以将存放数据的数组创建成 11*11 比较合适。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rOISNBws-1691422964509)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230729105616668.png)]

继续分析,在棋盘上布置了雷,棋盘上雷的信息(1)和非雷的信息(0),假设排查了某一个位置后,这个坐标处不是雷,这个坐标的周围有1个雷,那么需要将排查出的雷的数量信息记录存储,并打印出来,作为排雷的重要参考信息。

如果把雷的个数信息存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能产生混淆和打印上的困难。

如果雷和非雷的信息不使用数字,而使用某些字符,这样做会导致棋盘上除了有有雷和非雷的信息外,还有排查出的雷的个数信息,就比较混乱,不够方便。

比较好的方案是,专门给一个棋盘(对应一个数组mine)存放布置好的雷的信息,再给另外一个棋盘(对应另外一个数组show)存放排查出的雷的信息。这样就互不干扰,把雷布置到 mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考。

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0K0pfTZi-1691422964509)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230729110257455.png)]

为什么选择用字符’ 0 ‘和字符’ 1 '来表示雷和非雷而不是数字0和数字1或者其他别的符号,这里暂且不表,我们先接受这个设定。

对应的数组应该是:

char mine[11][11];//⽤来存放布置好的雷的信息
char show[11][11];//⽤来存放排查出的雷的个数信息

两个数组都采用字符类型,这样就可以用同一个函数来实现它们的打印。

接下来,就可以开始对核心代码进行实现。

2.2.2 game函数

在实现函数的初始阶段,我们可能会写出下面的代码:

void game()
{
	char mine[11][11];
	char show[11][11];
	//棋盘初始化
	InitBoard(mine, 11, 11,'0');
	InitBoard(show, 11, 11,'*');
	//布置雷
	SetMine(mine, 9, 9);
	//棋盘打印
	DisplayBoard(show, 9, 9);
	//排查雷
	FindMine(mine, show, 9, 9);
}

这段代码虽然逻辑没有问题,最后其实也能正常运行,但如果后期我们想让雷盘更大一些,比如变成20*20的雷盘的时候,你会发现我们需要把上面设计雷盘大小的参数全部替换,这会非常不方便,所以我们可以在头文件 game.h 中定义一些常量的符号,需要使用这些符号的时候直接包含这个头文件即可,而需要更改雷盘大小也只需要在头文件中更改即可。

更加高效的代码:

//game.h
#include <stdio.h>
#include <Windows.h>

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2
//test.c
void game()
{
	char mine[ROWS][COLS];
	char show[ROWS][COLS];
	//棋盘初始化
	InitBoard(mine, ROWS, COLS,'0');
	InitBoard(show, ROWS, COLS,'*');
	//布置雷
	SetMine(mine, ROW, COL);
	system("cls");//用于清除刚才打印的菜单,使游戏界面美观整洁,调用需要引用头文件<Windows.h>
	//棋盘打印
	DisplayBoard(show, ROW, COL);
	//DisplayBoard(mine, ROW, COL); //用于自己调试观察,比如观察雷是否被正确布置等,确认无误后注释掉即可
	//排查雷
	FindMine(mine, show, ROW, COL);
}

game函数中,对游戏设计的基本逻辑进行了展示,其中调用的具体功能的代码将在 game.c 中实现。

2.2.2.1 InitBoard函数

InitBoard函数用来实现棋盘的初始化。

写这个函数前,我们先在头文件中对其进行声明。

//game.h
void InitBoard(char board[ROWS][COLS], int rows, int cols);//初始化棋盘

然后,我们在 game.c 中对其进行实现。

在实现函数的初始阶段,我们可能会写出下面的代码:

//game.c
void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
	int i = 0;
	for (i = 0; i < rows; i++)
	{
		int j = 0;
		for (j = 0; j < cols; j++)
		{
			board[i][j] = '0';
		}
	}
}

但写完之后我们会发现,这个函数只能实现对雷盘初始化为字符’ 0 ‘的功能,如果要把雷盘初始化为’ * ',就需要再写一个函数,这就变麻烦了,我们正是因为想用同一个函数就可以解决问题,所以才把两个雷盘都设为字符类型。

如何实现把雷盘想初始化成什么就初始化成什么呢?

其实很好解决,我们只需要把想要初始化的字符作为参数传到函数中即可。

更加高效的代码:

//game.h
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
//game.c
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;
		}
	}
}
2.2.2.2 DisplayBoard函数

将雷盘初始化后,我们可能想看一下打印出来的效果,这个时候我们就需要一个函数来实现对雷盘的打印,而棋盘的打印其实就是打印数组。

和前面一样,我们依然需要先对函数进行声明,然后再进行实现。

代码:

//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;
	printf("----- 扫雷游戏 -----\n");
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);//打印行号
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);//打印列号
		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
	printf("----- 扫雷游戏 -----\n");
}

效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zD80z4PW-1691422964509)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807211724040.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yPiVPnfy-1691422964509)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807211848884.png)]

2.2.2.3 SetMine函数

SetMine函数用来实现布置雷。

实现布置雷的函数前,我们需要注意两个地方:
1.雷的布置要满足随机性,所以我们要用到生成随机数的函数,而用于随机生成坐标的rand函数的种子srand函数只需要在main函数中使用一次即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pFGO4Tp5-1691422964510)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807214016077.png)]

2.在布置雷的时候需要判断该位置是否已经布置过雷,以免重复。

为了方便修改雷的个数,我们在头文件中专门定义一个常量来表示雷的个数。

这里我们用while函数来实现布置雷的,每布置一个雷,待布置的雷就减一个,直到待布置的雷为0,函数也不再运行。

代码:

//game.h
#define EASY_COUNT 10//表示雷的个数为10个
//game.c
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')//是字符'0'说明这个位置没有被布置过
		{
			mine[x][y] = '1';
			count--;
		}
	}
}
2.2.2.4 FindMine函数

FindMine函数用来实现排查雷。

想象一下,查找雷的时候,我们需要先在mine数组中找到排查坐标处周围雷的信息,找到后还要放到show数组中对应的位置里去,所以,mine数组和show数组都要作为参数传到FindMine函数中去。

排查雷的时候我们首先需要让用户输入需要排查的坐标,然后判断坐标的合法性及该坐标是否被排查过,其次再判断该坐标是否有雷,如果没有,就继续排查,直到满足游戏胜利的条件,此处涉及到的循环我们用while函数来实现。

而不是雷的时候,我们需要收集周围雷的信息,所以我们还需要一个函数来计算坐标周围雷的个数,我们将其命名为GetMineCount。

GetMineCount统计出的雷的个数最终会被放到show数组中去,而show数组中存放的数字又都是字符的形式,这里就涉及到一个数值数字和字符数字转换的问题。

从ASCII码表中可以知道,字符数字0~9在表中的值的范围为48-57。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f9tB9kjW-1691422964510)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807220836160.png)]

而我们发现,字符’ 1 ‘减去字符’ 0 ‘恰好就是数字1,同理,字符’ 2 ‘减去字符’ 0 ‘得到数字2,依此类推,我们就实现了字符数字和数值数字之间的转换,这也是之前我们为什么选择用字符’ 0 ‘和字符’ 1 '来表示雷和非雷的原因,如果用其他符号,这个地方想要实现把GetMineCount函数以及把统计出的雷的个数传到show数组中去就会变得麻烦。

要实现GetMineCount函数,我们还需要知道二维数组中一个坐标的周围坐标的表示形式,下图就很好地表示了坐标之间的相对关系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rIjOsDZv-1691422964510)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807222315361.png)]

这个时候实现GetMineCount函数和FindMine函数思路就比较清晰了:

//game.h
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷
//game.c
int GetMineCount(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');//字符数字减去字符0得到对应的数值数字
	//由于该函数是专门用于函数FindMine的,只在game.c内运行,
	//所以不需要在game.h中声明
}
//game.c
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	printf("请输入要排查的坐标:>");
	scanf("%d %d", &x, &y);
	while (win < row * col - EASY_COUNT)//当win不再小于时,说明所有的雷都已排完,跳出循环
	{
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
		{
			if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
			{
				printf("该坐标被排查过,重新输入坐标\n");
				continue;
			}
			else if (mine[x][y] == '1')
			{
				system("cls");
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, ROW, COL);//被炸死了就打印mine数组,让用户知道雷的正确信息
				break;
			}
			else
			{
				int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
				show[x][y] = count + '0';//数字加上字符0得到对应数字的字符
				system("cls");
				DisplayBoard(show, ROW, COL);
				win++;

			}
		}
		else
		{
			printf("坐标非法,重新输入\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		system("cls");
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

2.3 游戏完整代码

2.3.1 game.h

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
//把头文件都放在"game.h"里面,这样在其他文件直接包含"game.h"即可

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

//函数的声明
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);//打印棋盘
void SetMine(char mine[ROWS][COLS], int row, int col);//布置雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷

2.3.2 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 DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("----- 扫雷游戏 -----\n");
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);//打印行号
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);//打印列号
		int j = 0;
		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')//是字符'0'说明这个位置没有被布置过
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

int GetMineCount(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');//字符数字减去字符0得到对应的数值数字
	//由于该函数是专门用于函数FindMine的,只在game.c内运行,
	//所以不需要在game.h中声明
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	
	while (win < row * col - EASY_COUNT)//当win不再小于时,说明所有的雷都已排完,跳出循环
	{
		printf("请输入要排查的坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
		{
			if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
			{
				printf("该坐标被排查过,重新输入坐标\n");
				continue;
			}
			 if (mine[x][y] == '1')
			{
				system("cls");
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, ROW, COL);//被炸死了就打印mine数组,让用户知道雷的正确信息
				break;
			}
			else
			{
				int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
				show[x][y] = count + '0';//数字加上字符0得到对应数字的字符
				system("cls");
				DisplayBoard(show, ROW, COL);
				win++;

			}
		}
		else
		{
			printf("坐标非法,重新输入\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		system("cls");
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

2.3.3 test.c

#include "game.h"
void menu()
{
	printf("**********************\n");
	printf("******  1.play  ******\n");
	printf("******  0.exit  ******\n");
	printf("**********************\n");
}

void game()
{
	char mine[ROWS][COLS];
	char show[ROWS][COLS];
	//棋盘初始化
	InitBoard(mine, ROWS, COLS,'0');
	InitBoard(show, ROWS, COLS,'*');
	//布置雷
	SetMine(mine, ROW, COL);
	system("cls");//用于清除刚才打印的菜单,使游戏界面美观整洁,调用需要引用头文件<Windows.h>
	//棋盘打印
	DisplayBoard(show, ROW, COL);
	//DisplayBoard(mine, ROW, COL); //用于自己调试观察,比如观察雷是否被正确布置等,确认无误后注释掉即可
	//排查雷
	FindMine(mine, show, ROW, COL);
}

int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();//打印菜单
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)//对用户输入的结果进行判断
		{
		case 1:
			game();//结果为1,开始游戏
			break;
		case 0://结果为0,退出游戏
			printf("退出游戏\n");
			break;
		default://两个结果都不是,重新输入
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

2.4 后续优化

2.4.1 ExplosionSpread函数

刚才游玩我们自己设计的扫雷时,可以很明显地感受到一个缺陷,就是我们每次只能排查一个坐标,也就是说就算每次都没有踩到雷,在雷的个数为10个的情况下,也要至少排查71次才能通过,这无疑是很影响游戏体验的。而通过游玩网页版的扫雷可以发现,当用户点击一个坐标,如果该坐标及其周围的坐标都没有雷,那么雷盘就会一次性展开一片。而我们可以利用递归的方式来实现这样的效果。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgjxlzJ6-1691422964510)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807230515551.png)]

代码实现:

//game.c
void ExplosionSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw)
{
	if (x >= 1 && x <= row && y >= 1 && y <= col) //判断输入的坐标是否在排查范围内,否则递归的坐标可能出到棋盘外
	{
		int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
		if (count == 0)
		{
			show[x][y] = ' ';
			(*pw)++;
			int i = 0;
			int j = 0;
			for (i = x - 1; i <= x + 1; i++)
			{
				for (j = y - 1; j <= y + 1; j++)
				{
					if (show[i][j] == '*')
						ExplosionSpread(mine, show, row, col, i, j, pw);
				}
			}
		}
		else
		{
			show[x][y] = count + '0';
			(*pw)++;
		}
	}
}

代码实现后,ExplosionSpread函数就可以用在FindMine函数中来帮助我们提升排雷的效率。

//game.c
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	
	while (win < row * col - EASY_COUNT)//当win不再小于时,说明所有的雷都已排完,跳出循环
	{
		printf("请输入要排查的坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
		{
			if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
			{
				printf("该坐标被排查过,重新输入坐标\n");
				continue;
			}
			 if (mine[x][y] == '1')
			{
				system("cls");
				printf("很遗憾,你被炸死了\n");
				DisplayBoard(mine, ROW, COL);//被炸死了就打印mine数组,让用户知道雷的正确信息
				break;
			}
			else
			{
                ExplosionSpread(mine, show, row, col, x, y, pw);  //爆炸式展开
				system("cls");  //清空屏幕
				DisplayBoard(show, ROW, COL);//打印棋盘
			}
		}
		else
		{
			printf("坐标非法,重新输入\n");
		}
	}
	if (win == row * col - EASY_COUNT)
	{
		system("cls");
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

2.4.2 MarkMine函数

在网页版的扫雷中我们还可以发现,如果我们确定一个坐标一定是雷时,我们可以利用标记功能来标识该坐标,方便我们后续的判断

本代码中,我们用字符 ! 来标识雷。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEXL5450-1691422964513)(C:\Users\HackerKevin\AppData\Roaming\Typora\typora-user-images\image-20230807232320520.png)]

代码实现:

void MarkMine(char board[ROWS][COLS], int row, int col,int x,int y)
{
	
	while (1)
	{
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
		{
			if (board[x][y] == '*')//标记
			{
				board[x][y] = '!';
				break;
			}
			if (board[x][y] == '!')//取消标记
			{
				board[x][y] = '*';
				break;
			}
			else
			{
				printf("\n输入错误,请输入未被排查的坐标!\n");
				continue;
			}
		}
		else
		{
			printf("输入错误,请输入正确的坐标!\n");
		}
	}
}

想要执行MarkMine函数,我们应该以不同的输入方式来程序识别我们是否要标记。我们知道,scanf函数的返回值是返回成功读取的变量个数,那么我们可以利用这一点来让程序进行区分,如果我们输入的是两个数字表示的坐标,那么就说明我们是想排查那个坐标,而如果两个数字前还有一个字符,那么就表示我们要对那个坐标进行标记。如果想要严谨一点,我们可以添加一个执行判断的函数,只有输入了正确的字符,才会进行标记。在这里,为了省事,我们可以不限定输入的字符是什么。

把MarkMine函数应用在FindMine函数中,我们就可以实现雷的标记。

代码实现:

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	int* pw = &win;
	char ch = 0;
	int lose = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入要排查的坐标(如要标记雷,请在坐标前输入\"!\":>");
		while ((ch = getchar()) != '\n');//输入数字后回车,这个回车会留在待读取的区域,需要用getchar吸收
		
		if (scanf("%d %d", &x, &y) == 2)//排查雷
		{
			if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
			{
				if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
				{
					printf("该坐标被排查过,重新输入坐标\n");
					continue;
				}
				else if (mine[x][y] == '1')
				{
					system("cls");
					printf("很遗憾,你被炸死了\n");
					lose = 1;
					DisplayBoard(mine, ROW, COL);
					break;
				}
				else
				{
					ExplosionSpread(mine, show, row, col, x, y, pw);
					//int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
					//show[x][y] = count + '0';//数字加上字符0可以得到对应数字的字符
					system("cls");
					DisplayBoard(show, ROW, COL);
				}
			}
			else
			{
				printf("坐标非法,重新输入\n");
			}
			if (win == row * col - EASY_COUNT)
			{
				system("cls");
				printf("恭喜你,排雷成功\n");
				DisplayBoard(mine, ROW, COL);
			}
		}
		else if (scanf("%c %d %d", &ch, &x, &y) == 3)//标记雷
		{
			MarkMine(show, row, col, x, y);
			system("cls");
			DisplayBoard(show, ROW, COL);
			continue;
		}
	}
}

2.5 优化后的完整代码

2.5.1 game.h

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
//把头文件都放在"game.h"里面,这样在其他文件直接包含"game.h"即可
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10

//函数的声明
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);//打印棋盘
void SetMine(char mine[ROWS][COLS], int row, int col);//布置雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷
void ExplosionSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw);//爆炸式展开

2.5.2 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 DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("----- 扫雷游戏 -----\n");
	for (i = 0; i <= row; i++)
	{
		printf("%d ", i);//打印行号
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		printf("%d ", i);//打印列号
		int j = 0;
		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')//是字符'0'说明这个位置没有被布置过
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

int GetMineCount(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');//字符数字减去字符0得到对应的数值数字
	//由于该函数是专门用于函数FindMine的,只在game.c内运行,
	//所以不需要在game.h中声明
}

void MarkMine(char board[ROWS][COLS], int row, int col,int x,int y)
{
	
	while (1)
	{
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
		{
			if (board[x][y] == '*')//标记
			{
				board[x][y] = '!';
				break;
			}
			if (board[x][y] == '!')//取消标记
			{
				board[x][y] = '*';
				break;
			}
			else
			{
				printf("\n输入错误,请输入未被排查的坐标!\n");
				continue;
			}
		}
		else
		{
			printf("输入错误,请输入正确的坐标!\n");
		}
	}
}

void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;
	int* pw = &win;
	char ch = 0;
	int lose = 0;
	while (win < row * col - EASY_COUNT)
	{
		printf("请输入要排查的坐标(如要标记雷,请在坐标前输入\"!\":>");
		while ((ch = getchar()) != '\n');//输入数字后回车,这个回车会留在待读取的区域,需要用getchar吸收
		
		if (scanf("%d %d", &x, &y) == 2)//排查雷
		{
			if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
			{
				if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
				{
					printf("该坐标被排查过,重新输入坐标\n");
					continue;
				}
				else if (mine[x][y] == '1')
				{
					system("cls");
					printf("很遗憾,你被炸死了\n");
					lose = 1;
					DisplayBoard(mine, ROW, COL);
					break;
				}
				else
				{
					ExplosionSpread(mine, show, row, col, x, y, pw);
					//int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
					//show[x][y] = count + '0';//数字加上字符0可以得到对应数字的字符
					system("cls");
					DisplayBoard(show, ROW, COL);
				}
			}
			else
			{
				printf("坐标非法,重新输入\n");
			}
			if (win == row * col - EASY_COUNT)
			{
				system("cls");
				printf("恭喜你,排雷成功\n");
				DisplayBoard(mine, ROW, COL);
			}
		}
		else if (scanf("%c %d %d", &ch, &x, &y) == 3)//标记雷
		{
			MarkMine(show, row, col, x, y);
			system("cls");
			DisplayBoard(show, ROW, COL);
			continue;
		}
	}
}

void ExplosionSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw)
{
	if (x >= 1 && x <= row && y >= 1 && y <= col) //判断输入的坐标是否在排查范围内,否则递归的坐标可能出到棋盘外
	{
		int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
		if (count == 0)
		{
			show[x][y] = ' ';
			(*pw)++;
			int i = 0;
			int j = 0;
			for (i = x - 1; i <= x + 1; i++)
			{
				for (j = y - 1; j <= y + 1; j++)
				{
					if (show[i][j] == '*')
						ExplosionSpread(mine, show, row, col, i, j, pw);
				}
			}
		}
		else
		{
			show[x][y] = count + '0';
			(*pw)++;
		}
	}
}

2.5.3 test.c

#include "game.h"

void menu()
{
	printf("**********************\n");
	printf("******  1.play  ******\n");
	printf("******  0.exit  ******\n");
	printf("**********************\n");
}

void game()
{
	char mine[ROWS][COLS];
	char show[ROWS][COLS];
	//棋盘初始化
	InitBoard(mine, ROWS, COLS,'0');
	InitBoard(show, ROWS, COLS,'*');
	//布置雷
	SetMine(mine, ROW, COL);
	system("cls");//用于清除刚才打印的菜单,使游戏界面美观整洁,调用需要引用头文件<Windows.h>
	//棋盘打印
	DisplayBoard(show, ROW, COL);
	//DisplayBoard(mine, ROW, COL); //用于自己调试观察,比如观察雷是否被正确布置等,确认无误后注释掉即可
	//排查雷
	FindMine(mine, show, ROW, COL);
}
int main()
{
	int input = 0;
	srand((unsigned int)time(NULL));
	do
	{
		menu();//打印菜单
		printf("请选择:>");
		scanf("%d", &input);
		switch (input)//对用户输入的结果进行判断
		{
		case 1:
			game();//结果为1,开始游戏
			break;
		case 0://结果为0,退出游戏
			printf("退出游戏\n");
			break;
		default://两个结果都不是,重新输入
			printf("选择错误,重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

2.5.4 游戏效果演示

扫雷演示

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HackerKevn

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值