前言:
本文介绍的扫雷游戏的实现,所用到的知识点不难,只需大致掌握以下知识点的基本内容即可:数组,循环语句,分支语句,函数的定义及实现,简单的宏定义。
注:本文章中标题后面带“!!!”代表需要有需要注意的细节或包含实用干货。
由于本人写作经验尚浅,完整的代码实现可查看本人的gitee仓库:扫雷游戏实现
要想实现扫雷,首先我们得先了解它的游戏过程和实现的基本逻辑。
—> 以下步骤应该是一个循环的过程
一个菜单的基本选项是开始与结束。(本文章设置输入1为开始游戏,0为结束游戏,具体方便之处可以先参考实现代码)。
//void game()
//{
// char mine[ROWS][COLS];
// char show[ROWS][COLS];
// InitBoard(mine, ROWS, COLS, '0');
// InitBoard(show, ROWS, COLS, '*');
// DisplayBoard(show, ROW, COL);
// SetMineBoard(mine, ROW, COL);
///* FineMine1(mine, show, ROW, COL); */ //排雷初始版本
// FineMine2(mine, show, ROW, COL); //排雷进阶版本
//}
void menu() //简易菜单的打印
{
printf("************************\n");
printf("******* 1. play ********\n");
printf("******* 0. exit ********\n");
printf("************************\n");
}
int main()
{
int input;
do
{
menu();
printf("请选择:>");
scanf("%d", &input);
while (getchar() != '\n'); //可防止用户输入非法字符导致死循环,如:'a',"asd"等
switch (input)
{
case 0:
printf("退出游戏\n");
break;
case 1:
game(); //用于集中管理游戏实现的各个函数
break;
default:
printf("输入错误\n");
break;
}
} while (input);
return 0;
}
若用户正常输入数字1或0,使用输入的整型变量input作为循环条件可保证开始与结束,输入其他数字则进行提示后重新进行输入。
**!!!**但这里可能出现另一种情况,用户若输入一个字符或者字符串,则该程序会进入死循环,原因是:当输入非法字符时(要求的类型和实际输入的类型不同),scanf会直接跳过该次输入,并且输入的内容会被存放在缓冲区内。此时input为初始值,重新进入循环条件判断,因为scanf的缓冲区已有内容,所以会自动跳过input的输入,造成switch语句每次都执行default的内容。
—>解决这个问题的办法是:循环使用getchar()直到将缓存区中输入的非法字符读取完。
while (getchar() != '\n');
首先,由扫雷的逻辑可知需要创建2个棋盘:一个用于雷区的存储,一个用于扫雷过程中雷区分布情况的展示。
棋盘的创建可以使用二维数组,同时为了改变扫雷游戏的难度,能更加方便地调整棋盘大小,这里推荐使用宏定义来决定二维数组的大小。
对用户来说,棋盘的展示实际只需要9 * 9的大小,因此可以定义两个宏ROW和COL分别对应行和列。
考虑到后面用户排雷时选择坐标可能选择到棋盘四周的位置,在遍历其3 * 3范围内雷的分布情况时,(假设此时游戏棋盘大小为9 * 9),若真实创建的二维数组大小也为9 * 9,则遍历雷的分布情况时可能造成数组的越界访问,因此真实创建的数组的 行ROWS 和 列COLS 都应该各加上2。
#define ROW 9 //用户展示棋盘大小
#define COL 9
#define ROWS ROW+2 //实际创建棋盘大小
#define COLS COL+2
char mine[ROWS][COLS]; //布雷棋盘
char show[ROWS][COLS]; //用户展示棋盘
** ** 实际创建的棋盘如下图所示:
对于雷区布置的棋盘,本文推荐对某位置存在雷,存放字符** ‘1’** ;对于其它非雷区存放字符** ‘0’ **。后面5.2.1中信息展示的实现会具体说明原因。(实际的实现也可以依个人喜好存放不同的字符)
对用户展示的棋盘,本文章用字符** ‘*’ **进行初始化,具体实现可以用个人喜欢的字符替代。
实现的代码如下:
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*'); //都放在game()函数中
void InitBoard(char board[ROWS][COLS], int row, int col, char set)
{
int i, j;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
本文中对雷的分布位置存放字符 ‘1’,并且每局游戏生成的雷区都不一样。那么问题来了:如何在不同位置随机布置雷呢?
—>这里需要用到**int rand(void)**函数,它的作用是:产生一个随机数。
** !!!而使用rand函数之前,需要先使用另一个函数void srand(unsigned int seed)**,*它是随机数发生器的初始化函数。由函数的参数可知:实参需要输入一个无符号整型,但如果实参是一个常量或一个不会自动变化的变量a,如srand(1)、srand(a),则rand函数返回值也是一个固定的整型值。
那么如何解决这个问题呢?
—>这里需要在srand函数中使用另一个函数time_t time(time_t timer)**作为实参,它的作用是:返回当前时间的时间戳。(具体的原理这里不展开讲述,如果感兴趣可自行去了解)
注:srand( (unsigned int) time(NULL) )在整个程序中只需使用一次,因此可直接放在主函数main()之中。
解决了随机数的生成问题,就可以进行随机坐标的生成了。(以9 * 9的游戏棋盘为例,棋盘随机分布了10颗雷)
前面说过:实际创建的棋盘大小为11 * 11,而使用的区间为9 * 9,所以随机生成的坐标可用:
int x = rand() % 9 + 1;
布置雷的具体代码实现如下:
#include //使用rand函数与srand函数所需的头文件
#include //使用time函数所需的头文件
srand((umsigned int)time(NULL)); //放在主函数main()中
void SetMineBoard(char board[ROWS][COLS], int row, int col)
{
int x, y; //x,y分别是随机生成的雷的横纵坐标
int count = Easy_Mine_Count; // Easy_Mine_Count为自定义布置的雷的数量
while (count > 0)
{
x = rand() % 9 + 1;
y = rand() % 9 + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
用户游戏棋盘的打印比较简单,但有几个需要注意的点:
具体的实现代码如下:
DisplayBoard(show, ROW, COL); //放在game()函数中
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i, j;
printf("******扫雷游戏******\n");
for (j = 0; j <= col; j++) //打印棋盘前先打印所有列号
printf("%c ", j + '0');
printf("\n");
for (i = 1; i <= row; i++) //打印用户棋盘
{
printf("%c ", i + '0'); //打印每行内容前,先打印一个行号
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
对于获取的排雷坐标,可能出现以下4种情况:
当用户输入合法坐标且所在位置未被排查过时,将该坐标的信息替换成四周雷的数量,并打印游戏棋盘的信息。
若用户输入的坐标为(x,y),当mine数组中字符 ‘1’ 表示该位置存在雷时,若mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * ‘0’ 即可得到该位置四周的雷的数量。
注:‘0’ + a 可得到字符 ‘a’。(a为整型,且0<= a <= 9)
若用其他字符作为雷的标志,则需另外创建一个计数器count,逐个对比每个位置是否为设定的雷的标志,是则count++。
在9 * 9的棋盘中,若布置了10颗雷,则需要排查71个位置,即展示71次雷区信息就能获得胜利,因此可以设置一个整型标志win,每成功排查一次,win的值加1,直到win的值为71时结束排查,游戏胜利。
整个模块具体的实现代码如下:
int GetMineCount(char board[ROWS][COLS], int x, int y)
{
return board[x - 1][y - 1] + board[x - 1][y] + board[x - 1][y + 1] + board[x][y - 1] + board[x][y + 1]
+ board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] - 8 * '0';
}
void FineMine1(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) //排雷 初始版本
{
int x, y;
int win = 0;
while (win < ROW * COL - Easy_Mine_Count)
{
printf("请输入你要排查雷的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("输入坐标无效,请重新输入");
break;
}
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了,本局游戏结束\n");
DisplayBoard(mine, ROW, COL);
printf("注:数字1代表该位置有雷\n");
break;
}
else
{
int count = GetMineCount(mine, x, y); //得到雷的数量
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
}
if (win == ROW * COL - Easy_Mine_Count)
printf("所有的雷已排完,恭喜你获得本局游戏的胜利!\n");
}
经过上面的步骤,扫雷的整个运行逻辑似乎已经完成了,但不知大家是否有这样一个疑问:“这个扫雷跟我平时玩过的有点不一样,它每次得到的有效信息这么少,这样每局游戏玩的时间和游戏难度不是大大增加了吗?”
—>没错,上面的游戏实现可以算是一种“残血”版的扫雷。
<—排查坐标为(5,5)
**!!!**那么怎么实现排查一个位置,一下子得到出一大片的有效信息呢?
通过观察我们可以得知:
当选择的排雷位置四周不存在雷时,这个位置不显示任何信息,并对四周的每个位置再进行一次雷区情况的排查,若四周其中任意一个位置四周依然不存在雷,则再对该位置的四周进行雷区情况的排查,直到所排查的位置四周存在至少1个雷时停止该操作,只显示当前排查位置四周雷的信息。
—>通过这个规律易知:此操作具有递归性。
这里的递归操作有2个需要注意的点:
例如:假设粉红色区域为第一次排查的位置,它的四周即蓝色斜线区域都不存在雷,并且粉红色区域左上角位置的周围也没有雷,也需要进行递归操作,但是排查的位置包含了第一次排查过的位置,即粉色区域的位置,如果不对递归加以限制,递归就会进入死循环。
—>因此,我们可以先将排查过的且四周无雷的位置存放字符空格 ’ ’ ,在递归过程中,若访问的位置show数组中字符为 ’ ’ 时,不再进行雷区情况的遍历。
因此限制条件如下:
if(show[x][y] == ' ' )
return;
因此限制条件如下:
if(x < 1 || x > 9 || y < 1 || y > 9)
return;
与初阶版的扫雷一样,游戏同样需要一个标志以判断游戏的胜利,所以这里采取将整型变量win作为函数参数,每排查一个位置,win的值加1。(win需要传址,即&win)
整个模块具体的实现代码如下:
void GetMineCount2(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* win)
{
if (show[x][y] == ' '|| x < 1 || x > 9 || y < 1 || y > 9)
return;
int count = mine[x - 1][y - 1] + mine[x - 1][y] + mine[x - 1][y + 1] + mine[x][y - 1] + mine[x][y + 1]
+ mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0';
if (count == 0 )
{
show[x][y] = ' ';
(*win)++;
GetMineCount2(mine, show, x - 1, y - 1, win);
GetMineCount2(mine, show, x - 1, y, win);
GetMineCount2(mine, show, x - 1, y + 1, win);
GetMineCount2(mine, show, x, y - 1, win);
GetMineCount2(mine, show, x, y + 1, win);
GetMineCount2(mine, show, x + 1, y - 1, win);
GetMineCount2(mine, show, x + 1, y, win);
GetMineCount2(mine, show, x + 1, y + 1, win);
}
else if(count && show[x][y] == '*')
{
show[x][y] = '0' + count;
(*win)++;
}
}
void FineMine2(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x, y;
int win = 0;
while (win < ROW * COL - Easy_Mine_Count)
{
printf("请输入你要排查雷的坐标:>");
scanf("%d%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("输入坐标无效,请重新输入\n");
}
else if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, row, col);
printf("注:数字1代表该位置有雷\n");
break;
}
else
{
GetMineCount2(mine, show, x, y, &win);
DisplayBoard(show, row, col);
printf("\n\n");
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
}
if (win == ROW * COL - Easy_Mine_Count)
printf("所有的雷已排完,恭喜你获得本局游戏的胜利!\n");
}
结语:这是我的第二篇个人博客,若有错误或者不足的地方,欢迎大家指出错误或给出建议,谢谢大家的观看。
由于本人写作经验尚浅,完整的代码实现可查看本人的gitee仓库:扫雷游戏实现