扫雷游戏【C语言实现】

在这里插入图片描述

本期介绍

主要介绍:如何一步一步的把整个扫雷游戏的所有功能实现,详细的讲解其中每一个功能从无到有的思考过程以及代码上的实现👀。



1. 扫雷游戏

  扫雷是一款大众类的益智小游戏,这款游戏的玩法是在一个9 * 9(初级),16 * 16(中级),16 * 30(高级)大小的方块矩阵中随机布置一定量的地雷,然后扫除这些地雷(初级为10个,中级为40个,高级为99个)。游戏规则如下:

  1. 游戏的目标是在最短的时间内找出所有非雷棋子,同时避免踩到雷,踩到雷即游戏结束。
  2. 游戏由多个方格组成,点击到非雷的棋子,会显示棋子周围3x3区域中地雷的个数,一般为8个格子,对于边块为5个格子,对于角块为3个格子,因此扫雷中最大的数字为8。
  3. 可以标记雷,即在小方格上插上小红旗。
  4. 当场上只剩下地雷未被打开后,游戏胜利。在这之前如果你打开了地雷方块,则游戏失败。

2. 程序实现思路

  在玩扫雷游戏的时候大家会发现,这个棋盘由两层构成。第一层放置棋盘上所有数据,包括:地雷、周围地雷的个数。第二层则是用于覆盖第一层信息的遮挡层,如下图所示。
  按照这个思路在开始游戏前,就要将数据层所有的地雷以及显示周围有雷数量的棋子都布置好,隐藏于覆盖层下。然后根据玩家每次点击掀开棋子,判断是否掀到地雷,以及显示翻开棋子后的信息显示,等等。按照这个思路最终是能够实现扫雷游戏的,但相较于另一个思路,就显得较为复杂了。

在这里插入图片描述
  另一个思路,同样将扫雷游戏的棋盘分为两层,第一层藏于暗处存放地雷的信息,由0、1组成,0表示非地雷,1表示地雷。第二层则基于第一层棋盘存储的数据,把用户操作后棋盘的信息显示出来,如下图所示。

在这里插入图片描述

  接下来,思考一下扫雷游戏有哪些功能呢?首先,进入计算机里的扫雷游戏后棋盘上所有的格子都被掩盖着,然后随机去翻开一些格子,你肯定能发现会出现如下所示的几种情况,当一一实现这些功能后,扫雷游戏就完成了。

  1. 当翻开的是“地雷”时,你会被炸死游戏结束。
  2. 当翻开的格子周围一圈当中存在“地雷”时,翻开的格子下面会显示周围一圈格子内存在“地雷”的个数,地雷个数最大不会超过8。
  3. 当翻开的格子周围没有“地雷”时,会一下子展开一大片区域。

3. 实现流程

在这里插入图片描述


4. 棋盘越界访问的解题思路

  根据扫雷游戏规则可知,当排查非雷的棋子且周围存在雷时,会将该棋子周围3x3格子内地雷的数量显示出来。也就说想要实现该功能,需要将棋子周围一圈都访问一遍。但值得注意的是,当排查最边界的那一圈棋子时,不能再以3x3的范围进行访问了,不然你就会越界访问。就如下图所示:
在这里插入图片描述
  为了不越界访问,必须根据不同方位的边角采用不同的访问方式,这是一种解决的方法,但该方法的实现太过于麻烦。大家思考一下,能不能从另一个角度来解决这个问题。解题思路:把9×9的棋盘往外扩张一圈,只拿该棋盘中间的9×9的格子用作和以前一样的操作,问题就迎刃而解了。如下图所示:
在这里插入图片描述


5. 炸金花式展开的实现思路

  根据扫雷游戏可以得知,当点击到周围没有地雷的非雷棋子后,会展开一大片区域,直至展开到周围有雷的棋子为止,如下图所示。

在这里插入图片描述
  要想实现这个功能,就需要用到递归函数了。那该怎么实现该递归函数呢?递归的解题思路:把一个大且复杂的问题层层转化为一个与原问题相似但规模较小的问题。当排查的位置没有雷且该位置周围没有雷时,就展开其周围一圈的8个坐标,然后看这8个坐标是否可以再逐个向其自身周围接着展开,这样一次就递归调用函数自身8次的展开速度就像爆炸了一样,所以称其为:炸金花式展开


5.1 死递归

  基于上面的所思所想,当排查某个周围没有雷的坐标时,该坐标会向周围一圈展开,然后展开的这些坐标会继续再向外围展开,继而把所有周围没有地雷的坐标统统排查出来。如果按照这个思路去编写代码一定会出现一种情况“ 程序陷入死递归 ”。这个问题当时也时困扰了我很久,代码调试一直走不下去,直到我动手画了张图慢慢分析后才发现了问题的所在。
  大家会发现不管你先展开哪一个位置你都将陷入死递归当中。举个例子如下图所示,坐标(x,y)坐标和(x-1,y+1)坐标,都在相互展开的范围内。这就必然导致两个坐标之间相互无止境的调用,必然导致死递归。在这里插入图片描述
  解题思路:只需要在递归中加入一个判断:只有还未被翻开的棋子才能进行递归,否则将直接逃过此次递归。如此就可以限制住坐标在那相互疯狂调用,因为程序这样设计后每个坐标向外展开的次数就只有一次了。


5.2 越界访问

  大家在编写代码时由于考虑不周,还会出现一种错误:数组的越界访问。使用炸金花展开后,当展开到棋盘的边界时,程序是不会停下来的,任然会继续向外展开。这就必然导致了越界访问,程序奔溃,如下图所示。
在这里插入图片描述

  解题思路:在递归中加一条限制,只能在棋盘范围内进行递归调用,一旦出来这个范围,就跳过此次递归。


6. 实现代码

#define _CRT_SECURE_NO_WARNINGS 1

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

//简单难度
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define CHECKER_NUM (ROW * COL)
#define MINE_NUM 10

typedef struct CheckerBroad
{
	char backbroad[ROWS][COLS];//幕后存放地雷数据的棋盘
	char frontbroad[ROWS][COLS];//展示给用户状态的棋盘
	int TurnOver_num;//已经排查棋子的数量
	int perr;//异常信号
}CheckerBroad;

//进入扫雷游戏
int sweepMine_game();
//初始化棋盘
void init_checker_broad(CheckerBroad* p_checker_broad, int Rows, int Cols);
//布置地雷
void set_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num);
//打印展示棋盘
void display_broad(const CheckerBroad* p_checker_broad, int Row, int Col, int num);
//开始扫雷
int start_sweep_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num);
//检查输入坐标是否正确
int check_input(const CheckerBroad* p_checker_broad, int x, int y, int Row, int Col);
//获取坐标周围3x3内地雷的个数
int get_mine_num(const CheckerBroad* p_checker_broad, int x, int y);
//炸金花展开周围没有地雷的棋子
void burst_golden_flower(CheckerBroad* p_checker_broad, int x, int y, int Row, int Col);
//显示棋盘上所有地雷
void show_total_mine(CheckerBroad* p_checker_broad, int Row, int Col);
//标注信息
void mark_message(CheckerBroad* p_checker_broad, int Row, int Col, int num);
//标记地雷
void mark_mine(CheckerBroad* p_checker_broad, int Row, int Col);
//取消地雷标记
void unmark_mine(CheckerBroad* p_checker_broad, int Row, int Col);

初始化棋盘
void init_checker_broad(CheckerBroad* p_checker_broad, int Rows, int Cols)
{
	if (p_checker_broad == NULL)
	{
		perror("init_checker_broad:");
		return;
	}
	//1.初始化隐藏棋盘
	int i = 0;
	for (i = 0; i < Rows; i++)
	{
		int j = 0;
		for (j = 0; j < Cols; j++)
		{
			p_checker_broad->backbroad[i][j] = 0;
		}
	}
	//2.初始化前面展示棋盘
	for (i = 0; i < Rows; i++)
	{
		int j = 0;
		for (j = 0; j < Cols; j++)
		{
			p_checker_broad->frontbroad[i][j] = '#';
		}
	}
	//3.初始化已经排查棋子的数量
	p_checker_broad->TurnOver_num = 0;
	//4.初始化异常信息
	p_checker_broad->perr = 0;
}


//布置地雷
void set_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("set_mine:");
		return;
	}
	int i = 0;
	//初始化rand随机函数的种子
	srand((unsigned int)time(NULL));
	//随机布置10个地雷
	for (i = 0; i < num;)
	{
		int x = rand() % Row + 1;
		int y = rand() % Col + 1;
		if (p_checker_broad->backbroad[x][y] == 1)
		{
			continue;
		}
		p_checker_broad->backbroad[x][y] = 1;
		i++;
	}
}

//打印展示后端棋盘
void display_backbroad(const CheckerBroad* p_checker_broad)
{
	if (p_checker_broad == NULL)
	{
		perror("display_backbroad:");
		return;
	}
	//打印幕后棋盘
	int i = 0;
	printf("    ");
	for (i = 1; i <= ROW; i++)
	{
		printf("%d ", i);
	}
	printf("\n\n");
	for (i = 1; i <= ROW; i++)
	{
		int j = 0;
		printf(" %d  ", i);
		for(j = 1; j <= COL; j++)
		{
			printf("%d ", p_checker_broad->backbroad[i][j]);
		}
		printf("\n");
	}
}



//打印展示前端棋盘
void display_broad(const CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("display_broad:");
		return;
	}
	Sleep(300);
	system("cls");
	printf("---------------  扫雷  ----------------\n\n");
	printf("地雷数量>:%d        剩余棋子数量>:%d\n\n", num, Row * Col - num - p_checker_broad->TurnOver_num);
	int i = 0;
	//打印列号
	printf("  ");
	for (i = 1; i <= Col; i++)
	{
		printf("  %d ", i);
	}
	printf("\n");
	//打印棋盘
	for (i = 1; i <= Row; i++)
	{
		int j = 0;
		//分割区
		printf("  ");
		for (j = 1; j <= Col; j++)
		{
			printf("|---");
		}
		printf("|\n");
		//棋子
		for (j = 1; j <= Col; j++)
		{
			//打印行号
			if (j == 1)
			{
				printf("%d ", i);
			}
			if (p_checker_broad->frontbroad[i][j] == '@')
			{
				printf("|\033[0;31;40m @ \033[0m");//打印地雷标志为红色的‘@’
			}
			else
			{
				printf("| %c ", p_checker_broad->frontbroad[i][j]);
			}
		}
		printf("|\n");
	}
	//打印分割区
	printf("  ");
	for (i = 1; i <= Col; i++)
	{
		printf("|---");
	}
	printf("|\n");
	printf("\n---------------------------------------\n");

}

//开始扫雷游戏
int start_sweep_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("start_sweep_mine:");
		return -1;
	}
	int x = 0;
	int y = 0;

	//frontbroad[3][4] = '3';
	//display_broad(frontbroad);

	//判断扫雷游戏是否胜利
	while (p_checker_broad->TurnOver_num < Row * Col - num)
	{
		//display_backbroad(p_checker_broad);//打印后端界面
		//1.输入行和列
		do
		{
			printf("请输入想要翻开棋子的行和列>:");
			scanf("%d %d", &x, &y);
		} while (1 != check_input(p_checker_broad, x, y, Row, Col));//判断输入的行和列是否正确
		
		//2.翻开棋子
		if (p_checker_broad->backbroad[x][y] == 1)//翻到地雷
		{
			show_total_mine(p_checker_broad, Row, Col);//显示棋盘上所有的地雷
			display_broad(p_checker_broad, Row, Col, num);
			return 0;
		}
		else//没有翻到地雷
		{
			//炸金花展开棋子
			burst_golden_flower(p_checker_broad, x, y, Row, Col);
			if (p_checker_broad->perr == -1)
				return -1;
		}
		//3.打印棋盘
		display_broad(p_checker_broad, Row, Col, num);
		//4.标记地雷(只起到注释的功能)
		mark_message(p_checker_broad, Row, Col, num);
	}
	show_total_mine(p_checker_broad, Row, Col);//显示棋盘上所有的地雷
	display_broad(p_checker_broad, Row, Col, num);
	return 1;//扫雷结束
}

//检查输入坐标是否正确,返回值若为:
//-1:表示frontbroad指针为NULL,0表示输入坐标错误
// 0:表示输入坐标错误
// 1:表示输入坐标正确
int check_input(const CheckerBroad* p_checker_broad, int x, int y, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("check_input:");
		return -1;
	}
	//坐标超范围
	if ((x < 1) || (x > ROW) || (y < 1) || (y > COL))
	{
		printf("输入坐标超出棋盘范围,请重新输入想要翻开棋子的坐标>:");
		return 0;
	}
	//坐标位置棋子已经被翻开
	else if ((p_checker_broad->frontbroad[x][y] != '#' && p_checker_broad->frontbroad[x][y] != '!'))
	{
		printf("该坐标位置的棋子已经被翻,请重新输入坐标>:");
		return 0;
	}
	else//输入正确
	{
		return 1;
	}
}

//获取棋子周围3x3格子内地雷的个数
int get_mine_num(const CheckerBroad* p_checker_broad, int x, int y)
{
	if (p_checker_broad == NULL)
	{
		perror("get_mine_num:");
		return -1;
	}
	int sum = 0;
	int i = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		int j = 0;
		for (j = y - 1; j <= y + 1; j++)
		{
			sum += p_checker_broad->backbroad[i][j];
		}
	}
	return sum;
}

//炸金花式翻开棋子
void burst_golden_flower(CheckerBroad* p_checker_broad, int x, int y, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("burst_golden_flower:");
		return;
	}
	//棋子必须在9x9棋盘内,出了这个范围必然导致越界访问
	if ((x >= 1 && x <= Row) && (y >= 1 && y <= Col))
	{
		//防止重复递归
		if ((p_checker_broad->frontbroad[x][y] == '#') || (p_checker_broad->frontbroad[x][y] == '!'))
		{
			//1.获得该坐标棋子周围地雷的个数
			int count = get_mine_num(p_checker_broad, x, y);
			//2.按情况分析
			if (count == 0)//周围没有地雷
			{
				//翻开棋子&&记录
				p_checker_broad->frontbroad[x][y] = ' ';
				p_checker_broad->TurnOver_num++;
				//炸金花展开
				int i = 0;
				for (i = x - 1; i <= x + 1; i++)
				{
					int j = 0;
					for (j = y - 1; j <= y + 1; j++)
					{
						if ((i == x) && (j == y))
							continue;
						burst_golden_flower(p_checker_broad, i, j, Row, Col);
					}
				}
			}
			else if (count >= 1 && count <= 8)//周围有地雷
			{
				//翻开棋子,将周围的地雷数显示到棋盘上
				p_checker_broad->frontbroad[x][y] = count + '0';
				//记录
				p_checker_broad->TurnOver_num++;

			}
			else
			{
				//获取周围地雷个数出现异常
				perror("get_mine_num:");
				p_checker_broad->perr = -1;
				return;
			}
		}
	}
}

//显示棋盘上所有地雷
void show_total_mine(CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("show_total_mine:");
		return;
	}
	int i = 0;
	for (i = 1; i <= Row; i++)
	{
		int j = 0;
		for (j = 1; j <= Col; j++)
		{
			if (p_checker_broad->backbroad[i][j] == 1)
			{
				p_checker_broad->frontbroad[i][j] = '@';
			}
		}
	}
}

void menu_mark()
{
	printf("***************************************\n");
	printf("*****            0.exit           *****\n");
	printf("*****            1.mark           *****\n");
	printf("*****           2.unmark          *****\n");
	printf("***************************************\n");
}

//标记地雷
void mark_mine(CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("mark_mine:");
		return;
	}
	int x = 0;
	int y = 0;
	//输入行和列
	do
	{
		printf("请输入需要标记为地雷的坐标>:");
		scanf("%d %d", &x, &y);
	} while (1 != check_input(p_checker_broad, x, y, Row, Col));//判断输入的行和列是否正确
	if (p_checker_broad->frontbroad[x][y] == '#')
	{
		//标记
		p_checker_broad->frontbroad[x][y] = '!';
		printf("标记成功\n");
	}
	else
	{
		//已经标记过了
		printf("该坐标已经标记过了\n");
	}
}

//取消地雷标记
void unmark_mine(CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("mark_mine:");
		return;
	}
	int x = 0;
	int y = 0;
	//输入行和列
	do
	{
		printf("请输入需要标记为地雷的坐标>:");
		scanf("%d %d", &x, &y);
	} while (1 != check_input(p_checker_broad, x, y, Row, Col));//判断输入的行和列是否正确
	if (p_checker_broad->frontbroad[x][y] == '!')
	{
		//取消标记
		p_checker_broad->frontbroad[x][y] = '#';
		printf("已取消标记\n");
	}
	else
	{
		//没有标记过,无法取消标记
		printf("该坐标没有被标记,无法取消标记\n");
	}
}

//标注信息(只起到注释的作用)
void mark_message(CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("mark_mine:");
		return;
	}

	int input = 0;
	do
	{
		menu_mark();
		printf("是否需要标注地雷?>:");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出标注\n");
			break;
		case 1:
			mark_mine(p_checker_broad, Row, Col);
			//打印展示棋盘
			display_broad(p_checker_broad, Row, Col, num);
			break;
		case 2:
			unmark_mine(p_checker_broad, Row, Col);
			//打印展示棋盘
			display_broad(p_checker_broad, Row, Col, num);
			break;
		default:
			printf("选择错误,请重新重新输入选项>:\n");
			break;
		}
	} while (input);
}

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

void descriotion()
{
	printf("扫雷游戏的规则如下:\n");
	printf("1.游戏的目标是在最短的时间内找出所有非雷格子,同时避免踩到雷,踩到雷即游戏结束。\n");
	printf("2.游戏由多个方格组成,点击方格可以显示其中的数字。数字表示该方格周围3×3区域中的地雷数,\n");
	printf("  一般为8个格子,对于边块为5个格子,对于角块为3个格子,因此扫雷中最大的数字为8。\n");
	printf("3.可以标记雷,即在小方格上插上小红旗。\n");
	printf("4.当场上只剩下地雷方块未被打开后,游戏胜利。在这之前如果你打开了地雷方块,则游戏失败。\n");
}

int sweepMine_game()
{
	//1.创建扫雷游戏棋盘
	CheckerBroad checker_broad;
	//2.初始化棋盘
	init_checker_broad(&checker_broad, ROWS, COLS);
	//3.设置地雷
	set_mine(&checker_broad, ROW, COL, MINE_NUM);
	//4.打印棋盘
	//display_broad(backbroad);
	display_broad(&checker_broad, ROW, COL, MINE_NUM);
	//5.开始扫雷
	int result = start_sweep_mine(&checker_broad, ROW, COL, MINE_NUM);
	if (result == 1)
	{
		printf("恭喜玩家扫除所有埋藏的地雷,通关成功!\n");
		return 1;
	}
	else if (result == 0)
	{
		printf("很遗憾你已被地雷炸死,通关失败!\n");
		return 0;
	}
	else if (result == -1)
	{
		printf("扫雷游戏运行失败,请检查\n");
		return -1;
	}
	else
	{
		return -1;
	}
}

int main()
{
	int input = 0;
	do
	{
		menu();
		printf("请选择>:");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出游戏成功\n");
			break;
		case 1:
			printf("开始扫雷游戏\n");
			int flag = sweepMine_game();
			if (flag == -1)
				input = 0;
			break;
		case 2:
			descriotion();//打印游戏说明
			break;
		default:
			printf("输入错误,请重新输入!\n");
			break;
		}
	} while (input);
	
	return 0;
}

7. 实现不同难度的扫雷游戏

  想要实现不同难度的扫雷游戏,棋盘的大小就必须是可变的,也就是说数组的大小能够根据选择游戏难度的变化而变化。但是数组大小在编译之初就已经确定,一旦确定就固定死了,后期无法更改。这就要引入动态内存管理的概念了,动态申请的空间是可以随意变化大小的,动态内存管理函数:malloc()calloc()realloc()free()
  实现思路:使用#define定义不同难度下棋盘的行和列,以及地雷的个数。更改结构成员的类型,将原先的char型数组改成指针,用于维护后期动态开辟的空间。初始化棋盘时,使用calloc()函数申请两块空间,记得结束扫雷游戏时free()释放掉,否则会导致内存泄漏。
  注意事项:由于指向棋盘指针类型的变化char arr[Row][Col] —> char* arr,再用二维数组的访问方式arr[x][y]就必然导致语法错误。而现在管理棋盘的指针类型为char*,指向棋盘的首元素的地址。想要访问下标为(x,y)的空间,就需要知道该空间与首元素的相对地址。如何计算这个相对位置,如下图所示。
在这里插入图片描述

  大家知道,二维数组在内存中其实是按行存放的,下标(n,m)的元素前共有(n * j + m)个元素(其中j是这个二维数组的列数),那么该下标与首元素的相对位置就是(n * j + m)


8. 最终实现代码

#define _CRT_SECURE_NO_WARNINGS 1

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

//简单难度
#define ROW_SIMPLE 9
#define COL_SIMPLE 9
#define ROWS_SIMPLE ROW_SIMPLE + 2
#define COLS_SIMPLE COL_SIMPLE + 2
#define CHECKER_NUM_SIMPLE (ROW_SIMPLE * COL_SIMPLE)
#define MINE_NUM_SIMPLE 10
//普通难度
#define ROW_NORMAL 16
#define COL_NORMAL 16
#define ROWS_NORMAL ROW_NORMAL + 2
#define COLS_NORMAL COL_NORMAL + 2
#define CHECKER_NUM_NORMAL (ROW_NORMAL * COL_NORMAL)
#define MINE_NUM_NORMAL 40
//困难难度
#define ROW_DIFFICULTY 16
#define COL_DIFFICULTY 30
#define ROWS_DIFFICULTY ROW_DIFFICULTY + 2
#define COLS_DIFFICULTY COL_DIFFICULTY + 2
#define CHECKER_NUM_DIFFICULTY (ROW_DIFFICULTY * COL_DIFFICULTY)
#define MINE_NUM_DIFFICULTY 99

enum Difficulty
{
	Simple = 1,
	Normal = 2,
	Difficulty = 3
};

typedef struct CheckerBroad
{
	char* backbroad;//幕后存放地雷数据的棋盘
	char* frontbroad;//展示给用户状态的棋盘
	int TurnOver_num;//已经排查棋子的数量
	int perr;//异常信号
}CheckerBroad;

//初始化棋盘
void init_checker_broad(CheckerBroad* p_checker_broad, int Rows, int Cols);
//销毁棋盘
void DestoryCheckerBroad(CheckerBroad* p_checker_broad);
//布置地雷
void set_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num);
//打印展示棋盘
void display_broad(const CheckerBroad* p_checker_broad, int Row, int Col, int num);
//开始扫雷
int start_sweep_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num);
//检查输入坐标是否正确
int check_input(const CheckerBroad* p_checker_broad, int x, int y, int Row, int Col);
//获取坐标周围3x3内地雷的个数
int get_mine_num(const CheckerBroad* p_checker_broad, int Row, int Col, int x, int y);
//炸金花展开周围没有地雷的棋子
void burst_golden_flower(CheckerBroad* p_checker_broad, int x, int y, int Row, int Col);
//显示棋盘上所有地雷
void show_total_mine(CheckerBroad* p_checker_broad, int Row, int Col);
//标注信息
void mark_message(CheckerBroad* p_checker_broad, int Row, int Col, int num);
//标记地雷
void mark_mine(CheckerBroad* p_checker_broad, int Row, int Col);
//取消地雷标记
void unmark_mine(CheckerBroad* p_checker_broad, int Row, int Col);

初始化棋盘
void init_checker_broad(CheckerBroad* p_checker_broad, int Rows, int Cols)
{
	if (p_checker_broad == NULL)
	{
		perror("init_checker_broad:");
		return;
	}
	//1.向内存申请一块长Rows宽Cols的空间,并使用backbroad指针维护这块空间
	p_checker_broad->backbroad = (char*)calloc(Rows * Cols, sizeof(char));
	if (p_checker_broad->backbroad == NULL)
	{
		perror("init_checker_broad:");
		return;
	}

	//2.向内存申请一块长Rows宽Cols的空间,并使用frontbroad指针维护这块空间
	p_checker_broad->frontbroad = (char*)calloc(Rows * Cols, sizeof(char));
	if (p_checker_broad->frontbroad == NULL)
	{
		perror("init_checker_broad:");
		return;
	}

	//3.初始化展示棋盘
	int i = 0;
	for (i = 0; i < Rows; i++)
	{
		int j = 0;
		for (j = 0; j < Cols; j++)
		{
			p_checker_broad->frontbroad[Cols * i + j] = '#';
		}
	}
	//3.初始化已经排查棋子的数量
	p_checker_broad->TurnOver_num = 0;
	//4.初始化异常信息
	p_checker_broad->perr = 0;
}

//销毁棋盘
void DestoryCheckerBroad(CheckerBroad* p_checker_broad)
{
	if (p_checker_broad == NULL)
	{
		perror("DestoryCheckerBroad:");
		return;
	}
	free(p_checker_broad->backbroad);
	p_checker_broad->backbroad = NULL;
	free(p_checker_broad->frontbroad);
	p_checker_broad->frontbroad = NULL;
	printf("销毁成功\n");
}



//布置地雷
void set_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("set_mine:");
		return;
	}
	int i = 0;
	//初始化rand随机函数的种子
	srand((unsigned int)time(NULL));
	//随机布置10个地雷
	for (i = 0; i < num;)
	{
		int x = rand() % Row + 1;
		int y = rand() % Col + 1;
		if (p_checker_broad->backbroad[Col*x+y] == 1)
		{
			continue;
		}
		p_checker_broad->backbroad[Col * x + y] = 1;
		i++;
	}
}

//打印展示后端棋盘
void display_backbroad(const CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("display_backbroad:");
		return;
	}
	//打印幕后棋盘
	int i = 0;
	printf("    ");
	for (i = 1; i <= Row; i++)
	{
		printf("%d ", i);
	}
	printf("\n\n");
	for (i = 1; i <= Row; i++)
	{
		int j = 0;
		printf(" %d  ", i);
		for(j = 1; j <= Col; j++)
		{
			printf("%d ", p_checker_broad->backbroad[Col*i+j]);
		}
		printf("\n");
	}
}



//打印展示前端棋盘
void display_broad(const CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("display_broad:");
		return;
	}
	Sleep(300);
	system("cls");//清屏
	printf("---------------  扫雷  ----------------\n\n");
	printf("地雷数量>:%d        剩余棋子数量>:%d\n\n", num, Row * Col - num - p_checker_broad->TurnOver_num);
	int i = 0;
	//打印列号
	printf("  ");
	for (i = 1; i <= Col; i++)
	{
		printf(" %-3d", i);
	}
	printf("\n");
	//打印棋盘
	for (i = 1; i <= Row; i++)
	{
		int j = 0;
		//分割区
		printf("  ");
		for (j = 1; j <= Col; j++)
		{
			printf("|---");
		}
		printf("|\n");
		//棋子
		for (j = 1; j <= Col; j++)
		{
			//打印行号
			if (j == 1)
			{
				printf("%2d", i);
			}
			if (p_checker_broad->frontbroad[Col * i + j] == '@')
			{
				printf("|\033[0;31;40m @ \033[0m");//打印地雷标志为红色的‘@’
			}
			else
			{
				printf("| %c ", p_checker_broad->frontbroad[Col*i+j]);
			}
		}
		printf("|\n");
	}
	//打印分割区
	printf("  ");
	for (i = 1; i <= Col; i++)
	{
		printf("|---");
	}
	printf("|\n");
	printf("\n---------------------------------------\n");

}

//开始扫雷游戏
int start_sweep_mine(CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("start_sweep_mine:");
		return -1;
	}
	int x = 0;
	int y = 0;

	//frontbroad[3][4] = '3';
	//display_broad(frontbroad);

	//判断扫雷游戏是否胜利
	while (p_checker_broad->TurnOver_num < Row * Col - num)
	{
		//display_backbroad(p_checker_broad);//打印后端界面
		//1.输入行和列
		do
		{
			printf("请输入想要翻开棋子的行和列>:");
			scanf("%d %d", &x, &y);
		} while (1 != check_input(p_checker_broad, x, y, Row, Col));//判断输入的行和列是否正确
		
		//2.翻开棋子
		if (p_checker_broad->backbroad[Col*x+y] == 1)//翻到地雷
		{
			show_total_mine(p_checker_broad, Row, Col);//显示棋盘上所有的地雷
			display_broad(p_checker_broad, Row, Col, num);
			return 0;
		}
		else//没有翻到地雷
		{
			//炸金花展开棋子
			burst_golden_flower(p_checker_broad, x, y, Row, Col);
			if (p_checker_broad->perr == -1)
				return -1;
		}
		//3.打印棋盘
		display_broad(p_checker_broad, Row, Col, num);
		//4.标记地雷(只起到注释的功能)
		mark_message(p_checker_broad, Row, Col, num);
	}
	show_total_mine(p_checker_broad, Row, Col);//显示棋盘上所有的地雷
	display_broad(p_checker_broad, Row, Col, num);
	return 1;//扫雷结束
}

//检查输入坐标是否正确,返回值若为:
//-1:表示frontbroad指针为NULL,0表示输入坐标错误
// 0:表示输入坐标错误
// 1:表示输入坐标正确
int check_input(const CheckerBroad* p_checker_broad, int x, int y, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("check_input:");
		return -1;
	}
	//坐标超范围
	if ((x < 1) || (x > Row) || (y < 1) || (y > Col))
	{
		printf("输入坐标超出棋盘范围,请重新输入想要翻开棋子的坐标>:");
		return 0;
	}
	//坐标位置棋子已经被翻开
	else if ((p_checker_broad->frontbroad[Col*x+y] != '#' && p_checker_broad->frontbroad[Col * x + y] != '!'))
	{
		printf("该坐标位置的棋子已经被翻,请重新输入坐标>:");
		return 0;
	}
	else//输入正确
	{
		return 1;
	}
}

//获取棋子周围3x3格子内地雷的个数
int get_mine_num(const CheckerBroad* p_checker_broad, int Row, int Col, int x, int y)
{
	if (p_checker_broad == NULL)
	{
		perror("get_mine_num:");
		return -1;
	}
	int sum = 0;
	int i = 0;
	for (i = x - 1; i <= x + 1; i++)
	{
		int j = 0;
		for (j = y - 1; j <= y + 1; j++)
		{
			sum += p_checker_broad->backbroad[Col * i + j];
		}
	}
	return sum;
}

//炸金花式翻开棋子
void burst_golden_flower(CheckerBroad* p_checker_broad, int x, int y, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("burst_golden_flower:");
		return;
	}
	//棋子必须在9x9棋盘内,出了这个范围必然导致越界访问
	if ((x >= 1 && x <= Row) && (y >= 1 && y <= Col))
	{
		//防止重复递归
		if ((p_checker_broad->frontbroad[Col * x + y] == '#') || (p_checker_broad->frontbroad[Col * x + y] == '!'))
		{
			//1.获得该坐标棋子周围地雷的个数
			int count = get_mine_num(p_checker_broad, Row, Col, x, y);
			//2.按情况分析
			if (count == 0)//周围没有地雷
			{
				//翻开棋子&&记录
				p_checker_broad->frontbroad[Col * x + y] = ' ';
				p_checker_broad->TurnOver_num++;
				//炸金花展开
				int i = 0;
				for (i = x - 1; i <= x + 1; i++)
				{
					int j = 0;
					for (j = y - 1; j <= y + 1; j++)
					{
						if ((i == x) && (j == y))
							continue;
						burst_golden_flower(p_checker_broad, i, j, Row, Col);
					}
				}
			}
			else if (count >= 1 && count <= 8)//周围有地雷
			{
				//翻开棋子,将周围的地雷数显示到棋盘上
				p_checker_broad->frontbroad[Col * x + y] = count + '0';
				//记录
				p_checker_broad->TurnOver_num++;

			}
			else
			{
				//获取周围地雷个数出现异常
				perror("get_mine_num:");
				p_checker_broad->perr = -1;
				return;
			}
		}
	}
}

//显示棋盘上所有地雷
void show_total_mine(CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("show_total_mine:");
		return;
	}
	int i = 0;
	for (i = 1; i <= Row; i++)
	{
		int j = 0;
		for (j = 1; j <= Col; j++)
		{
			if (p_checker_broad->backbroad[Col * i + j] == 1)
			{
				p_checker_broad->frontbroad[Col * i + j] = '@';
			}
		}
	}
}

void menu_mark()
{
	printf("***************************************\n");
	printf("*****            0.exit           *****\n");
	printf("*****            1.mark           *****\n");
	printf("*****           2.unmark          *****\n");
	printf("***************************************\n");
}

//标记地雷
void mark_mine(CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("mark_mine:");
		return;
	}
	int x = 0;
	int y = 0;
	//输入行和列
	do
	{
		printf("请输入需要标记为地雷的坐标>:");
		scanf("%d %d", &x, &y);
	} while (1 != check_input(p_checker_broad, x, y, Row, Col));//判断输入的行和列是否正确
	if (p_checker_broad->frontbroad[Col * x + y] == '#')
	{
		//标记
		p_checker_broad->frontbroad[Col * x + y] = '!';
		printf("标记成功\n");
	}
	else
	{
		//已经标记过了
		printf("该坐标已经标记过了\n");
	}
}

//取消地雷标记
void unmark_mine(CheckerBroad* p_checker_broad, int Row, int Col)
{
	if (p_checker_broad == NULL)
	{
		perror("mark_mine:");
		return;
	}
	int x = 0;
	int y = 0;
	//输入行和列
	do
	{
		printf("请输入需要标记为地雷的坐标>:");
		scanf("%d %d", &x, &y);
	} while (1 != check_input(p_checker_broad, x, y, Row, Col));//判断输入的行和列是否正确
	if (p_checker_broad->frontbroad[Col * x + y] == '!')
	{
		//取消标记
		p_checker_broad->frontbroad[Col * x + y] = '#';
		printf("已取消标记\n");
	}
	else
	{
		//没有标记过,无法取消标记
		printf("该坐标没有被标记,无法取消标记\n");
	}
}

//标注信息(只起到注释的作用)
void mark_message(CheckerBroad* p_checker_broad, int Row, int Col, int num)
{
	if (p_checker_broad == NULL)
	{
		perror("mark_mine:");
		return;
	}

	int input = 0;
	do
	{
		menu_mark();
		printf("是否需要标注地雷?>:");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出标注\n");
			break;
		case 1:
			mark_mine(p_checker_broad, Row, Col);
			//打印展示棋盘
			display_broad(p_checker_broad, Row, Col, num);
			break;
		case 2:
			unmark_mine(p_checker_broad, Row, Col);
			//打印展示棋盘
			display_broad(p_checker_broad, Row, Col, num);
			break;
		default:
			printf("选择错误,请重新重新输入选项>:\n");
			break;
		}
	} while (input);
}

#include"game.h"

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

void menu_difficulty()
{
	printf("***************************************\n");
	printf("*****           1.Simple          *****\n");
	printf("*****           2.Normal          *****\n");
	printf("*****          3.difficult        *****\n");
	printf("***************************************\n");
}


void descriotion()
{
	printf("扫雷游戏的规则如下:\n");
	printf("1.游戏的目标是在最短的时间内找出所有非雷格子,同时避免踩到雷,踩到雷即游戏结束。\n");
	printf("2.游戏由多个方格组成,点击方格可以显示其中的数字。数字表示该方格周围3×3区域中的地雷数,\n");
	printf("  一般为8个格子,对于边块为5个格子,对于角块为3个格子,因此扫雷中最大的数字为8。\n");
	printf("3.可以标记雷,即在小方格上插上小红旗。\n");
	printf("4.当场上只剩下地雷方块未被打开后,游戏胜利。在这之前如果你打开了地雷方块,则游戏失败。\n");
}


//选择游戏难度:1.简单 2.普通 3.困难
int ChooseDifficulty()
{
	int choose = 0;
	do
	{
		menu_difficulty();
		printf("请选择扫雷游戏难度>:");
		scanf("%d", &choose);
		switch (choose)
		{
		case Simple :
			printf("选择难度为:简单\n");
			return 1;
		case Normal:
			printf("选择难度为:普通\n");
			return 2;
		case Difficulty:
			printf("选择难度为:困难\n");
			return 3;
		default:
			printf("输入错误,请重新选择\n");
			break;
		}
	} while (choose);
}

void GameStep(CheckerBroad* p_checker_broad, int Row, int Rows, int Col, int Cols, int MineNum, int* pflag)
{
	if (p_checker_broad == NULL)
	{
		perror("GameStep:");
		return;
	}
	//1.初始化棋盘
	init_checker_broad(p_checker_broad, Rows, Cols);
	//2.设置地雷
	set_mine(p_checker_broad, Row, Col, MineNum);
	//3.打印棋盘
	//display_broad(backbroad, Row, Col);
	display_broad(p_checker_broad, Row, Col, MineNum);
	//4.开始扫雷
	int result = start_sweep_mine(p_checker_broad, Row, Col, MineNum);
	if (result == 1)
	{
		printf("恭喜玩家扫除所有埋藏的地雷,通关成功!\n");
		*pflag = 1;
		return;
	}
	else if (result == 0)
	{
		printf("很遗憾你已被地雷炸死,通关失败!\n");
		*pflag = 0;
		return;
	}
	else if (result == -1)
	{
		printf("扫雷游戏运行失败,请检查\n");
		*pflag = -1;
		return;
	}
	else
	{
		printf("扫雷游戏返回值未知,请检查\n");
		*pflag = -1;
		return;
	}
}

void sweepMine_game(int degree, int* pflag)
{
	//1.创建扫雷游戏棋盘
	CheckerBroad checker_broad;
	//2.根据游戏难度分类
	switch (degree)
	{
	case Simple :
		GameStep(&checker_broad, ROW_SIMPLE, ROWS_SIMPLE, COL_SIMPLE, COLS_SIMPLE, MINE_NUM_SIMPLE, pflag);
		break;
	case Normal:
		GameStep(&checker_broad, ROW_NORMAL, ROWS_NORMAL, COL_NORMAL, COLS_NORMAL, MINE_NUM_NORMAL, pflag);
		break;
	case Difficulty:
		GameStep(&checker_broad, ROW_DIFFICULTY, ROWS_DIFFICULTY, COL_DIFFICULTY, COLS_DIFFICULTY, MINE_NUM_DIFFICULTY, pflag);
		break;
	default:
		printf("游戏难度选择错误,请检查\n");
		return;
	}
	//3.销毁棋盘
	DestoryCheckerBroad(&checker_broad);
}

int main()
{
	int input = 0;
	int flag = 0;
	int DifficultyDegree = 0;
	do
	{
		menu();
		printf("请选择>:");
		scanf("%d", &input);
		switch (input)
		{
		case 0:
			printf("退出游戏成功\n");
			break;
		case 1:
			printf("开始扫雷游戏\n");
			DifficultyDegree = ChooseDifficulty();//选择游戏难度
			sweepMine_game(DifficultyDegree, &flag);
			if (flag == -1)
				input = 0;
			break;
		case 2:
			descriotion();//打印游戏说明
			break;
		default:
			printf("输入错误,请重新输入!\n");
			break;
		}
	} while (input);
	
	return 0;
}

在这里插入图片描述

这份博客👍如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位🔎点赞👍评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧👀。

  • 8
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值