我们先来看看之前讲扫雷基础的时候留下的一些问题:
要注意的一点是选择游戏难度以目前我们讲过的内容还暂时写不出来,在后面学习到相关内容时,我们再进行讲解,接下来就解决后三个问题 我们的进阶还是在之前写过的基础版之上添加,如果还没有看过之前基础版,可以在我的主页查看,现在我们开始通过我们学过的知识解决这三个进阶需求
这个函数我们的目标就是实现:如果输入的坐标不是雷,那么查看周围是否有雷,如果至少有一个雷,那么就直接显示周围雷的个数,如果没有雷,把那个坐标换位字符’ ',也就是字符空格,而不再显示0,这样更加好看,随后向周围扩展,扩展到该坐标周围至少有一个雷时
我们需要做一点更改,就是把输入的当前坐标,查看周围有几个雷放入这个函数,方便递归,现在可能有点懵,后面会慢慢解释
这个函数实现起来有一定难度,必须要悟透递归的使用方法,同时还要注意数组越界等等问题,还要使用一点点超纲内容,就是指针,但是是最简单的指针内容,并且篇幅只有一点点,主要还是要体会循环递归的方式,以及递归的两个必要条件:
接下来我们就进入这个函数的设计:
//指针变量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)接下来思考从哪里开始递归,也就是递归的入口,经过前面的分析,很显而易见,我们要把递归的入口放在刚刚那个循环里面,这样构成了循环递归,将每一个周围的坐标作为中心,向周围扩展,但我们需要注意一些问题
//循环递归:
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);
}
}
}
}
}
简单思路就是,每当玩家排查一次雷后,就询问是否要标记雷,如果回答是,那么就开始标记雷,回答否那么就继续排雷
玩家要标记雷就是认为那个地方肯定不是雷,如果用一个通俗的符号表示否定,很容易就想到使用大写字母X,把它当做叉叉使用,所以实现标记雷就是把那个坐标位置的字符改为大写字符X,听上去就很容易实现,接下来我们仔细分析将其设计为一个函数:
//实现标记雷函数:
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");
}
}
从原理上这个问题很好解决,就是游戏结束的时间减去游戏开始的时间,要解决这样一个问题我们就要重新回顾一下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)很简单呢?我们拭目以待吧!