本期介绍
主要介绍:如何一步一步的把整个扫雷游戏的所有功能实现,详细的讲解其中每一个功能从无到有的思考过程以及代码上的实现。
扫雷是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。这款游戏的玩法是在一个9 * 9(初级),16 * 16(中级),16 * 30(高级),或自定义大小的方块矩阵中随机布置一定量的地雷(初级为10个,中级为40个,高级为99个)。由玩家逐个翻开方块,以找出所有地雷为最终游戏目标。如果玩家翻开的方块有地雷,则游戏结束。值得注意的是:若翻开的格子下没有地雷,则会标记其周围一圈格子中包含雷的个数,所以若想顺利通关就必须利用好这些显示出来的数字。如下图所示。(而我们今天要实现的是9×9方格的扫雷游戏,棋盘上一共放置了10个地雷)
首先,我们要思考一下怎样才能实现的和计算机里的扫雷游戏一样呢?进入计算机里的扫雷游戏,首先映入眼帘的是9×9的棋盘上所有的格子都被掩盖了。然后随机去翻开一些格子,你肯定能发现会出现几种情况:
1.当翻开的是“地雷”时,你会被炸死游戏结束。
2.当翻开的格子周围一圈当中存在“地雷”时,翻开的格子下面会显示周围一圈格子内存在“地雷”的个数。
3.当翻开的格子周围没有“地雷”时,会一下子展开一大片区域。
然后不断地翻开格子,直到把所有的非“地雷”的格子全部翻开后,游戏提示:扫雷成功!那我们该怎么实现这里双层结构的棋盘呢?你会发现双层结构的第一层是用来覆盖住第二层上显示的内容,一个想法就油然而生了:我们是不是只要创建两个二维数组,其中一个隐藏在暗处用来存放这些地雷啊、数字的信息,用户是看不见的;另一个则是专门向用户展示的,是摆在明面上的棋盘。(这期会实现:可以自己标注地雷位置的操作,还有炸金花式展开的功能)
游戏菜单:选择是否开始玩儿游戏,1:开始游戏,0:退出游戏。
游戏流程:创建两个格子棋盘(其中一个是隐与幕后存放地雷信息用的,另一个是向用户展示的)→ 初始化两个棋盘 → 随机放置地雷 → 实现打印棋盘 → 开始扫雷(用户输入要查找的位置的坐标 → 判断是否有雷 → 有则游戏结束,若没有则显示其周围一圈中地雷的个数或者炸金花式展开一片)→ 直至扫完所有非雷区 → 游戏结束。
代码如下:
#include"game.h"
void menu()
{
printf("*****************************************\n");
printf("********* 1.play 0.exit *********\n");
printf("*****************************************\n");
}
void game()
{
//存放地雷的棋盘
char mine[ROWS][COLS] = { 0 };
//展示给用户看的棋盘
char show[ROWS][COLS] = { 0 };
//初始化棋盘
init_board(mine, ROWS, COLS, '0');
init_board(show, ROWS, COLS, '*');
//放置地雷
set_mine(mine, ROW, COL);
system("cls");//清空屏幕
//打印棋盘
//display_board(mine, ROW, COL);
display_board(show, ROW, COL);
//开始扫雷
find_mind(mine, show, ROW, COL);
}
int main()
{
//设置随机数的起点
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();//菜单
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
注意:仔细看上面的代码,你会我并没有引
相信看到这肯定有同学问:“不是说实现的是9×9的扫雷吗,为什么这里是用11×11的二维数组来存放哒???”这就得夸夸那些牛逼的大佬想出来的方法了。现在假设我用9×9的二维数组来存放信息,当排查某个非雷的坐标时,我们知道这时是需要将该坐标周围一圈坐标逐个排查一遍的。但当你去排查最边界的那一圈坐标时,你会发现你必须得以特殊的排查法来处理这些坐标,不然你就会越界访问。就如下图所示:
所以要在排查的时候不越界访问你就必须给这最边缘一圈的坐标设计出专门的排查规则,这是一种解决的方法,但该方法的实现太过于麻烦了。所以一些大佬就思考:能不能从另一个角度以更加简便的方法来解决这个问题呢?其中有些人就发现,似乎只要把9×9的棋盘扩张成11×11的棋盘,然后只拿该棋盘中间的9×9的格子用作和以前一样的操作,问题不就迎刃而解了。这是为什么呢?因为我操作时只会去排查中间那9行9列的格子,且就算我去排查最边上的一圈坐标也不会出现越界访问的情况,如下图所示。你看仅仅只需要把数组的大小改一改就能解决困扰你很久的问题,完全没有必要在那限制这限制那的。所以有些时候不能硬想啊,一定要学会换一个角度来思考问题的解决之法!!!
该函数做到了可以任意对棋盘进行初始化,想初始化什么就传参传进来就行了。
//初始化棋盘
void init_board(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;//set为外部传进来要初始化的字符
}
}
}
该函数是通过rand()随机数函数来设置地雷的,如果我之前有一期是专门讲解了如何去创建随机数,如果这里感觉不是很明确,建议先了解一下随机数再来看下去。链接:如何创建随机数。
//设置地雷
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = COUNT;//COUNT定义在头文件中为常量10
while (count)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
该函数可以实时的打印出你想要的几行几列的棋盘,只需更改头文件中的常量ROW和COL就行。
//打印棋盘
void display_board(char board[ROWS][COLS], int row, int col)
{
printf("---------------扫雷游戏----------------\n");
int i = 0;
for (i = 0; i <= row; i++)//打印列号
{
printf(" %d ", i);
}
printf("\n");
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf(" %d ", j);//打印行号
//打印棋子行
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
//打印分隔行
if (i <= row - 1)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("---");
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
}
}
printf("---------------扫雷游戏----------------\n");
}
演示一下效果,就如下图所示。mine棋盘其中字符1代表设置的地雷,字符0表示该地没有地雷,而show棋盘中字符 ‘ * ’ 表示方格还没有被翻开,是未知的意思。
该函数是由很多子函数嵌套调用而成,总体实现的功能是:先创建一个循环,然后要求用户输入要排查的下标,接着判断用户输入下标是否有效,再判断是否排查到雷。若排到地雷则直接退出循环游戏结束,否则直接进入炸金花式展开的递归调用,直到排查完所有的非雷方格则游戏胜利(炸金花式展开是啥,下面会仔细说明,不要着急)。
//开始扫雷
void find_mind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;//记录排查出不是地雷位置的个数
char ch = 0;
while (win < row * col - COUNT)
{
printf("请输入要排查的位置下标:");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入下标是否有效
{
if (mine[x][y] == '1')//排查到了地雷
{
break;
}
//此时没有排查到地雷
else
{
//炸金花式展开
explode_spread(mine, show, row, col, x, y);
system("cls");
//打印棋盘
display_board(show, row, col);
printf("需要标注地雷就输入:Y,不需要标注地雷则输入:N\n");
//清空一下缓冲区
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
switch (ch)
{
case 'Y':
//标记雷的位置
sign_mine(show, row, col);
break;
default:
break;
}
}
}
else
{
printf("输入下标非法,请重新输入:\n");
}
}
//把所有mine中地雷全部显示到show上
show_all_mine(mine, show, row, col);
system("cls");
//打印棋盘
display_board(show, row, col);
//判断是否排查成功
if (win >= row * col - COUNT)
{
printf("恭喜你排查出所有的地雷!!!\n");
}
else
{
printf("扫雷失败,你被炸死了!!!\n");
}
}
该函数是实现:当我们在排查某个位置的时候,会计算该位置周围一圈存在地雷的个数,并返回给这个值。如下图中的数字那样:
//计算周围一圈范围内存在地雷的个数
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
//把3×3方格内所有的字符(‘1’或‘0’)加起来,
//然后再统一减去9个字符‘0’得到的结果就是地雷的个数
int i = 0;
int sum = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
sum += mine[i][j];
}
}
return (sum - 9 * '0');
}
该函数若放到调用它的扫雷函数中去执行,会实现下面这种情况:若我不小心扫到了地雷或扫雷成功时将会显示曝光所有的地雷在用户界面上向你展示。如下图所示:
//把所有mine中地雷全部显示到show上
void show_all_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (mine[i][j] == '1')
{
show[i][j] = '@';//地雷用字符‘@’在用户界面表示
}
}
}
}
这是一个递归函数(对于新手来水非常难,我估计光看我写的代码是很难理解透彻这个函数的,所以我会逐步的来分析这个代码),该函数实现的功能是:若排查的位置周围没有雷则向四周爆炸式展开,直至遇到周围有地雷的坐标时停下来。如下图所示:
那该怎么实现该递归函数呢?当排查的位置没有雷且该位置周围没有雷时,就展开其周围一圈的8个坐标,然后看这8个坐标是否可以再逐个向其自身周围接着展开,这样一次就递归调用函数自身8次的展开速度就像爆炸了一样,所以称其为:炸金花式展开。
如果按照这个思路去编写代码一定会出现一种情况“ 程序陷入死递归 ”。这个问题当时也时困扰了我很久,代码调试一直走不下去,直到我动手画了张图慢慢分析后才发现了问题的所在。看下图所示:
基于上面的所思所想:我们认为当排查某个周围没有雷的坐标时,该坐标会向周围一圈展开,然后展开的这些坐标会继续再向外围展开,继而把所有周围没有地雷的坐标统统排查出来。可事实真是如此吗?你会发现不管你先展开哪一个位置你都将陷入死递归当中。就拿上图所示的这个坐标为例,当要展开(x,y)坐标周围的一圈坐标时,假如其首先会从(x-1,y+1)位置开始,然后又由该坐标向其自身周围的一圈展开时,你会发现坐标(x,y)也在需要展开的范围内。这样一来不就会重复要求再次展开坐标(x,y),然后在由(x,y)要求展开(x-1,y+1),两个坐标在那相互疯狂调用,你不死递归谁死递归???
解题思路:那该怎么解决呢,说难不难当也不简单,要看你自己在思考的过程中能不能灵光一现。你看,现在我们完全是站在存放地雷这个棋盘(mine_board)的角度在思考解决问题的办法。这样必然局限了我们的思维,不妨跳出来看看,换一个角度去寻求解题之法。不知道大家有没有发现我们似乎略了用户棋盘(show_board)了呀,可不能只认为用户棋盘只是用来展示的,如若这两个棋盘配合的够好,你会发现此法可以完美的解决上面的难题。
如何实现:若每次判断一个坐标上没有雷且该坐标周围一圈同样没有雷时,则将向我们展示的棋盘(show_board)上表示未知的字符 ‘ * ’ 改成空格。然后在之后的递归调用前给一个判断:如果这个将要被递归调用的坐标在(show_board)棋盘上存放的是字符 ‘ * ’ 时才能进行下一步,否则将直接跳过此次递归。如此就可以限制住坐标在那相互疯狂调用,因为程序这样设计后每个坐标向外展开的次数就只有一次了。
解决了上面的难题后你还会遇到一种情况,程序调试时出现错误,编译器给的理由是:非法访问内存空间。其实在很多情况下会导致这里出现“ 非法访问内存空间” ,但该处出现的原因是:数组越界访问了。肯定有同学会问:怎么又会越界访问呢?上面不是已经解决这个问题了吗?那就只能说明你思考问题的时候太片面。上面的确解决了这个问题,但你要知道这里可是出现了向外扩张的递归啊,你那拓展出来最外围的一圈坐标完全无法起到限制的作用,无法阻止炸金花式的向外展开。所以当然会出现越界访问啦!!!
那该怎么解决呢?其实很简单只要能想到上面那层,就很容易得出:每次进入递归函数后加一条限制不就行了。具体怎么做看下面的代码自己领悟吧。
//炸金花式展开函数
void explode_spread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
//限制非法坐标的展开
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//计算该位置附近四周地雷的个数
int count = get_mine_count(mine, x, y);
//若四周没有一个地雷,则需要向该位置的四周展开,直到展开到某个位置附近存在地雷为止
if (count == 0)
{
//把附近没有地雷的位置变成字符 “空格”
show[x][y] = ' ';
int i = 0;
//向四周共8个位置递归调用
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
//限制对点位置的重复展开调用,使得每一个位置只能向四周展开一次
if (show[i][j] == '*')
{
explode_spread(mine, show, row, col, i, j);
}
}
}
}
//若四周存在地雷则应该在这个位置上标注上地雷的个数
else
{
show[x][y] = count + '0';
}
}
}
该功能实现的是在每次排查后都有一次选择的机会,是否要标记地雷的位置。若要则输入‘ Y ’ ,之后需要输入想要标注的坐标位置,棋盘上是以字符 ‘ !’ 作为标记的;若不要则输入 ‘ N’ ,程序接着执行下一步。
//标记地雷位置(方便排查游戏中的地雷)
void sign_mine(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要标记的坐标:");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
show[x][y] = '!';
break;
}
}
system("cls");
display_board(show, row, col);
}
头文件
#include
#include
#include
#define ROW 9
#define COL 9
#define ROWS ROW + 2
#define COLS COL + 2
#define COUNT 10
void init_board(char board[ROWS][COLS], int rows, int cols, char set);
void set_mine(char mine[ROWS][COLS], int row, int col);
void display_board(char board[ROWS][COLS], int row, int col);
void find_mind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
源文件
#include"game.h"
void menu()
{
printf("*****************************************\n");
printf("********* 1.play 0.exit *********\n");
printf("*****************************************\n");
}
void game()
{
//存放地雷的棋盘
char mine[ROWS][COLS] = { 0 };
//展示给用户看的棋盘
char show[ROWS][COLS] = { 0 };
//初始化棋盘
init_board(mine, ROWS, COLS, '0');
init_board(show, ROWS, COLS, '*');
//放置地雷
set_mine(mine, ROW, COL);
system("cls");//清空屏幕
//打印棋盘
//display_board(mine, ROW, COL);
display_board(show, ROW, COL);
//开始扫雷
find_mind(mine, show, ROW, COL);
}
int main()
{
//设置随机数的起点
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();//菜单
printf("请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
源文件
#include"game.h"
//初始化棋盘
void init_board(char board[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
//设置地雷
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = COUNT;
while (count)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
//打印棋盘
void display_board(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("---------------扫雷游戏----------------\n");
//打印列号
for (i = 0; i <= row; i++)
{
printf(" %d ", i);
}
printf("\n");
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
//打印行号
printf(" %d ", i);
//打印棋子行
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
//打印分隔行
if (i <= row - 1)
{
printf(" ");
for (j = 1; j <= col; j++)
{
printf("---");
if (j <= col - 1)
{
printf("|");
}
}
printf("\n");
}
}
printf("---------------扫雷游戏----------------\n");
}
//计算周围一圈范围内存在地雷的个数
int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
//把3×3方格内所有的字符(‘1’或‘0’)加起来,然后再统一减去9个字符‘0’,得到的结果就是地雷的个数
int i = 0;
int sum = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
sum += mine[i][j];
}
}
return (sum - 9 * '0');
}
//把所有mine中地雷全部显示到show上
void show_all_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (mine[i][j] == '1')
{
show[i][j] = '@';//地雷用字符‘@’在用户界面表示
}
}
}
}
//炸金花式展开函数
void explode_spread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
//限制非法坐标的展开
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//计算该位置附近四周地雷的个数
int count = get_mine_count(mine, x, y);
//若四周没有一个地雷,则需要向该位置的四周展开,直到展开到某个位置附近存在地雷为止
if (count == 0)
{
//把附近没有地雷的位置变成字符 “空格”
show[x][y] = ' ';
int i = 0;
//向四周共8个位置递归调用
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
//限制对点位置的重复展开调用,使得每一个位置只能向四周展开一次
if (show[i][j] == '*')
{
explode_spread(mine, show, row, col, i, j);
}
}
}
}
//若四周存在地雷则应该在这个位置上标注上地雷的个数
else
{
show[x][y] = count + '0';
}
}
}
//标记地雷位置(方便排查游戏中的地雷)
void sign_mine(char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while (1)
{
printf("请输入要标记的坐标:");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
show[x][y] = '!';
break;
}
}
system("cls");
display_board(show, row, col);
}
//开始扫雷
void find_mind(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;//记录排查出不是地雷位置的个数
char ch = 0;
while (win < row * col - COUNT)
{
printf("请输入要排查的位置下标:");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入下标是否有效
{
if (mine[x][y] == '1')//排查到了地雷
{
break;
}
//此时没有排查到地雷
else
{
//炸金花式展开
explode_spread(mine, show, row, col, x, y);
system("cls");
//打印棋盘
display_board(show, row, col);
printf("需要标注地雷就输入:Y,不需要标注地雷则输入:N\n");
//清空一下缓冲区
while ((ch = getchar()) != '\n');
scanf("%c", &ch);
switch (ch)
{
case 'Y':
//标记雷的位置
sign_mine(show, row, col);
break;
default:
break;
}
}
}
else
{
printf("输入下标非法,请重新输入:\n");
}
}
//把所有mine中地雷全部显示到show上
show_all_mine(mine, show, row, col);
system("cls");
//打印棋盘
display_board(show, row, col);
//判断是否排查成功
if (win >= row * col - COUNT)
{
printf("恭喜你排查出所有的地雷!!!\n");
}
else
{
printf("扫雷失败,你被炸死了!!!\n");
}
}
这就是我今天这一期所带来的扫雷游戏的实现,希望多多关注我的其他博客,我想你会收获很多你完全不了解的新知识!!!再次感谢你的捧场。
这份博客如果对你有帮助,给博主一个免费的点赞以示鼓励欢迎各位点赞评论收藏⭐️,谢谢!!!
如果有什么疑问或不同的见解,欢迎评论区留言欧。