【保姆及教程】详解扫雷(排查、标记、修改、递归展开)


完整代码在 扫雷完整版

1. 游戏展式

本程序设置的是9 * 9网格
先看一下最终程序运行出来的结果

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 整体思路

一个较大的程序至少需要分2个文件写,扫雷游戏我分3个文件

  • 头文件
  • 主函数文件
  • 函数实现文件
头文件中存放对宏常量的定义,函数的声明,全局变量的声明,头文件的调用
头文件中不能定义变量,变量放在主函数文件中定义
从游戏展式中我们可以知道该程序至少有打印棋盘、排查、标记、修改等功能
将这些功能封装到函数实现文件中
主函数用来搭建逻辑框架

3. 头文件

通过头文件先清楚需要用到哪些变量、函数

#pragma once
#include<stdio.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 MAX_MINE 1
extern int result;
extern int restMine;
extern struct ZuoBiao arr[ROW * COL - MAX_MINE];
//初始化函数
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ch);
//设置雷函数
void SetMine(char MineState[ROWS][COLS], int row, int col);
//打印棋盘函数
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//用户排雷函数
void UserClearMine(int*x, int*y);
//用户标记雷函数
void UserMarkMine(char MineState[ROWS][COLS], char MineNum[ROWS][COLS]);
//用户修改标记处函数
void UserModify(char MineState[ROWS][COLS], char MineNum[ROWS][COLS]);
//判断是否踩雷函数
void JudgeHitMine(char MineState[ROWS][COLS],char MineNum[ROWS][COLS] ,int x, int y);
//展开棋盘函数
void ExpansionMine(char MineState[ROWS][COLS], char MineNum[ROWS][COLS], int x, int y);
//是否被排查过函数
int IsClear(char MineNum[ROWS][COLS], int x, int y);

4. 整体框架

首先程序运行时会打印游戏菜单,如果我们想玩完一次后接着玩,我们就需要将打印菜单这个操作放入循环中
因为0代表退出游戏,所以我们自己来输入循环变量,当循环变量是0时,刚好结束循环退出游戏

4.1 main函数

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

int main()
{
	//随机数种子
	srand((unsigned int)time(NULL));
	int input;
	do
	{
		menu();
		printf("请选择->\n");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
		{
			printf("将所有雷标记出来即可胜利!\n");//这里提示一下胜利规则
			game();
			break;
		}
		case 0: printf("退出游戏\n");
			break;
		default:printf("选择错误,请重新选择->\n");
			break;
		}
	} while (input);
	return 0;
}

输入1之后我们就可以进入游戏了,重点来看如何实现game函数

3.2 game函数

在这里插入图片描述
在这里插入图片描述

3.2.1 主要步骤

1. 在进入游戏后,将棋盘初始化
2. 一轮游戏中需要将棋盘随机设置雷一次
3. 打印棋盘
4. 打印可以选择的功能
5. 用户输入功能
6. 实现该功能 (ps:4、5步和最开始打印游戏菜单相似,所以可以用switch语句)
7. 判断游戏是否结束 (没有结束重复 3 4 5 6 7)

3.2.2 细节

1. 需要定义一个二维字符数组来存储雷的状态(开发者看)
2. 需要定义一个二维字符数组来存储雷的数量(用户看)
3. 两个数组类型定义一样为了功能一样的函数定义一次,两个数组可以使用同样的函数
4. 两个数组定义时都需要扩容–>行和列要比棋盘的各多2个单位(后面会解释)
5. 将雷的个数、雷盘行、列、数组行、列定义成宏常量,方便统一修改
6. 定义全局变量result表示游戏的结果
7. 定义全局变量restMine表示剩下雷的个数

3.2.3 代码

int result = 0; //0表示没出结果 1表达玩家排出所有雷 -1表示玩家排雷失败,被炸死
int restMine = MAX_MINE; //最开始剩下雷的个数就是设置了多少雷

void Command()
{
	printf("选择需要的功能->\n");
	printf("******************************\n");
	printf("*******1.  排查*******\n");
	printf("*******2.  标记*******\n");
	printf("*******3.  修改*******\n");
	printf("******************************\n");
}
void game()
{

	//记录输入的坐标
	int x, y;
	int ret = 0;
	//雷个数数组(展现给用户看)
	char MineNum[ROWS][COLS];
	//雷状态数组(开发者自己看的)
	char MineState[ROWS][COLS];
	//初始化雷个数数组 数组所有元素都需要初始化
	InitBoard(MineNum, ROWS, COLS, '*'); 
	//初始化雷状态数组
	InitBoard(MineState, ROWS, COLS, '0');
	//设置雷状态数组中雷的位置

	SetMine(MineState, ROW, COL);
	//打印雷个数数组
	DisplayBoard(MineNum, ROW, COL);
	//打印雷状态数组
	DisplayBoard(MineState, ROW, COL);//这条代码是给开发者知道哪个地方有雷,发给别人时记得注释掉
	while (1)
	{
		printf("##当前雷的个数-->%d##\n", restMine);
		//用户选择功能
		Command();
		int command = 0;
		scanf("%d", &command);
		Sleep(1000);
		switch (command)
	{
			
		case 1:
		{
			//用户排雷
			UserClearMine(&x, &y);
			//判断是否踩雷
			JudgeHitMine(MineState, MineNum, x, y);
			break;
		case 2:
		{
			//用户标记雷
			UserMarkMine(MineState, MineNum);
			break;
		}
		
		case 3:
		{
			//用户修改标记
			UserModify(MineState, MineNum);
			break;
		}
		default:
			printf("选择错误\n");
			break;
		}
	}
		//踩雷,跳出循环,结束游戏
		if (-1 == result)
		{
			Sleep(1000);
			printf("很遗憾,你被炸死了\n");
			putchar('\a');//发出蜂鸣声
			Sleep(1000);
			DisplayBoard(MineState, ROW, COL);
			break;

		}

		//成功将所有雷排完 跳出循环,结束游戏
		else if (1 == result)
		{
			break;
		}

		//没出结果
		else if (0 == result)
		{
			Sleep(1000);
			DisplayBoard(MineNum, ROW, COL);
		}

	}
}

5. 函数实现

当函数参数是二维数组时,数组的列不可以省略、为了方便理解,本内容数组的行也没有省略

5.1 初始化棋盘

我们需要将雷状态、雷数量函数初始化

雷状态数组用'0'表示无雷、'1'表示有雷
雷数量函数最开始用'*'来填充,排雷后再将'*'替换成该位置周围雷的个数对应的字符

1. 函数需要初始化数组的所有元素、数组是11行11列,
2. 函数需要接受数组、数组的行、列、初始化的字符

void InitBoard(char board[ROWS][COLS], 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;
		}
	}
}

5.2 设置雷

将雷状态数组初始化后,需要随机选择几个位置进行布雷
关于随机数函数,可以参考这篇文章随机数函数

布雷的位置应该是屏幕上打印出来的位置,打印出来的是9行9列,但是数组是11行11列,数组的边界时[0, 10],所以布雷的坐标对数组的行和列有要求
1.雷的横坐标应该是[1, 9]
2.雷的纵坐标应该是[1, 9]

void SetMine(char MineState[ROWS][COLS], int row, int col)
{
	int x, y;
	int cnt = 0;
	while (cnt < MAX_MINE)
	{
		x = rand() % ROW + 1; //x的范围是[1, 9]
		y = rand() % COL + 1; //y的范围是[1, 9]
		//该位置没有设置雷
		if (MineState[x][y] == '0')
		{
			MineState[x][y] = '1';
			cnt++;
		}
	}
}

5.3 打印棋盘

在这里插入图片描述
打印的棋盘不止是*,需要周围加上数字,方便用户看出坐标的具体位置
因为棋盘是9X9的,但是数组是11X11的,所以打印棋盘只需要打印数组里面的一层

void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
	int i, j;
	for (i = 0; i <= 9; i++)
	{
		printf("%d ", i);
	}
	printf("\n");
	for (i = 1; i <= row; i++) //从数组的第2行开始打印 倒数第2行结束
	{
		printf("%d ", i);
		for (j = 1; j <= col; j++) //从数组的第2列开始打印 倒数第2列结束
		{
			printf("%c ", board[i][j]);
		}
		puts("");
	}
}

5.4 用户排雷

game函数中定义变量x,y用来存储排雷的坐标

//形参一定要是指针,因为要通过解引用形参改变实参的值
void UserClearMine(int* x, int* y) 
{
	while (1)
	{
		printf("请输入需要排查位置的坐标->");
		scanf("%d%d", x, y);
		Sleep(1000);
		if (*x < 1 || *x > 9 || *y < 1 || *y > 9)
		{
			printf("输入的坐标不合法,请重新输入\n");
			continue;
		}
		else
		{
			break;
		}
	}
}

5.5 判断是否踩雷

每排一次雷都要判断是否踩雷,如果踩雷,游戏结束,否则游戏继续
函数需要接受雷状态数组和排雷的坐标便于判断是否踩雷,如果没踩雷,则需要将该坐标周围雷的个数放进雷数量数组对应的位置

void JudgeHitMine(char MineState[ROWS][COLS], char MineNum[ROWS][COLS], int x, int y)
{
	//踩雷游戏结束
	if ('1' == MineState[x][y])
	{
		result = -1; 	
		return;
	}
	//没有踩雷
	else
	{
		result = 0;
		//先不用管这个函数
		ExpansionMine(MineState, MineNum, x, y);
		if (MineNum[x][y] != ' ')
		{
			//雷数量数组是字符型的,所以需要加上'0'
			MineNum[x][y] = SumAroundMine(MineState, x, y) + '0';
		}
		return;
	}
}

5.5.1 统计周围雷个数

在判断是否踩雷函数中,如果没踩雷,我们需要将该坐标周围雷的个数转换成字符存放在雷数量数组的对应位置
怎么统计雷的位置呢?统计该坐标周围8个点是否有雷
那如果坐标处于9X9棋盘的边上怎么统计?如果数组是9X9的 这个时候如果访问坐标周围8个点可能会数组越界
细节4就很好的规避了这一点,将数组定义成11X11的,此时即使坐标处于9X9棋盘边上,因为数组是11行11列,所以访问边界上的8个点不会产生越界

int SumAroundMine(char board[ROWS][COLS], int x, int y)
{
	int sum = 0;
	// x+1,y+1,x-1,y-1不会超过数组边界
	sum = board[x + 1][y + 1] + board[x + 1][y] + board[x + 1][y - 1]
		+ board[x][y + 1] + board[x][y - 1]
		+ board[x - 1][y] + board[x - 1][y - 1] + board[x - 1][y + 1]
		- 8 * '0'; //返回的是整形,最后要减去'0'
	return sum;
}

5.5.2展开一片

在JudgeHitMine函数中,如果没有ExpansionMine函数,运行结果是这样
在这里插入图片描述
在JudgeHitMine函数中,如果有ExpansionMine函数,运行结果是这样
在这里插入图片描述

如果没有ExpansionMine函数
排查位置1 1的周围没有雷,排查后用户自己知道1 1大的周围不存在雷,但是1 1周围仍然是*,这样用户可能之后忘记了这个根本不会存在雷的地方,排查了1 2,这样会浪费用户寻找雷的时间,如果排查点周围不存在雷,就从当前排查点展开,展开点变成空格,直到展开点周围存在雷,因此,某个点能作为展开点需要满足以下条件

该点不是雷
该点周围没有雷
该点没有被排查过(假设a点是b点的周围点,则b点也是a点的周围点,从a点展开时会展开b点,从b点展开时又会展开a点,这样下去永远不会结束)
void ExpansionMine(char MineState[ROWS][COLS], char MineNum[ROWS][COLS], int x, int y)
{
	/*
	该坐标不是雷
	该坐标周围没有雷
	该坐标没有被排查(防止死递归)
	*/
	int flag = 0;
	//该坐标不是雷
	if ('1' != MineState[x][y])
		flag++;
	//该坐标周围没有雷
	if (0 == SumAroundMine(MineState, x, y))
		flag++;
	//该坐标没有被排查过(防止死递归)
	if (!IsClear(MineNum, x, y))
		flag++;
	//满足三个条件,则展开该位置周围所有点
	if (3 == flag)
	{
		//展开处用' '
		MineNum[x][y] = ' ';
		ExpansionMine(MineState, MineNum, x + 1, y + 1);
		ExpansionMine(MineState, MineNum, x + 1, y);
		ExpansionMine(MineState, MineNum, x + 1, y - 1);
		ExpansionMine(MineState, MineNum, x, y + 1);
		ExpansionMine(MineState, MineNum, x, y - 1);
		ExpansionMine(MineState, MineNum, x - 1, y + 1);
		ExpansionMine(MineState, MineNum, x - 1, y + 1);
		ExpansionMine(MineState, MineNum, x - 1, y + 1);
	}
}

5.5.2.1 该坐标是否被排查过

展开函数需要3个条件,我们单独用一个函数来判断是否满足第三个条件

int IsClear(char MineNum[ROWS][COLS], int x, int  y)
{
	//该坐标在MineNum中不是*说明被排查过
	if (MineNum[x][y] != '*')
	{
		return 1;
	}
	else
	{
		return 0;
	}
}

5.6 用户标记

游戏最终的胜利条件是用户将所有雷的位置标记出来,所以用户还可以自己标记已经知道雷所在的位置
我们需要通过标记来改变全局变量restMine的值,当每正确标记出雷的位置,restMine的值就减一,当restMine的值为0时,另全局变量result = 1表示用户取得成功

void UserMarkMine(char MineState[ROWS][COLS], char MineNum[ROWS][COLS])
{
	printf("请输入需要标记的坐标->");
	int x, y;
	if ((scanf("%d%d", &x, &y) == 2) && x != 0 && y != 0)
	{
		//标记处用'!'表示
		MineNum[x][y] = '!';
		//成功标记有雷的地方
		if (MineState[x][y] == '1')
		{
			restMine--;
		}
		Sleep(1000);

		//将雷成功全部标记出来
		if (0 == restMine)	
		{
			printf("恭喜你!找到所有雷的位置了\n");
			result = 1;
		}
	}
}

5.7 用户修改

用户有时候标记的不一定是正确的,如果用户想要修改标记处,则需要调用修改功能
1. 修改的前提是想要修改的位置已经被标记过
2. 修改之后变成什么字符取决于标记前是什么字符,修改后的字符和标记前的字符一样

void UserModify(char MineState[ROWS][COLS], char MineNum[ROWS][COLS])
{
	printf("请输入需要修改标记的坐标->");
	int x, y;
	scanf("%d%d", &x, &y);
	//该位置被标记
	if ('!' == MineNum[x][y])
	{
		//取消标记位置处周围没有雷 说明该位置一定被展开过,为' ',所以取消标记时将'!'还原成' '
		if (0 == SumAroundMine(MineState, x, y) && '1' != MineState[x][y])
		{
			MineNum[x][y] = ' ';
		}
		//该位置没有被展开,将'!'还原成'*'
		else
		{
			MineNum[x][y] = '*';
		}
		//取消标记有雷的地方
		if ('1' == MineState[x][y])
		{
			restMine++;
		}
	}
	else
	{
		Sleep(1000);
		printf("该坐标未被标记\n");
	}
}

完整代码在扫雷完整版

最后

看到最后,如果您觉得对您有帮助,请不要吝啬手中的赞,这对我来说很重要,也是我创作的动力,如果您觉得哪里说的不清楚或者有问题,欢迎评论区留言

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值