【C语言练习】扫雷游戏(含递归展开)

1. 游戏规则

当点开一个格子的时候,如果点开一个格子不是雷,并且周围也没有雷,那么就将这一片全都展开。如果点开不是雷,但是周围有雷,里面会出现一个数字,而这个数字代表了该格子周围有多少颗雷,需要玩家有很强的逻辑推理能力,要推理出炸弹在哪里并避开它。当把所有没有炸弹的格子都点开之后,雷就被排了,玩家就赢了。反之,如果点开有炸弹的格子,玩家输。
扫雷游戏链接👉👉扫雷游戏网页版

2. 思路分析

本次编写的扫雷游戏是9x9的雷区
这里我将整个代码分为三个部分:test.cgame.cgame.h

test.c:扫雷游戏测试文件,用do…while循环构建游戏框架。设置一个用户交互菜单,根据玩家输入值,用switch语句判断是否进行游戏,以及当一轮扫雷游戏结束后是否继续进行游戏。

game.c:扫雷游戏实现的主体部分,实现设置棋盘、初始化棋盘、打印棋盘、设置雷、排雷、判断输赢等功能。

game.h:放置与扫雷游戏相关的函数声明、符号声明、包含的头文件。

3. 代码实现

3.1 test.c文件的编写

主函数内用do…while语句实现循环功能,首先利用menu()函数打印菜单,提醒玩家输入1或0以决定是否开始游戏,并用switch语句判断游戏是否进行:
若玩家输入1,则进行游戏;

若玩家输入0,则退出游戏;

若玩家输入的既非1也非0,则提示玩家重新输入。
当一轮游戏结束后,系统会再次提醒玩家输入1或0以决定是否再进行游戏。

3.1.1 构建主函数

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"

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");
			break;
		default:
			printf("输入错误,请重新输入\n");
		}
	} while (input);
	return 0;
}

3.1.2 构建menu()函数

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

3.2 game.c文件的编写

1.设置棋盘:
扫雷需要两个棋盘,一个是设置雷的“mine棋盘”,一个是向玩家展示的“show棋盘”,这里分别用mine[][],show[][]两个二维数组来设置。
对于mine数组,我们规定:字符 ‘0’ 代表示无雷,字符 ‘1’ 表示有雷。
对于show数组,我们规定:未翻格子前,用字符 ‘*’ 来对其进行遮盖。而格子翻转以后,如果周围有雷,则显示周围雷的个数;而如果无雷,将这一片展开。
讲到这儿,又出现了一个新的问题。二维数组设置成几行几列合适?
很多小伙伴会说,我们的棋盘是9x9,那应该数组也应该设置成9行9列。事实上,我们应该把数组设置成11行11列那这是为什么呢?下面我将详细解答原因。
上面我们规定了,若翻转的格子不是地雷,并且周围存在雷时,则显示周围雷的个数。这一操作对雷区中间区域的格子不会产生任何影响,但对雷区边界的格子执行这一操作可能会导致数组的越界访问。

由此我们可以得出结论,创建一个9行9列的数组并不合适!
那我们该创建几行几列的数组呢?
既然对9行9列的二维数组的边界元素进行操作时,会导致数组越界访问,那我们干脆就直接将二维数组扩大一圈,将那些会导致越界访问的范围包括在数组内,从源头上解决问题,这是一个非常巧妙的办法!

2.初始化棋盘:
利用Initboard()函数将mine数组和show数组里的元素分别初始化为 ‘0’ 和 ‘*’ 。当我们把数组名、行和列都传给Initboard()的时候,我们会发现一个问题,如果先初始化mine数组为 ‘0’,就没办法将show数组初始化为 ‘*’;相反如果先把show数组初始化为 ‘*’,mine数组就不能初始化为 ‘0’。
怎样才能解决这个问题呢?
其实很简单,在传参的时候,我们将初始化的内容也一并传给Initboard()函数即可。

 void Initboard(int board[ROWS][COLS], int rows, int cols, char set);
 //ROWS、COLS值为11,ROW、COL值为9
 //board[ROWS][COLS]来接收mine数组和show数组
 //初始化棋盘传参为ROWS、COLS,用rows、cols分别来接收
 //用set来接收初始化内容

注意:初始化棋盘是初始化整个11x11的棋盘,而下面安排雷的时候是在中间9x9棋盘上布置雷,传的参数是不一样的。
3.安排雷:
将mine数组和show数组初始化好以后,就可以开始埋雷了,埋雷是利用Setmine()函数。我们在传参的时候,数组一定传的是ROWS、COLS,而行和列一定是ROW、COL。

void Setmine(int board[ROWS][COLS], int row, int col);
//用row、col来接收ROW、COL

4.打印show数组(或者mine数组):
设置好雷以后,为了查看雷是否布置成功,可以利用Displayboard()函数打印mine数组进行查看。若雷已经成功设置,则打印show数组。Displayboard()函数传参同Setmine()函数一致。

void Displayboard(int board[ROWS][COLS], int row, col);

5.找雷并判断输赢:
完成以上功能后,最后就是找雷并判断输赢,这也是整个游戏最核心的功能。为了实现这个功能,采用Findmine()函数。Findmine()函数传参的时候需要同时传递mine数组、show数组两个数组。

void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)

3.2.1 构建game()函数

void game()
{
	char mine[ROWS][COLS] = { 0 };//安排雷的棋盘11*11
	char show[ROWS][COLS] = { 0 };//排查雷的棋盘11*11

	//将mine初始化为'0',show棋盘初始化为'*'
	Initboard(mine, ROWS, COLS, '0');
	Initboard(show, ROWS, COLS, '*');

	//安排雷
	Setmine(mine, ROW, COL);//注意:埋雷的时候也是传ROW、COL

	//打印mine和show
	//Displayboard(mine, ROW, COL);设置雷的棋盘不用打印
	Displayboard(show, ROW, COL);

	//打印mine
	//Displayboard(mine, ROW, COL);

	//找雷
	Findmine(mine, show, ROW, COL);

}

3.2.2 构建Initboard()函数

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.2.3 构建Setmine()函数

本次游戏设置的雷的个数为10个。首先利用rand()函数生成1-9的随机值,根据生成的值得到相应的坐标。若坐标为空,则安排雷;反之,就跳过。调用rand()函数之前,需要在主函数中先调用srand()函数。

void Setmine(char board[ROWS][COLS], int row, int col)
{
	int count = number;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % row + 1;
		if (board[x][y]== '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

3.2.4 构建Display()函数

为了让棋盘看起来更加的方便,在棋盘的上边和左边分别打印了对应的行数和列数。

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

效果展示:

注意:真正的游戏过程中是不会打印mine数组的,这里我只是向大家展示效果😊。

3.2.5 构建Findmine()函数

实现以上功能后,到这里才算得上真正意义上的扫雷。
首先需要创建三个 int 类型的整形变量,x、y、win。x,y用于存放用户输入的坐标,win用于判断玩家是否获胜。
ROW * COL - number表示非雷的个数,本次扫雷游戏中number=10,则还有71坐标为非雷坐标。
每当玩家输入一个坐标,并且该坐标不是地雷时,win++。
如果win < ROW * COL -number,则表示玩家还没有排雷成功。
win == ROW * COL -number时,表示玩家已经将所有的非雷坐标排查完毕,即排雷成功。

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 - number)
	{
			printf("请输入坐标:>");
			scanf("%d %d", &x, &y);
			if (x >= 1 && x <= row&&y >= 1 && y <= col)//判断玩家输入的坐标是否合法,合法的话进入
			{
				if (show[x][y] != '*')//判断坐标是否被排查过,若 show 数组在该坐标的元素为 ‘*’ 则表示该坐标未被排查
				{
					printf("该坐标已被排查,请重新输入!\n");
				}
				else
				{
					if (mine[x][y] == '1')// mine数组该坐标的元素为 ‘1’,则表示该坐标为地雷
					{
						printf("很遗憾,你被炸死了\n");
						Displayboard(mine, ROW, COL);//让玩家死得明白
						break;
					}
					else
					{
						win++;
						Expand(mine, show, row, col, x, y);//展开一篇雷区函数,采用递归实现,后面着重讲解
						Displayboard(show, ROW, COL);//统计该坐标周围8个格子雷的个数,并将在show数组中显示
					}
				}
			}
			else
			{
				printf("坐标非法,请重新输入!\n");//坐标不合法,提醒玩家重新输入
			}
	}
	if (win == row*col - number)
	{
		printf("恭喜你,你赢了\n");
		Displayboard(mine, ROW, COL);
	}		
}

3.2.6 构建Expand()函数

在扫雷游戏中,当我们点击的格子不是地雷,且周围也没有地雷时,会直接展开一片雷区,具体效果如下图。

如何才能做到上图所示的效果呢?
其实很简单,只需要我们遍历玩家输入坐标周围的 8 个坐标,统计该坐标周围所存在的地雷个数。而这需要我们利用Expand()函数的递归,配合定义的get_mine_count()函数来实现。

如果get_mine_count()函数的返回值为0,说明坐标周围没有雷,这时我们就可以利用递归函数Expand()进行展开。相反,如果get_mine_count()函数的返回值为非0,在show数组该坐标上显示雷的个数。

void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	if (get_mine_count(mine, x, y) == 0)//get_mine_count()函数的返回值为0,满足if条件
	{
		show[x][y] = ' ';
		int i = 0;
		int j = 0;
		for (i = -1; i < 1; i++)
		{
			for (j = -1; j < 1; j++)
			{
				if (show[x + i][y + j] == '*' && x + i >=1 && x + i <= row && y + j >=1  && y + j <= col)
				//展开雷区需要满足的条件
				{
					Expand(mine, show, row, col, x + i, y + j);
				}
			}
		}
	}
	else
	{
		show[x][y] = get_mine_count(mine, x, y) + '0';//get_mine_count()函数的返回值为非0,在show数组显示雷的个数
	}
}

3.2.7 构建get_mine_count函数

计算周围雷的个数,这里我采用的是循环的方法,也可以直接返回。

//直接返回
int get_mine_count(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';
}
//循环
int get_mine_count(char board[ROWS][COLS], int x, int y)
{
	int i = 0;
	int sum = 0;
	for (i = -1; i <= 1; i++)
	{
		int j = 0;
		for (j = -1; j <= 1; j++)
		{
			int ret = board[x + i][y + j] - '0';
			//注意:我这里其实算了9个坐标,但是由于中间坐标是'0','0'-'0'依然为0,对结果没有任何影响
			sum = sum + ret;
		}
	}
	return sum;
}

3.3 game.h文件的编写

game.h文件中放置的都是以上各种功能函数实现的声明、包含的头文件以及定义的宏。

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

//定义9*9棋盘(数组)
#define ROW 9
#define COL 9

//由于棋盘边界雷的个数不好统计,所以要在棋盘上加两行
#define ROWS ROW+2
#define COLS COL+2

#define number 10

//game()函数声明
void game();

//Initboard()函数声明
void Initboard(char board[ROWS][COLS], int rows, int cols, char set);

//Displayboard()函数声明
void Displayboard(char board[ROWS][COLS], int row, int col);

//Setmine()函数声明
void Setmine(char board[ROWS][COLS], int row, int col);


//Findmine()函数声明
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

//get_mine_count()函数声明
int get_mine_count(char board[ROWS][COLS], int x, int y);

//Expand()函数声明
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);

为什么要在头文件中定义行和列呢?
这是为了在以后如果想要增加游戏的难度(行列数),直接在头文件中更改就显得非常的方便,也有利于代码的维护。

4. 代码汇总

game.h

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

//定义9*9棋盘(数组)
#define ROW 9
#define COL 9

//由于棋盘边界雷的个数不好统计,所以要在棋盘上加两行
#define ROWS ROW+2
#define COLS COL+2

#define number 10

void game();
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);
int get_mine_count(char board[ROWS][COLS], int x, int y);
void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y);

test.c

#define _CRT_SECURE_NO_WARNINGS
#include "game.h"

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

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");
			break;
		default:
			printf("输入错误,请重新输入\n");
		}
	} while (input);
	return 0;
}

game.c

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

void Setmine(char board[ROWS][COLS], int row, int col)
{
	int count = number;
	while (count)
	{
		int x = rand() % row + 1;
		int y = rand() % row + 1;
		if (board[x][y]== '0')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

int get_mine_count(char board[ROWS][COLS], int x, int y)
{
	int i = 0;
	int sum = 0;
	for (i = -1; i <= 1; i++)
	{
		int j = 0;
		for (j = -1; j <= 1; j++)
		{
			int ret = board[x + i][y + j] - '0';
			sum = sum + ret;
		}
	}
	return sum;
}

void Expand(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
	if (get_mine_count(mine, x, y) == 0)
	{
		show[x][y] = ' ';
		int i = 0;
		int j = 0;
		for (i = -1; i < 1; i++)
		{
			for (j = -1; j < 1; j++)
			{
				if (show[x + i][y + j] == '*' && x + i >=1 && x + i <= row && y + j >=1  && y + j <= col)
				{
					Expand(mine, show, row, col, x + i, y + j);
				}
			}
		}
	}
	else
	{
		show[x][y] = get_mine_count(mine, x, y) + '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 - number)
	{
			printf("请输入坐标:>");
			scanf("%d %d", &x, &y);
			if (x >= 1 && x <= row&&y >= 1 && y <= col)
			{
				if (show[x][y] != '*')
				{
					printf("该坐标已被排查,请重新输入!\n");
				}
				else
				{
					if (mine[x][y] == '1')
					{
						printf("很遗憾,你被炸死了\n");
						Displayboard(mine, ROW, COL);
						break;
					}
					else
					{
						win++;
						//没有被炸死,统计周围雷的个数
						Expand(mine, show, row, col, x, y);
						Displayboard(show, ROW, COL);
					}
				}
			}
			else
			{
				printf("坐标非法,请重新输入!\n");
			}
	}
	if (win == row*col - number)
	{
		printf("恭喜你,你赢了\n");
		Displayboard(mine, ROW, COL);
	}		
}

void game()
{
	char mine[ROWS][COLS] = { 0 };//安排雷的棋盘11*11
	char show[ROWS][COLS] = { 0 };//排查雷的棋盘11*11

	//将mine初始化为'0',show棋盘初始化为'*'
	Initboard(mine, ROWS, COLS, '0');
	Initboard(show, ROWS, COLS, '*');

	//安排雷
	Setmine(mine, ROW, COL);//注意:埋雷的时候也是传ROW、COL

	//打印mine和show
	//Displayboard(mine, ROW, COL);设置雷的棋盘不用打印
	Displayboard(show, ROW, COL);

	//打印mine
	//Displayboard(mine, ROW, COL);

	//找雷
	Findmine(mine, show, ROW, COL);

}

5. 游戏展示

好了,以上就是本期博客的全部内容了,希望看完本篇博客对你有所帮助😎。
在这里插入图片描述

👉你的关注,点赞,评论,收藏都是对我创作最大的鼓励👈

若本篇博客存在错误,望指出,感谢!更多代码资料见GitHub:C-language

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值