在实现扫雷游戏的过程中,应该有一段代码模拟大致的框架,有一段代码实现游戏的功能,有一段代码放置头文件和函数声明。所以在实现扫雷时,我将代码分为了以下三个部分:
1、test.c 2、game.c 3、game.h
分别存放上述的三部分代码
一、游戏的功能实现
1、大致框架的模拟
1.1、逻辑分析:
进入游戏以后,应当给出游戏菜单,菜单上应该至少有1、开始游戏、2、退出游戏这两个选项,当选择开始游戏以后,则进入游戏,调用游戏的函数,当选择退出游戏以后就结束游戏。如果选择了别的选项(即不是1和0),应该提示玩家选择错误,重新选择。当玩家玩过一局扫雷后,应该重新进入菜单界面,再次询问玩家是开始游戏还是退出游戏。
1.2、功能实现:
由于玩家只要一直选择1、开始游戏就会循环进行游戏,所以这里用循环结构比较好。这里首先进入游戏以后,一般都会直接给出扫雷游戏的初始菜单界面,就相当于刚开始就已经执行了一次打印菜单功能了,这种情况下应该选择三种循环结构中的do while循环。而循环条件就应该是当玩家选择的不是零的时候。所以循环框架应该是这样。
int main()
{
int input = 0;
do {
} while (input != 0);
return 0;
}
在内部的话,首先应该打印出菜单,让玩家看到菜单,因此定义menu()函数如下
void menu()
{
printf("**************************\n");
printf("******* 1、开始游戏*******\n");
printf("******* 0、退出游戏*******\n");
printf("**************************\n");
}
随后提醒玩家输入,用到printf和scanf函数
下面根据玩家输入的数字来执行不同的命令,可以想到用switch语句的分支结构。1的情况下就调用game()函数开始游戏,0的情况就break跳出循环,结束游戏。不是0和1就提示输入错误。可用下面这段代码来完成任务。
int main()
{
int input = 0;
do {
menu();
printf("请输入:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏");
break;
default:
printf("输入错误");
break;
}
} while (input != 0);
return 0;
}
这样游戏的大致框架就写好了。下面进行game()函数游戏部分的构建。
2、游戏部分的构建
2.1、逻辑分析:
大致的游戏思路:扫雷游戏有一个二维的棋盘,上面随机放着雷,玩家通过分析选择所有没有雷的格子来获胜。因此需要设置棋盘的行数和列数,要有一个函数完成棋盘上地雷的随即放置,要有一个函数完成玩家选择某个位置。
在布置地雷时,可以用1表示有地雷,0表示没有地雷,如下图
细节:
1、在玩家的视角里,棋盘最开始应该全部是 * ,当玩家选择一个位置以后,会统计这个位置周围的地雷的数量,然后在这个位置标志出来。所以应该设置两个棋盘,其中一个给玩家看到的,上面显示*以及已经选过的位置周围的地雷个数,一个棋盘用来放置0和1(即地雷和无地雷)。因此设置show棋盘和mine棋盘。
2、在一局游戏开始前,棋盘应该被重置(初始化),并且再次重新放置地雷。
3、当玩家选到地雷以后应该给玩家看一下所有雷的位置。
游戏逻辑:
1、先初始化棋盘,将show棋盘全部变成*,将mine棋盘全部变成0。
2、再放置地雷,这一步应该随机将mine棋盘中的一些位置变成1。而show棋盘不发生变化。
3、把show棋盘打印给玩家看。
4、让玩家选择位置(这里采用横纵坐标来表示棋盘上的某个位置),判断该位置是不是雷,是雷的话就提醒玩家被炸死了,游戏结束,不是雷就统计它四周雷的个数,将show棋盘上此位置的*改成四周雷个数。
5、当玩家查找了所有的非雷的位置后,应该提醒玩家获胜。也就是说当玩家查找了(长*宽-地雷个数)次后,还没有被炸死,说明玩家获胜。
2.2参数设置:
在game.h中设置好想要的棋盘的ROW长,COL宽,NUM地雷的个数。这里还定义了ROWS和COLS,其实也就是让棋盘多两行两列,这样让我在后续处理问题的过程中会方便很多,后面会讲到。
#define NUM 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
2.3功能实现:
2.3.1、初始化棋盘
创建两个二维数组,为了下面调用函数方面,都两个二维数组都创建为char[][]类型的。
定义initboard()函数,只需要嵌套for循环,遍历棋盘中的每一个元素,将其改为设定的元素即可,因此需要传参 数组 行 列 元素
在game.h中声明
void initboard(char arr[ROWS][COLS], int rows, int cols,char set);
在game.c中编写代码
void initboard(char arr[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++)
{
arr[i][j] = set;
}
}
在test.c中调用
initboard(mine, ROWS, COLS, '0');
initboard(show, ROWS, COLS, '*');
这样就实现了棋盘的初始化。
2.3.2、地雷的安置
安置地雷的最大问题就是怎么随机。这里可以采用rand函数,并且使用当前时间作为种子,这样也可以模拟“随机”。具体的安置地雷的方法就是随机出一个行数,随机出一个列数,然后在该行该列安置一个地雷,如果此处已经有地雷了,那么就不安置,如果没有地雷,那么就安置。可以使用while循环来实现这个功能。
循环的中止方法:可以设置一个计数器,在安置地雷后计数器加一,当计数器大于要安置的地雷个数后停止安放。
有个细节,由于rand函数随机数可能会超过行和列,所以此处可以进行取模后加一,这样随机数的取值就在1和行数(列数)之间了。(ps:取模后值的范围为0到行数-1(列数-1))
在game.h中声明
void Setmine(char arr[ROWS][COLS], int rows, int cols, int num);
在game.c中编写代码
void Setmine(char arr[ROWS][COLS],int row,int col,int num)
{
int i = 0;
int count = 0;
while (count < num)
{
int a = rand() % row+1;
int b = rand() % col+1;
if (arr[a][b] == '0')
{
//成功布置一个雷,count--
arr[a][b] = '1';
count += 1;
}
}
}
在test.c中调用
Setmine(mine,ROW,COL,NUM);
2.3.3、玩家查找功能的实现
这一部分是最复杂的部分,要完成的功能有:玩家输入坐标、判断坐标处是否有地雷、如果有地雷直接结束游戏并且打印出棋盘、如果没有地雷则需要统计周围一圈的地雷个数然后放到show数组相应位置处、打印新的show数组、循环往复。
先完成最简单的玩家输入坐标的函数,并且先想好需要用到的函数:打印棋盘 print()函数,统计周围一周的地雷个数 numcount函数。
以下是这段代码
int a = 0;
int b = 0;
int count = 0;
printf("请输入坐标:");
scanf("%d %d", &a, &b);
if (a >= 1 and a <= row and b >= 1 and b <= col)
{
if (mine[a][b] == '1')
{
printf("很抱歉,你被炸死了\n");
print(mine, ROW, COL);
}
else
{
int c = numcount(mine, a, b);
show[a][b] = c+'0';
count += 1;
print(show, ROW, COL);
}
}
else
printf("输入坐标不合适,请重新输入");
想要循环往复的话,则需要加一个while循环语句,而跳出循环的条件可以设置为已经查找的次数是否大于(长*宽-地雷个数),当查找了这么多次后还没有踩雷,说明已经查找完了所有的空处,游戏获胜。
因此这段代码完善后应该是这样(接下来的部分,相应的函数声明和函数调用不再展示,此处仅展示函数的实现)
void Findboard(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int num)
{
int a = 0;
int b = 0;
int count = 0;
while (count<row*col-num)
{ printf("请输入坐标:");
scanf("%d %d", &a, &b);
if (a >= 1 and a <= row and b >= 1 and b <= col)
{
if (mine[a][b] == '1')
{
printf("很抱歉,你被炸死了\n");
print(mine, ROW, COL);
break;
}
else
{
int c = numcount(mine, a, b);
show[a][b] = c+'0';
count += 1;
print(show, ROW, COL);
}
}
else
printf("输入坐标不合适,请重新输入");
}
}
接下来实现print 打印棋盘的函数功能。
其实这个也是比较简单的,只需要嵌套循环遍历就可以了。但是这样打印出来只是一个棋盘,不能方便玩家查看坐标,所以可以把每行每列的坐标也打印上去。
void print(char arr[ROWS][COLS], int row, int col)
{
int i = 1;
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i=1; i <= row; i++)
{
int j = 1;
printf("%d ", i);
for (j=1; j <= col; j++)
printf("%c ", arr[i][j]);
printf("\n");
}
}
打印的结果如图所示,相比于没有序号,是不是就更加清晰了?
接下来实现numcount函数。
函数的逻辑也很清晰,就是将所选位置周围八个位置的数字加起来。因为有雷的数字是1,没雷的数字是0,所以他们的和恰好就是它周围的雷的数量。需要注意的是,我们在上面的棋盘上插入的地雷实际上都是字符类型的数字,所以在进行计算的时候还需要把他们减去字符‘0’,这样才是他们代表的数字。
是不是会想到那边上的呢?边上的位置周围就没有八个数字了呀。还记得在设置参数的时候多加了两行两列吗?他们的功能在这里就体现出来了。多设置的两行两列相当于在棋盘周围多加一圈我们没有办法去查看的‘0’,这样就方便了我们对所有位置的地雷位置的统计。即使是边缘的位置,在计算的时候只是会加上边缘的边缘上的0而已,(即多加的两行两列上的0)
可以参考下面这个图片。红色线框内是棋盘,多加的两行两列如图在上下左右围成一圈。当选择了问号位置的时候,它就会计算它周围一圈的数字和,而在棋盘之外的都是0,所以加起来不会影响实际数值。
接下来实现代码。
int numcount(char mine[ROWS][COLS], int x, int y)
{
return mine[x][y - 1] +
mine[x - 1][y] +
mine[x - 1][y - 1] +
mine[x - 1][y + 1] +
mine[x][y + 1] +
mine[x + 1][y ] +
mine[x +1][y - 1] +
mine[x + 1][y + 1] -8*'0';
也就是把周围八个位置的值加起来,最后别忘了减去八个‘0’的值就好了。
二、一些问题
不难发现,这个代码实际上只简单的完成了功能,但是距离还原原本扫雷还有很多问题。
1、关于自动成片散开的问题。原版扫雷中,选择了一个位置,如果它周围的地雷数是0的话,那么他就会自动的把四周八个位置也显示出来,而我们写的扫雷则需要自己一个一个地方去排查。
2、其实在结束游戏的循环是有一些问题的,比如如果玩家连续输入同一个不为地雷的坐标(长*宽-地雷个数)次数后,也会提示玩家获胜。
3、此外还有界面的优化问题,如上代码运行起来的在控制台的界面相当混乱,对玩家非常不友好。
除此之外还有许许多多的问题,读者有兴趣可以自己尝试着解决。这里只讲了游戏最基本能实现的代码。