之前给大家讲过两个用C语言实现的小游戏——猜数字游戏和井字棋游戏,两个小游戏都不是特别的难,可以说是用简单的所学知识就把童年的经典游戏复刻了,让我们既熟悉了编程知识,提高了对于编程学习的兴趣,又重温了童年的那些小小的美好。而提到童年经典游戏,扫雷绝对是初遇电脑的我们绕不开的典中典游戏。今天,就在这里和大家一起实现一个简易版的扫雷吧。
目录
扫雷游戏是一款经典的小游戏,网页版或者是windows系统自带的扫雷游戏有分难度,每个难度有不一样的雷的个数以及棋盘大小,比如简单难度:9*9的棋盘里面包含了10个雷。扫雷游戏的规则是在尽量短的时间内依照所点击格子的数字提示,点开所有没有布置过雷的格子,同时避免点到存在雷的格子,如果踩到雷游戏就结束。
如果点击的格子周围8个格子没有雷,则就会向周围展开,如果有雷,就会在格子上显示数字:
同时我们可以标记或者清除标记,标记的位置不能被探查:
?可见,扫雷游戏可实现的功能很多,今天我们要实现的扫雷游戏是9*9大小棋盘含有10个地雷的简单难度的扫雷。
还记得我在猜数字游戏的实现中说过的一句话吗?没错,我们在写编程题或者小游戏代码时,最重要的就是理清实现思路——主体是什么?为了实现目的要创建哪些函数?函数的功能都是什么?只有当我们心中有了一个大体的框架,知道该做些什么时,我们才能更高效地编写代码,完成程序设计。
扫雷游戏的算法实现如下:
1.menu(提供游戏菜单,由玩家选择是否进行游戏:按“1”开始游戏,按“0”退出游戏,按其他则显示“选择错误,请重新选择”。)
2.game( )(进行游戏)
3.本轮游戏结束,打印游戏菜单并再次询问玩家选择。
在上面的算法实现框里面没有写game函数的具体算法实现过程,因为对于扫雷游戏来说,game函数是相当的复杂,写在里面比较麻烦。
我们在思考大体框架时不用过多在意具体函数的实现方法,可以先起个能表达其功能的函数名并把它放在需要的位置。(说白了就是先搞一个空壳函数占位置,等到大体框架调试完毕不再有问题后,再去实现这些空壳函数)大体框架代码如下:
int main()
{
int input=0;
srand((unsigned)time(NULL));//因为雷的位置是随机的,所以大家应该明白这个函数是干嘛的吧?
do
{
menu();
printf("请选择:>");
scanf("%d",&input);
switch(input)
{
case 1:
printf("扫雷!\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
}while(input);
return 0;
}
?我们定义了一个菜单函数,同时在主函数里开始运行,运用switch语句,输入1则进入游戏,输入0则退出游戏,输入其他数字则重新输入,同时我们想:玩一把不过瘾想要继续玩怎么办?用循环结构就好了,我们选择do-while语句先执行后判断,由input控制循环,如果选择了0那正好不符合while的条件退出循环。(注:大部分游戏等都是采用do……while语句,因为大都是先执行后判断)
menu函数代码如下:
void menu()
{
printf("*********************************\n");
printf("********** 1.play ***********\n");
printf("********** 0.exit ***********\n");
printf("*********************************\n");
}
因为game函数比较复杂,所以我们把它提出来单独讲解,并且为了让代码的可读性更好,我们要使用之前在井字棋中讲到的知识点——多文件程序。(有对于多文件程序不了解的,可以看下面文章中的第三部分知识)有趣的代码——井字棋游戏的实现-CSDN博客
?今天我们的代码就分别放在三个文件:test.c、game.c、game.h中。test.c源文件主要用来测试整个小游戏;game.c源文件主要用来完成小游戏各功能的实现;game.h头文件中则用来包含所有的库函数并声明game.c中的函数。这样分文件书写代码段的好处在于:结构化清晰,便于团队合作;易于维护、修改和功能扩张;代码的可读性高。
扫雷游戏的棋盘我们很容易想到用二维数组来建立,但考虑到我们在玩网页版扫雷时,棋盘会根据我们的操作产生变化(如提示周围多少雷和标记可能是雷等等),所以,我们在建立二维数组,应该建立两个二维数组,一个用于存放雷的情况,另一个用于我们进行操作和标记。
注意:二维数组的行列大小确定,玩家点击格子,系统会对该格子的周围8个格子进行探查是否存在雷,如果我们点击的四个边的格子,可能会出现越界的现象,如下图:
为避免越界情况出现(可能会造成错误),我们在建立数组时除了考虑本身要使用的9*9棋盘之余?,还要把四周考虑到,故最终应该建立11*11大小的数组,如下图所示:
//雷的信息存储
//1.布置好的雷的信息
char mine[ROWS][COLS]={0};//11*11
//2.排查出的雷的信息
char show[ROWS][COLS]={0};
这里我们使用宏定义定义ROW、COL为9,ROWS和COLS为ROW+2,这样方便棋盘的扩大和游戏的升级。
上面我们已经建立了两个二维数组,分别是存放布置好的雷的信息的主体数组mine和存放排查出的雷的信息的展示数组show。接下来我们要把两个数组分别进行初始化,在最初的考虑中是想分别建立两个函数进行初始化,但后来想到两个数组本质上一样的,只是我们初始化时赋的初值不同,那么只要我们把相应欲赋的字符也传进去,即可满足两个数组初始化的需求。
代码如下图所示:
/* game.c中的内容 */
void Initboard(char mine[ROWS][COLS], int rows, int cols,char set)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
mine[i][j] = set;
}
}
}
/* tect.c中的内容 */
Initboard(mine, ROWS, COLS,'0');//初始化存放地雷的数组
Initboard(show, ROWS, COLS,'*');//初始化显示信息的数组
我们在玩扫雷游戏过程中,游戏是会及时给我们反馈的。(当我们没有扫到雷,它会提醒我们周围的雷数;当我们扫到雷时,会打印布置的雷的信息)换句话说,就是无论我们扫雷的情况是啥,程序应该都会给我们打印操作后的棋盘,只不过区别在于踩雷时,打印的时布置雷信息的棋盘,让我们看到本次游戏中哪里有雷,而未踩雷时,打印的是我们排查出的雷的信息。所以,我们建立的打印棋盘函数理应打印两个数组,代码如下图所示:
/* game.c中的打印函数实现 */
void DisplayBoard(char board[ROWS][COLS],int row,int col)//传入的数组该是几行就是几行,不能改变数组本身,但是row这种参数,则是根据需要传入
{
int i=0;
int j=0;
//打印列号
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");
}
}
/* tect.c中的内容 */
printf("初始棋盘如下:\n");
DisplayBoard(mine,ROW,COL);
printf("\n");
printf("覆盖,埋雷\n");
DisplayBoard(show,ROW,COL);
具体效果如下图所示(左图存放为布置的雷的信息-暂时未放雷,右图为排查的雷的信息-未排查啥信息也没有)?
就像上面图展示,到目前为止棋盘中是没有雷的,那我们就需要布置雷。
存放地雷的二维数组之前被我们全部初始化为'0',那么我们就让电脑在9*9的范围内随机更改二维数组的值为'1',表示放置地雷。
设置地雷就需要用到随机数的三个函数:
int rand(void)? ? ? ? ? ?void srand(unsigned int seed)? ? ? ? ?time_t time(time_t* timer)
如果对三个函数的用法不太了解,可以访问我的文章:有趣的代码——猜数字游戏的实现-CSDN博客里面的第三部分有相关知识的详细介绍。
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]=='0')
{
board[x][y]='1';
count--;
}
}
}
EASY_COUNT这里我们用符号常量表示雷的个数,方便我们进行调试和修改,同时使代码的可读性更好,便于拓展新功能和新模式。
游戏既然叫扫雷,那么排查地雷很显然是最重要的一环了,在我们今天的简易版扫雷中排查雷只需要能够解决三种情况即可——①失败时:踩雷了,判定游戏失败;②游戏继续时:未踩雷,但也没有把雷排完,需显示周围雷的情况;③获胜时:雷排完了,判定游戏成功。
上面我们已经把排查雷的思路梳理完毕,代码如下图所示:
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col)
{
int x=0;
int y=0;
int win=0;
while(win<row*col-EASY_COUNT)
{
printf("请输入排查雷的坐标:>\n");
scanf("%d%d",&x,&y);
if(x>=1&&x<=row&&y>=1&&y<=col)
{
//坐标的合法性
//1.踩雷
if(mine[x][y]=='1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine,row,col);
break;
}
if(mine[x][y]!='0')
{
printf("您已标记过该坐标,请重新输入\n");
continue;
}
//2.不是雷
else//计算坐标周围几个雷
{
int count=get_mine_count(mine,x,y);
show[x][y]=count+'0';//count本来是个整型+'0'变成char
DisplayBoard(show,row,col);//这里实际上应该有扩展式扫雷,但目前程度不够解决该问题。
win++;
}
}
else
{
printf("输入坐标非法,请重新输入!\n");
}
}
if(win==row*col-EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine,row,col);//获胜了,打印一下布置雷的信息,看看都是哪些地方有雷,也符合原版扫雷
}
}
上面FindMine函数里面还嵌套了一个未讲解的函数get_mine_count,其功能是当游戏处于②继续进行时用于判断周围雷的情况,之所以在定义一个函数,是为了避免函数冗长。
get_mine_count函数的代码如下所示:
int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
return mine[x-1][y]+mine[x-1][y-1]+mine[x][y-1]+mine[x+1][y-1]+mine[x+1][y]+mine[x+1][y+1]+mine[x][y+1]
+mine[x-1][y+1]-8*'0';
}
大家也可以用for循环来解决,当然直接写时间效率高一些,不用嵌套循环。
tect.c的代码如下图所示:
/* tect.c */
#include"game.h"
void menu();
void game();
int main()
{
int input=0;
srand((unsigned)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d",&input);
switch(input)
{
case 1:
printf("扫雷!\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择!\n");
break;
}
}while(input);
return 0;
}
void menu()
{
printf("*********************************\n");
printf("********** 1.play ***********\n");
printf("********** 0.exit ***********\n");
printf("*********************************\n");
}
void game()
{
//雷的信息存储
//1.布置好的雷的信息
char mine[ROWS][COLS]={0};//11*11
//2.排查出的雷的信息
char show[ROWS][COLS]={0};
//初始化
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');//保持神秘感,看不到是否是雷
//打印棋盘
printf("初始棋盘如下:\n");
DisplayBoard(mine,ROW,COL);
printf("\n");
printf("覆盖,埋雷\n");
DisplayBoard(show,ROW,COL);
//布置雷
SetMine(mine,ROW,COL);
//扫雷
FindMine(mine,show,ROW,COL);
}
game.h的代码如下图所示:
/* game.h */
#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<time.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);//函数声明后面有分号
game.c的代码如下图所示:
/* game.c */
#include"game.h"
void InitBoard(char board[ROWS][COLS],int rows,int cols,char set)
{
int i=0;
int j=0;
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)//传入的数组该是几行就是几行,不能改变数组本身,但是row这种参数,则是根据需要传入
{
int i=0;
int j=0;
//打印列号
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");
}
}
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]=='0')
{
board[x][y]='1';
count--;
}
}
}
int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
return mine[x-1][y]+mine[x-1][y-1]+mine[x][y-1]+mine[x+1][y-1]+mine[x+1][y]+mine[x+1][y+1]+mine[x][y+1]
+mine[x-1][y+1]-8*'0';
}
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col)
{
int x=0;
int y=0;
int win=0;
while(win<row*col-EASY_COUNT)
{
printf("请输入排查雷的坐标:>\n");
scanf("%d%d",&x,&y);
if(x>=1&&x<=row&&y>=1&&y<=col)
{
//坐标的合法性
//1.踩雷
if(mine[x][y]=='1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine,row,col);
break;
}
if(mine[x][y]!='0')
{
printf("您已标记过该坐标,请重新输入\n");
continue;
}
//2.不是雷
else//计算坐标周围几个雷
{
int count=get_mine_count(mine,x,y);
show[x][y]=count+'0';//count本来是个整型+'0'变成char
DisplayBoard(show,row,col);//这里实际上应该有扩展式扫雷,但目前程度不够解决该问题。
win++;
}
}
else
{
printf("输入坐标非法,请重新输入!\n");
}
}
if(win==row*col-EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine,row,col);//获胜了,打印一下布置雷的信息,看看都是哪些地方有雷,也符合原版扫雷
}
}
上面有一个地方值得大家注意,game.h里面的函数声明没有get_mine_count,这样对吗?其实,没什么问题的,因为我们在主函数main里面并没有用到get_mine_count函数,而get_mine_count函数唯一使用的地方是game.c文件中,但是get_mine_count函数在game.c中有定义,所以就不存在什么问题。
①没有实现展开功能,即不能点一个位置,清理附近无雷的地方。这样的话,我们想要获得游戏胜利的难度就比较大,因为我们需要一个一个点,最多可能要点9*9-雷的个数次,这样游戏的可玩性比较低。
②没有标志和取消标志功能,即对于存疑地方进行标注。同样,这样功能的缺失使游戏的可玩性比较低,影响游戏的进行。
总之,本次扫雷游戏的代码实现分享就到此结束了,希望大家能从中收获乐趣和知识。另外,大家感兴趣的话,也可以自行对游戏代码进行完善,当然,后面应该也会出一篇完善版,大家可以期待一下。