在这里插入图片描述

一、扫雷进阶留下的问题

我们先来看看之前讲扫雷基础的时候留下的一些问题:

  • 是否可以选择游戏难度
        ◦ 简单 9 * 9 棋盘,10个雷
        ◦ 中等 16 * 16棋盘,40个雷
        ◦ 困难 30 * 16棋盘,99个雷

  • 如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚

  • 是否可以标记雷

  • 是否可以加上排雷总共耗费的时间

    要注意的一点是选择游戏难度以目前我们讲过的内容还暂时写不出来,在后面学习到相关内容时,我们再进行讲解,接下来就解决后三个问题
    我们的进阶还是在之前写过的基础版之上添加,如果还没有看过之前基础版,可以在我的主页查看,现在我们开始通过我们学过的知识解决这三个进阶需求

二、坐标非雷扩展周围函数

    这个函数我们的目标就是实现:如果输入的坐标不是雷,那么查看周围是否有雷,如果至少有一个雷,那么就直接显示周围雷的个数,如果没有雷,把那个坐标换成字符’ ',也就是字符空格,而不再显示0,这样更加好看,随后向周围扩展,扩展到该坐标周围至少有一个雷时

    我们需要做一点更改,就是把输入的当前坐标,查看周围有几个雷放入这个函数,方便递归,现在可能有点懵,后面会慢慢解释

    这个函数实现起来有一定难度,必须要悟透递归的使用方法,同时还要注意数组越界等等问题,还要使用一点点超纲内容,就是指针,但是是最简单的指针内容,并且篇幅只有一点点,主要还是要体会循环递归的方式,以及递归的两个必要条件:

  • 递归存在限制条件,当满⾜这个限制条件的时候,递归便不再继续,如果没有限制,可能会陷入死递归
  • 每次递归调⽤之后越来越接近这个限制条件

接下来我们就进入这个函数的设计:

  1. 函数的命名:我将其命名为exdboard,exd是extend的缩写,含义是扩展,函数名就理解为扩展棋盘,也可自行命名

  2. 函数参数:
    (1)由于需要操作数组show和数组hide,所以要将这两个数组传过去
                                                                                               
    (2) 由于需要扩展周围的坐标,我们可以直接把当前坐标也一次性涵盖进去,就是先对当前坐标进行操作,查看周围是否有雷,如果没有雷就开始对周围坐标递归,所以我们需要传过去当前玩家输入的坐标,也就是x,y                                                                                           
    (3)然后由于我们要避免函数递归导致数组越界访问,所以我们要把行和列传过去,用来判断递归坐标的合法性                                                                                           
    (4)最后由于我们每递归一次,可能就会扩展一个坐标,而每扩展一次坐标我们需要win++一次,但是由于直接传win过去属于传值调用,在exdboard里面对win进行更改不会对真实的win产生影响,所以我们需要使用指针进行传址调用,这里听不懂没关系,后面会专门详细的为大家介绍指针,现在只需要跟着使用就行,并且在该函数它只有一个作用,那就是每次递归,如果扩展了一个坐标,就对win++一次,所以函数exdboard的最后一个参数是win的地址                                                                                           
    (5)所以函数exdboard的参数有:show和hide数组,玩家输入的坐标x,y,棋盘行和列,win的地址

  3. 函数的声明:我们要将函数设计好后,直接放在函数findboard中,所以可以直接在函数findboard上方进行实现,免去了在game.h里声明

  4. 函数的实现:
    (1)首先我们需要获取玩家输入的坐标位置周围雷的个数,如果没有雷的话,也就是show[x][y]==‘0’时,把这个位置改成字符空格,这样更加好看                                                                                           
    (2)执行完(1)的操作,相当于就是已经排查了一个坐标,我们要win++一次,如果还不了解指针的可以先照抄,学习后再来理解,反正在这里只用了很简单的指针知识,如下:

//指针变量int* ptr
//用来接收win传来的地址
//对它解引用就找到了win
(*ptr)++

    (3)随后我们开始思考该怎么递归,我们可以这样想,想要实现这个函数的功能,我们不能一口气直接完成,需要一步一步来,也就是把大事化小,如果当前坐标周围没有雷,我们可以以当前坐标为中心,进行扩展,一个一个找出周围的坐标,将它们当作新的中心进行扩展,如图:
在这里插入图片描述
    这样我们就将大问题化为了小问题,把扩展中心1周围的雷,化解为找多个中心,扩展它们周围的雷,形成递归,因为解决中心1和解决中心2的扩展是同类问题,方法相似,只要写出解决中心1的扩展,也就解决了其它中心的扩展,难点在于怎么找其它的中心呢?

    (4)我们可以定义一个i和j,表示新的中心的行和列,这时候我们可以用一个循环,找出中心1周围的坐标的行和列,然后将它们作为新的中心进行递归,如下:

for(i=x-1;i<=x+1;i++)
{
   for(j=y-1;j<=y+1;j++)
   {
      
   }
}

    这下我们就找出了坐标为x,y周围的所有坐标,可以将它们当作新的中心

    (5)我们现在开始思考整个递归的模型,我们说过递归一定要有尽头,有限制条件,每进行一次递归就要越来越靠近这个条件,我们可以称为递归的出口,经过思考,我们一定是要坐标周围没有雷,也就是show[x][y]!=‘ ’这个条件,如果这个条件满足,说明周围至少有一个雷,那么我们就可以返回了,不要递归下去了,由于每个中心的周围都只有8个坐标,再加上有雷的存在,所以迟早遇到某一个中心周围有雷,那么递归就开始返回了,不会死递归,如:

if (show[x][y]!=' ')
{
	return;
}

    (6)接下来思考从哪里开始递归,也就是递归的入口,经过前面的分析,很显而易见,我们要把递归的入口放在刚刚那个循环里面,这样构成了循环递归,将每一个周围的坐标作为中心,向周围扩展,但我们需要注意一些问题

  • 我们的递归不能让数组越界,也就是我们的新中心show[i][j],不能越界,必须满足
    i>=1 && i<=row && j>=1 && j<=col
  • 我们不能重复递归,比如2,4这个坐标已经做过中心了,如果递归递归着,2,4突然又做了中心,就重复递归了,就像使用递归求第n个斐波那契数一样,重复递归太多次导致效率太低,甚至低到我们不能接受,所以我们使用了循环,也就是迭代,那我们怎么避免重复递归呢?我们只需要判断一下show[i][j]是不是字符 *,如果是字符 ,说明这个坐标肯定还没有迭代过,可以放心迭代,所以我们可以再加一个条件show[i][j]=='',然后进入递归,如:
//循环递归:
for (i = x - 1; i <= x + 1; i++)
{
	for (j = y - 1; j <= y + 1; j++)
	{
		if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col)
		{ 
			      //递归入口:
				exdboard(show, hide, i, j, row, col, ptr);
		}		
	}
}

    (7)经过我们的努力分析,现在我们基本上可以将这个函数构建出来了,参考代码如下:

//查看排查雷的那个坐标周围是否有雷,如果至少有一个雷就直接显示有几个雷
//如果周围没有雷,那么就对周围进行扩展
void exdboard(char show[Rows][Cols], char hide[Rows][Cols], int x,int y ,int row,int col, int* ptr)
{
	int i = 0;
	int j = 0;
	int ret = getcount(hide, x, y);
	show[x][y] = ret + '0';
	(*ptr)++;
	if (show[x][y] == '0')
	{
		show[x][y] = ' ';
	}
	
	//递归出口:
	if (show[x][y]!=' ')
	{
		return;
	}
	else
	{
		//循环递归:
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col)
				{ 
					      //递归入口:
						exdboard(show, hide, i, j, row, col, ptr);
				}		
			}
		}
	}
}
  1. 函数的使用:我们可以直接放在排查雷函数中,如图:
    在这里插入图片描述

三、标记雷函数

    简单思路就是,每当玩家排查一次雷后,就询问是否要标记雷,如果回答是,那么就开始标记雷,回答否那么就继续排雷

    玩家要标记雷就是认为那个地方肯定不是雷,如果用一个通俗的符号表示否定,很容易就想到使用大写字母X,把它当做叉叉使用,所以实现标记雷就是把那个坐标位置的字符改为大写字符X,听上去就很容易实现,接下来我们仔细分析将其设计为一个函数:

  1. 函数命名:我命名为markmine,mark是标记,做记号的意思,mine有雷的意思,做一个参考,也可自行命名
  2. 函数参数: 由于我们需要更改展示给用户的数组show,所以我们需要把show数组传过去, 由于需要查看用户输入的坐标是否合法,所以要把实际棋盘大小的行和列,也就是Row和Col传过去。所以函数参数有show数组、Row和Col
  3. 函数声明:我们可以思考一下,我们设计的函数markmine,他的作用是玩家排查雷后,然后询问是否标记雷,回答是后,再进行标记,我们可以这样,将函数markmine直接放入函数findboard的最后,然后在findboard上方实现,这样就不用再gam.h中专门声明了
  4. 函数实现:现在我们按点来分析一下总体思路
    (1)在排查完一次雷后,进入函数询问玩家是否标记雷,然后让用户输入一个值,用来给我们判断,输入是就标记雷,输入否就不标记                                                                                           
    (2)由于要比较两个字符串是否相等,不能直接用==,要使用strcmp,并且在game.h声明头文件string.h,如果strcmp返回值是0,那么两个字符串相等,反之就不相等                                                                                           
    (3)有时玩家可能需要标记多个雷,所以我们可以在玩家输入是后再次询问,需要标记几个雷,并且给予提示,还需要标记多少个雷                                                                                           
    (4)我们可以使用变量num来接收玩家输入的数量,然后将其作为while的循环条件,每标记一次雷,num–,到最后num为0时循环就自动结束了                                                                                           
    (5)为了防止用户输入的坐标越界,我们需要对用户输入的坐标进行判断,必须满足棋盘大小                                                                                           
    (6)为了防止用户不小心输入已经排除的位置,比如坐标6,5已经知道不是雷了,用户却不小心输入错误,标记成雷了,我们可以加一个限制条件,就是show[x][y]的位置必须是字符*,也就保证了标记的位置不会是已经排除的位置                                                                                           
    (7)最后一步就很简单了,只需要将用户输入坐标的位置改成大写字母‘X’                                                                                           
    (8)代码如下:
//实现标记雷函数:
void markmine(char show[Rows][Cols], int row, int col)
{
	int x = 0;
	int y = 0;
	int i = 0;
	int num = 0;
	char arr[10] = { 0 };
	printf("\n*****是否标记雷*****:");
	scanf("%s",arr);
	if (strcmp(arr, "是") == 0)
	{
		printf("\n***请输入要标记的雷的个数***:");
		scanf("%d", &num);
		while (num)
		{
			printf("\n请输入要标记的雷的坐标,还需要标记%d个雷:",num);
			scanf("%d %d", &x, &y);
			if (show[x][y]=='*' && x >= 1 && x <= row && y >= 1 && y <= col)
			{
				show[x][y] = 'X';
				num--;
			}
			else
			{
				printf("输入不合法,请重新输入!\n");
			}
			
		}
	}
	else
	{
		printf("\n");
	}
}

  1. 函数运用:最后运用函数时记得将其放在排查雷函数后,如图:
    在这里插入图片描述

四、查看排雷总时间

    从原理上这个问题很好解决,就是游戏结束的时间减去游戏开始的时间,要解决这样一个问题我们就要重新回顾一下time函数

    当time函数的参数为NULL时,它会返回一个时间戳,就是从1970年1月1日0时0分0秒到现在这个时间有多少秒,返回类型是time_t,本质上是一个32位或者64位的整型,可以使用占位符%td打印它

    如果我们要算游戏总共花费多少时间,我们可以直接在游戏开始时,在game函数最上面创建一个变量start来接收游戏开始时,在game函数最下面,也就是游戏结束时创建一个变量end来接收结束时的时间戳,然后将它们相减即可

    为了方便,我们可以显示精确到秒的时间,同时显示大约多少分钟,分钟就用秒数除以60即可,代码如下:

void game()
{
	//游戏开始的时间戳
	time_t start = time(NULL);

	//不显示出来,用于布置雷
	char hide[Rows][Cols];

	//显示出来,用于排查雷
	char show[Rows][Cols];

	//初始化棋盘:
	initboard(hide, Rows, Cols, '0');
	initboard(show, Rows, Cols, '*');

	//布置雷:
	setboard(hide, Count);

	//打印棋盘:
	printboard(show, Row, Col);
	printboard(hide, Row, Col);

	//排查雷:
	findboard(show, hide, Row, Col, Count);

	//游戏结束时的时间戳
	time_t end = time(NULL);
	printf("共花费%td秒,大约%td分钟\n\n",end-start,(end-start)/60);
}

五、扫雷进阶源码及总结

game.h

#pragma once

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

//显示的棋盘大小
#define Row 9
#define Col 9

//实际的棋盘大小
#define Rows Row+2
#define Cols Col+2

//雷的个数:
#define Count 10

//声明初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x);

//声明打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col);

//声明布置雷函数:
void setboard(char board[Rows][Cols], int count);

//声明排查雷函数
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row, int col, int count);

game.c

#define _CRT_SECURE_NO_WARNINGS

#define _CRT_SECURE_NO_WARNINGS

#include "game.h"

//实现初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x)
{
	int i = 0;
	int j = 0;
	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < cols; j++)
		{
			board[i][j] = x;
		}
	}
}

//实现打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col)
{
	int i = 1;
	int j = 1;
	//打印棋盘标志,让棋盘更显眼
	printf("------ 扫雷 -------\n");
	//打印列号:
	for (i = 0; i <= col; i++)
	{
		printf("%d ", i);
	}
	//列号打印完进行换行
	printf("\n");

	for (i = 1; i <= row; i++)
	{
		//打印行号
		printf("%d ", i);
		for (j = 1; j <= col; j++)
		{
			printf("%c ", board[i][j]);
		}
		//每打印完一行就要进行一次换行:
		printf("\n");
	}
	//打印棋盘标志,让棋盘更显眼
	printf("------ 扫雷 -------\n");
	//棋盘打印完之后进行换行
	printf("\n");
}

//实现布置雷函数:
void setboard(char board[Rows][Cols], int count)
{
	while (count)
	{
		int x = rand() % Row + 1;
		int y = rand() % Col + 1;
		if (board[x][y] != '1')
		{
			board[x][y] = '1';
			count--;
		}
	}
}

//如果坐标不是雷,获取坐标周围雷的个数
int getcount(char hide[Rows][Cols], int x, int y)
{
	return (hide[x][y - 1] + hide[x][y + 1] +
		hide[x - 1][y] + hide[x - 1][y - 1] + hide[x - 1][y + 1] +
		hide[x + 1][y] + hide[x + 1][y - 1] + hide[x + 1][y + 1] - 8 * '0');
}

//查看排查雷的那个坐标周围是否有雷,如果至少有一个雷就直接显示有几个雷
//如果周围没有雷,那么就对周围进行扩展
void exdboard(char show[Rows][Cols], char hide[Rows][Cols], int x,int y ,int row,int col, int* ptr)
{
	int i = 0;
	int j = 0;
	int ret = getcount(hide, x, y);
	show[x][y] = ret + '0';
	(*ptr)++;
	if (show[x][y] == '0')
	{
		show[x][y] = ' ';
	}
	
	//递归出口:
	if (show[x][y]!=' ')
	{
		return;
	}
	else
	{
		//循环递归:
		for (i = x - 1; i <= x + 1; i++)
		{
			for (j = y - 1; j <= y + 1; j++)
			{
				if(show[i][j] == '*' && i>=1 && i<=row && j>=1 && j<=col)
				{ 
					      //递归入口:
						exdboard(show, hide, i, j, row, col, ptr);
				}		
			}
		}
	}

}

//实现标记雷函数:
void markmine(char show[Rows][Cols], int row, int col)
{
	int x = 0;
	int y = 0;
	int i = 0;
	int num = 0;
	char arr[10] = { 0 };
	printf("\n*****是否标记雷*****:");
	scanf("%s",arr);
	if (strcmp(arr, "是") == 0)
	{
		printf("\n***请输入要标记的雷的个数***:");
		scanf("%d", &num);
		while (num)
		{
			printf("\n请输入要标记的雷的坐标,还需要标记%d个雷:",num);
			scanf("%d %d", &x, &y);
			if (show[x][y]=='*' && x >= 1 && x <= row && y >= 1 && y <= col)
			{
				show[x][y] = 'X';
				num--;
			}
			else
			{
				printf("输入不合法,请重新输入!\n");
			}
			
		}
	}
	else
	{
		printf("\n");
	}
}

//实现排查雷函数:
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row, int col, int count)
{
	int x = 0;
	int y = 0;
	int win = 0;
	printf("请输入英文逗号隔开坐标!\n\n");
	while (win < Row * Col - Count)
	{
		printf("请输入要排查的坐标:");
		//这里需要用到之前学过的%*c,就是
		//scanf的赋值忽略符*
		//使用过后就可以输入空格或者英文逗号隔开坐标,最好英文逗号
		//但是不能用中文
		//这里中文和英文逗号不能都使用
		//所以我们最好在开始时给予一些提示
		scanf("%d%*c%d", &x, &y);
		printf("\n");
		if (x >= 1 && x <= row && y >= 1 && y <= col)
		{
			if (hide[x][y] == '1')
			{
				printf("很遗憾,你踩到雷了,游戏失败!\n\n");
				printf("显示的0代表不是雷,1表示雷\n\n");
				printboard(hide, Row, Col);
				break;
			}
			else
			{
				//扩展棋盘:
				exdboard(show, hide,x,y, row, col, &win);
				printboard(show, Row, Col);

				//标记雷
				markmine(show, row, col);
				printboard(show, Row, Col);
			}
		}
		else
		{
			printf("输入不合法,请重新输入!\n");
		}
	}
	if (win == Row * Col - Count)
	{
		printf("恭喜你,扫雷成功!\n\n");
		printf("显示的0代表不是雷,1表示雷\n\n");
		printboard(hide, Row, Col);
	}
}

test.c

#define _CRT_SECURE_NO_WARNINGS

#define _CRT_SECURE_NO_WARNINGS

#include "game.h"

void menu()
{
	printf("********************\n");
	printf("**** 1.开始游戏 ****\n");
	printf("**** 0.退出游戏 ****\n");
	printf("********************\n\n");
	printf("提示:请输入1或0\n");
}


void game()
{
	//游戏开始的时间戳
	time_t start = time(NULL);

	//不显示出来,用于布置雷
	char hide[Rows][Cols];

	//显示出来,用于排查雷
	char show[Rows][Cols];

	//初始化棋盘:
	initboard(hide, Rows, Cols, '0');
	initboard(show, Rows, Cols, '*');

	//布置雷:
	setboard(hide, Count);

	//打印棋盘:
	printboard(show, Row, Col);
	//printboard(hide, Row, Col);

	//排查雷:
	findboard(show, hide, Row, Col, Count);

	//游戏结束时的时间戳
	time_t end = time(NULL);
	printf("共花费%td秒,大约%td分钟\n\n",end-start,(end-start)/60);
}


int main()
{
	srand((unsigned int)time(NULL));
	int input = 0;
	do
	{
		menu();
		printf("\n请选择:");
		scanf("%d", &input);
		switch (input)
		{
		case 1:
			game();
			break;
		case 0:
			printf("游戏退出成功\n");
			break;
		default:
			printf("\n选择错误,请重新输入!\n\n");
			break;
		}
	} while (input);
	return 0;
}

    本文内容偏难,如果有疑问欢迎在评论区提问,一定会及时答复
    对于扫雷进阶(2)也就是最后一点内容:选择游戏难度,我会在后面讲到相关知识点后出一篇文,希望不要被当前这个扫雷进阶(1)难到而放弃,继续往后面学习,你就会发现它很简单,在扫雷进阶(2)的时候是否会觉得扫雷进阶(1)很简单呢?我们拭目以待吧!

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐