【C】C语言实现扫雷(含递归展开)

本文详细介绍了如何使用C语言实现一个简单的扫雷游戏,包括菜单设计、棋盘初始化、随机布雷、检查坐标等功能,以及模块化编程和递归方法的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在这里插入图片描述


和光同尘_我的个人主页

没有了对高处的恐惧,就体会不到高处之美。 --三体II·黑暗森林

🔒前言

扫雷,那段全神贯注的时光,追逐着隐藏在方块下的雷,是我回不去的童年记忆。如今,岁月不曾停留,回忆依然鲜活。终于到了有一天,自己也能实现简单的扫雷,以另一种方式找回童年的美好。

1. 准备

首先,对于此类功能函数较多的项目,我们最好使用模块化编程方式,即分别将函数的声明、实现以及功能测试放在mine.h mine.c test.c中,使代码更具可读性、更便于维护👍

  • 确定游戏功能与逻辑
  1. 打印菜单,选择是否进行游戏
  2. 布置雷,并打印出棋盘信息
  3. 玩家选择位置信息,进行排查
  4. 检查坐标,不为雷:展开同样不为雷的格子;旁边有雷:显示个数;为雷结束游戏
  5. 重复3-4步骤
  6. 成功排雷结束游戏
  • 函数功能设计
    为了实现以上功能,我们一共需要两个二维数组,一个为show,用于展示给用户,没有选中检查时打印‘*’,旁边有雷时打印周围雷的个数,另一个为mine,用于放置雷
函数功能
InitBoard初始化棋盘
DisplayBoard打印棋盘
SetMine随机设置雷
FindMine输入坐标,判断是否为雷
GetMineCount获得坐标周围雷的数量
ExposeMine检查坐标周围没有雷时,展开周围同样不为雷的格子

定义雷的数量

#define MINE_NUM 10

2. 函数功能实现

2.1. 菜单的打印与选择

  • 这个比较简单,通过 printf 和 switch 即可实现,
  • 唯一值得注意的就是,使用while循环,在游戏结束后重新进入菜单页面,重新进行选择是否进行游戏
void menu()
{
	printf("**************************\n");
	printf("********  1.play  ********\n");
	printf("********  0.exit  ********\n");
	printf("**************************\n");
}

int main()
{
	srand((unsigned int)time(NULL));
	int sel = 0;
	do
	{
		menu();
		printf("请选择->");
		scanf("%d", &sel);
		switch (sel)
		{
		case 1:
			printf("开始游戏\n");
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	}while (sel);

	return 0;
}

2.2. InitBoard:初始化棋盘

需要注意的是,由于我们在统计检查格子周围雷的数量是,需要遍历旁边一圈的格子,把雷的数量相加,而如果检查格子在数组边缘,就无法读取周围的数据,相比于通过特殊情况来控制,我们可以通过创建一个行为ROW+2,列为COL+2的二维数组,而在打印时,只打印中心区域,即行为ROW,列为ROW的区域,就可以解决这个问题

因此,我们可以在“game.h”文件通过#define定义以下内容,便于后期修改与维护

用于打印二维数组的大小

#define ROW 10
#define COL 10

用于创建二维数组的大小

#define ROWS ROW+2
#define COLS COL+2
  • 由于我们需要初始化showmine两个棋盘,而这两个棋盘大小一样,仅在初始化内容上有区别,所以我们在设计函数时,可以多设置一个参数,用于传递用于初始化的数据,初始化show就传递*,初始化mine就传递0.
    在这里插入图片描述

二维数组的初始化,两层循环遍历数组并赋值即可

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.3. DisplayBoard:打印棋盘

  • 前面提过,我们创建的棋盘行为ROW+2,列为COL+2,打印的棋盘行为ROW,列为ROW,所以打印函数设计时,参数既要传递行为ROW+2,列为COL+2的二维数组,也要传递ROWCOL用于打印
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	for (i = 1; i <= row; i++)
	{
		int j = 0;
		for (j = 1; j <= col; j++)
		{
			printf(" %c ", board[i][j]);
		}
		printf("\n");
	}
}

运行结果:
在这里插入图片描述

  • 此时我们会发现,数据确实打印出来了,但是缺少分割线,不太美观;同时如果没有一个坐标指引,不方便下一步选择坐标。我们稍稍改进一下
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i = 0;
	printf("|");
	for (i = 0; i <= row; i++)
	{
		printf("-----|");
	}
	printf("\n|");
	for (i = 0; i <= row; i++)
	{
		printf(" %2d  |", i );
	}
	printf("\n|");
	for (i = 0; i <= row; i++)
	{
		printf("-----|");
	}
	printf("\n");
	for (i = 1; i <= row; i++)
	{
		int j = 0;
		printf("| %2d  |", i);
		for (j = 1; j <= col; j++)
		{
			printf("  %c  |", board[i][j]);
		}
		printf("\n|");
		for (j = 0; j <= col; j++)
		{
			printf("-----|");
		}
		printf("\n");
	}
}
  • 这样打印出来就比较美观了
    在这里插入图片描述
    *同时,我们在打印show棋盘的同时,在这一步也可以打印mine棋盘,看看mine棋盘是否成功初始化
    在这里插入图片描述

2.4. SetMine: 随机设置雷

  • 随机数的设置我们可以使用rand生成随机数,不过单独使用rand函数的话我们每次执行这段代码生成的随机数都是一样的,所以还要使用srand函数初始化随机种子,使每次执行生成不一样的随机数
    srand函数的调用应当放在主函数中并包含头文件<time.h>

  • 主函数

int main()
{
	srand((unsigned int)time(NULL));
	int sel = 0;
	do
	{
		menu();
		printf("请选择->");
		scanf("%d", &sel);
		switch (sel)
		{
		case 1:
			printf("开始游戏\n");
			game();
			break;
		case 0:
			printf("退出游戏\n");
			break;
		default:
			printf("选择错误,重新选择\n");
			break;
		}
	}while (sel);

	return 0;
}
  • 由于我们创建的二维数组行为ROW+2,列为COL+2,而在打印时,只打印行为ROW,列为ROW的区域,所以我们调用rand函数生成的随机数%row+1就可以让随机数生成的范围限定在[1,ROW]中,即我们要打印的区域下标范围
    在这里插入图片描述
  • 另外,我们在布置雷时,要避免在同一位置重复布雷,所以在随机生成雷的坐标后,要对坐标进行检测,如果为0,就说明不为雷,将此坐标赋值为1;如果为1,就说明这个坐标已经有雷了,我们跳过
void SetMine(char board[ROWS][COLS], int row, int col)
{	
	//创建num等于事先定义的雷的数量
	int num = MINE_NUM;

	//循环num次,布置雷
	while (num)
	{
		int x = rand() % row + 1;
		int y = rand() % col + 1;
		
		//判断坐标是否布置过雷
		if (board[x][y] == '0')
		{
			board[x][y] = '1';
			num--;
		}
	}
}

2.4. GetMineCount:获得检查坐标周围雷的数量

由于我们在设计函数时已经为获得坐标周围雷的数量,多给二维数组加了一圈,并且在mine二维数组布雷时,有雷的格子赋值为1,所以获得检查坐标周围雷的数量,只需要遍历mine二维数组坐标周围8个格子,求和即可

int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
	int a = mine[x - 1][y - 1] + mine[x - 1][y] + mine[x][y - 1]
		+ mine[x + 1][y] + mine[x][y + 1] + mine[x - 1][y + 1]
		+ mine[x + 1][y - 1] + mine[x + 1][y + 1]-8*'0';
	return a;
}

2.5. FindMine:输入坐标,判断是否为雷

这个函数主要实现3个功能,我们一个个讲下
1. 输入坐标:对于输入的坐标,我们要进行范围检查,即判断坐标是否在[1,ROW]区间内
2. 判断是否为雷:如果输入坐标对应的mine二维数组上的位置为雷(即值为1),结束游戏,打印所有雷的位置;不为雷,显示周围雷的数量
3. 游戏结束的条件判断:定义一个win变量,每次成功找到一个不为雷的格子,win--,直到win==格子总数-雷的数量,游戏胜利

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 - MINE_NUM)
	{
		printf("输入查找坐标");
		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;
			}
		
			//判定不为雷,win++,显示雷的数量
			else if (mine[x][y] == '0')
			{
				win++;
				show[x][y] = GetMineCount(mine, x, y) + '0';
				DisplayBoard(show, ROW, COL);
			}
		}
		else
			printf("坐标非法,重新输入\n");
	}
		if (win == ROW * COL - MINE_NUM)
		{
			printf("成功\n");
			DisplayBoard(mine, ROW, COL);
		}
}

在这里插入图片描述

  • 不过仅仅这样设置的话,一次只能显示一个格子周围雷的数量,那如何实现一次性展开周围所有不为雷的格子呢,这就用到了接下来递归展开。

2.6. ExposeMine:检查坐标周围没有雷时,展开周围同样不为雷的格子

递归的思路也很简单,只有遍历坐标周围的8个格子,依次计算每个格子周围雷的数量,如果有雷,把雷的数量赋值到show二维数组对应的位置;如果没有雷,就把同样周围没有雷的格子再作为参数传入ExposeMine函数,直到周围没有雷的格子都被展开。而后记得没调用一次ExposeMine函数,win--一次,这样才能正确判断游戏胜利

void ExposeMine(char mine[ROWS][COLS],char show[ROWS][COLS], int x, int y, int* win)
{
	if (x >= 1 && x <= ROW && y >= 1 && y <= COL)
	{
		int i = x;
		int j = y;
		int a = GetMineCount(mine, i, j);
		if (a == 0)
		{
			show[i][j] = ' ';
			for (i = x - 1; i <= x + 1; i++)
			{
				for (j = y - 1; j <= y + 1; j++)
				{
					if (show[i][j] == '*')
						ExposeMine(mine, show, i, j, win);
				}
			}
		}
		else
			show[i][j] = a + '0';

		(*win)++;
	}
}

这样我们就可以实现展开了
在这里插入图片描述


🗝️总结

  • 这样我们就实现了一个简单的扫雷,希望大家看完文章后能自己进行实践和改进,还有一些功能可以试着自己去实现,例如如何保证第一次输入坐标不被炸死、对已经判断为雷的格子进行标记👍
本节完~~,如果你在实现过程中遇到任何问题,欢迎在评论区指出或者私信我!💕

新人博主创作不易,如果有收获可否👍点赞✍评论⭐收藏一下?O(∩_∩)O

THANKS FOR WATCHING
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

和光同尘Viloet

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

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

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

打赏作者

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

抵扣说明:

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

余额充值