【C语言】扫雷(递归展开 + 标记功能)

1. 前言

扫雷,相信大家并不陌生,这是一款充斥着童年回忆的经典益智游戏。它能锻炼我们的思维,完成游戏也可以获得巨大的成就感。

本篇博客将采用C语言来模拟实现简单的扫雷游戏,以纪念我心目中的怀旧游戏No.1!

2. 整体思路

扫雷游戏棋盘大小:9 * 9

扫雷,不仅需要自动布置雷,而且需要输入坐标进行雷的排查,排查后需要在排查点展示周围八个点雷的个数。
所以我们需要用二维数组来表示扫雷的棋盘,但仅仅定义一个棋盘,那么在一个棋盘中需要表示的要素太多,且在排查雷时难度过大。

所以我们不妨换一种思路,使用两个棋盘,棋盘一用来放置雷,棋盘二用来存放排查后的数据。

棋盘分工:

  • 棋盘一:非雷用0表示,雷用1表示,此棋盘不暴露,仅仅用来布置雷而已。
  • 棋盘二:排查位置周围雷数用数字表示,未排查部分均用*表示,此棋盘需要在每次排查后展示。

示意图:

image-20220728220618526

但是进行排查时,对于边界部分。对周围元素进行访问时,如果大小定为9 * 9会出现数组访问越界的情况,所以我们将行和列的大小调整为11 * 11,最后打印时打印中间区域即可。

示意图:

image-20220728221633534

3. 游戏分工

该项目分为三个文件:

  1. test.c:游戏的逻辑
  2. game.h:函数声明,符号的定义
  3. game.c:游戏的实现

其中game.h,game.c为游戏模块,test.c为测试区域

4. 游戏菜单

依然是大家熟悉的菜单界面。
游戏开始前,需要有菜单来供玩家选择进入游戏,游戏至少进行一次,因此使用do...while循环。

对应代码:

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");
			break;
		default:
			printf("选择错误,请重新选择n");
			break;
		}
	}while(input);
}

运行结果:

image-20220712151612501

5. 游戏功能

友情提示:

  1. 以下模块均用于实现游戏功能,对应的功能均在test.cgame()函数中调用、game.h中进行声明和相关常量的定义,函数功能在game.c中实现。
  2. 在.c文件中均需引自定义的头文件#include"game.h"
  3. 对于函数的声明我会省略,以下模块展示的内容主要为在game.c文件中实现的游戏功能。

5.1 前提准备

扫雷棋盘打印大小为 9 * 9,但在排查雷时,数组容易越界,为避免越界,我们将每个边界周围加上一行或一列,数组大小定义为11 * 11,打印时输出9 * 9即可:

#pragma once

#define ROW 9
#define COL 9

#define ROWS ROW + 2 //11 * 11
#define COLS COL + 2

5.2 棋盘的初始化

  • 布置雷的棋盘的初始化:将元素都初始化为0
  • 排查雷的棋盘的初始化:将元素都初始化为*

思路:由于初始化的元素不同,所以在传参时可以把想初始化的元素传过去,在函数用字符类型进行接收。

test.c文件中的准备:

void game()
{
	//布置雷的棋盘
	char mine[ROWS][COLS] = { 0 };
	//排查雷的棋盘
	char show[ROWS][COLS] = { 0 };
	//初始化棋盘
	init_board(mine, ROWS, COLS, '0');//雷盘初始化为0
	init_board(show, ROWS, COLS, '*');//展示盘初始化为*
}

对应代码:

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

5.3 布置雷

思路

  • 对于雷的布置,我们仅需要布置9 * 9区域的雷即可。
  • 布置雷的个数为10个,我们采用定义的方式,方便随时修改雷的个数,雷的个数需要在头文件中定义:#define EASY_COUNT 10
  • 自动随机值的提供需要利用时间戳来完成,利用rand、srand、time函数来完成相关操作,srand函数在test中调用,布置雷的坐标为1-9,因此随机值表现形式为:rand() % row or col + 1
  • 雷的布置采用循环的方式

对应代码:

void set_mine(char mine[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;
	int x = 0;
	int y = 0;
	while (count)
	{
		x = rand() % row + 1;//1-9
		y = rand() % col + 1;//1-9
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}

5.4 打印棋盘

对于棋盘的打印,我们只需要打印出分隔线和对应的坐标即可。

思路

  • 棋盘打印依旧是 9 * 9。
  • 棋盘只需打印存放排查后数据的棋盘,不要暴露布置雷的棋盘,否则就被看到结果了。
  • 需要对棋盘对应的行列进行打印,方便玩家查看坐标。
  • 使用分隔线,避免上下棋盘黏在一起。

对应代码:

void show_board(char arr[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 <= row; i++)
	{
		int j = 0;
		printf("%d ", i);//输出对应行信息
		for (j = 1; j <= col; j++)
		{
			printf("%c ", arr[i][j]);
		}
		printf("n");
	}
	printf("------------扫雷------------n");
}

打印效果:

image-20220728204839654

5.5 新手保护机制

扫雷对于新手玩家和运气比较差的玩家体验很不友好,可能第一次就踩到雷被炸死了(比如我)。

该机制用于将第一次踩雷的坐标上的雷进行转移,让玩家有良好的游戏体验~

思路

利用rand函数生成1-9的随机数,然后转移并给出提示。

对应代码:

static void change_place(char mine[ROWS][COLS], int row, int col, int x, int y)
{
	x = rand() % row + 1;//1 - 9的随机值
	y = rand() % col + 1;
	mine[x][y] == '1';//转移
	printf("第一次踩雷,可真有你的,重新选!n");
}

5.6 标记与取消标记

在扫雷游戏中,右击鼠标可以对你认为是雷的点进行标记,并且可以取消。

我们是否可以用C语言实现一下呢?

示意图:

image-20220713102657636

5.6.1 标记菜单

由于显示界面使用键盘操作,无法使用鼠标。所以不妨我们采用如下思想,设计一个菜单,让玩家决定排雷,或标记,或取消标记。

对应代码:

static void flag_menu()
{
	printf("####################################n");
	printf("######### 1.选择非雷区域  ##########n");
	printf("######### 2.标记雷的位置  ##########n");
	printf("######### 3.取消雷的标记  ##########n");
	printf("####################################n");
}

5.6.2 设置标记

思路

将需要标记的坐标和所需参数传过去,首先需要清楚的一点是,标记 = 雷数。每标记一个位置,标记数都需要自增,这里我们选择传址调用的方式改变标记数的大小(注意:形参是实参的一份临时拷贝,所以将标记数直接传入并不能改变值)。

若坐标被排查,则需重新输入,坐标非法需要提示。

对应代码:

static void set_flag(char show[ROWS][COLS], int row, int col,int *pf)
{
	int x = 0;
	int y = 0;
	if (*pf == EASY_COUNT)
	{
		return ;//返回,停止标记
	}
	while (1)
	{
		printf("请输入标记坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
		{
			if (show[x][y] == '*')
			{
				show[x][y] = '#';
				(*pf)++;//自增
				break;//标记完退出
			}
			else
			{
				printf("该位置已被排查,请重新输入!n");
				continue;
			}
		}
		else
		{
			printf("坐标非法,请重新输入!n");
			continue;
		}
	}
}

标记效果:

image-20220728205454527

5.6.3 取消标记

思路

与标记的参数相同,该函数需要判断是否被标记,若被标记,则取消标记改为*,并且标记数需要减少。若未标记,则退出循环,让用户重新选择是否标记,坐标非法需提示。

注:未标记后不要用cotinue,避免无法取消标记导致死循环。

对应代码:

static void cancel_flag(char show[ROWS][COLS], int row, int col, int *pf)
{
	int x = 0;
	int y = 0;
	while (1)
	{
		printf("请输入取消标记的坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && y >= 1 && x <= row && y <= col)
		{
			if (show[x][y] == '#')
			{
				show[x][y] = '*';
				(*pf)--;
				break;//取消标记后退出
			}
			else
			{
				printf("该位置未标记,无法取消标记n");
				break;//一定要break,不能用continue,否则会导致死循环
			}
		}
		else
		{
			printf("坐标非法,请重新输入!n");
			continue;
		}
	}
}

5.7 获取周围雷的个数

排雷过程中点击某点,当该点无雷时,需要展示该点周围八个坐标的雷的个数。

思路

采用循环对排查坐标及周围八个点进行遍历即可。

对应代码:

static int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int count = 0;
	//x-1 ~ x+1 && y-1 ~ y+1 
	for (i = -1; i <= 1; i++)
	{
		int j = 0;
		for (j = -1; j <= 1; j++)
		{
			if (mine[x + i][y + j] == '1')
				count++;
		}
	}
	return count;
}

5.8 递归式展开一片和接收雷的个数

扫雷游戏中,当用户点击一个坐标,如果该坐标及其周围的坐标都没有雷,通过连续判断,那么雷盘就会一次性展开一片。

相比较于自己排雷71次,这样的设计也优化了玩家的体验。

示意图:

那么这个思想的原理是什么呢?
展开一片,无非就是对于排查坐标和周围八个坐标进行排查,若周围坐标周围均无雷,便可以循环往复,直到周围坐标已被排查,这也正是递归的思想。

另外对于周围雷数的返回值也可以在这个函数中接收,并将返回值转化为字符储存到展示的棋盘中。

注:p为需排查数:win的地址,递归展开时仍需要自增,让游戏正常结束。

对应代码:

static void boom_board(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* p)
{
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		int ret = get_mine_count(mine, x, y);//接收排查点周围坐标雷的个数
		if (ret == 0)
		{
			(*p)++;//自增
			show[x][y] = ' ';//置为空
			int i = 0;
			for (i = -1; i <= 1; i++)//遍历
			{
				int j = 0;
				for (j = -1; j <= 1; j++)
				{
					if (show[x + i][y + j] == '*')//未排查则递归,避免重复形成死递归
						boom_board(mine, show, row, col, x + i, y + j, p);
				}
			}
		}
		else
		{
			(*p)++;//自增
			show[x][y] = ret + '0';//转化为字符
		}
	}
}

5.9 排查雷

美名其曰是排雷,其实该过程就是一个调用展开一片,标记,取消标记等函数,以及根据结果判断是否获胜的过程。

话不多说,我们在代码中看:

void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;//排查数,目前为71
	int* p = &win;
	int op = 0;//菜单选项
	int fch = 1;//判断是否为第一次排查的变量
	unsigned int flag_count = 0;//该变量为标记数,恒>0,标记数<=雷数
	int* pf = &flag_count;
	while (win < row * col - EASY_COUNT)
	{
	again:
		flag_menu();//调用菜单
		scanf("%d", &op);
		if (op == 1)
		{
			printf("请输入要排查的坐标:>");
			scanf("%d %d", &x, &y);
			if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
			{
				if (fch == 1 && mine[x][y] == '1')//第一次排查且为雷
				{
					change_place(mine, row, col, x, y);
					fch++;//自增防止多次调用换位函数
				}
				else
				{
                    if(show[x][y] == '*')
                    {
                        
						if (mine[x][y] == '1')
						{
							system("cls");//清屏
							printf("很遗憾,你被雷炸死了!n");
							printf("游戏结束!n");
							show_board(mine, row, col);//复盘
							break;
						}
						else
						{
							boom_board(mine, show, row, col, x, y, p);//展开一片
							system("cls");//清屏
							show_board(show, row, col);
						}
						fch++;//自增防止多次调用换位函数
					}
                	else
                	{
                    	printf("该坐标已被排查,请重新输入n");
                	}
                }
			}
			else
			{
				printf("非法坐标,请重新输入!n");
				continue;
			}
		}
		else if (op == 2)//标记
		{
            set_flag(show, row, col, pf);//传址
			flag_count = *pf;
			system("cls");//清屏
            if (*pf == 10)
			{
				printf("标记数和雷数相等,无法标记!n");
			}
			show_board(show, row, col);
		}
		else if (op == 3)
		{
            cancel_flag(show, row, col, pf);//传址
			flag_count = *pf;
			system("cls");//清屏
			show_board(show, row, col);
		}
		else
		{
			printf("选择错误,请重新选择:>n");
			goto again;//跳转到选择处
		}
	}
	if (win == row * col -EASY_COUNT)
	{
		system("cls");
		show_board(show, ROW, COL);//最后一步展示
		printf("恭喜你,扫雷成功!n");
		printf("获得称号,排雷战士!n");
		printf("答案揭晓:n");
		show_board(mine, ROW, COL);//初始答案展示
	}
}

6. 完整代码

game.h

#pragma once

#define ROW 9
#define COL 9

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

#define EASY_COUNT 10//雷数

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

//初始化棋盘
void init_board(char arr[ROWS][COLS], int rows, int cols, char set);
//布置雷
void set_mine(char mine[ROWS][COLS], int row, int col);
//打印棋盘
void show_board(char arr[ROWS][COLS], int row, int col);
//排查雷
void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);

game.c

#define _CRT_SECURE_NO_WARNINGS 1 
#include"game.h"
void init_board(char arr[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++)
		{
			arr[i][j] = set;
		}
	}
}
void set_mine(char mine[ROWS][COLS], int row, int col)
{
	int count = EASY_COUNT;
	int x = 0;
	int y = 0;
	while (count)
	{
		x = rand() % row + 1;
		y = rand() % col + 1;
		if (mine[x][y] == '0')
		{
			mine[x][y] = '1';
			count--;
		}
	}
}
void show_board(char arr[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 <= row; i++)
	{
		int j = 0;
		printf("%d ", i);//输出对应行信息
		for (j = 1; j <= col; j++)
		{
			printf("%c ", arr[i][j]);
		}
		printf("n");
	}
	printf("------------扫雷------------n");
}
static void flag_menu()
{
	printf("####################################n");
	printf("######### 1.选择非雷区域  ##########n");
	printf("######### 2.标记雷的位置  ##########n");
	printf("######### 3.取消雷的标记  ##########n");
	printf("####################################n");
}
//标记雷的位置
static void set_flag(char show[ROWS][COLS], int row, int col,int *pf)
{
	int x = 0;
	int y = 0;
	if (*pf == EASY_COUNT)
	{
		return ;//返回,停止标记
	}
	while (1)
	{
		printf("请输入标记坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
		{
			if (show[x][y] == '*')
			{
				show[x][y] = '#';
				(*pf)++;//自增
				break;//标记完退出
			}
			else
			{
				printf("该位置已被排查,请重新输入!n");
				continue;
			}
		}
		else
		{
			printf("坐标非法,请重新输入!n");
			continue;
		}
	}
}
//取消标记
static void cancel_flag(char show[ROWS][COLS], int row, int col, int *pf)
{
	int x = 0;
	int y = 0;
	while (1)
	{
		printf("请输入取消标记的坐标:>");
		scanf("%d %d", &x, &y);
		if (x >= 1 && y >= 1 && x <= row && y <= col)
		{
			if (show[x][y] == '#')
			{
				show[x][y] = '*';
				(*pf)--;
				break;//取消标记后退出
			}
			else
			{
				printf("该位置未标记,无法取消标记n");
				break;//一定要break,不能用continue,否则会导致死循环
			}
			}
		}
		else
		{
			printf("坐标非法,请重新输入!n");
			continue;
		}
	}
}
static void change_place(char mine[ROWS][COLS], int row, int col, int x, int y)
{
	x = rand() % row + 1;
	y = rand() % col + 1;
	mine[x][y] == '1';
	printf("第一次踩雷,可真有你的,重新选!n");
}
static int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
	int i = 0;
	int count = 0;
	for (i = -1; i <= 1; i++)
	{
		int j = 0;
		for (j = -1; j <= 1; j++)
		{
			if (mine[x + i][y + j] == '1')
				count++;
		}
	}
	return count;
}
//爆炸展开
static void boom_board(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* p)
{
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		int ret = get_mine_count(mine, x, y);//接收排查点周围坐标雷的个数
		if (ret == 0)
		{
			(*p)++;//自增
			show[x][y] = ' ';//置为空
			int i = 0;
			for (i = -1; i <= 1; i++)//遍历
			{
				int j = 0;
				for (j = -1; j <= 1; j++)
				{
					if (show[x + i][y + j] == '*')//未排查则递归,避免重复形成死递归
						boom_board(mine, show, row, col, x + i, y + j, p);
				}
			}
		}
		else
		{
			(*p)++;//自增
			show[x][y] = ret + '0';//转化为字符
		}
	}
}
void fine_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int win = 0;//排查数,目前为71
	int* p = &win;
	int op = 0;//菜单选项
	int fch = 1;//判断是否为第一次排查的变量
	unsigned int flag_count = 0;//该变量为标记数,恒>0,标记数<=雷数
	int* pf = &flag_count;
	while (win < row * col - EASY_COUNT)
	{
	again:
		flag_menu();//调用菜单
		scanf("%d", &op);
		if (op == 1)
		{
			printf("请输入要排查的坐标:>");
			scanf("%d %d", &x, &y);
			if (x >= 1 && y >= 1 && x <= row && y <= col)//合法判断
			{
				if (fch == 1 && mine[x][y] == '1')//第一次排查且为雷
				{
					change_place(mine, row, col, x, y);
					fch++;//自增防止多次调用换位函数
				}
				else
				{
                    if(show[x][y] == '*')
                    {
                        
						if (mine[x][y] == '1')
						{
							system("cls");//清屏
							printf("很遗憾,你被雷炸死了!n");
							printf("游戏结束!n");
							show_board(mine, row, col);//复盘
							break;
						}
						else
						{
							boom_board(mine, show, row, col, x, y, p);//展开一片
							system("cls");//清屏
							show_board(show, row, col);
						}
						fch++;//自增防止多次调用换位函数
					}
                	else
                	{
                    	printf("该坐标已被排查,请重新输入n");
                	}
                }
			}
			else
			{
				printf("非法坐标,请重新输入!n");
				continue;
			}
		}
		else if (op == 2)//标记
		{
            set_flag(show, row, col, pf);//传址
			flag_count = *pf;
			system("cls");//清屏
            if (*pf == 10)
			{
				printf("标记数和雷数相等,无法标记!n");
			}
			show_board(show, row, col);
		}
		else if (op == 3)
		{
            cancel_flag(show, row, col, pf);//传址
			flag_count = *pf;
			system("cls");//清屏
			show_board(show, row, col);
		}
		else
		{
			printf("选择错误,请重新选择:>n");
			goto again;//跳转到选择处
		}
	}
	if (win == row * col -EASY_COUNT)
	{
		system("cls");
		show_board(show, ROW, COL);//最后一步展示
		printf("恭喜你,扫雷成功!n");
		printf("获得称号,排雷战士!n");
		printf("答案揭晓:n");
		show_board(mine, ROW, COL);//初始答案展示
	}
}

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] = { 0 };
	char show[ROWS][COLS] = { 0 };
	//初始化棋盘
	init_board(mine, ROWS, COLS, '0');
	init_board(show, ROWS, COLS, '*');
	//布置雷
	set_mine(mine, ROW, COL);
	system("cls");
	//打印棋盘
	show_board(show, ROW, COL);
	//排查雷
	fine_mine(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");
			break;
		default:
			printf("选择错误,请重新选择:>n");
			break;
		}
	} while (input);
	return 0;
}

7. 动画展示

8. 结语

到这里,一个还原度较高的扫雷游戏就实现成功了!

以上就是C语言实现扫雷的全部内容,如果觉得anduin写的还不错的话,还请一键三连!

我是anduin,一名C语言初学者,我们下期见!

(ps:如果大家觉得粘贴代码比较费劲的话,也可以从我的gitee打包下载源码:戳我得扫雷源码)

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>