【C语言游戏】扫雷游戏(含递归展开,难度选择,雷位标记等功能)

一、项目介绍

1. 游戏功能

        1. 难度选择:

                提供了easy,normal,hard三种难度。棋盘大小,雷数随难度增加而增大。

        2. 为棋盘动态分配内存,模拟实现二维数组。

        3. 自动调整窗口大小:

                使窗口容纳下相应难度的棋盘

        4. 标记雷位:

                玩家可以对自己确定的雷位进行标记,已经标记的雷位不能翻开。防止遗忘和误输造成的错误

        5. 递归展开

                1>> 检查选定的位置周围有没有雷

                2>> 如果没有雷自动翻开周围的位置

                3>> 重复递归进行以上步骤,就可以造成翻开一片的效果

        6. 优化游戏界面,提高游戏体验

二、设计思路

1. 准备工作

        1>> 两个棋盘:一个棋盘负责向玩家展示,另一个棋盘放置炸弹。

        1>> 分配内存:难度不同,棋盘的大小也不同,所以应该根据需要动态分配棋盘的内存空间

        2>> 初始化:分配好内存空间后,初始化两个棋盘的内容

        3>> 放置炸弹:开始游戏前,在随机位置安放预定数量的炸弹。

        4>> 释放内存:不同于一维动态内存空间,二维的动态内存应先释放低维空间,再释放高维空间

 2. 游戏主体流程图

        【C语言游戏】扫雷游戏(含递归展开,难度选择,雷位标记等功能)_第1张图片

 三、完整代码详解

//文件包含
#define _CRT_SECURE_NO_WARNINGS
#include 
#include 
#include 

//定义标识符,统一控制空格
#define MENUBLANK "\t\t"
#define BOARDBLANK "\t"

1. main函数

        处理玩家选项,决定游戏难度或是退出程序。

enum option{
	EXIT,
	EASY,
	NORMAL,
	HARD
};

int main(){

	enum option input;
	srand((unsigned)time(NULL));
	do
	{
		//**修改窗口颜色,大小**
		GameInterface(0);
		//打印菜单
		Menu();
		//处理玩家选项
		printf("请选择:");
		fflush(stdin);
		scanf("%d", &input);
		switch (input)
		{
		case EASY:
			//根据玩家所选难度调整窗口大小,以容纳整个棋盘
			GameInterface(EASY);
			//Playgame函数的三个参数:行,列,雷数
			Playgame(9, 9, 5);
			system("pause");
			break;
		case NORMAL:
			GameInterface(NORMAL);
			Playgame(16, 16, 30);
			system("pause");
			break;
		case HARD:
			GameInterface(HARD);
			Playgame(16, 30, 3);
			system("pause");
			break;
		case EXIT:
			break;
		default:
			printf("输入错误,请重新输入!\n");
			system("pause");
			break;
		}
	} while (input);
}

2. GameInterface函数

        根据游戏难度调整窗口大小,修改窗口背景色

void GameInterface(int cmd){
	switch (cmd)
	{
		//难度不同,棋盘大小也不同
	case EASY:
		system("mode con cols=55 lines=30");
		break;
	case NORMAL:
		system("mode con cols=85 lines=45");
		break;
	case HARD:
		system("mode con cols=140 lines=45");
		break;
	default:
		system("color B0");
		system("mode con cols=55 lines=25");
		break;
	}
}

3. Menu函数

        打印菜单以供玩家选择  

void Menu(){
	system("cls");
	printf("\n\n\n\n\n");
	printf(MENUBLANK" Mine Sweeper\n");
	printf("\n\n");
	printf(MENUBLANK"*************************\n");
	printf(MENUBLANK"******    1.Easy    *****\n");
	printf(MENUBLANK"*************************\n");
	printf(MENUBLANK"******    2.Normal  *****\n");
	printf(MENUBLANK"*************************\n");
	printf(MENUBLANK"******    3.Hard    *****\n");
	printf(MENUBLANK"*************************\n");
	printf(MENUBLANK"******    0.Exit    *****\n");
	printf(MENUBLANK"*************************\n");
	printf("\n");
}

4. PlayGame函数

        动态内存分配,初始化棋盘,调用函数开始游戏,释放内存空间

void Playgame(int row, int col, int minenum){

	char** mineboard;//雷区
	char** showboard;//展示区

	//为“二维数组”动态分配内存
	//多分配了两行两列是为了方便后续排雷
	//边上的一圈和中间的位置就可以统一处理了
	mineboard = MallocArr2d(row + 2, col + 2);
	if (mineboard == NULL){
		exit(EXIT_FAILURE);
	}
	showboard = MallocArr2d(row + 2, col + 2);
	if (showboard == NULL){
		exit(EXIT_FAILURE);
	}

	//初始化雷区和展示区
	Initboard(mineboard, row + 2, col + 2, ' ');
	Initboard(showboard, row + 2, col + 2, '#');

	//为雷区安放炸弹
	Setmine(mineboard, row, col, minenum);
	//排雷
	Findmine(showboard, mineboard, row, col, minenum);

	//释放动态分配的内存
	FreeArr2d(mineboard, row + 2);
	mineboard = NULL;
	FreeArr2d(showboard, row + 2);
	showboard = NULL;
}

5. MallocArr2d函数

        分配二维动态内存,模拟实现二维数组

char** MallocArr2d(int x, int y){

	char** arr2d;
	//先为第一维(行)分配内存
	arr2d = malloc(x * sizeof(char*));//第一维的每个元素都为指针类型
	if (arr2d == NULL){
		perror("Arrmalloc2d");
		return NULL;
	}
	//再为第二维(列)分配内存
	int i = 0;
	for ( i = 0; i < x; i++)
	{
		arr2d[i] = malloc(y*sizeof(char));//第二维的元素才是char类型
		if (arr2d[i] == NULL){
			perror("Arrmalloc2d");
			return NULL;
		}
	}
	return arr2d;
}

6. FreeArr2d函数

        释放二维动态内存

void FreeArr2d(char** arr2d, int x){

	int i = 0;
	//释放内存时,由内而外。
	//先释放第二维内存空间
	for ( i = 0; i < x; i++)
	{
		free(arr2d[i]);
	}
	//再释放第一维内存空间
	free(arr2d);

}

7. Displayboard函数

        打印棋盘:包括行号,列号,分割线

void Displayboard(char** board, int row, int col){

	system("cls");
	printf("\n\n");
	int i, j;

	//打印列号	
	printf(BOARDBLANK"   ");
	for ( i = 1; i <= col; i++)
	{
		printf("| %-2d", i);
		if (i == col)
		{
			printf("|");
		}
	}
	printf("\n");

	for ( i = 1; i <= row; i++)
	{
		//打印横线
		printf(BOARDBLANK"---");
		for ( j = 1; j <= col; j++)
		{
			printf("|---");
			if (j == col)
			{
				printf("|");
			}
		}
		printf("\n");

		//打印行号
		printf(BOARDBLANK"%2d ", i);

		//打印棋盘内容
		//因为棋盘多分配了两行两列的内存
		//而打印,安放炸弹,排雷等操作的范围不包括外围的一圈
		//所以行列坐标都应该是从1开始的闭区间
		for (j = 1; j <= col; j++)
		{
			printf("| %c ",board[i][j]);
			if (j == col)
			{
				printf("|");
			}
		}
		printf("\n");

		//最后一行打印最后一条横线
		if (i == row)
		{
			printf(BOARDBLANK"---");
			for (j = 1; j <= col; j++)
			{
				printf("|---");
				if (j == col)
				{
					printf("|");
				}
			}
		}
		
	}

	printf("\n\n");

}

8. Initboard函数

        初始化棋盘

void Initboard(char** board, int rows, int cols, char ch){

	int i, j;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++){
			board[i][j] = ch;
		}
	}

}

9. Setmine函数

        为雷区安放炸弹

void Setmine(char** mineboard, int row, int col, int minenum){
	int i;
	int x, y;

	for (i = 0; i < minenum;)
	{
		x = rand() % row+1;
		y = rand() % col+1;

		if (mineboard[x][y] != '*')
		{
			mineboard[x][y] = '*';
			i++;
		}
	}
}

10. Findmine函数

        体现游戏设计思路的主体函数,通过调用游戏具体实现的函数,控制游戏的整个流程。分标记雷位;排雷;判断输赢三大块内容

void Findmine(char** showboard, char** mineboard, int row, int col, int minenum){

	int x, y;
	while (1)
	{	
		//打印展示区
		Displayboard(showboard, row, col);
		//接受玩家输入
		printf("请输入排雷坐标:");
		fflush(stdin);
		scanf("%d %d", &x, &y);
		// # 标记雷位 #
		if (x == 0 && y == 0){
			SIGN:
			printf("请输入标记坐标:");
			fflush(stdin);
			scanf("%d %d", &x, &y);
			if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否在范围内
			{
				if (showboard[x][y] == '#' || showboard[x][y] == '!')//判断坐标是否被排除或已经被标记
				{
					showboard[x][y] = showboard[x][y]=='!'?'#':'!';
					//如果未被标记就标记,反之取消标记
					continue;
				}
				else
				{
					printf("此坐标已排除!\n");
					goto SIGN;
				}
			}
			else
			{
				printf("坐标非法!\n");
				goto SIGN;
			}
		}
		// # 排雷 #
		if (x >= 1 && x <= row && y >= 1 && y <= col)//判断坐标是否在范围内
		{
			if (showboard[x][y] == '#')//判断坐标是否被排除
			{
				if (mineboard[x][y] != '*')//判断是否是雷
				{
						Chain_reflect(showboard, mineboard, row, col, x, y);//触发链式反应
				}
				else
				{
					//排雷失败打印棋盘
					Displayboard(showboard, row, col);
					printf("Bang!\n");
					printf("排雷失败!\n");
					break;
				}
			}
			else
			{
				printf("此坐标已排除!\n");
				system("pause");
			}
		}
		else
		{
			printf("坐标非法!\n");
			system("pause");
		}
		// # 判断是否完成排雷 #
		if (Iswin(showboard, row, col, minenum) == 1)
		{
			Displayboard(showboard, row, col);
			printf("恭喜你,排雷成功!\n");
			break;
		}		
	}
}

11. get_minenum函数

        检查给定坐标周围位置的雷数,并将雷数转换为char型返回

char get_minenum(char** mineboard, int x, int y){

	int i, j;
	int count = 0;

	for (i = -1; i <= 1; i++)
	{
		for (j = -1; j <= 1; j++)
		{
			if (mineboard[x + i][y + j] == '*')
				count++;
		}
	}

	return count + '0';//count + '0'将int转换为char型返回
}

12. Chain_reflect函数

        连锁反应,实现递归展开的功能

void Chain_reflect(char** showboard, char** mineboard, int row, int col, int x, int y){

	int i, j;
	//检查坐标是否在范围内
	if (x >= 1 && x <= row && y >= 1 && y <= col)
	{
		//判断坐标是否被排除
		if (showboard[x][y] == '#')
		{
			//判断周围是否有雷
			if (get_minenum(mineboard, x, y) == '0')
			{
				//将雷位置为空格
				showboard[x][y] = ' ';
				
				//传递周围雷位
				for (i = x - 1; i <= x + 1; i++)
				{
					for (j = y - 1; j <= y + 1; j++)
					{
						Chain_reflect(showboard, mineboard, row, col, i, j);
					}
				}

			}
			else
			{
				showboard[x][y] = get_minenum(mineboard, x, y);
			}
		}	
	}	
	return;
}

13. Iswin函数

        判断是否排雷成功

int Iswin(char** showboard, int row, int col, int minenum){

	//如何判断输赢:遍历展示区统计'#'(未排)和'!'(标记)的数量
	//如果总数等于雷数,证明玩家已经找出所有的雷位,排雷成功

	int i, j;
	int count = 0;
	for (i = 1; i <= row; i++){

		for (j = 1; j <= col; j++)
		{
			if (showboard[i][j] == '#' || showboard[i][j] == '!'){
				count++;
			}
		}
	}
	if (count == minenum){
		return 1;
	}
	else
	{
		return 0;
	}
}

四、重难点讲解

1. 二维动态内存空间的分配和释放

        1. 我们知道二维数组名是其元素一维数组的地址,即二维数组名的数据类型为 char(*arr)[10](数组指针)。

            由于我们并不能确定到底有多少列,也就是说不能确定一维数组的元素个数。所以不能将mineboard和showboard设置成数组指针的类型。

        2. 但是我们可以模仿二维数组的实现方式,即二维数组名是一维数组的地址(char(*arr)[10])

            而一维数组的地址就是数组首元素的地址(char*),虽然数据类型不同,但两个地址完全相同。

        3. 因此我们可以将mineboard和showboard设置为 char** (二级指针),以showboard为例(假设m行n列):

            先为showboard分配m个char*类型的元素空间,再为每个char*元素分配n个char类型的元素空间

【C语言游戏】扫雷游戏(含递归展开,难度选择,雷位标记等功能)_第2张图片

2.system执行系统命令

system("cls");

清屏

system("pause");

程序暂停

system("color 5e");

修改背景和字体颜色

system("mode con cols=60 lines=30");

调整控制台大小

system("Shutdown -s -t 60");

设置60秒后自动关机

system("Shutdown -a");

取消关机计划

        颜色属性由两个十六进制数字指定

        第一个:对应于背景,第二个:对应于前景。

        每个数字可以为以下任何值:

0 = 黑色

1 = 蓝色

2 = 绿色

3 = 浅绿色

4 = 红色

5 = 紫色

6 = 黄色

7 = 白色

 8 = 灰色

9 = 淡蓝色

 A = 淡绿色

B = 淡浅绿色

C = 淡红色

D = 淡紫色

 E = 淡黄色

 F = 亮白色

3.关于递归展开的一些问题

  •         问题一:为什么要多分配两行两列的内存空间?

        答:调用get_minenum函数统计坐标周围雷数时,如果位置恰好在边脚就会造成越界访问。(Chain_reflect函数也会有类似情况)

                与其分各种情况讨论,不如在最外层多分配一圈内存空间,这样就统一了边角和中间位置的访问方法。

                在下面的问题中,我把最外层多分配的一圈元素简称为“外层元素”。

  •         问题二:在Findmine函数中已经判断过坐标的合法性了,为什么在Chain_reflect函数中又要判断一次?

        答:在递归展开的过程中,可能外层元素的坐标传递给下一层函数,如果不加判断直接调用get_minenum函数会造成越界访问。

  •         问题三:为什么要在Chain_reflect函数中判断坐标是否被排除(showboard[x][y] == '#')?

        答:在递归展开的过程中,可能会将其他递归线路已经展开的坐标再次传递给下一层函数,如果不进行判断的话就会出现栈溢出的问题(stack overflow)

五、游戏效果

六、分享交流

        最后将完整源码分享给大家,希望大家能与我在交流中共同学习,共同进步:

https://gitee.com/zty857016148/C_OS_Project/tree/master/Mine_Sweeperhttps://gitee.com/zty857016148/C_OS_Project/tree/master/Mine_Sweeper

        如果大家还有什么问题欢迎提问,我抽空一定回答!

        若是各位大佬发现了代码中的错误或是遗漏之处还请海涵,最好在评论区与我交流助我一臂之力!

【C语言游戏】扫雷游戏(含递归展开,难度选择,雷位标记等功能)_第3张图片

上一篇:【初级C语言】详解井字棋游戏(电脑下棋算法优化)https://blog.csdn.net/zty857016148/article/details/126721652

你可能感兴趣的:(游戏,c++,c语言)