扫雷游戏实现超详细讲解——C语言

目录

一、扫雷游戏规则介绍

 二、游戏设计思路

 三、代码的实现

3.1 打印菜单

3.2 初始化数组

 3.3 打印棋盘 

3.4 布置雷

 3.5 排查雷

四、游戏优化

 4.1 爆炸式展开

4.2 标记地雷

五、完整的代码

5.1 头文件:

5.2 源文件:

5.3 源文件:


一、扫雷游戏规则介绍

        扫雷是一款很大众的益智类游戏,游戏规则也很简单,在最短时间把所有的所有的雷排查出来即为胜利,如果在期间踩到雷则游戏失败(结束)。这款游戏分为初级(9*9),中级(16*16),高级(16*30),或则自定义大小方块矩阵中随机布置一定量的炸弹(初级为10个,中级为40个,高级为99个炸弹)。由玩家逐个翻开方块,以找出所有炸弹为目的。

        注意:如果翻开的格子下面没有炸弹,则会显示周围一圈八个格子中炸弹的个数,如下图所示(这里的1指的就是在该格子周围一共有一个雷),所以想要过关就要利用好这些显示出来的数据。

 二、游戏设计思路

        首先,我们需要思考怎样制造一个这样的矩阵以及存放雷呢?这时我们就需要一个二维数组来存放这样的数据。也就是说,扫雷游戏需要存储布置好雷的信息,我们需要一个二维数组,那需要一个多大的二维数组呢,我们继续分析。

        接下来我们要思考,如何把雷和非雷区分开呢,这时候大家首先能想到的应该是雷用‘*’号,非雷用‘#’号对吧,这种想法没问题,但这里小编给大家提一个思路就是,雷用字符‘1’表示,非雷用字符‘0’表示,小编这样建议呢,肯定有一定的好处,其实就是在后面,我们排查雷的时候,这样容易计算格子周围一圈雷的个数。

        假设当我们布置完十个雷之后,开始游戏,然后随机点一个格子,如果这个格子是雷,则游戏结束,如果这个格子不是雷,那么就会显示这个格子周围一圈的雷的个数,如果周围是一个雷的话,则显示‘1’,那么问题来了,这里的‘1’,到底是该格子周围雷的个数呢,还是雷呢?所以为了解决这个问题,我们需要建立两个二维数组,一个用来我们布置雷‘1’和非雷‘0’,这个是用户看不见的,另一个用于展示给玩家看的棋盘,存放的是排查雷的信息,这样两边就不会冲突了。展示给玩家看的数组为了保持神秘感,我们最开始放的全是✳,这时我们创建的数组就是字符数组。

        紧接着,我们思考一下,我们翻格子的时候,如果这个格子不是雷,那么就会排查这个格子周围八个格子存在雷的个数,假设我们翻开左上角的格子的时候,但是该格子上面和左边没有格子了怎么办?那就会出现越界访问的情况,为了避免这个情况出现,我们只需要把9*9的格子扩张成11*11的格子就可以了,如图所示,这样遍历的时候就不会出现越界访问的情况了。

 三、代码的实现

        这里创建文件的思路还是和三子棋的思路一样,创建一个test.c文件(用于测试基本函数),创建一个game.c(用于编写具体的游戏实现代码)以及game.h的文件(用于游戏函数的声明和头文件的引用)

3.1 打印菜单

        首先我们需要打印菜单,如下代码。

void menu()
{
	printf("*****************************\n");
	printf("*********1.play**************\n");
	printf("*********0.exit**************\n");
	printf("*****************************\n");
}
int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			printf("扫雷开始\n");
			break;
		case 0:
			printf("退出游戏\n");
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

         菜单打印完之后,我们就开始游戏的实现吧。

3.2 初始化数组

        首先我们定义两个数组,一个是用来存放布置雷的信息,另一个是存放排查雷的信息。为了方便我们以后对代码的修改以及对游戏的优化,我们这里在头文件中把行和列分别定义出来,如果以后想玩更大的棋盘,我们只需在头文件中进行修改即可。

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

        然后我们对数组进行初始化操作,我们期望用一个函数就可以对两个数组分别进行初始化,所以我们在初始化数组的时候,需要把初始的内容也传过去,这样同一个函数调用两次即可。如图所示。

//初始化数组
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;
		}
	}
}

 3.3 打印棋盘 

        当我们初始完之后,我们想要看看棋盘到底是什么样的,那么我们就需要打印棋盘,打印棋盘的时候,展示给我们的其实是9*9的棋盘,我们最开始设成11*11的棋盘只是为了防止越界访问。

打印的时候行和列都是从1开始,9结束的。具体代码如下所示

//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("----------------扫雷游戏----------------\n");
	for (i = 1; i <= 9; i++)
	{
		int j = 0;
		for (j = 1; j <= 9; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

       

         打印之后,我们会发现,我们不能够快速找到某个位置的行和列,所以需要在打印棋盘前打印行和列,这样就可以很容易找到行和列,也可以很好的通过下标索引来排查雷。怎么实现呢,小编这里的方法是,在一开始就把列号打印出来,然后可以在每一行打印之前,先打印行号,具体代码和运行结果如下。

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("----------------扫雷游戏----------------\n");
	for (i = 0; i <= col; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= 9; i++)
	{
		printf("%d ", i);
		int j = 0;
		for (j = 1; j <= 9; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

         其实我们每次打印的时候只需要打印show数组,另一个mine数组是不用打印的,mine数组是不需要打印的,刚刚只是为了测试我们初始化的结果,所以才把两个数组都打印了出来。

3.4 布置雷

        接下来我们需要布置雷,大家思考一下,我们布置雷实际上还是把雷放在中间的9*9的数组中去,也就是说我们这里布置雷只需要操作mine数组。怎么实现呢,这里我们假设布置十个雷,那么我们就需要生成十个随机坐标,来放置雷。但是为了以后方便更改数据,所以这里我们可以把雷的数量在头文件中。在放置雷的时候,我们需要判断位置能不能放置,具体代码和运行结果如下图所示。

//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
	//布置10个雷
	//随机生成十个随机坐标,布置雷
	int count = COUNT;
	while (count)
	{
		int x = rand()%row+1;
		int y = rand()%col+1;
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

 3.5 排查雷

        布置完之后,接下来我们需要排查雷,大家可以思考一下,我们排查雷是需要在mine数组里面查找,然后将查到的数据放在show数组中展示。我们这个排查雷的函数需要将两个数组都传过去。具体怎么实现呢?代码如下

//统计周围雷的个数
int  GetMineCount(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');
}

//排查雷的函数
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-COUNT)
	{
		printf("输入所以排查的目标:\n");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)//检查坐标是否合法
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了。\n");
				DisplayBoard(mine,ROW,COL);
				break;
			}
			else
			{
				//该位置不是雷,统计该位置周围有几个雷
				int count = GetMineCount(mine, x, y);
				show[x][y] = count + '0';//转换成数字字符
				DisplayBoard(show, ROW, COL);
				win++;
			}
		}
		else
		{
			printf("坐标非法,重新输入:\n");
		}
	}
	if (win == ROW * COL - COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

        看见上面代码,大家需要注意的是,小编这里是如何将字符数字和数字字符进行转换的,我们知道字符‘0’的ASCLL码值是48,数字0的ASCLL码值是0,当数字0加上‘0’的时候,得到的结果就是字符数字‘0’。还需要注意的问题是,我们如何来判断游戏的结束,我们知道对应9*9的棋盘来说,有10个雷,也就是我们需要排查71次才能将十个雷完全排查出来,使得游戏结束。

四、游戏优化

 4.1 爆炸式展开

        我们真正在玩三子棋的时候,会发现,有时候点一个位置时会展开一大片,这是为什么呢?

当出现什么情况时会展开以一篇呢?其实当我们点击的位置不是雷,并且周围八个位置都不是雷的情况下,才会展开一篇。

如图所示,我们这里点开 ①所在的位置,发现该位置不是雷,且周围一圈都不是雷的情况下,我们把该位置的‘※’号改成空格,然后会继续遍历该位置周围八个位置,如果仍然遍历到某个位置和这个位置情况一样时(如上图②位置所示),那么就继续按照这样的方式在该位置周围一圈遍历。显然是使用递归的方式来实现这些操作

但是到这里,还会出现两个问题,第一个问题那就是我们在遍历②周围位置时,还会再一次遍历①这个位置,那么就会出现死递归的现象,那么如何解决这个问题呢,那么就需要我们我们在递归前判断一下该位置是不是已经被遍历过了(已经被置成空格),如果是,那就跳过该位置的递归。加上这一限制条件就可以了。

第二个问题就是当我们遍历最旁边的一行时,会出现越界访问的情况,假设我们排查左上角的位置,我们会发现它的左边和上面都没有雷,然后如果继续按照如上方法遍历就会出现越界访问,那么如何解决这个问题呢?我们需要给递归加上限制条件即可。具体代码如下图所示。

//爆炸展开
void ExplodeBoard(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y,int* p)
{
	int temp = *p;
	//限制条件
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		//计算该位置周围雷的个数
		int count = GetMineCount(mine, x, y);
		if (count == 0)
		{
			//把该位置变成空格
			show[x][y] = ' ';
			int i = 0;
			//向周围进行递归遍历
			for (i = x - 1; i <= x + 1; i++)
			{
				int j = 0;
				for (j = y - 1; j <= y + 1; j++)
				{
					//限制对重复递归调用的条件,避免死递归
					if (show[i][j] == '*')
					{
						ExplodeBoard(mine, show, row, col, i, j,&temp);
						temp--;
					}
				}
			}
		}
		else
		{
			show[x][y] = count + '0';
		}
	}
}

4.2 标记地雷

        为了游戏更好的进行,当我们确定地雷时,我们可以对地雷进行标记。具体代码如下所示。

//标记雷的函数
void Signmine(char board[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)
		{
			if (board[x][y] == '*')
			{
				board[x][y] = '!';
				break;
			}
			else
			{
				printf("该位置不能被标记,请重新输入:\n");
			}
		}
		else
		{
			printf("坐标非法,请重新输入:\n");
		}
	}
}

五、完整的代码

5.1 头文件<game.h>:

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

#define ROW 9
#define COL 9

#define ROWS ROW+2
#define COLS COL+2

//雷的个数
#define 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 board[ROWS][COLS], int row, int col);

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

5.2 源文件<test.c>:

#define _CRT_SECURE_NO_WARNINGS 1
#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];//存放排查雷的信息
	//初始化棋盘
	//1,mine数组最开始全是‘0’
	//2,show数组最开始全是‘*’
	InitBoard(mine, ROWS, COLS, '0');
	InitBoard(show, ROWS, COLS, '*');
	//打印棋盘
	//DisplayBoard(mine, ROW, COL);
	DisplayBoard(show, ROW, COL);
	//布置雷
	SetMine(mine, 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();
			break;
		case 0:
			printf("退出游戏\n");
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
	return 0;
}

5.3 源文件<game.c>:

#define _CRT_SECURE_NO_WARNINGS 1
#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 <= col; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= 9; i++)
	{
		printf("%d ", i);
		int j = 0;
		for (j = 1; j <= 9; j++)
		{
			printf("%c ", board[i][j]);
		}
		printf("\n");
	}
}

//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
	//布置10个雷
	//随机生成十个随机坐标,布置雷
	int count = COUNT;
	while (count)
	{
		int x = rand()%row+1;
		int y = rand()%col+1;
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}


//统计周围雷的个数
int  GetMineCount(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');
}


//爆炸展开
void ExplodeBoard(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y,int* p)
{
	int temp = *p;
	//限制条件
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		//计算该位置周围雷的个数
		int count = GetMineCount(mine, x, y);
		if (count == 0)
		{
			//把该位置变成空格
			show[x][y] = ' ';
			int i = 0;
			//向周围进行递归遍历
			for (i = x - 1; i <= x + 1; i++)
			{
				int j = 0;
				for (j = y - 1; j <= y + 1; j++)
				{
					//限制对重复递归调用的条件,避免死递归
					if (show[i][j] == '*')
					{
						ExplodeBoard(mine, show, row, col, i, j,&temp);
						temp++;
					}
				}
			}
		}
		else
		{
			show[x][y] = count + '0';
		}
	}
}

//标记雷的函数
void Signmine(char board[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)
		{
			if (board[x][y] == '*')
			{
				board[x][y] = '!';
				break;
			}
			else
			{
				printf("该位置不能被标记,请重新输入:\n");
			}
		}
		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;
	char ch = 0;
	while (win < ROW*COL-COUNT)
	{
		printf("输入所要排查的目标:\n");
		scanf("%d %d", &x, &y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)//检查坐标是否合法
		{
			if (mine[x][y] == '1')
			{
				printf("很遗憾,你被炸死了。\n");
				DisplayBoard(mine,ROW,COL);
				break;
			}
			else
			{
				//爆炸展开
				win++;
				ExplodeBoard(mine, show, row, col,x,y,&win);
				//打印棋盘
				DisplayBoard(show, ROW, COL);
				printf("需要标注地雷输入:Y,不需要则输入:N\n");
				//清空缓冲区
				while ((ch = getchar()) != '\n');
				scanf("%c", &ch);
				if (ch == 'Y')
				{
					//标记雷的位置
					Signmine(show, ROW, COL);
				}
			}
		}
		else
		{
			printf("坐标非法,重新输入:\n");
		}
		//打印棋盘
		DisplayBoard(show, ROW, COL);
	}
	if (win == ROW * COL - COUNT)
	{
		printf("恭喜你,排雷成功\n");
		DisplayBoard(mine, ROW, COL);
	}
}

        这就是我今天为大家带来的一期扫雷游戏实现的讲解,如果有哪里写的不好的地方,欢迎在评论区指点,如果认为小编写的还不错的,可以留下你们点赞 收藏和关注,这是对小编极大的认可也是小编继续走下去的动力。谢谢大家!

 

  • 27
    点赞
  • 40
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 23
    评论
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

褪色~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值