扫雷(含递归展开)

目录

前言

游戏实现效果

游戏概况

游戏规则

游戏实现

创建头文件

游戏实现

1.游戏菜单

2.棋盘初始化

3.布雷

4.开始排雷

游戏代码

Saolei.h

game.c

Saolei.c

总结


前言

     扫雷相信大家都玩过把,这是一款经典的小游戏,以前没有网的时候,相信大家都玩过这个游戏。点击下面的链接大家都可以先去玩一玩,熟悉一下。

https://www.saolei123.com/

游戏实现效果

游戏概况

首先我们先来讲一下扫雷,它是一个9×9的矩形,里面有若干个雷(看难度),我们通过鼠标点击方块,根据所给的信息把雷排完,就算胜利,如果点到了雷,就会被炸死,游戏失败。

注意:本章所讲的内容主要是9×9的模式。

游戏规则

首先我们先随机点击一块小方格,会看到有些小方格的里面显示了一些数字,1,2,3,4都有,这代表了以该小方格位中心周围的8个格子的雷的个数,如果没有则不显示。

我们可以通过右键进行标记我们已经找到的雷,当只剩下最后10个全是雷的时候,就赢了。当然,在这之前如果任意碰到了一个雷,都会导致游戏失败。这个游戏可以很好的锻炼我们的思维,耐心,和推理能力,是一个非常好的游戏,大家都可以玩一玩。

游戏实现

1.首先,我们乍一看肯定觉得这跟我们所学的数组的知识相关,确实,这个游戏所用到的知识也不会太多,就需要函数,递归,数组,循环的知识。

2.我们首先需要初始化棋盘,我们所看到的是一个9×9的棋盘,实际上,它是一个11×11的棋盘,它周围的一圈被隐藏了而已。

3.我们需要设置雷,这需要是随机的,所以我们需要用到能生成随机值的函数。是雷我们就放置‘1’,不是雷我们就放置‘0’。

4.一切都准备就绪,就要开始排查雷了,排查雷我们就需要用到循环,当我们排查到不是雷或者还没排查完的时候,就会一直循环排查。

创建头文件

我们先来介绍一下这些文件的用处。

1.Saolei.h 用来进行一些头文件的声明,还有一些宏定义。

2.game.c 用来进行一些函数的实现。

3.Saolei2.0.c 用来实现一些整体逻辑的实现,还有一些函数的调用。

下面我们就开始吧,lets go!!!

游戏实现

1.游戏菜单

首先我们先写一个菜单,让我们的游戏有一个入口。

void menu()
{
	puts("*****  扫雷游戏 ******");
	puts("**********************");
	puts("******  1.play   *****");
	puts("******  0.exit   *****");
	puts("**********************");
}

我们可以定义一个变量input, 输入1就开始玩游戏,输入0就退出游戏,输入其他的就提示重新输入。我们可以使用do~~while循环来完成这个操作。

void test()
{
	menu();       //打印菜单
	int input = 0;
	do
	{		     //操作主界面
		printf("请选择->(1/0):");
		scanf("%d", &input);
		system("cls");
		switch (input)
		{
		case 1:
			printf("游戏开始");
			Sleep(1000);
			system("cls");
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择\n");
		}
	} while (input);
}

我们先来看看效果如何。

可以看到,这就是我们想要的效果。

在这里面,我们为了让玩家体验更好,我们使用了Sleep()这个函数,它可以让程序停留1000ms也就是一面再进行下面的代码,使用了system("cls")清理屏幕,让屏幕看起来更加整洁。

2.棋盘初始化

我们可以看到左边的蓝色格子,是我们最终希望呈现再屏幕上的样子,但是可以发现,当我们再排查最外面的一圈的格子的时候,会造成数组越界,所以我们为了放置这一情况发生,我们需要再它的外面再加上一圈,就像右边一样,我们需要再加上一圈绿色的格子,但是我们在屏幕上打印显示的其实还是里面的9×9个矩形,我们的逻辑实现则需要11×11的格子。

因为我们需要实现两个棋盘,一个棋盘用来布置雷,另一个棋盘用来显示排查情况,因为两个棋盘实现的逻辑是一样的所以我们只需要,实现一个函数就行了。我们结合着打印棋盘的函数Display来一起看看。

注意:以下的row代表排,col代表列,都为9,rows和cols都在它们的基础上加上了一个2。

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;        
		}
	}
}        
//使用char 数组便于我们寻找初始化棋盘,我们只需要把所需要初始化的值set通过函数传入不同的形参得以改变
void Display(char arr[rows][cols], int ROW, int COL)
{
	for (int i = 0; i <= ROW; i++)
	{
		printf(" %d", i);
	}
	printf("\n");
	for (int i = 1; i < ROW + 1; i++)
	{
		printf(" %d", i);
		for (int j = 1; j < COL + 1; j++)
		{
			printf(" %c", arr[i][j]);
		}
		printf("\n");
	}
}

为了准确定位,我们在打印棋盘的时候,需要加上行列的信息,便于玩家精确的查找。我们使用的char数组,为了使雷和我们的提示信息不产生误会,所以我们初始化的时候的‘0’也使用的字符零。

3.布雷

棋盘已经初始化好了,下面我们就开始布置雷了,因为要保证每次玩的游戏的雷的位置不同,我们需要产生不同的雷的位置信息,所以就需要用到srand函数。

它的作用就是产生一些随机数。

srand((unsigned int)time(NULL));(包含在头文件<stdlib.h>中)

在程序的开始,我们需要进行上面的初始化,向srand函数里面放一个种子,让它能够真正的产生随机数。在我们布雷的时候,我们需要把雷布置在9×9的矩形中,我们就需要产生的横纵坐标值的大小在0~9之间。

int x=rand()%9+1; int y =rand()%9+1;

我们将布置的雷设置位‘1’,注意这里是字符1,为了和排查雷时,提示雷的数目发生冲突。

void Set_mine(char arr[rows][cols], int ROW, int COL)
{
	int count = easy;
	while (count > 0)
	{
		int x = rand() % ROW + 1;
		int y = rand() % COL + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}

首先我们在宏定义设置雷的数量easy代表简单难度的雷数量有10个,把它赋给count,再进入while循环,每当产生了一个雷,count就自减一,我们用了if语句防止布置的雷重复。

在我们设置雷的时候,要使用到产生随机变量的函数rand,下面就来介绍一下。

(1) rand() 函数

在这里我们就需要用到随机数生成函数rand(),使用这个函数需要包含一个头文件 stdlib.h,但我们需要注意的是,由于随机数生成器的实现方式,生成的随机数可能不是完全随机的,而是一种伪随机数,在一些固定的模式下以它的种子为基准值生通过某种算法根据成的可以算的伪随机数。而rand函数的种子一般是默认为一个固定值,那么现在我们可以知道,如果想让rand函数生成一个真正的随机数就需要时刻改变它的种子就可以了。

(2) srand() 函数

现在我们就要用到srand()函数了,它一般用于初始化随机数生成器,通常是为rand函数设置种子,以便生成不同的随机数序列。所以我们在每次调用rand函数之前应先调用srand函数,以传入不同的种子。而srand函数通过参数seed来设置它的随机生成数,也就是说我们需要种子的种子不是一个固定值,只有这样,rand函数生成的数才是真正的随机数。

(3) time()函数

因为时间是每时每刻都在变化的,所以我们一般使用当前时间作为srand函数的种子,这里我们就需要使用time函数了,time函数的原型是time_t time(time_t *timer)这样的,使用时需要包含头文件time.h,其实time函数所返回的时间就是自1970年1月1日 00:00:00 UTC(协调世界时)以来的秒数,它的返回值是一个长整数time_t类型的值,表示秒数。如果time函数的参数timer不为空,那么time函数就会将这个返回当前时间的值,并将当前时间存储在参数所指向的变量中。但是如果参数为空,time函数将会返回当前时间的值,而不存储当前时间。
 

现在我们已经布置好雷了,先用Display函数打印mine数组来看一看。

  

可以看到,确实是布置了10个雷。

这个时候,我们在show棋盘上是看不见的,它还是会显示全是‘*’。

4.开始排雷

在排雷之前,我们先看看下面这幅图。

我用红圈标记了一下,每当我们排雷的时候,都需要知道以该雷为中心,周围的8个格子里的雷的个数,如果雷数是0,则将该格子变为空格,否则,显示周围的雷的个数。这个时候,我们在开始定义的11×11的方阵就排上用场了,它很好的解决了数组越界这个问题。在我们传参时,就需要在原来的长宽上加上2。

 int  count_mine(char mine[rows][cols], int x, int y)
{

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

这种方法直截了当,直接计算周围的雷的字符总数再减去,字符0的值,这样就得到了周围雷的数目。当然也可以用循环来写。参考下面。


int count_mine(char mine[rows][cols], int i, int j)
{
	int count = 0;
	for (int a = i - 1; a <= i + 1; a++)
	{
		for (int b = j - 1; b <= j + 1; b++)
		{
			if (mine[a][b] == '1')
				count++;
		}
	}
	return count;
}

两种方法都可以,看自己的喜好。

雷布置好以后,我们就要开始排雷了,这也是最重要的一步了,首先我们要排雷,我们需要创建一个排雷函数,这个函数的参数需要知道雷盘的信息,也需要知道展示盘的信息,还需要排雷的坐标,知道了这些,还不够,因为我们是排雷,我们如果没踩雷就需要继续排雷,我们需要找到一个结束条件,结束这个循环,因为我们设定的雷的数量是10个,所以,我们只需要定义一个变量win,并让win<row*col-easy 就行了。

void  Search_mine(char mine[rows][cols], char show[rows][cols], int ROW, int COL)
{
	int i = 0;
	int j = 0;
	int win = 0;
	while (win < row * col - easy)
	{
	again:
		printf("请输入您要排查的坐标(中间用空格隔开):\n");

		scanf("%d %d", &i, &j);
		if (i >= 1 && j >= 1 && i <= ROW && j <= COL && show[i][j] == '*')//坐标合法,计算周围的雷的数目
		{
			if (mine[i][j] == '1')
			{
				printf("很遗憾,您被炸死了\n");
				Display(mine, row, col);
				break;
			}
			else   //排雷成功
			{
				expendboard(mine, show, i, j);
				system("cls");
				Display(show, row, col);
				if (Endmine(show, row, col) == 1)
				{
					printf("恭喜你排雷成功!\n");
					Display(mine, row, col);
					break;
				}
			}
		}
		else if (show[i][j] != '*')
		{
			printf("坐标不合法,重新输入\n");
			goto again;
		}
	}
}

可以看到在Search_mine中,进入到while循环,需要先判定坐标是否合法,因为我们需要保证我们排查的坐标在1~9,并且是未排查过的坐标,否则就会提示重新输入坐标。当坐标合法的时候,我们 需要对该坐标判断是否是雷,如果是雷,则提醒被炸死了,游戏结束,如果布是雷的话,就会进入到我们的expendboard函数,它是用来判断周围有几颗雷,如果没有雷,则展开,看周围的坐标,一直递归,知道有雷的时候停止递归,为了让我们的屏幕看起来整洁一点,然后我们设置了一个清屏的操作,使用到了system("cls"),它包含在头文件<windows.h>中,清理完屏幕,我们就需要再一次展示棋盘,我们又设定了一个判断语句,如果Endmine函数返回的是1,则说明我们排雷成功了,跳出while循环,并展示最终的mine棋盘。下面我们就来讲讲expendboard函数和Endmine函数。

void expendboard(char mine[rows][cols], char show[rows][cols], int i, int j)
{	
	int a = 0;
	int b = 0;
	int count= count_mine(mine, i, j);
	if (count!=0)
	{
		show[i][j] = count + '0';
	}
	else
	{
		show[i][j] = ' ';
		for (int a = i - 1; a <= i + 1; a++)
		{
			for (int b = j - 1; b <= j + 1; b++)
			{	
				if (a > 0 && b > 0 && a < row+ 1 && b < col+ 1 && show[a][b] == '*' && mine[a][b] != '1')
				 expendboard(mine, show, a, b);
			}
		}
	}
}

首先我们看函数的参数,需要mine,show两个棋盘的信息,因为是排查坐标,肯定也需要定义两个整形形参来接受坐标,当我们进入到函数内部,定义了一个count变量来接受该坐标的周围的雷的个数,这里也调用到了count_mine函数,上面我们已经讲过了,当count不等于0的时候,就直接将信息传给我们的show棋盘,然后就直接出函数了,这就是递归的结束条件,当该坐标周围的雷的数量是0的时候,我们先将它变为空格,然后再浏览它周围一圈的8个格子是否也满足同样的条件,再次进到函数,如果周围的坐标还是0的话,又继续递归,知道该坐标周围的雷的数量不为0,就结束,加入expendboard可以极大减少我们的排雷次数,想想一下,如果不这样,一共81个格子,10颗雷,我们就需要排除剩余的71个格子,这样就会显得非常烦躁。同时我们也加入了if来判断坐标是否合法,我们需要满足,它在show数组中是‘*’,在mine数组中不是‘1’。因为我们是递归的未拍过的雷,否则就会堆栈错误。

int Endmine(char show[rows][cols], int ROW, int COL)
{
	int count = row*col;
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (show[i][j] != '*')
				count--;
		}
	}
	if (count == easy)
		return 1;
	else
		return 0;
}

该函数是用来判断是排雷的数量的,我们初始的count是81,每当我们排一次雷,我们就遍历一下整个棋盘,少一个‘*' 就减1,知道最后只剩下最后10个全部都是雷的是后就返回1,最终出到 Search_mine函数中判断是1,就提醒游戏胜利,并跳出while循环。好啦,以上就是全部的代码讲解。

游戏代码

Saolei.h

#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
#include <synchapi.h>
#define row 9
#define col 9
#define rows row+2
#define cols col+2
#define easy 10

.h表示头文件,这里面都是一些对一些头文件的声明和包含,加上一些宏定义。

game.c

void menu()            //游戏菜单
{
	puts("*****  扫雷游戏 ******");
	puts("**********************");
	puts("******  1.play   *****");
	puts("******  0.exit   *****");
	puts("**********************");
}

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 Display(char arr[rows][cols], int ROW, int COL)
{    
    //棋盘打印代码
	for (int i = 0; i <= ROW; i++)
	{
		printf(" %d", i);
	}
	printf("\n");
	for (int i = 1; i < ROW + 1; i++)
	{
		printf(" %d", i);
		for (int j = 1; j < COL + 1; j++)
		{
			printf(" %c", arr[i][j]);
		}
		printf("\n");
	}
}


void Set_mine(char arr[rows][cols], int ROW, int COL)
{    
    //随机布雷
	int count = easy;
	while (count > 0)
	{
		int x = rand() % ROW + 1;//产生随机数
		int y = rand() % COL + 1;
		if (arr[x][y] == '0')
		{
			arr[x][y] = '1';
			count--;
		}
	}
}


 int  count_mine(char mine[rows][cols], int x, int y)
{    
    //判断周围雷的数量
	return mine[x - 1][y] + mine[x + 1][y] + mine[x - 1][y - 1] +
		mine[x][y - 1] + mine[x + 1][y - 1] +
		mine[x - 1][y + 1] + mine[x][y + 1] +
		mine[x + 1][y + 1] - 8 * '0';
}


void expendboard(char mine[rows][cols], char show[rows][cols], int i, int j)
{	
       //递归展开
	int a = 0;
	int b = 0;
    //调用count_mine函数
	int count= count_mine(mine, i, j);    
	if (count!=0)    //递归结束条件
	{
		show[i][j] = count + '0';
	}
	else
	{
		show[i][j] = ' ';
		for (int a = i - 1; a <= i + 1; a++)
		{
			for (int b = j - 1; b <= j + 1; b++)
			{	
				if (a > 0 && b > 0 && a < row+ 1 && b < col+ 1 && show[a][b] == '*' && mine[a][b] != '1')
				 expendboard(mine, show, a, b);
			}
		}
	}
}

int Endmine(char show[rows][cols], int ROW, int COL)
{
    //游戏是否胜利判断
	int count = row*col;
	for (int i = 1; i <= ROW; i++)
	{
		for (int j = 1; j <= COL; j++)
		{
			if (show[i][j] != '*')
				count--;
		}
	}
	if (count == easy)
		return 1;
	else
		return 0;
}
void  Search_mine(char mine[rows][cols], char show[rows][cols], int ROW, int COL)
{    

    //开始排雷
	int i = 0;
	int j = 0;
	int win = 0;
	while (win < row * col - easy)
	{
	again:
		printf("请输入您要排查的坐标(中间用空格隔开):\n");

		scanf("%d %d", &i, &j);
		if (i >= 1 && j >= 1 && i <= ROW && j <= COL && show[i][j] == '*')//坐标合法,计算周围的雷的数目
		{
			if (mine[i][j] == '1')
			{
				printf("很遗憾,您被炸死了\n");
				Display(mine, row, col);
				break;
			}
			else   //排雷成功
			{
				//expendboard(mine, show, i, j);
				//system("cls");
				//Display(show, row, col);
				expendboard(mine, show, i, j);
				system("cls");
				Display(show, row, col);
				if (Endmine(show, row, col) == 1)
				{
					printf("恭喜你排雷成功!\n");
					Display(mine, row, col);
					break;
				}
			}
		}
		else if (show[i][j] != '*')
		{
			printf("坐标不合法,重新输入\n");
			goto again;
		}
	}
}

void game()
{    

    //游戏整体逻辑实现
	char mine[rows][cols] = { 0 };   //布置雷棋盘
	char show[rows][cols] = { 0 };   //展示棋盘
	//首先初始化棋盘
	InitBoard(mine, rows, cols, '0');

	InitBoard(show, rows, cols, '*');

	//Display(mine, row, col);    //打印棋盘
	//Display(show, row, col);

	//开始布置雷
	//Display(show, row, col);
	Set_mine(mine, row, col);
	//Display(mine, row, col);
	Display(show, row, col);

	Search_mine(mine, show, row, col);
}


void test()
{
	menu();       //打印菜单
	int input = 0;
	do
	{		     //操作主界面
		printf("请选择->(1/0):");
		scanf("%d", &input);
		system("cls");
		switch (input)
		{
		case 1:
			printf("游戏开始");
			Sleep(1000);
			system("cls");
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择\n");
		}
	} while (input);
}

该源文件中是一些函数的实现。

Saolei.c

int main()
{
	srand((unsigned int)time(NULL));
	test();
	return 0;
}

整个游戏的入口,并改变rand函数的种子。

总结

    总结一下,扫雷是一款非常优秀的游戏,它将我们前面所学的函数,递归,数组,循环,条件语句,还有随机数函数结合起来,整体逻辑感强,但又不复杂,很好的总结了我们前面的知识,大家下去也可以尝试写一下代码,检验一下前面的学习成果。如果有不懂的知识,可以点击我的主页,有关于上述说的函数,数组,条件,循环,递归等知识,还有指针等的知识。

     以上就是我给大家分享的关于扫雷的知识,有什么细微的错误希望大家指出,大家一起进步,点赞+收藏!!!谢谢大家了!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值