今天我们一块利用C语言的知识来实现一个经典游戏——扫雷。
一、 游戏规则的介绍:
这里给大家展示一个扫雷游戏网页版,如图一所示:
(图一)
棋盘中有数字,有空白区域,有未知区域,空白区域和图一中的数字都是属于无雷的安全区域,那图中数字有啥含义呢:它表示以它为中心周围的八个坐标中雷的个数,而边界不及八个的则统计的就是以它为中心周围五个坐标中雷的个数。同时在网页版的扫雷游戏中玩家可以在他认为是雷的位置标记一个旗帜,而在这里我们就不做这一功能的实现了。
二、设计游戏的思路讲解附棋盘设计代码:
扫雷游戏思路简单总结如下:
- 由于一个字符数组元素不能在同一时间赋值两个不同的值,因此扫雷游戏的实现我们需要 至少两个棋盘,其中一个棋盘用来存放雷与其他不是雷的坐标。另一个棋盘我们给玩家进行展示,因此需要两个数组,我们不妨将前面的数组叫做mine数组,后面接下来要展示给玩家的数组我们称之为show数组。
- 如果玩家踩到了雷,我们应该去让我们的游戏结束;同时如果所有的雷都被玩家找到了,我们也应该让游戏结束。
- 如果这个地方不是雷,我们就去统计它周围的坐标有多少个雷,并将结果对我们的玩家进行展示,以便玩家后续的推理。那这个非雷坐标周围没有雷呢,我们就将他置空(而非赋值为0!)
思路比较清晰了之后,我们接下来来思考两个问题:
其一:我们的数组应该是什么类型的;其二:数组的长度怎么去设计最为合适。
问题一很显然,我们设计两个字符数组即可,字符数组可以有各种各样的图案供我们去选择,以此就能很好区分雷与非雷,以及其他后续我们会考虑到的情况。
问题二我们来想一下,我们游戏的棋盘是9*9的大小(以图一为例),我们因此去设计一个9*9的大小的棋盘。。。好像有道理哦!但是我们有说过如果这个坐标不是雷,我们需要去统计它周围8个坐标雷的情况,而位于边界的坐标,我们只需要统计周围5个坐标就可以了,嗯......这种不同坐标操作的不协调性很不利于我们后面写代码的方便性,代码会很复杂的。所以这里我们不妨在原来9*9的大小规格上再加一圈,使用11*11大小的棋盘规格,这样去检查周围有多少个雷的时候就都是按照周围8个坐标的标准去检查了!
综上的探讨之后,我们不难设计出以下程序代码,展示出来的棋盘效果则如图二所示(棋盘的样式全凭自己的爱好来设计即可,作者的棋盘样式仅供参考哦!):
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i, j;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i, j;
//n是列坐标从1开始
int n = 1;
printf("-----------------扫雷-----------------\n");
printf(" ");
//打印行坐标
for (i = 1; i <= row; i++)
{
printf(" %d ", i);
}
//打印行坐标后换行打印棋盘
printf("\n");
for (i = 1; i <=row+1; i++)
{
printf(" ");
//每一行打印 |---|---|图案,打印10列“|”
for (j = 0; j < col+1; j++)
{
printf("|");
//打印9列“---”
if (j < col)
{
printf("---");
}
}
//打印一行之后换行
printf("\n");
//数据行只需要9行,用if语句进行控制,使其只打印9行
if (i <=row)
{
printf("%d", n);
for (j = 1; j <=col + 1; j++)
{
printf("|");
if (j <=col)
{
printf(" %c ", board[i][j]);
}
}
printf("\n");
n++;
}
}
}
(图二)
三、玩家操作游戏,扫雷功能的实现:
首先我们要在棋盘中布置好雷,对不对!(这里我们用字符0表示非雷,1表示雷),这是rand随机数函数需要给我们做的事情,前面一些博客我们已经对这一点有了详细的解读和介绍,这里便不再赘述。然后我们需要让我们的玩家去输入一个坐标(x, y),这就是玩家他要去排查雷的坐标。然后我们程序,我们的计算机要去做的事情,就是去检查这个坐标是否合法,如果是个合法的坐标,我们再去看一看这个位置的坐标是不是为雷,是雷了我们接下来要去做什么,不是雷我们又要去做什么,对不对!而这些都可以通过if else的多分支判断来解决,这也不是问题。接下来我们需要重点来讨论一下的是“如果这个位置不是雷,我们需要做什么”的问题。
有朋友在这里可能会想,这个位置不是雷,我统计一下这个位置有多少个雷,然后将这个信息通过我们的show数组告诉给我们的玩家不就可以了吗?是这么一回事,但问题的关键在于怎么去传递信息的问题。如果我们在检查的时候,只针对玩家输入的这个排雷坐标(x, y)这一个位置进行检查的话,那么一次只能展示一个空格的信息。。。嗯......万一有一天我输入的这个坐标周围没有雷的,那玩家一眼就能明白,这周围没有雷嘛!那他接下来要去做的事情则是去依次输入以(x, y)为中心周围八个坐标,将它们一个一个展开,这很浪费时间的,也是一些没有意义的操作,这会让我们的游戏体验很不爽,玩游戏嘛!注重的就是一个游戏体验。所以我们想一下,可不可以让计算机来为我们做这件事情呢?
这是可以做到的,接下来我们给大家介绍一种通过函数递归调用的方式,来展开这么一大片区域的功能。
函数递归调用思路的讲解:
- 我检查这个(x, y)坐标周围有没有雷,如果发现没有雷,我再去检查一下它周围的八个坐标是不是也没有雷,有雷我们就停下来输出信息告诉我们玩家,这个坐标周围有多少个雷;没有雷我们就接着重复这一过程直到发现周围有雷的坐标出现为止。
- 注意事项:其一,我们为了减少我们程序运行过程中的压力,已经检查过的位置(被置空或者已经被标记数字,换句话说,不再是未知区域的坐标)我们就不再对它进行检查。其二,我们只对数组下标在1-9的位置的数组元素进行检查,不在这个范围内的坐标不且不能进行检查,因为进行检查了可能就会导致我们数组越界访问的问题,这会导致我们最后的程序出现bug。
基于以上的讨论和考虑,我们可以设计出如下程序代码来实现我们的功能:
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % ROW + 1;
int y = rand() % COL + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
static int GetmineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y] + mine[x + 1][y] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y + 1] + mine[x - 1][y - 1] + mine[x + 1][y - 1] + mine[x - 1][y + 1] - 8 * '0';
}
static void SpreadBoard(char mine[ROWS][COLS],char show[ROWS][COLS], int x, int y)
{
int c = GetmineCount(mine, x, y);
int i, j;
/*c == 0保证了这个位置周围八个坐标都不是雷
其中这八个坐标,有一部分周围无雷,我们就去空置它*/
if (c == 0)
{//c==0了,但考虑到这个位置可能已经被检查了,我们使用show[x][y]=='*',只对未检查的区域进行检查
show[x][y] = ' ';
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if(show[x+i][y+j]=='*' && (x + i <= ROW && x + i >= 1) && (y + j <= COL && y + j >= 1))
{
SpreadBoard(mine, show, x + i, y + j);
}
}
}
}
/*还有一部分周围有雷,我们就再去显示它周围有几个雷*/
else
{
show[x][y] = c + '0';
}
}
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int i, j;
int win = 0;
while (win!=EASY_COUNT)
{
win = 0;
printf("排查雷,请输入一个坐标:>\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
printf("将在5s之后进行下一次选择\n");
DisplayBoard(mine, ROW, COL);
Sleep(5000);
system("cls");
break;
}
else
{
SpreadBoard(mine, show, x, y);
system("cls");
DisplayBoard(show, ROW, COL);
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (show[i][j] == '*')
win++;
}
}
}
}
else
{
printf("坐标输入非法,请重新输入坐标:>");
}
}
if (win == EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
}
}
这个程序里面用到了两个可能对于部分小伙伴不太熟悉的C语言库的函数:system和Sleep,这里将它们的功能简单陈述如下:
system | |
头文件 | #include<stdlib.h> |
格式 | system("命令字符串"); |
功能 | 根据命令提示在程序运行终端去执行这些命令,比较常见的命令有: cls:清空控制台的信息 dir:展开当前的文件目录信息 shutdow -s -t 60:让计算机在60s之后关机,其中60表示时间,可更改 shutdow -a:终止shutdow -s -t的效果 |
返回值 | 无 |
Sleep | |
头文件 | #include<windows.h> |
格式 | Sleep(unsigned int time); |
功能 | 让程序运行时在这里停留一段时间,time参数是我们需要去输入的时间,这个时间的单位是毫秒。 |
返回值 | 无 |
四、设计主函数,让程序跑起来吧:
最后我们再对我们的界面进行简单美化处理,将前面所提到的功能在这里整合一下,并且写上我们的主函数main,代码就能跑起来了!需要的小伙伴们可自取哦:
#define _CRT_SECURE_NO_WARNINGS 1
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
#include<stdio.h>
#include<stdlib.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//展示游戏棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col);
//排雷
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
void menu()
{
printf("********** 扫雷 **********\n");
printf("********** 1.开始游戏 **********\n");
printf("********** 0.退出游戏 **********\n");
printf("********************************\n");
}
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
//对两个棋盘进行初始化的过程
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//展示棋盘中对玩家可见的部分
DisplayBoard(show, ROW, COL);
//在棋盘中布置雷
SetMine(mine, ROW, COL);
//让玩家去排雷
Findmine(mine, show, ROW, COL);
}
int main()
{
int input;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新选择:>\n");
break;
}
} while (input);
}
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set)
{
int i, j;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i, j;
//n是列坐标从1开始
int n = 1;
printf("-----------------扫雷-----------------\n");
printf(" ");
//打印行坐标
for (i = 1; i <= row; i++)
{
printf(" %d ", i);
}
//打印行坐标后换行打印棋盘
printf("\n");
for (i = 1; i <=row+1; i++)
{
printf(" ");
//每一行打印 |---|---|图案,打印10列“|”
for (j = 0; j < col+1; j++)
{
printf("|");
//打印9列“---”
if (j < col)
{
printf("---");
}
}
//打印一行之后换行
printf("\n");
//数据行只需要9行,用if语句进行控制,使其只打印9行
if (i <=row)
{
printf("%d", n);
for (j = 1; j <=col + 1; j++)
{
printf("|");
if (j <=col)
{
printf(" %c ", board[i][j]);
}
}
printf("\n");
n++;
}
}
}
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
int x = rand() % ROW + 1;
int y = rand() % COL + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
static int GetmineCount(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y] + mine[x + 1][y] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y + 1] + mine[x - 1][y - 1] + mine[x + 1][y - 1] + mine[x - 1][y + 1] - 8 * '0';
}
static void SpreadBoard(char mine[ROWS][COLS],char show[ROWS][COLS], int x, int y)
{
int c = GetmineCount(mine, x, y);
int i, j;
/*c == 0保证了这个位置周围八个坐标都不是雷
其中这八个坐标,有一部分周围无雷,我们就去空置它*/
if (c == 0)
{//c==0了,但考虑到这个位置可能已经被检查了,我们使用show[x][y]=='*',只对未检查的区域进行检查
show[x][y] = ' ';
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if(show[x+i][y+j]=='*' && (x + i <= ROW && x + i >= 1) && (y + j <= COL && y + j >= 1))
{
SpreadBoard(mine, show, x + i, y + j);
}
}
}
}
/*还有一部分周围有雷,我们就再去显示它周围有几个雷*/
else
{
show[x][y] = c + '0';
}
}
void Findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int i, j;
int win = 0;
while (win!=EASY_COUNT)
{
win = 0;
printf("排查雷,请输入一个坐标:>\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
printf("将在5s之后进行下一次选择\n");
DisplayBoard(mine, ROW, COL);
Sleep(5000);
system("cls");
break;
}
else
{
SpreadBoard(mine, show, x, y);
system("cls");
DisplayBoard(show, ROW, COL);
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (show[i][j] == '*')
win++;
}
}
}
}
else
{
printf("坐标输入非法,请重新输入坐标:>");
}
}
if (win == EASY_COUNT)
{
printf("恭喜你,排雷成功!\n");
}
}