【C语言】万字教学,带你分步实现扫雷游戏(内含递归函数解析),剑指扫雷,一篇足矣

在这里插入图片描述

君兮_的个人主页

勤时当勉励 岁月不待人

C/C++ 游戏开发


前言

Hello,这里是君兮_,今天更新一篇关于利用C语言实现扫雷游戏的博客。对于初学者来说,这也是一个非常容易上手的小项目,看完不妨自己试试哦!
废话不多说,我们直接开始吧!

一. 扫雷游戏的介绍以及内部需要实现的功能解析

1.什么是扫雷游戏

相信很多人在小时候都玩过扫雷游戏,但是还是为了防止某些同学不知道扫雷具体是在做什么,我这里还是带大家快速过一遍哦

在这里插入图片描述

  • 这是网页上随便搜索出来的一个扫雷游戏,链接如下,感兴趣的可以去试试:扫雷游戏

  • 扫雷游戏玩法是这样的

  • 1.如图所示,上面显示的是此时棋盘上有几颗雷
    在这里插入图片描述

  • 2.当我们点击某个坐标时,它会提示周边有几颗雷
    在这里插入图片描述

  • 3**.当我们点击的坐标中有雷时,我们就被“炸死”了,此时游戏结束**

在这里插入图片描述

  • 4.当我们把所有没有雷的坐标都点开后,说明我们排雷成功,游戏结束
  • 这里就不放成功的图片啦,以博主的实力(咳咳)过个扫雷游戏肯定是洒洒水啦~(其实是试了十分钟一次没过放弃了)

2.扫雷游戏所需的几个步骤

讲完什么是扫雷游戏后,咱们来讲讲我们要利用C语言实现这个”简单“小游戏的需要的几个步骤然后咱们一步一步实现。
根据以往的惯例,咱们还是画图说明吧

在这里插入图片描述

  • 好了,分析完具体要实现哪几个部分,咱们来分步实现这个小游戏

二.扫雷游戏的具体实现

1.打印菜单

  • 非常简单的一步,我就直接放代码啦
void menu()//打印菜单
{
    
    
	printf("*****************************\n");
	printf("*********** 1.play **********\n");
	printf("*********** 0.exit **********\n");
	printf("*****************************\n");

}

菜单上的选择功能

  • 对于分支选择的功能,switch语句非常满足我们的需要,代码如下:
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("退出游戏\n");
			break;

		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);//退出即输入0,结束循环


	return 0;
}
  • 上面代码中,玩家输入1即开始游戏,输入0即退出游戏,输入其它数则提醒玩家输入错误并给与玩家重新选择的机会。

2.初始化以及打印棋盘

  • 现在我们来进行下一步,初始化及打印棋盘。
  • 在开始前我们先分析一下我们现在需要做的:
  • 1.我们希望打印两个棋盘,第一个棋盘中埋地雷,这个棋盘玩家应该是不可见的.第二个棋盘就像上面网页版扫雷游戏一样填充格子,玩家应该是可见的并且能够往上面输入坐标掀开格子判断是否有雷
  • 2.在第二个棋盘中出了打印格子,我们还应打印一些语句提醒玩家应该怎样玩游戏并把棋盘的行列标注出来。
  • 3.分析完这两点后,我们发现,二维数组能很好的满足我们的要求,此时我们不妨定义两个二维数组来实现两个棋盘的初始化与打印。
  • 4.于此同时,我们把没有雷的地方初始化为‘0’,把玩家能看到的棋盘初始化为‘*’。
    做完了具体分析,我们来用代码具体实现一下。
    注意:在定义或者使用某个函数时,我们应该优先在头文件中声明,下面是初始化函数与打印棋盘函数在头文件中的声明:
#define Row 9
#define Col 9
#define Rows Row+2
#define Cols Col+2


void InitBoard(char mine[Rows][Cols], int rows, int cols, char set);
void DisplayBoard(char board[Rows][Cols], int row, int col);

  • 我们现在打印的是一个9*9的棋盘,为了方便我们以后对这个棋盘大小的更改,我们用常数9定义了Row与Col,如果我们以后想更改棋盘大小只需改变定义Row与Col的常数大小即可。
    在这里插入图片描述
    - 前面我们提到了,我们会把周围地雷的数量放在中间这个格子中,为了防止出现图中这种情况,我们需要为这个棋盘多加看不见的两行两列,也就是传进去数组的大小应该加2.这就是上面传进的字符数组为:
//(Rows,Cols大小分别为Row+2,Col+2)
char mine[Rows][Cols]
char board[Rows][Cols]

的原因。

初始化函数InitBoard

void InitBoard(char mine[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++)
		{
    
    
			mine[i][j] = set;
		}
	}
}
  • 代码里set起到的作用就是初始化棋盘,当传进去需要埋雷的棋盘时set为’0’,当传进去玩家见到的棋盘时,初始化为‘*’

打印棋盘的DisplayBoard函数

  • 此时我们多传进去的数组的行数与列数可以用作修饰棋盘为我们的玩家做行与列的提示
  • 代码如下:
void DisplayBoard(char board[Rows][Cols], int row, int col)
{
    
    
	printf("******扫雷游戏开始*****\n");//提示玩家游戏开始
	int i = 0;
	for (i = 0; i <= row; i++)
	{
    
    
		printf("%d ", i);//打印列数
	}
	printf("\n");//打印完列数换行
	for (i = 1; i<=row; i++)
	{
    
    
		int j = 0;
		printf("%d ", i);//打印行数
		for (j = 1; j <=col; j++)
		{
    
    
			
			printf("%c ", board[i][j]);//打印棋盘
		}
		printf("\n");//每打印完一行换行

	}
}

  • 来看看具体效果
    在这里插入图片描述
  • 怎么说?效果是不是非常好?如果你看懂了,我们继续

3.设置地雷

  • 把棋盘设置好后,我们就该考虑一下我们地雷的设置以及如何放置了。
  • 首先,我们想到,如果在埋雷的棋盘中我们把所有坐标都给初始化为‘0’了,那我们就把有雷的地方设为‘1’吧(这里很重要,虽然设为别的也可以,但是这里设置为‘1’,自然是有原因的,我们稍后会讲到)。
  • 那我们的设好的雷应该怎么放呢?
  • 对于咱们这个扫雷游戏来说,自然希望每一次雷放置的位置都是不同的,不然一旦玩的次数多了,玩家摸清了我们放雷的老底,这个游戏岂不是玩不下去了?

在这里插入图片描述
那这个时候,我们就需要使用随机数函数

#include<stdio.h>//使用该函数所需头文件
#include<time.h>//使用time函数所需头文件
rand();
srand((unsigned int)time(NULL));//把时间函数置空传给srand同时由于srand要求参数必须为unsigned int型,把time(NULL)强制类型转换一下

设置地雷SetMine函数

上面分析了这么多,我们来通过代码具体实现一下:

  • 引用该函数时先在头文件声明一下:
#define EasyCount 10//埋10个地雷
void SetMine(char mine[Rows][Cols], int row, int col);
  • 具体代码:
void SetMine(char mine[Rows][Cols], int row, int col)
{
    
    
	
	int count = EasyCount;//埋地雷的个数
	int i = 0;
	while (count)
	{
    
    
          //生成在1-9范围内的整数	
		int x = rand() % row + 1;
		int y = rand() % row + 1;
		//判断该位置是否已被埋雷
		if (mine[x][y] == '0')
		{
    
    
			mine[x][y] = '1';
			count--;//埋雷成功,所需埋的地雷减1
		}
	}
}
  • 上面代码中还是有很多需要注意的地方的:
  • 1.我们为了判定我们到底需要埋多少个雷,定义了一个count,每当成功埋雷一次,count就减1,其中为了方便我们以后对所需埋雷数量的修改,我们在头文件中把一个常数赋给了EasyCount并让count引用,这样改变常数的大小即可改变我们想要埋雷的多少。
  • 2.我们不想把雷埋在棋盘外,为了防止这种情况的发生,我们把生成的随机数对棋盘大小取模再+1,确保我们的雷埋在棋盘中。
  • 效果如下:
    在这里插入图片描述
  • 这样咱们是不是就把雷埋进棋盘中了。

4.玩家游玩及判断输赢

当我们做好棋盘上的一切准备时,就该考虑玩家应该怎么玩了。

  • 第一,提示并引导玩家输入想要排雷的坐标。
  • 第二 ,判断输入的坐标是否合法,总不能排雷排到棋盘外吧。如果输入坐标不合法就提示玩家重新输入
  • 第三,输入正确坐标后,我们得判断该坐标是否有雷,即对于埋雷的棋盘,这里存放的是不是‘1’,是‘1’,说明此格子有雷,但被“炸死”我们也得让玩家“死”的明白,此时把埋雷的棋盘打印一遍并结束游戏。
  • 第四,不是雷,为了让游戏变得简单一点,我们可以把玩家输入坐标周围所有不是雷的格子全部显示出来,并在附近有雷的格子上显示此时附近所埋的地雷数,如图所示:
    在这里插入图片描述
  • 我们还是先把这部分拆分一下,先实现在中间的格子中显示周围地雷数

获取周围地雷数函数GetMineCount

  • 先在头文件声明该函数:
int GetMineCount(char mine[Rows][Cols], int x, int y);
  • 具体代码:
int GetMineCount(char mine[Rows][Cols], int x, int y)
{
    
    
	return mine[x - 1][y] + 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] + mine[x - 1][y + 1]-8*'0';
}
  • 这里咱们把有地雷的坐标设为‘1’就起作用了。
    在这里插入图片描述

  • 如图,此时把周围所有有地雷的位置的‘1’加起来,是不是就得到周围存在的地雷数了?这就是上面一定要在埋雷坐标里存放‘1’的原因。

  • 好了好了,我知道这里其实还有很多疑问,我来一一解释一下:

  • 1. 为啥在最后要减去‘0’呢?

  • 注意:由于棋盘在初始化时是字符数组,咱们存进去的其实是字符‘1’,而此时我们希望传出去的是周围所有数字1之和,因此这里需要把所有‘1’相加之后再减去‘0’,这样传出的就是一个数字了。

  • 2.既然我们这里需要一个整型,为啥在初始化时传进去的不是一个整型数组呢?

  • 在咱们初始化时,初始化函数InitBoard是同时需要把两个棋盘都初始化的,由于玩家初始化时棋盘中为字符‘*’,因此定义InitBoard所需传进的数组必须是一个字符型的。当然,从实际上讲你也可以再重新定义一个函数进行埋雷棋盘的初始化,但是你觉得是重新定义一个函数简单还是把‘0’,‘1’转为对应的数字简单?答案显而易见。

递归函数ExpandBoard函数

这里比较难以理解,我尽量讲的细一点,如果看完还没理解的话,欢迎在评论区或者私信我指出你的疑问,我会第一时间回复的。

  • 当我们完成了在中间显示周围地雷数的目标后,为了使游戏难度降低一点,我们需要完成这样一件事:
    在这里插入图片描述
  • 判断该坐标周围的八个格子(即红色处)的周围除了该坐标剩下7个坐标中是否有雷,如果有雷,就在该格子处显示周围的雷的个数,不再进入下一次递归。
  • 如果没有雷,就把该坐标周围的格子全部点亮,就像这样。然后继续递归判断
    在这里插入图片描述
  • 有雷不再递归点亮格子的情况:
    在这里插入图片描述
    - 总结:也就是说,递归的条件是中间的格子为空,然后把它周边的8个格子当新一层的递归函数中的中心格子来计算,如果还为空就继续向外递归,不为空就停止了(即中间的格子为空时,把周边的8个格子带入下一层的递归中)。
  • 注意:在递归的过程中,不要把上一层已经判断过的空格子再判断一次(即此时里面的值已经不为’*'了),否则就会让该递归陷入死循环!!!
  • 代码实现如下:
  • 头文件中声明:
void ExpandBoard(char board[Rows][Cols], char mine[Rows][Cols], int row, int col, int* win);
  • 该函数定义:
void ExpandBoard(char board[Rows][Cols], char mine[Rows][Cols], int x, int y, int* win)
{
    
    
	
	if (x >= 1 && x <= Row && y >= 1 && y <= Col)//坐标必须在棋盘内
	{
    
    
		int ret = GetMineCount(mine, x, y);//获取周围雷的个数
		if (ret == 0)周围没雷
		{
    
    
			board[x][y] = '0';//把没雷的地方展开点亮为0
			int i = 0;
			//向四周八个格子递归
			for (i = x - 1; i <= x + 1; i++)
			{
    
    
				int j = 0;
				for (j = y - 1; j <= y + 1; j++)
				{
    
    
					if (board[i][j] == '*')//防止已经展开的部分再次递归,进入死循环
					{
    
    
						ExpandBoard(board, mine, i, j, win);//递归
					}
				}
			}

		}
		else
		{
    
    
			board[x][y] = ret + '0';//周围有雷,把雷的个数以字符的形式放在中间格子中
		}

		  (*win)++;//记录展开的没有雷的格子的数量,方便判断输赢
	}
}
  • 建议上面那段话多读几遍,最好自己把网页版的扫雷游戏玩几次,结合它的展开方式理解上面关于递归的那段话

在这里插入图片描述

  • 当我第一次看上面关于递归的那段代码时,不禁感叹程序员就挺“秃然”的。
  • 加油,就剩最后一步辣
  • 如果你能坚持到最后,那我觉得这件事情------------------------------
    在这里插入图片描述
  • 小小放松一下,咱们继续

判断输赢函数FindMine

当理解了上面两个函数后,下面判断输赢的这个函数就比较好理解了。

  • 直接放代码:
  • 头文件中对函数的声明:
void FindMine(char board[Rows][Cols], char mine[Rows][Cols], int row, int col);
  • 该函数的定义:
void FindMine(char board[Rows][Cols],char mine[Rows][Cols], int row, int col)
{
    
    
	int x = 0;
	int y = 0;
	int win = 0;
	while(win < row * col - EasyCount)//排的雷小于放置的雷,就继续排雷
	{
    
    
		printf("请输入要扫雷的坐标:>\n");
		scanf("%d %d",&x,&y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断排雷坐标合法性
		{
    
    
			if (board[x][y] == '*')//判断该坐标是否排查过
			{
    
    
				if (mine[x][y] == '1')
				{
    
    
					printf("恭喜你,被雷炸死了\n");
					DisplayBoard(mine, Row, Col);//打印埋雷棋盘,“死”的明白
					break;
				}
				else
				{
    
    
					
					ExpandBoard(board, mine, x, y, &win);//递归展开
		
					DisplayBoard(board, Row, Col);//打印展开后的棋盘
					printf("--------------还需翻开%d格--------------\n", row * col - EasyCount - win);//提示一下还要排多少格才能成功

					

				}
			}
			else
			{
    
    
				printf("该坐标已被排查过了,请重新输入\n");
			}
		}
		else
			printf("坐标非法,请重新输入\n");
		

	}
	if (win == (Row * Col - EasyCount))//所有雷都被找到
	{
    
    
		printf("恭喜你,排雷成功!\n");
		DisplayBoard(mine, Row, Col);
		
	}
}


  • 前面都基本解释过了,这里不做过多展开。有不明白的地方自己看上面代码注释即可。

5.game函数

当我们把所有模块功能都写好后,是时候写一个函数把所有模块封装在一起了 ,这就是我们接下来的game函数

  • 在主函数所在文件中,不需要声明,代码如下:
void game()
{
    
    
	char mine[Rows][Cols] = {
    
    0};//存放埋雷棋盘的数组
	char board[Rows][Cols] = {
    
    0};//存放玩家看到的棋盘的数组
	InitBoard(mine, Rows, Cols,'0');//初始化埋雷棋盘为'0'
	InitBoard(board, Rows,Cols,'*');//初始化玩家看到的棋盘为‘*’
	SetMine(mine, Row, Col);
	//DisplayBoard(mine, Row, Col);//打印埋雷棋盘,一般玩家不可见,自己调试的时候使用
	DisplayBoard(board, Row, Col);//打印玩家棋盘
	FindMine(board,mine, Row, Col);//判断输赢的函数
	
}

试玩一下扫雷游戏

  • 注意哦,当程序有bug的时候,不要傻愣愣的只设10个雷,这样你测一天都测不完的。
  • 测试结果如下:(棋盘中有80个雷)
  • 在这里插入图片描述
  • 正常结果如图(10个雷):
    在这里插入图片描述

源码

  • 把源码放这里哦,有需要自取。

game.h(头文件)

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#define Row 9
#define Col 9
#define Rows Row+2
#define Cols Col+2
#define EasyCount 10

void InitBoard(char mine[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 board[Rows][Cols], char mine[Rows][Cols], int row, int col);
int GetMineCount(char mine[Rows][Cols], int x, int y);
void ExpandBoard(char board[Rows][Cols], char mine[Rows][Cols], int row, int col, int* win);

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] = {
    
    0};//存放埋雷棋盘的数组
	char board[Rows][Cols] = {
    
    0};//存放玩家看到的棋盘的数组
	InitBoard(mine, Rows, Cols,'0');//初始化埋雷棋盘为'0'
	InitBoard(board, Rows,Cols,'*');//初始化玩家看到的棋盘为‘*’
	SetMine(mine, Row, Col);
	//DisplayBoard(mine, Row, Col);//打印埋雷棋盘,一般玩家不可见,自己调试的时候使用
	DisplayBoard(board, Row, Col);//打印玩家棋盘
	FindMine(board,mine, 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("退出游戏\n");
			break;

		default:
			printf("输入错误,请重新输入\n");
			break;
		}
	} while (input);


	return 0;
}

game.c(定义函数的文件)

#include"game.h"

void InitBoard(char mine[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++)
		{
    
    
			mine[i][j] = set;
		}
	}
}


void DisplayBoard(char board[Rows][Cols], int row, int col)
{
    
    
	printf("******扫雷游戏开始*****\n");
	//for(i=0;i<)
	int i = 0;
	for (i = 0; i <= row; i++)
	{
    
    
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i<=row; i++)
	{
    
    
		int j = 0;
		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 = EasyCount;
	int i = 0;
	while (count)
	{
    
    
		int x = rand() % row + 1;
		int y = rand() % row + 1;
		if (mine[x][y] == '0')
		{
    
    
			mine[x][y] = '1';
			count--;
		}
	}
}
int GetMineCount(char mine[Rows][Cols], int x, int y)
{
    
    
	return mine[x - 1][y] + 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] + mine[x - 1][y + 1]-8*'0';
}
void ExpandBoard(char board[Rows][Cols], char mine[Rows][Cols], int x, int y, int* win)
{
    
    
	
	if (x >= 1 && x <= Row && y >= 1 && y <= Col)
	{
    
    
		int ret = GetMineCount(mine, x, y);
		if (ret == 0)
		{
    
    
			board[x][y] = '0';
			int i = 0;
			for (i = x - 1; i <= x + 1; i++)
			{
    
    
				int j = 0;
				for (j = y - 1; j <= y + 1; j++)
				{
    
    
					if (board[i][j] == '*')
					{
    
    
						ExpandBoard(board, mine, i, j, win);
					}
				}
			}

		}
		else
		{
    
    
			board[x][y] = ret + '0';
		}

		  (*win)++;
	}
}
void FindMine(char board[Rows][Cols],char mine[Rows][Cols], int row, int col)
{
    
    
	int x = 0;
	int y = 0;
	int win = 0;
	while(win < row * col - EasyCount)
	{
    
    
		printf("请输入要扫雷的坐标:>\n");
		scanf("%d %d",&x,&y);
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
    
    
			if (board[x][y] == '*')
			{
    
    
				if (mine[x][y] == '1')
				{
    
    
					printf("恭喜你,被雷炸死了\n");
					DisplayBoard(mine, Row, Col);
					break;
				}
				else
				{
    
    
					
					ExpandBoard(board, mine, x, y, &win);
		
					DisplayBoard(board, Row, Col);
					printf("--------------还需翻开%d格--------------\n", row * col - EasyCount - win);

					

				}
			}
			else
			{
    
    
				printf("该坐标已被排查过了,请重新输入\n");
			}
		}
		else
			printf("坐标非法,请重新输入\n");
		

	}
	if (win == (Row * Col - EasyCount))
	{
    
    
		printf("恭喜你,排雷成功!\n");
		DisplayBoard(mine, Row, Col);
		
	}
}



总结

  • 今天的内容到此为止啦,扫雷游戏真的不算特别难,我希望看到这里的人都可以亲手独立实现一下(能坚持下来的大家都是好样的),只有你能够独立的把代码写出来,才能算真正的学会了。
  • 好了,如果你有任何的疑问欢迎在评论区指出,也欢迎随时私信我与我讨论,我看到会第一时间回复的,我们下次再见啦!!!

写到这里已经11000+了,这篇文章从早上开始写一直写到现在,主要花时间的地方是斟酌语句,我自己第一次看别人有关的文章时候雀氏是没怎么懂的,所以我希望我自己写的时候能配合图片啊啥的为大家解释的清楚容易理解一点。
新人创作不易,如果觉得这篇文章内容对你有所帮助的话,点个三连再走吧,谢谢大家了!
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/syf666250/article/details/131269919