目录
hello吖,大家!
今天我们要实现的是一个精简版的扫雷游戏,还记得十多年前,在学校机房里没有网络跟同学一起偷偷玩扫雷游戏,被老师逮到的情景,至今记忆深刻,难以忘怀啊!
借着那深刻且被老师爱戴过的回忆,让我们一起带着感情进入这场扫雷游戏的实现!
序言
我们要从整体的视角、思路和代码实现的方式来进行扫雷游戏逻辑的实现,这里我把它们分为了四个部分来进行一一讲解:
第一部分:我们的预期
第二部分:文件结构(其实就是头文件和源文件的使用)
第三部分:游戏逻辑分析
第四部分:游戏逻辑实现+分析
一、我们的预期
扫雷游戏,我们大家都不陌生,就是在一个正方形棋盘上踩格子,踩到地雷就被炸死了,没有踩到就会显示这个格子周围有几个雷以此来作为提示信息,帮助我们通关游戏,如果不够玩,还可以再来一把,这就是我们对扫雷游戏的游戏运行预期,但从游戏开发者的视角来看,就不仅仅是玩游戏了,更多的是对相关知识理解的深入 和 用心码完整个工程的那种由内而外散发出来的成就感。正是因为视角的不同,我们才可以用心去对待我们正在做的事——但没什么事情,是比坚持下来更酷啦,相信我,如果你对自己不自信,那就请认真的看完这篇博客,我保证你可以深有体会,感同身受!!!
为什么,你可以这么肯定?因为我跟大家一样,对未知充满了疑惑,恐惧和不安,但我也知道,当我勇敢的去面对这些未知时,那些疑惑、恐惧和不安都会消失而答案则会渐渐清晰,正是因为有行动,所以才产生了答案!!!
你能看到这篇博客,正是因为你行动了;你的行动,促成了我们之间的缘分,而这缘分正是我俩共同进步的桥梁!!!
二、文件结构
c语言程序通常分为两个文件,一个文件是专门保存程序需要内容的声明——头文件(.h), 另一个文件是用于实现程序执行逻辑的 / 函数的定义(关于游戏相关函数的实现)——源文件(.c)
1、游戏所需文件
我们一般把整个工程(整个程序)分为三个文件来实现:
①、test.c源文件:用于测试整个工程的执行逻辑
②、game.c源文件:用于实现游戏相关函数的定义(定义嘛,就是我定义这个函数该怎么做它就怎么做)
③、game.h头文件:用于保存游戏所需内容的声明——头文件的包含啊、符号的定义啊、函数的声明啊等等知道了这些我们正式进入主题,开始我们扫雷游戏的实现
三、游戏逻辑分析
1、实际上扫雷游戏的本质是这样的:我们扫雷的时候,扫一个 横纵 坐标的位置时,如果不是雷,它就会显示它周围八个坐标一共有几个雷(对于9×9的棋盘来说就是显示它周围一圈有几个雷),如果是雷,那就被炸死了,游戏结束!!!
2、我们来看游戏的整个框架:它是一个9×9的正方形棋盘,里面有10颗地雷。根据上面解析我们得知,请看下图:
3、🐍再看下面我们对假设的信息进行分析:
我们肯定是 从无到有 开始实现的,我们不仅需要排查雷,还需要布置10颗雷
那把雷布置到哪里去呢?我们假设布置到一个9×9的二维数组里比较合适:
上面已经把雷布置进去了,布置完雷后,我们该排查雷了,那怎么排查雷呢?
我们刚刚说了:排查雷的时候,排查一个 横纵 坐标后,如果这个坐标处不是雷——那就显示这个坐标周围一圈八个坐标有几个雷,如果是雷就被炸死。
这就是扫雷游戏的大体框架,相信同志们多看几遍很容易就能理解了吧!但以上只是对它的片面分析,它的内核我们还没有分析,如果放到这里分析的话,我怕同志们会懵,所以我们决定先把游戏相关代码逻辑实现一下,然后边实现代码边分析关于扫雷内核的逻辑!!!
四、游戏逻辑实现+分析
🐍下面我们就开始分文件讲解关于扫雷游戏逻辑的代码实现+分析喽!!!
4.1 test.c文件——游戏整体框架的实现
//我们要在.c文件里包含一下.h文件,这样就可以使用.h里的所有内容了
//我们自己的头文件要用 “ ” ,库里的头文件是用< >
#include "game.h"
void menu(void) //void代表这个函数不需要有返回值、不需要有参数,光打印一个菜单就可以
{
printf("***********************\n");
printf("***** 1.play *****\n");//1.玩游戏
printf("***** 0.exit *****\n");//0.退出游戏
printf("***********************\n");
}
int main()
{
int input = 0;//初始化变量,局部变量不初始化,在内存的栈区是随机值
//我们选择do while循环来实现扫雷游戏代码逻辑的实现,上来直接就可以玩一把,因为这个循环是先执行在再
//判断嘛!!!
do
{
menu();//打印一个提示菜单
printf("请选择:");//提示玩家的信息(是玩游戏,还是退出游戏)
scanf("%d", &input);//输入值
switch(input)//switch开关接收input输入的值,切记括号里只能是整型表达式
{
case 1://选择 1 进入游戏内部执行游戏相关函数的逻辑实现
printf("扫雷游戏\n");//测试代码逻辑是否正常, 一定要边写边测, 不然代码多了错哪你都不知道
break;
case 0://选择 0 退出游戏
printf("退出游戏\n");
break;
default://选择其它的数字,将重新选择
printf("选择错误,请重新选择\n");
break;
}
} while(input);//根据接收的input值进行选择:
//1为真,循环上去,继续玩
//0为假,直接跳出循环,退出游戏
//其它数字都是非0,也是真,循环上去执行对应语句
return 0;
}
- 执行程序观看当前逻辑:
4.2 如何放雷、排查雷以及对二维数组的分析(分析)
4.2.1 如何放雷、排查雷
- 我们假设我们有一个二维数组,里面放的全是0,当我们要放进去雷的时候,就把0改成1,放一颗雷就改一次0,直到把10颗雷全部放进去的时候,我们的数组里,就有了10个1了(就是10个雷)
- 我们说当1代表雷的时候,那就有另一个地方出问题了,如果说我们把雷当成是1的话,那当我们排查雷的时候,它会显示我们排查雷的那个坐标处周围一圈有多少个雷的数字,这里就产生歧义了,假如我们排查的那个坐标周围有1颗雷,那显示的 那个1 到底是我们排查出来的信息1呢?还是雷代表的1呢?我们不知道。
- 关于这一点我们想了一个办法:就是我们可以创建两个二维数组(也就是两个棋盘),一个二维数组用来布置雷,一个二维数组用来排查雷,然后我们可以这样做:从布置雷的数组里获得排查出来雷的信息,然后把这个信息放到排查雷的数组里(这里有点绕,多读几遍就可以理解啦!),这样不就解决了关于“1”的歧义了吗!,当然给玩家看的棋盘也是排查雷的那个棋盘,打印;布置雷的那个棋盘可不能给玩家看,不打印。不然就没意义了!请看下图:
4.2.2 对二维数组的分析
- 假设我们为了让棋盘神秘一点,把排查雷的那个二维数组里面全部放成‘ * ’然后当排查一次雷的时候就显示排查出来周围雷的信息(也就是数字嘛),但是我们已经把数组里全部放成char类型的数据了,如果显示的是int类型的数字的话,那不就又产生歧义了嘛,所以我们决定把排查出来的信息(也就是数字),用字符数字来表示,再如果,排查雷的数组是char类型的数组的话,那布置雷的数组也用char类型的数组创建的话,那不就可以更方便的组合它们俩之间的关系了嘛,是雷,我用字符‘1’来表示,非雷我用‘0’来表示。
- 我们接着分析,当我们一切正常的时候,当我们排查边上一圈坐标的时候问题就出现了。请看下图(先看文字,再看对应的图):
- 所以我们最后决定创建两个11×11的二维数组来实现我们的扫雷游戏(访问11×11的空间,实际上我们只打印和用9×9的空间),一个布置雷的二维数组、一个排查雷的二维数组。
4.3 game.c文件——扫雷游戏的定义部分(就是用函数实现的过程)
🐍然后我们来单独实现game()函数的执行逻辑以及代码实现!!!咱把别的代码都注释了,只留了game函数,看它咋实现的就可以了,相关符号定义、头文件的包含、函数的声明都放在下面game.h文件里了
所需步骤:
1、创建两个11x11的二维数组——布置雷、排查雷
2、初始化数组
3、打印排查雷的数组
4、布置雷
5、排查雷
//我们要在.c文件里包含一下.h文件,这样就可以使用.h里的所有内容了
//我们自己的头文件要用 “ ” ,库里的头文件是用< >
#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)
{
int i = 0;
int j = 0;
//我们希望棋盘好看易观察一些,所以先把行号和列号打印出来
//打印列号
for(j = 0;j<=col;j++)
{
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 mine[ROWS][COLS],int row,int col)
{
//首先定义雷的个数 —— COUNT
//再生成随机坐标 —— x y
//这个库函数就是生成随机数的意思 % row就可以生成0-8之间的随机数,+1可以生成1-9之间的随机数
//这里注意要使用rand 必须 配合srand一起使用,而且srand函数参数部分还需要调用time函数来进行操作
//因为随机数的生成要想让它一直变,就得配合上我们的时间戳,因为时间是一直在变化的,而time函数
//的返回值刚好是时间戳,所以可以一气呵成,完成我们想要的效果。
//这里注意srand只需要在main函数里调用一次就可以了,如果调用多次的话,时间重置的太快,会导致
//随机坐标重复。
int count = COUNT;//我们定义的雷的个数
while(count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
//如果mine数组里元素是'0',那就给它放一个'1'(雷)进去
//利用循环就把10颗雷全部放进去了
if(mine[x][y]=='0')
{
mine[x][y] = '1';
count--;
}
}
}
//get_num函数
//因为这个函数只是给FindMine用的所以可以给它加上一个
static int get_num(char mine[ROWS][COLS],int x,int y)
{
//mine不是字符数组?怎么返回值能用int,接下来看操作,你就知道了,这里面有我自己的一些门道
//下面返回的是我们排查的x、y坐标处周围8个坐标的位置
//因为我们mine数组里放的不是'0'非雷,就是'1'雷
//当我们把8个不同‘0’、‘1’组成的字符加起来 减去 8*‘0’不就等于我们要排查出来的那个整型数字了吗
//举个例子:
//假如x、y坐标处周围有3个雷那就是3个‘1’嘛,那还剩5个‘0’,那3‘1’+5‘0’-8‘0’不就等于3吗。
//是不是,3个‘1’是3个‘49’ 比 3个‘0’多 3个1,那这三个1算出来不就是数字3吗,多看两眼,应该可以理解
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');
}
//排查雷
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-COUNT))//当win<71的时候我们就赢了
{
printf("请输入要排查的坐标:")//提示信息
scanf("%d %d",&x,&y);//输入坐标
//然后判断坐标合法性
if(x>=1 && x<=row && y>=1 && y<=col)
{
//如果排查的坐标已经排查过了,那我们还得提示一下该坐标已经被排查过了
if(show[x][y]!= '*')
{
printf("该坐标已被排查,请重新排查\n");
continue;
}
if(mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine,ROW,COL);//被炸死之后,给玩家看一下棋盘,雷都放哪了
break;//跳出循环即可
}
else//这种情况没被炸死,就统计周围一圈雷的个数嘛
{
//还记得我们要从mine数组里x 、y坐标处统计周围8个坐标的信息放到show数组里吧
//这里我们涉及到一个小知识,我来给大家讲解一下
//关于ASCII的知识科普:
//'0'的ASCII值是48,'1'的ASCII值是49,那数字1 + '0'是不是就等于'1':因为1+48==49嘛
//所以我们可以利用这个规律来完成把数字转化为字符数字的动作
//这个get_num函数是专门给排查雷的数组用的,所以不用在头文件里声明,那我们写一下它吧
int n = get_num(mine,x,y)//得到mine数组xy坐标处周围8个坐标的信息,
show[x][y] = n+'0';//我们在得到8个坐标的信息之后只要加上'0'就可以得出排查出来的信息了
DisplayBoard(show,ROW,COL);//统计完信息,再展示给玩家就可以了
win++;
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
//到最后我们的win++如果==71那我们就赢啦
if(win == (row * col - COUNT))
{
printf("恭喜你,排雷成功!!!\n");
}
}
4.3.1 test.c文件里关于game()函数内相关函数的调用
//我们要在.c文件里包含一下.h文件,这样就可以使用.h里的所有内容了
//我们自己的头文件要用 “ ” ,库里的头文件是用< >
#include "game.h"
void game(void)
{
//根据以上代码分析,我们得知我们需要两个二维数组:布置雷、排查雷
//为了防止排查坐标时数组越界,我们把最初9x9的棋盘,扩大到了11x11的棋盘
//用我们只用9x9的空间、但访问我们访问的是11x11的空间
char mine[ROWS][COLS] = {0};//布置雷的数组
char show[ROWS][COLS] = {0};//排查雷的数组
//初始化棋盘
//我们上面分析的时候希望show数组最开始都是‘*’,而mine数组最开始都是‘0’
InitBoard(mine,ROWS,COLS,'0');
InitBoard(show,ROWS,COLS,'*');
//接着我们想看看棋盘了,那就打印一下看看现在棋盘里的状态的吧
//这里我们要打印的是9x9的棋盘,所以传过去的参数是ROW、COL,但我们还是在11x11的棋盘里操作它们
//DisplayBoard(mine,ROW,COL);布置雷的数组是不打印的,这里我们便于调试
DisplayBoard(show,ROW,COL);
//布置雷
SetMine(mine,ROW,COL);
//DisplayBoard(mine,ROW,COL);布置完雷我们打印一下看看,运行的时候可不打印,这里我们就调试看一下
//排查雷
//我们上面分析的时候说:排查雷是从mine数组里排查,然后排查出来的信息放到show数组里
//所以参数部分需要有两个数组——mine数组、show数组
//我们依然是在11x11的棋盘上访问,然后操作9x9的棋盘,所以依然传参ROW、COL
FindMine(mine,show,ROW,COL);
}
int main()
{
//我们在这里调用srand函数,当然完整版下面还要很多语句,我们这里只是演示rand函数需要调用srand函数
//srand函数的参数部分是无符号整型,所以我们把time函数的返回值强制类型转换成无符号整型
//而time函数的参数部分是NULL(空指针,也就是0)
//当然别忘了引用头文件
//srand/rand:#include<stdlib.h>
//time:#include<time.h>
srand((unsigned int)time(NULL));
game( );//假设我们的main函数只调用了一个game函数。等讲解完直接看完整版!!!
}
4.4 game.h文件——包含函数的声明、符号的定义、头文件的包含等
//因为我们要打印9x9的棋盘所以也要定义一个9行9列的符号
#define COUNT 10
#define ROW 9//行
#define COL 9//列
//11行11列的符号定义
#define ROWS ROW+2//行
#define COLS COL+2//列
//头文件的包含
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//函数的声明
//初始化棋盘
void InitBoard(char board[ROWS][COLS],int rows,int cols,set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS],int row,int col);
//布置雷
void SetMine(char mine[ROWS][COLS],int row,int col);
//排查雷
void FindMine(char mine[ROWS][COLS],char show[ROWS][COLS],int row,int col);
五、总结
🐍🐍🐍上面我们把扫雷游戏深度解析了一下,乍一看可能有些乱,因为我们不熟悉它,跟它没有建立深度联系,所以可能有点蒙,不过没关系!下面我给大家发整个工程比较清晰一点,如果哪里不懂,你们可以对照上面的分析和深度解析,进行理解,多花些时间来弄一弄,肯定没问题的,因为上天不会辜负有心人的!
六、扫雷游戏完整版
6.1 test.c
#include "game.h"
void menu(void)
{
printf("***************************\n");
printf("**** 1.play ****\n");
printf("**** 0.exit ****\n");
printf("***************************\n");
}
void game(void)
{
char mine[ROWS][COLS] = { 0 };//布置好的雷的信息
char show[ROWS][COLS] = { 0 };//排查出的雷的信息
//初始化棋盘
//功能一样,参数不一样那就可以这么写
Initboard(mine, ROWS, COLS, '0');
Initboard(show, ROWS, COLS, '*');
//打印棋盘
//Displayboard(mine, ROW, COL);//这个不展示的
Displayboard(show, ROW, COL);
//布置雷
set_mine(mine, ROW, COL);
//Displayboard(mine, ROW, COL);
//排查雷
find_mine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
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);
return 0;
}
//待拓展
//1、能够展开一片的操作
//2、标记和取消雷
//3、显示剩余雷的个数
6.2 game.c
#include"game.h"
//初始化盘
void Initboard(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 Displayboard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
//控制列号
printf("-----------扫雷------------- \n");
for (j = 0; j <= col; j++)
{
printf("%d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
//控制行号
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("-----------扫雷------------- \n");
}
//布置雷
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int count = COUNT;
while (count)
{
//生成随机下标
int x = rand() % row + 1;
int y = rand() % col + 1;
//布置雷
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
static int get_num(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');
}
//排查雷
void find_mine(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 - COUNT))//win小于71时,就没赢
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
//检查坐标合法性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')//被排查过的坐标都会放到show数组里,如果show数组的坐标不是’*‘,那就是被排查过了
{
printf("该坐标已被排查,请重新排查\n");
continue;
}
//被炸死
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
Displayboard(mine, ROW, COL);
break;
}
//显示排查信息
else
{
int n = get_num(mine, x, y);
show[x][y] = n + '0';//数字+字符0就可以==字符数字 because字符0的ASILL值是48 1、2、3依次是49、50、51
Displayboard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
if (win == (row * col - COUNT))
{
printf("恭喜你,排雷成功!!!\n");
}
}
6.3 game.h
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define COUNT 10
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
//初始化盘
void Initboard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void Displayboard(char board[ROWS][COLS], int row, int col);
//布置雷
void set_mine(char mine[ROWS][COLS], int row, int col);
//排查雷
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Itsrealonepiece/article/details/146165773