To be a Minesweeper

To be a Minesweeper

前言: 本文详细实现了扫雷的各个功能(自动翻开区域,计时,标记), 文末有我创作的Minesweepr专属emoji彩蛋~

Never sweep, how to be a minesweeper? (一屋不扫,何以扫天下)

微软开发的扫雷

开发目的: 训练用户用鼠标快速精确地点击

纵观微软的每一款系统自带游戏都有其深层的含义 Minesweeper 教会习惯输入命令行的用户 过渡到如何利用鼠标左右键点击
Solitaire(纸牌)则让用户自学了如何使用鼠标拖拽,我们至今仍在同鼠标拖拽,还是要归功于他
FreeCell(空当接龙)更是一个精妙的 测试设计 FreeCell被放在一段关键的程序中 只需测试其能否正常运行即可判断数据层是否安装得当

玩法: 通过翻开没有雷的区域,根据周围雷的数量排除地雷, 以达成胜利
扫雷包含的基本功能 点击方块会翻开    ( 1 ) 可以计时 , 标记雷    ( 2 ) 每操作一次会重新打印    ( 3 ) 判断输赢    ( 4 ) \begin{array}{|c} \hline 扫雷包含的基本功能\\ \hline \end{array} \begin{array}{|c|c|} \hline 点击方块会翻开& \ \ (1)\\ \hline 可以计时,标记雷& \ \ (2)\\ \hline 每操作一次会重新打印&\ \ (3)\\ \hline 判断输赢 & \ \ (4)\\ \hline \end{array} 扫雷包含的基本功能点击方块会翻开可以计时,标记雷每操作一次会重新打印判断输赢  (1)  (2)  (3)  (4)

如何用c语言实现扫雷?

请添加图片描述

基础版

通过对扫雷的观察我们可以发现用二维数组实现起来相对容易我们创建game.c文件来实现游戏功能, game.h文件声明.c文件里的函数, 而整个运行过程我们放入test.c
P r e p a r e    w o r k Prepare \ \ work Prepare  work
准备好设计游戏的尺寸大小(难易程度),一次扫雷的流程

<game.h>
#define ROW 9     //数组可操作范围
#define COL 9

#define ROWS 11 //见Build Function处的 思考
#define COLS 11

#define easy_set 10 // 游戏难度(雷数)

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

<test.c>
    #include"game.h"
void menu()
{
	printf("********************\n");
	printf("*****  1.play  *****\n");
	printf("*****  0.exit  *****\n");
	printf("********************\n");
}

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("输入有误,请重新输入>");
		printf("\n");
		break;
		}

		}
	} while (input);
	return 0;
}	

通过利用结构体do while我们很容易就实现了一个游戏的流程
B u i l d    F u n c t i o n Build\ \ Function Build  Function

思考

在写函数前我们需要思考一下: 因为扫雷要统计点击处周围的8个格子的雷数 所以如果二维数组是9 x 9就会产生越界访问(x=-1,10的两列)(y=-1,10的两行) 所以我们数组不妨创建成11 x 11 所以需要在 <game.h>加入"#define ROWS 11" , “#define COLS 11” , 而且在游玩的时候玩家习惯输入的坐标是1 ~ n , 而不是 0 ~ n-1 所以我们 在棋盘初始化的时候需要初始化 0 ~ 10 (ROW&COL) 这样我们就能打印出1 ~ 9

还有一个点就是玩家排雷的棋盘和放置雷的棋盘 两者应该被设计为相互独立的, 因为玩家游戏的界面肯定要与放置雷的界面有所差别
例如你未翻开的方块为 ‘?’ 而在放置雷的棋盘中有可能是 ‘*’ 或者 ’ ’ 如果这些操作都建立在一个二维数组中, 是不容易实现的,所以我们需要用两个数组分别放置玩家看到的棋盘放置雷的棋盘
函数名  ⁣ ⁣ 功能 I n i t B o a r d ( ) 初始化二维数组 D i s p l a y B o a r d ( ) 打印棋盘 S e t M i n e ( ) 放置雷 S c a n M i n e ( ) 扫雷 I s W i n ( ) 判断输赢 < t e s t . c > 调用的函数 \begin{array}{|c|c|} \hline 函数名&\!\!功能\\ \hline InitBoard()&初始化二维数组 \\ \hline DisplayBoard()&打印棋盘\\ \hline SetMine() & 放置雷\\ \hline ScanMine()&扫雷\\ \hline IsWin()&判断输赢\\ \hline \end{array} <test.c>调用的函数 函数名InitBoard()DisplayBoard()SetMine()ScanMine()IsWin()功能初始化二维数组打印棋盘放置雷扫雷判断输赢<test.c>调用的函数


<game.h>新写入的
void InitBoard(char Board[ROWS][COLS], int row, int col, char set); //初始化游戏区域

void DisplayBoard(char Board[ROWS][COLS], int row, int col);//打印界面

void SetMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//放置雷

void ScanMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//玩家扫雷

int IsWin(char show[ROWS][COLS], int row, int col);

<test.c> game()中新写入的
void game()
{
	char show_board[ROWS][COLS] = {0};//展示的棋盘 | '?'未扫区域 | ' '已扫安全 | '1~8'为的雷的个数| '*' 雷
	char mine_board[ROWS][COLS] = {0};//显示雷的位置 0/1 展示雷
    
	InitBoard(show_board, ROWS, COLS,'?'); //按照上方格式初始化对应数组
	InitBoard(mine_board, ROWS, COLS,'0');

	DisplayBoard(show_board, ROW, COL); //打印游戏区域
	SetMine(mine_board,show_board, ROW, COL);
	//DisplayBoard(mine_board, ROW, COL); //方便查看雷布置的情况

    
	ScanMine(mine_board, show_board,shaping_board, ROW, COL);
    IsWin(show_board,ROW,COL);
}

<game.c>
#include"game.h"	
void SetMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int count = easy_set;
	while (count)
	{							   // ※※※
		int x = rand() % row + 1;  // 不加 1 的后果 (随机数 % x) 的范围是 (0,x-1]
		int y = rand() % col + 1;  // 而棋盘的范围刚好是 1 ~ 9 所以 rand() % x+1
		if (mine[x][y] == '0')     // 一个式子就包括了产生和限制范围内的数字
		{
			mine[x][y] = '1';
			count--;
		}
	}

}

void InitBoard(char Board[ROWS][COLS], int row, int col, char set)
{
	int j = 0;
	int i = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			Board[i][j] = set;
		}
	}
}

void DisplayBoard(char Board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	printf("        "); //8个空格7 + 1 也可以用库函数计算输出)
	for (j = 1; j < col + 1; j++)
		printf("|%d| ", j);				      //列标
	printf("\n---\n");                        //利用换行对齐
	for (i = 1; i < row + 1; i++)
	{
		printf("★%d|    ", i);                //行标    //空格设计
		for (j = 1; j < col + 1; j++)
		{
			printf(" %c ", Board[i][j]);
			if (j < col)
				printf("|");
		}
		printf("\n");
		printf("---     ");                    //空格设计
		for (j = 1; j < col + 1; j++)
		{z
			if (i == col)
				break;
			printf("---");
			if (j < col)
				printf("|");
		}
		printf("\n");
	}
	printf("\n");
}

int IsWin(char show[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 1; i <= row; i++)
	{
		for (j = 1; j <= col; j++)
		{
			if (show[i][j] == '?')
			{
				count++;
			}
		}
	}
	return count == easy_set;
}

void ScanMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int i = 0;
	while (IsWin(show, row, col) == 0)
	{
		printf("输入需要翻开的坐标>");
		scanf("%d %d", &x, &y);
		if (show[x][y] == ' ' && x <= row && y <= col) // 需要修改 != '?'啊(已修改) !!!!
		{
			printf("请勿重复输入坐标\n");
			continue;
		}
		if (mine[x][y] == '*')
		{
			printf("你被炸死了 game over\n");
			DisplayBoard(mine, ROW, COL);
			break;
		}
		else
		{
			char flag = count(mine, x, y) + '0' ;   //不这样写为什么会有bug  数字 + '0' = 对应字符
			show[x][y] = flag;
			DisplayBoard(show, ROW, COL);
		}
		if (x >= row || y >= col || x <= 0 || y <= 0)
		{
			printf("坐标输入非法,请重新输入!\n");
			printf("\n");
		}

	}
	if (IsWin(show, row, col))
	{
		printf("恭喜你,通关成功!\n");
	}
}

好了,至此扫雷的雏形就基本建立完成了.

但我们不难发现一个明显的 bug:

请添加图片描述

①. 当我们输入(1,1) 只能弹出一个坐标.扫雷变成了完全的随机遇敌游戏 (因为当你翻开没雷的区域的时候,根本没有提示周围的情况)
所以我们需要统计翻开区域一圈的雷数 (显示以翻开坐标为中心外的8个方块雷数总和)

<game.c>中追加的函数:
int count(char board[ROWS][COLS],int x1, int y1)   
{
	int x = 0;
	int y = 0;
	int count1 = 0;
	for (x = -1; x <= 1; x++)
	{
		for (y = -1; y <= 1; y++)
		{
			if (board[x1 + x][y1 + y] == '1')
				count1++;
		}
	}
	if (count1)
	{
		return count1;          //bug: 这里看似把数字返回到了棋盘 但棋盘中每一个坐标定义的是字符
	}						    // 所以会出现乱码 如果外加一个  char flag = count1 + '0';
	else			            //然后 return flag 也是错误的 因为函数返回值已经被定义为 int
		return ' ';
}
//这样写完后需要在ScanMine()内部 进行数字与字符的转换(返回值 + '0')


//你也可以这样写 
char count(char board[ROWS][COLS],int x1, int y1)   
{
	int x = 0;
	int y = 0;
	int count1 = 0;
	for (x = -1; x <= 1; x++)
	{
		for (y = -1; y <= 1; y++)
		{
			if (board[x1 + x][y1 + y] == '1')
				count1++;
		}
	}
	char flag = count1 + '0';
	if (count1)
	{
		return flag;
	}
	else
		return ' ';
}
至此基础版的扫雷大功告成

进阶版

基础版的扫雷只能说基本完成了一个扫雷的雏形, 想要实现功能接近真实扫雷

我们就需要通过大量的扫雷来总结前人的设计.

通过对 Microsoft 旗下的扫雷测试我们不难得到以下基础版扫雷未实现的功能

进阶版扫雷具备的功能 点击没有雷的区域 , 会自动翻开没有雷的区域 第一次点击不是雷 , 且第一次点击在一块空白区域内 标记雷 \begin{array}{|c} \hline 进阶版扫雷具备的功能\\ \hline \end{array} \begin{array}{|c|} \hline 点击没有雷的区域,会自动翻开没有雷的区域\\ \hline 第一次点击不是雷, 且第一次点击在一块空白区域内\\ \hline 标记雷\\ \hline \end{array} 进阶版扫雷具备的功能点击没有雷的区域,会自动翻开没有雷的区域第一次点击不是雷,且第一次点击在一块空白区域内标记雷

思考

与写基础版扫雷函数时一样, 在写进阶版函数时也需要提前思考
①. 进阶版是建立在基础版之上的, 所以我们只需要往 <game.c> <test.c>中添加新函数即可

②. 第一次点击怎么才能避免不是雷呢? 为什么要写这个功能呢?
答案是: 第一次总是最美好的, 如果第一次输入就死了,肯定会让玩家扫兴(微软最新的扫雷版本还有这个bug
(虽然概率为1/8)

实现方法 : 分离出第一次输入, 在放置雷之前的空棋盘内进行第一次输入,这样 100% 不会选到雷
​ 既然分离出了第一次输入(并记录下坐标), 那意味着第一次的游戏结果也必须单独打印

③. 现在虽然我们实现了第一次点击不是雷, 但没有实现第一次点击一定在块无雷区域内
Q: 既然实现了第一次点击不是雷, 为什么还要再去写?

请添加图片描述

如你所见, 如果不把第一次输入的区域放置在无雷区内, 游戏就根本无法进行(无法判断) (就变成完全随机遇敌)

期望实现的:

请添加图片描述

实现方法 : 设置雷的时候 接收 第一次输入的坐标(防止撞车) 限制在该坐标周围一圈放置雷
函数名  ⁣ ⁣ 功能 难度 F r i s t S c a n ( ) 第一次输入 e a s y F r i s t D i s p l a y ( ) 打印第一次输入 e a s y S e t M i n e ( ) 放置雷并开辟一片无雷区 e a s y A u d i o F l i p ( ) 自动打印没雷的区域 m i d \begin{array}{|c|c|c|} \hline 函数名&\!\!功能&难度\\ \hline FristScan()&第一次输入&easy\\ \hline FristDisplay()&打印第一次输入&easy\\ \hline SetMine() & 放置雷并开辟一片无雷区&easy\\ \hline AudioFlip()& 自动打印没雷的区域&mid\\ \hline \end{array} 函数名FristScan()FristDisplay()SetMine()AudioFlip()功能第一次输入打印第一次输入放置雷并开辟一片无雷区自动打印没雷的区域难度easyeasyeasymid

<game.h>
void InitBoard(char Board[ROWS][COLS], int row, int col, char set); //初始化游戏区域

void DisplayBoard(char Board[ROWS][COLS], int row, int col);//打印界面

void SetMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int arr[]);//放置雷

void ScanMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//玩家扫雷

int IsWin(char show[ROWS][COLS], int row, int col);

void FristScan(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int arr[]);

void FristDisplay(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);


<test.c>
void game()
{
	int save[10] = { 0 };
	char show_board[ROWS][COLS] = { 0 };//展示的棋盘 | '?'未扫区域 | ' '已扫安全 | '1~8'附件的雷的个数| '1' 雷
	char mine_board[ROWS][COLS] = { 0 };//显示雷的位置 0/1 展示雷

	InitBoard(show_board, ROWS, COLS, '?'); //按照上方格式初始化对应数组
	InitBoard(mine_board, ROWS, COLS, '0');

	DisplayBoard(show_board, ROW, COL); //打印游戏区域
	FristScan(show_board,mine_board, ROW, COL, save);
	SetMine(mine_board, show_board, ROW, COL,save);
	FristDisplay(mine_board, show_board, ROW, COL, save);
	DisplayBoard(mine_board, ROW, COL);

	ScanMine(mine_board, show_board, ROW, COL);
	IsWin(show_board, ROW, COL);
}

<game.c>
void FristScan(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col, int arr[])
{
	int x, y = 0;
	while (1)
	{
		printf("请输入需要翻开的坐标>");
		scanf("%d %d", &x, &y);
		printf("\n");
		if (x > 0 && x <= col && y > 0 && y <= row)
		{
			arr[0] = x;                // 记录第一个坐标值避免放置雷的时候撞车
			arr[1] = y;
			break;
		}
		else
			printf("坐标非法,请重新输入!\n"); //只使用一次输入,并不需要检查重复
	}
}

void SetMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int arr[])
{
	int count = easy_set;
	int x = 0;
	int y = 0;


	while (count)
	{
		x = rand() % row + 1;
		y = rand() % col + 1;

		if (x == arr[0] && y == arr[1] 
            || arr[0] - 1 <= x && x <= arr[0] + 1 && arr[1] - 1 <= y && y <= arr[1] + 1)
		{                              //一个简单的限制语句就控制了一个九宫格内没有雷出现
			continue;
		}
		if (mine[x][y] == '0')	
		{
			mine[x][y] = '*';
			count--;
		}


	}
	DisplayBoard(mine, ROW, COL);
}

void FristDisplay(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int arr[])
{
	int x = arr[0];
	int y = arr[1];

	//AudioFlip

}

至此, 我们已经实现了前三个函数, 现在唯一的难点就是如何实现AudioFlip

实现这种具有重复行为的函数可选择的方法无非 递归/循环


请添加图片描述

递归实现:

这是一个扫雷成功的棋盘(空白区域是点击会翻开的部分), 我们可以看出,空白区域被 数字和雷分割成了三个区域
如果玩家输入的坐标在空白区域内, 那么这个坐标相对空白区域一定是随机的, (下文出现的**"空腔"就是一块独立的空白区域**)
那么我们写AudioFlip的时候,必须要写八个方向上的探索, 否则递归是不彻底的(虽然玩家不知道 🐶)

递归思路:

首先这个递归要能实现遇见空白坐标无限递归下去, 我们不如先递归输入坐标的这一列,如果遇到障碍就从末端向行递归,
这句话的通俗理解是: 从起始坐标开始,通过在空腔内不断的画圆圈(大 → 小) 来填满这个空白区域(缠胶带)

当你发现写出了这样的递归, 在画图中的油漆桶也用到了这样的方式寻找同一块相同的颜色
这个算法名为泛填洪算法

四个方向上的递归

请添加图片描述

八个方向上的递归

请添加图片描述

代码实现:
// ROW 与 COL 代表了最大宽度与高度
void AudioFlip(char mine[ROWS][COLS],char show[ROWS][COLS], int x, int y)
{

	char num = count(mine, x, y);


	if (mine[x][y] == '0' && num == ' ' && show[x][y] == '?')
	{
		show[x][y] = ' ';
	}
	else if (mine[x][y] == '0' && num != ' '&&show[x][y] == '?' )
	{
		show[x][y] = count(mine,x,y) ; 
	}
    if (x > 1 && mine[x - 1][y] == '0' && show[x - 1][y] == '?') AudioFlip(mine, show, x - 1, y);
    if (y > 1 && mine[x][y - 1] == '0' && show[x][y - 1] == '?') AudioFlip(mine, show, x, y - 1);
    if (x < ROW && mine[x + 1][y] == '0' && show[x + 1][y] == '?') AudioFlip(mine, show, x + 1, y);
    if (y < COL && mine[x][y + 1] == '0' && show[x][y + 1] == '?') AudioFlip(mine, show, x, y + 1);

写到这里,试着运行一下居然发现AudioFlip居然将不是雷的方块全部翻开了,游戏秒过关了… 这说明了我们的递归缺少限制条件.

AudioFlip bug

(如果在扫雷的源码中加入这个递归是不是就成作弊插件了呢)

	if (show[x][y] == ' ' ) 
	{
		if (x > 1 && mine[x - 1][y] == '0' && show[x - 1][y] == '?') AudioFlip(mine, show, x - 1, y);
		if (y > 1 && mine[x][y - 1] == '0' && show[x][y - 1] == '?') AudioFlip(mine, show, x, y - 1);
		if (x < ROW && mine[x + 1][y] == '0' && show[x + 1][y] == '?') AudioFlip(mine, show, x + 1, y);
		if (y < COL && mine[x][y + 1] == '0' && show[x][y + 1] == '?') AudioFlip(mine, show, x, y + 1);
	}

只需要将这四个if在翻开没雷的时候递归就可以实现翻开一块独立区域的功能了, 但测试发现这样只能打开99%的正确方块,还会剩下拐角的方块没有被翻开 如图:

请添加图片描述

所以不妨单独写个函数(比较简单的操作)
void Corner(char mine[ROWS][COLS], char show[ROWS][COLS])
{
	int x = 1;
	int y = 1;
	for (x = 1; x <= ROW; x++)
	{
		for (y = 1; y <= COL; y++)
		{
			
			if (show[x][y] == ' ' && mine[x+1][y-1] != '*' && show[x + 1][y - 1] == '?' &&x+1<=9 &&y-1>=1)
			{
				if (count(mine, x + 1, y - 1) != ' ')
					show[x + 1][y - 1] = count(mine, x + 1, y - 1);
				else
					AudioFlip(mine, show, x + 1, y - 1);
			}
			if (show[x][y] == ' ' && mine[x+1][y+1] != '*' && show[x + 1][y + 1] == '?'&&x+1<=9&&y+1<=9)
			{
				if (count(mine, x + 1, y + 1) != ' ')
					show[x + 1][y + 1] = count(mine, x + 1, y + 1);
				else
					AudioFlip(mine, show, x + 1, y + 1);
			}
			if (show[x][y] == ' ' && mine[x-1][y-1] != '*' && show[x - 1][y - 1] == '?'&&x-1>=1&&y-1>=1)
			{
				if (count(mine, x - 1, y - 1) != ' ')
					show[x - 1][y - 1] = count(mine, x - 1, y - 1);
				else
					AudioFlip(mine, show, x - 1, y- 1);
			}
			if (show[x][y] == ' ' && mine[x-1][y+1] != '*' && show[x - 1][y + 1] == '?'&&x-1>=1&&y+1<=9)
			{
				if(count(mine, x - 1, y + 1) != ' ')
				show[x - 1][y + 1] = count(mine, x - 1, y + 1);
				else
					AudioFlip(mine, show, x - 1, y +1);

			}

		}
	}
}

完整的FristDisplay函数

void FristDisplay(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int arr[])
{
	int x = arr[0];
	int y = arr[1];

	AudioFlip(mine,show, x, y);
	DisplayBoard(show, ROW, COL);
	Corner(mine, show);
	DisplayBoard(show, ROW, COL);

}

我们再将基础版中没动过的函数追加入game.c

void InitBoard(char Board[ROWS][COLS], int row, int col, char set)
{
	int j = 0;
	int i = 0;
	for (i = 0; i < row; i++)
	{
		for (j = 0; j < col; j++)
		{
			Board[i][j] = set;
		}
	}
}

void DisplayBoard(char Board[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	printf("        "); //8个空格7 + 1 也可以用库函数计算输出)
	for (j = 1; j < col + 1; j++)
		printf("|%d| ", j);				      //列标
	printf("\n---\n");                        //利用换行对齐
	for (i = 1; i < row + 1; i++)
	{
		printf("★%d|    ", i);                //行标    //空格设计
		for (j = 1; j < col + 1; j++)
		{
			printf(" %c ", Board[i][j]);
			if (j < col)
				printf("|");
		}
		printf("\n");
		printf("---     ");                    //空格设计
		for (j = 1; j < col + 1; j++)
		{
			if (i == col)
				break;
			printf("---");
			if (j < col)
				printf("|");
		}
		printf("\n");
	}
	printf("\n");
}

int IsWin(char show[ROWS][COLS], int row, int col)
{
	int i = 0;
	int j = 0;
	int count = 0;
	for (i = 1; i <= row; i++)
	{
		for (j = 1; j <= col; j++)
		{
			if (show[i][j] == '?')
			{
				count++;
			}
		}
	}
	return count == easy_set;
}

void ScanMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int i = 0;
	while (IsWin(show, row, col) == 0)
	{
		printf("输入需要翻开的坐标>");
		scanf("%d %d", &x, &y);
		if (show[x][y] != '?' && x <= row && y <= col) // 需要修改 != '?'啊(已修改) !!!!
		{
			printf("请勿重复输入坐标\n");
			continue;
		}
		if (mine[x][y] == '*')
		{
			show[x][y] = '*';
			printf("你被炸死了 game over\n");
			DisplayBoard(mine, ROW, COL);
			break;
		}
		else
		{
			char flag = count(mine, x, y);   //不这样写为什么会有bug  "+ '0' "
			if (flag != ' ')
			{
				show[x][y] = flag;
				DisplayBoard(show, ROW, COL);
			}
			else
			{

				AudioFlip(mine, show, x, y);
				Corner(mine, show);
				DisplayBoard(show, ROW, COL);

			}
		}
		if (x > row || y > col || x <= 0 || y <= 0)
		{
			printf("坐标输入非法,请重新输入!\n");
			printf("\n");
		}

	}
	if (IsWin(show, row, col))
	{
		printf("恭喜你,通关成功!\n");
	}
}
至此 递归实现完成度100%

迭代实现

其实迭代实现AudioFlip并不容易,但是我们把棋盘的空白区域想成涂上不同颜色来实现就 相对容易

函数名  ⁣ ⁣ 功能 难度 S h a p i n g B o a r d ( ) 计算扫雷成功的棋盘 e a s y L R s c a n ( ) 将棋盘的左右两边涂色 e a s y C o v e r ( ) 对列与行涂色 e a s y C h e c k H o l e ( ) 检查剩下的空腔 e a s y A u d i o F l i p ( ) 翻开对应颜色的区域 e a s y \begin{array}{|c|c|c|} \hline 函数名&\!\!功能&难度\\ \hline ShapingBoard()&计算扫雷成功的棋盘&easy\\ \hline LRscan()&将棋盘的左右两边涂色&easy\\ \hline Cover() & 对列与行涂色&easy\\ \hline CheckHole()& 检查剩下的空腔&easy\\ \hline AudioFlip()& 翻开对应颜色的区域&easy\\ \hline \end{array} 函数名ShapingBoard()LRscan()Cover()CheckHole()AudioFlip()功能计算扫雷成功的棋盘将棋盘的左右两边涂色对列与行涂色检查剩下的空腔翻开对应颜色的区域难度easyeasyeasyeasyeasy

思考:

为何要先计算出扫雷成功的棋盘呢?

因为,迭代不同于递归,要想翻开正确的区域可能会运行很多次,而且先计算出扫雷成功的棋盘也有助于我们调试、编写后续函数

ShapingBoard() 所需实现的功能如图所示

请添加图片描述

涂色功能所需实现的目标如图:

请添加图片描述

代码实现
整形(shapingBoard)
void ShapingBoard(char mine[ROWS][COLS], char shape[ROWS][COLS], int row, int col)
{
	int i, j = 1;
	for (i = 1; i <= col; i++)
	{
		for (j = 1; j <= row; j++)
		{
			if (mine[i][j] == '1')
			{
				shape[i][j] = '*';
			}
			else
			{
				int count = count_mine(mine,i,j);
				shape[i][j] = count + '0';
				if (count == 0)
					shape[i][j] = ' ';
			}
		}
	}

	for (i = 1; i <= row; i++)
	{
		for (j = 1; j <= col; j++)
		{
			mine[i][j] = shape[i][j];
		}
	}
}

这里我们创建一个shape数组接收计算mine数组的结果,再将shape传输给mine.

涂色(LRscan)
char flag = 'a'; //将空穴填充为'a' , 'b' .... (涂色)

void LRscan(char shape[ROWS][COLS], int row, int col)
{
	int count1 = 0;//统计分割行(一整行都没有空格)
	int count2 = 0;//统计不规则的分割行
	int i, j = 1;
	int find = 0;  //用于给不同的区域涂不同的颜色
	//check left side  (左扫描)
    for (i = 1; i <= col; i++)
    {
        j = 1;
        int count1 = 0;
        if (shape[i][j] == ' ')
        {
            for (j = 1; j <= row; j++)
            {
                if (shape[i][j] == ' ')
                {
                    shape[i][j] = flag;

                }
                else if (shape[i][j] != ' ' && j > 1)
                    goto end;
                if (j == row)
                    goto end;
            }
        }

        j = 1;
        if (shape[1][j] != 'a')
        {
            int find = 0;
            if (shape[i][1] != ' ');
            {
                count2++;
                find = i + 1;
            }
            if (count2 != 0)
            {
                flag += 1;
            }
            if (shape[find][1] != ' ')
            {
                flag -= 1;
            }
        }
        else
        {
            int find = 0;
            if (shape[i][1] != ' ');
            {
                count2++;
                find = i + 1;
            }
            if (count2 != 0)
            {
                flag += 1;
            }
            if (shape[find][1] != ' ')
            {
                flag -= 1;
            }
        }

        end:
        ;
    }

	//check right side 奇怪的循环(右扫描)
	flag += 1;
	i, j = easy_set;
	for (i = col; i >= 1; i--)
	{
		j = row;
		if (shape[i][j] == ' ')
		{
			for (j = row; j >= 1; j--)
			{
				if (shape[i][j] == ' ')
				{
					shape[i][j] = flag;
				}
				else if (shape[i][j] != ' ' && j < row)
					goto end1;

			}
		}

		j = row;
		if (97 <= shape[col][i] && shape[col][i] <= 122)
		{
			int find = 0;
			if (shape[i][row] != ' ');
			{
				count2++;
				find = i - 1;
			}
			if (count2 != 0)
			{
				flag += 1;
			}
			if (shape[find][row] != ' ')
			{
				flag -= 1;
			}
		}
		else
		{
			int find = row;
			if (shape[i][row] != ' ');
			{
				count2++;
				find = i - 1;
			}
			if (count2 != 0)
			{
				flag += 1;
			}
			if (shape[find][row] != ' ')
			{
				flag -= 1;
			}
		}

	end1:
		;
	}


}
填空(CheckHole)
void CheckHole(char shape[ROWS][COLS], int row, int col)
{
	int c, r = 0;
	int c2 = 0;

	flag = 'h';
	for (c = 1; c <= row; c++)
	{
		for (r = 1; r <= col; r++)
		{
			if (shape[c][r] == ' ')
			{
				shape[c][r] = flag;
				c2 = c+1;
				if (shape[c2][r] != ' ')
					flag += 1;
			} 
		}
	}
}
颜色覆盖(Cover)
void Cover(char shape[ROWS][COLS], int row, int col)
{
	int c, r = 0;
	//head
	flag += 1;
	for (r = 1; r <= row; r++)
	{
		if (shape[1][r] == ' ')
		{
			shape[1][r] = flag;
		}
	}

	//end
	flag += 1;
	for (r = row; r >= 1; r--)
	{
		if (shape[9][r] == ' ')
		{
			shape[9][r] = flag;
		}
	}

	int c2 = 0;
	//for (c = 2; c <= col; c++)
	//{
	//	c2 = c - 1;
	//	for (r = 2; r <= row; r++)
	//	{
	//		if (shape[c][r] == ' ' && 97 <= shape[c2][r] && shape[c2][r] <= 122)
	//			shape[c][r] = shape[c2][r];
	//	}
	//}

	//列覆盖, 一列中相邻的两元素 ASCII 大的换成小的
	//head
	for (c = 1; c <= row; c++)
	{
		for (r = 1; r <= col; r++)
		{
			c2 = c + 1;
			if (97 <= shape[c][r] && shape[c][r] <= 122 && 97 <= shape[c2][r] && shape[c2][r] <= 122)
			{
				if (shape[c][r] < shape[c2][r])
					shape[c2][r] = shape[c][r];
			}
			if (shape[c2][r] == ' ' && 97 <= shape[c][r] && shape[c][r] <= 122)
				shape[c2][r] = shape[c][r];


		}
	}
	//end
	for (c = row; c >= 1; c--)
	{
		for (r = col; r >= 1; r--)
		{
			c2 = c - 1;
			if (97 <= shape[c][r] && shape[c][r] <= 122 && 97 <= shape[c2][r] && shape[c2][r] <= 122)
			{
				if (shape[c][r] < shape[c2][r])
					shape[c2][r] = shape[c][r];
			}
			if (shape[c2][r] == ' ' && 97 <= shape[c][r] && shape[c][r] <= 122)
				shape[c2][r] = shape[c][r];
		}
	}

	//行覆盖, 一行中相邻的两元素 ASCII 大的换成小的
		//head
	int r2 = 0;
	for (c = 1; c <= row; c++)
	{
		for (r = 1; r <= col; r++)
		{
			r2 = r + 1;
			if (97 <= shape[c][r] && shape[c][r] <= 122 && 97 <= shape[c][r2] && shape[c][r2] <= 122)
			{
				if (shape[c][r] < shape[c][r2])
					shape[c][r2] = shape[c][r];
			}
			if (shape[c][r2] == ' ' && 97 <= shape[c][r] && shape[c][r] <= 122)
				shape[c][r2] = shape[c][r];


		}
	}

	//end
	for (c = row; c >= 1; c--)
	{
		for (r = col; r >= 1; r--)
		{
			r2 = r - 1;
			if (97 <= shape[c][r] && shape[c][r] <= 122 && 97 <= shape[c][r2] && shape[c][r2] <= 122)
			{
				if (shape[c][r] < shape[c][r2])
					shape[c][r2] = shape[c][r];
			}
			if (shape[c][r2] == ' ' && 97 <= shape[c][r] && shape[c][r] <= 122)
				shape[c][r2] = shape[c][r];
		}
	}


	//重复了一遍行覆盖
	//head
	for (c = 1; c <= row; c++)
	{
		for (r = 1; r <= col; r++)
		{
			r2 = r + 1;
			if (97 <= shape[c][r] && shape[c][r] <= 122 && 97 <= shape[c][r2] && shape[c][r2] <= 122)
			{
				if (shape[c][r] < shape[c][r2])
					shape[c][r2] = shape[c][r];
			}
			if (shape[c][r2] == ' ' && 97 <= shape[c][r] && shape[c][r] <= 122)
				shape[c][r2] = shape[c][r];


		}
	}

	//end
	for (c = row; c >= 1; c--)
	{
		for (r = col; r >= 1; r--)
		{
			r2 = r - 1;
			if (97 <= shape[c][r] && shape[c][r] <= 122 && 97 <= shape[c][r2] && shape[c][r2] <= 122)
			{
				if (shape[c][r] < shape[c][r2])
					shape[c][r2] = shape[c][r];
			}
			if (shape[c][r2] == ' ' && 97 <= shape[c][r] && shape[c][r] <= 122)
				shape[c][r2] = shape[c][r];
		}
	}

	CheckHole(shape, easy_set, easy_set);
}

void FristDisplay(char shape[ROWS][COLS],char show[ROWS][COLS], int row, int col,int arr[])
{
	int x = arr[0];
	int y = arr[1];
	if (shape[x][y] <= 122 && 97 <= shape[x][y])
	{
		AudioFlip(shape, show, easy_set, easy_set,x,y);
		//DisplayBoard(show, ROW, COL);
	}
	else
		show[x][y] = shape[x][y];
	DisplayBoard(show, ROW, COL); 
}

将每个空白区域涂上不同的颜色后,我们就可以使用AudioFlip翻开指定颜色的区域了

	void AudioFlip(char shape[ROWS][COLS], char show[ROWS][COLS], int row, int col,int r,int c)
{
	int flag1 = 0;
	int i, j = 0;
	int r1, c1 = 0;
	int miss_x = 0;
	int miss_y = 0;
	char mark = shape[r][c];
	char mark1 = 0;
	for (i = 1; i <= row; i++)
	{
		for (j = 1; j <= col; j++)
		{
			if (mark == shape[i][j] && i<= 9 &&j <= 9)
			{
				show[i][j] = ' ';
				for (miss_x = -1; miss_x <= 1; miss_x++)
				{
					for (miss_y = -1; miss_y <= 1; miss_y++)
					{
						if (1 <= i + miss_x && i + miss_x <= 9 && 1 <= j + miss_y && j + miss_y <= 9&&miss_x<=1&&miss_y<=1)
						{
							if (show[i + miss_x][j + miss_y] == '?')
								show[i + miss_x][j + miss_y] = shape[i + miss_x][j + miss_y];
							if (show[i + miss_x][j + miss_y] !=  mark && 97<=show[i + miss_x][j + miss_y] && show[i + miss_x][j + miss_y]<=122)
							{
								mark1 = show[i + miss_x][j + miss_y];
								flag1 = 1;
							}
						}
					}
				}
			}

		}
		

	}
	if (flag1 == 1)
	{
		for (r1 = 1; r1 <= row; r1++)
		{
			for (c1 = 1; c1 <= col; c1++)
			{
				if (shape[r1][c1] == mark1 && r1 <= 9 && c1 <= 9)
				{
					show[r1][c1] = ' ';
					for (miss_x = -1; miss_x <= 1; miss_x++)
					{
						for (miss_y = -1; miss_y <= 1; miss_y++)
						{
							if (1 <= r1 + miss_x && r1 + miss_x <= 9 && 1 <= c1 + miss_y && c1 + miss_y <= 9 && miss_x <= 1 && miss_y <= 1)
							{

								if (show[r1 + miss_x][c1 + miss_y] == '?')
									show[r1 + miss_x][c1 + miss_y] = shape[r1 + miss_x][c1 + miss_y];
							}
						}
					}
				}
			}
		}
	}

我们再将基础版中没动过的函数追加入game.c, game.h

<game.h>
#define ROW 9
#define COL 9

#define ROWS 11
#define COLS 11

#define easy_set 10

#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<Windows.h>
#include<string.h>
#include <conio.h>              //kbhit()/_kbhit()

void InitBoard(char Board[ROWS][COLS], int row, int col,char set); //初始化游戏区域

void DisplayBoard(char Board[ROWS][COLS], int row, int col);//打印界面

void FristScan(char show[ROWS][COLS], char mine[ROWS][COLS], int row, int col,int arr[]);//独立的第一次扫雷(第一次翻开不是雷的充分条件)

void SetMine(char mine[ROWS][COLS],char show[ROWS][COLS],char shape[ROWS][COLS], int row, int col,int arr[]);//放置雷

void ScanMine(char mine[ROWS][COLS], char show[ROWS][COLS], char shape[ROWS][COLS],int row, int col);//玩家扫雷

void ShapingBoard(char mine[ROWS][COLS],char shape[ROWS][COLS], int row, int col);

void LRscan(char shape[ROWS][COLS], int row, int col);

void Cover(char shape[ROWS][COLS], int row, int col);

void FristDisplay(char shape[ROWS][COLS],char show[ROWS][COLS], int row, int col);

int IsWin(char show[ROWS][COLS], int row, int col);


在这里我们可以顺便实现一些额外功能:计时, 标记雷.

计时器
#include <stdio.h>
#include <conio.h>              //kbhit()/_kbhit()
#include <Windows.h>            //Sleep(ms)
 
int main()
{
    int hour = 0, min = 0, sec = 0;
    int cnt = 0;
 
    printf("按任意键停止计时\n");
    while(!_kbhit())            //任意键退出循环(结束计时)
    {
        hour = cnt / 3600;      //获取计时小时数
        min = cnt / 60;          //获取计时分钟数
        sec = cnt % 60;         //获取计时秒数
        printf("  %02d:%02d:%02d\r", hour, min, sec);
        Sleep(1000);            //1s延时
        cnt++;
    }
    printf("\n程序退出\n");
    return 0;
}
改动一下放入ScanMine函数中,实现统计每次判断所需时间

Scanmine中追加标记功能

int count = 0;
void ScanMine(char mine[ROWS][COLS], char show[ROWS][COLS], char shape[ROWS][COLS], int row, int col)
{
	int x = 0;
	int y = 0;
	int i = 0;
	char string[10];
	int time = 0;
	while (1)
	{
		int hour = 0, min = 0, sec = 0;
		int cnt = 0;
		while (!_kbhit())
		{
			hour = cnt / 3600;      //获取计时小时数
			min = cnt / 60;          //获取计时分钟数
			sec = cnt % 60;         //获取计时秒数
			printf("  %02d:%02d:%02d\r", hour, min, sec);
			Sleep(1000);            //1s延时
			cnt++;
			time += 1;
		}
		printf("思考所用时间:%ds\n", time);
		printf("输入sweep/flag来翻开/标记坐标>");
		scanf("%s", string);
		int sz = strlen(string);
		if (string[0] == 's' && sz == 5)
		{
			printf("请输入要 翻开 的坐标>");
			scanf("%d %d", &x, &y);
			if (show[x][y] == ' ' || show[x][y] >= 49 && show[x][y] <= 56) // 需要修改 != '?'啊(已修改) !!!!
			{
				printf("请勿重复输入坐标\n");
				continue;
			}
			if (show[x][y] == '!')
			{
				show[x][y] = '?';
				count--;
				DisplayBoard(show, ROW, COL);
				printf("------------------------------|\n");
				printf("取消标记成功!剩余可标记数量:%d|\n",10-count);
				printf("------------------------------|\n\n");
				continue;

			}
			if (x > 0 && x <= col && y > 0 && y <= row )
			{
				if (mine[x][y] == '*')
				{
					printf("你被炸死了 game over\n");
					DisplayBoard(mine, ROW, COL);
					break;
				}
				else
				{
					if (shape[x][y] <= 122 && 97 <= shape[x][y])
					{
						AudioFlip(shape, show, easy_set, easy_set, x, y);
						DisplayBoard(show, ROW, COL);
					}
					else
						show[x][y] = shape[x][y];
					DisplayBoard(show, ROW, COL);
				}
			}
			else
			{
				printf("坐标输入非法,请重新输入!\n");
				printf("\n");
			}

		}
		else if (string[0] == 'f' && sz == 4)
		{
			printf("请输入要 标记 的坐标>");
			scanf("%d %d", &x, &y);

			if (x > 0 && x <= col && y > 0 && y <= row)
			{
				if (show[x][y] == '?')
				{
					show[x][y] = '!';
					count++;
				}
				else if (show[x][y] == '!')
				{
					show[x][y] = '?';
					count--;
				}
				else
				{
					printf("无效的标记\n");
					continue;
				}

				DisplayBoard(show, ROW, COL);
				printf("--------------------------|\n");
				printf("已标记:%d个,剩余可标记:%d   |\n", count, 10 - count);
				printf("--------------------------|\n\n");

			}
			else
				show[x][y] = shape[x][y];
			//DisplayBoard(show, ROW, COL);

		}
		else
		{
			printf("坐标输入非法,请重新输入!\n");
			printf("\n");
		}
	}

	
}

Minesweeper 运行测试


迭代版代码
递归版代码
扫雷exe,提取码:obub

文章到这里截止,感谢宁的阅读~

彩蛋

请添加图片描述
csdn爬虫泛滥😥, 不想辛苦做的图被盗,就做了防盗处理,如果你想要获取表情包,就发私信给我吧!

从hater变成gladiator
沙滩上寻找贝壳 我跪着
Greener的明天我向往,Minesweeper让我变得更强壮
评论 18
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值