??????扫雷的代码实现以及对于知识点和思路的一点解说
目录
1、??????扫雷的代码实现以及对于知识点和思路的一点解说
扫雷的基本逻辑
结 构
核心功能实现思路
1、棋盘展示
2、随机生成雷?
3、展开一片区域?
4、生成玩家排名
2、各功能代码实现以及分析?
创建棋盘
难度选择及对应棋盘打印
布置雷(随机生成)?
对雷的标记以及排查
获取周围格子中雷的个数
通过递归思想实现展开一片的功能
排行榜
扫雷的基本逻辑
结 构
说到底扫雷是个游戏程序,那它应该有游戏的基本结构——
?菜单界面的实现千篇一律,如果不是追求图形化界面(我这里不会讲到,笔者水平有限),我们应该着眼于游戏核心功能的实现。
再来就是核心功能——
- 收集玩家信息
- 随机生成雷
- 棋盘展示
- 标记雷
- 排查雷
- 展开一片区域
- 生成玩家排名
以上就是扫雷游戏的基本功能 ,我们不多废话,这就进入分析环节。
核心功能实现思路
?1、棋盘展示
先给你们看一个棋盘:
我问你这里面有什么玄机,你说不上来,那我只能给你这局扫雷0分。
?玄机就是:棋盘在看不到的地方(边缘)需要拓展。为什么呢?
?如图,假如我想要排查处于边缘的这格(排查格子都是一样的循环,和格子位置无关),它的周围只有5格,而不是非边缘格周围的8格。要解决这个问题很简单,就是初始化棋盘为11*11(上下左右各多一行),拓展后我们在排查后就能无误地得到对应格子周围的雷数。
初始化棋盘之后就是打印棋盘,你可能说这其中又有玄机,没错,你说对了。
由于我们的扫雷可以选择难度,不同难度自然有不同大小的棋盘,正常创建二维数组,打个比方,
char arr[10][10] = {0};
这是在栈空间上开辟了100个字节的空间,这种开辟空间的方式有以下特点:
1.空间开辟的大小是固定的
2.数组在申明的时候,必须指定数组的长度,它所需要的内存在编译时分配。?
此时我们需要根据不同难度设置不同大小的空间,我们对于空间的需求不再是上述情况,我们需要的空间大小在程序运行时才能知道,这时候就只能用 动态内存开辟 了。
我们需要使用?malloc函数
//创建二维数组
char** mine = (char**)malloc(sizeof(char*) *(row + 2));
char** show = (char**)malloc(sizeof(char*) *(row + 2));
for (int i = 0; i < row+2; i++)
{
mine[i] = (char*)malloc(sizeof(char) * (col + 2));
show[i] = (char*)malloc(sizeof(char) * (col + 2));
}
2、随机生成雷?
我们希望实现的是电脑自动布置,也就是每次随机生成雷的位置,这时就要用到?rand()函数
?为了调用函数,我们需要调用对应的头文件——
<stdlib.h>?
<time.h>
为了调用 rand()函数,我们还需要进行初始化:
srand((unsigned int)time(NULL));
//生产随机的下标
int x = rand() % row + 1;//范围是1到row
int y = rand() % col + 1;//范围是1到col
3、展开一片区域?
我们排查一个格子的过程也是排查其周围其余格子的过程,在这个重复的过程中我们可以活用递归的知识
//递归展开一片
void board(char** mine, char** show, int x, int y)
{
//判断坐标是否越界
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
//判断是否已经被排除
if (show[x][y] != '*' && show[x][y] != '@')
{
return;
}
int count = get_mine_count(mine, x, y);//周围雷的个数
if (count > 0)
{
show[x][y] = count + '0';//数字再转换为字符
return;
}
//递归拓展地图
else if (count == 0)
{
show[x][y] = '0';
board(mine, show, x - 1, y);
board(mine, show, x - 1, y - 1);
board(mine, show, x, y - 1);
board(mine, show, x + 1, y - 1);
board(mine, show, x + 1, y);
board(mine, show, x + 1, y + 1);
board(mine, show, x, y + 1);
board(mine, show, x - 1, y + 1);
}
}
}
4、生成玩家排名
我们需要通过文件操作来生成一个能显示前五名的游戏排行榜,用什么作为判断排行的依据呢?
你会说用时间,没错,用时间,但是怎么用呢?
在游戏开始之前,我调用一次?clock函数,将得到的值 强制类型转换 成 int型 保存在一个整型变量start里,在游戏结束后,再调用一次 clock函数,将它的返回值保存在变量end里,然后定义一个变量time = start – end,单位是毫秒,time的值越小,成绩越好。
获得了成绩之后,就是排序了,排序时我们需要用到?qsort函数。
如何操作呢?
?排查成功时,调用Update_Rank函数,把该玩家的用户名以及时间存入二进制文件,这里我们需要定义一个结构体,结构体的类型是Rank,结构体的成员是用户名和时间。
typedef struct Rank
{
char name[20];//用户名
int time;//时间(代表游戏的成绩)
}Rank;
游戏成功时,我们创建一个结构体变量把此时该玩家的用户名和时间赋值给这个结构体变量,并将其写入文件,再打印。
核心功能大致说的差不多了,接下来就是按照游戏运行时的流程来完善整个代码。
先上大致流程图:
为了让代码的可读性更高,思维性更强,创建了三个文件来完成这个项目
(1)test.c? ?—— 测试游戏
(2)game.h?—— 游戏函数的声明
(3)game.c?—— 游戏函数的实现
各功能代码实现以及分析?
?创建棋盘
初始化棋盘的思路已经在上文核心功能分析过,有关棋盘的外观呈现,和 “井字棋” 程序其实大同小异(打印棋盘的思路可以参考我上一篇关于《井字棋》的博客:http://t.csdn.cn/sJYlu,谢谢支持😘😘)
接下来展示初始化棋盘和打印棋盘的代码实现:
//初始化
void InitBoard(char**board, char set)
{
int i = 0;
int j = 0;
//初始化
for (i = 0;i < row+2;i++)
{
for (j = 0;j < col+2;j++)
{
board[i][j] = set;
}
}
}
//打印棋盘
void DisplayBoard(char** board)
{
printf(" ");//考虑到y轴占两格
for (int j = 0; j < col; j++)//打印x轴坐标
{
printf(" %d ", j + 1);
}
printf("\n");
printf(" |");
for (int j = 0; j < col; j++)//打印棋盘封顶
{
printf("---|");
}
printf("\n");
for (int i = 1; i <= row; i++)
{
for (int j = 0; j <= col; j++)
{
if (j == 0)
{
printf("%2d|", i);//顺带打印y轴坐标
}
else
printf(" %c |", board[i][j]);//打印数据
}
printf("\n");
for (int j = 1; j <= col + 1; j++)//注意col应该+1,因为j==1的情况
{
if (j == 1)
printf(" |");
else
printf("---|");
}
printf("\n");
}
}
难度选择及对应棋盘打印
要实现难度选择很简单,利用 switch 语句就能轻松解决:
scanf("%d", &input);
//选择难度
switch (input)
{
case 1:row = 4, col = 4, mine_num = 15; break;
case 2:row = 9, col = 9, mine_num = 10; break;
case 3:row = 11, col = 11, mine_num = 15; break;
default: printf("选择错误");
}
接下来我要问你一个问题:一张棋盘就能实现扫雷的所有功能?
答的上来吗?答不上来?对不起,你对这个游戏的理解我只能给0分。
要实现扫雷的功能,我告诉你,一张棋盘是不够的,你想想看,一张棋盘又要埋雷,又不能让你看见,这是要什么魔法才能实现?所以我们需要创建两张棋盘,一张来放置雷,另一张来放置掩人耳目的符号。大概效果就是下面这样:
?所以创建二维数组就需要创建两次,打印棋盘根据自己需要来决定打印一个或是全部打印:
//创建二维数组
char** mine = (char**)malloc(sizeof(char*) *(row + 2));
char** show = (char**)malloc(sizeof(char*) *(row + 2));
for (int i = 0; i < row+2; i++)
{
mine[i] = (char*)malloc(sizeof(char) * (col + 2));
show[i] = (char*)malloc(sizeof(char) * (col + 2));
}
//初始化mine数组为全字符'0'
InitBoard(mine, '0');
//初始化show数组为全'*'
InitBoard(show, '*');
//打印棋盘
DisplayBoard(show);//只打印9*9的内容
//布置雷
SetMine(mine);
//DisplayBoard(mine);
//这是不给玩家看到的
布置雷(随机生成)
说到随机生成,思考一下计算机中有什么是一直在变化的。没错,是时间。
我们可以利用 时间戳(可以通过自行搜索深入了解)来生成随机的数字,从而实现生成坐标的目的。
为了调用函数,我们需要调用对应的头文件——
<stdlib.h>? <time.h>
为了调用?rand()?函数,我们还需要进行初始化:
srand((unsigned int)time(NULL));
?坐标生成范围有所限制,我们写代码时也需要注意,并动用数学技巧来使随机值符合范围
rand() % row 的范围是多少?当然是 0 ~?row-1
代码实现如下:
//设置雷
void SetMine(char** mine)
{
int count = mine_num;//雷的个数
while (count)
{
//生产随机的下标
int x = rand() % row + 1;//范围是1到row
int y = rand() % col + 1;//范围是1到col
if (mine[x][y] == '0')//避免重复设置雷
{
mine[x][y] = '1';//设置为雷
count--;
}
}
}
对雷的标记以及排查
标记雷是扫雷游戏中人性化的功能,由于每次排查前玩家都要选择是否标记,我们就用 while?语句来写。进入循环后的内容思路就很清晰了。
代码如下:
//标记雷
void SignMine(char** show)
{
while (1)
{
int input = 0;
printf("-----------------------\n");
printf("******你想标记地雷吗****\n");
printf("***** 1.标记 *******\n");
printf("***** 0.不标记 *******\n");
printf("-----------------------\n");
scanf("%d", &input);
if (0 == input)
{
break;
//不想标记就退出循环
}
else if(input==1)
{
int x = 0;
int y = 0;
printf("请输入你想标记的坐标: ");
scanf("%d %d", &x, &y);
//坐标合法性检验
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
show[x][y] = '@';//标记你认为的雷的位置为@
DisplayBoard(show);//显示标记的结果
}
else
{
printf("非法的坐标,请重新标记\n");
}
}
else
{
printf("输入错误,请重新输入\n");
}
}
}
排查雷的实现思路也比较清晰,比较值得说的就是通过比较?win 、?row*col - mine_num?的数值来判定雷是否排查完毕。
具体代码如下:
//排查雷
int FindMine(char** mine, char** show)
{
//1.输入排查雷的坐标
//2.检查该坐标是不是雷
//(1)是雷 --> 很遗憾炸死了
//(0)不是雷 --> 统计坐标周围有几个雷-->存储排查雷的信息到show数组,游戏继续
int x = 0;
int y = 0;
int win = 0;
while (win < row*col - mine_num)//还没排查完就进入循环
{
SignMine(show);//标记雷
printf("请输入要排查的坐标: ");
scanf("%d %d", &x, &y);//x的范围是1~9,y的范围是1~9
//判断坐标的合法性
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine);//把怎么被炸死的显现出来
break;
}
else
{
//不是雷的话统计(x,y)坐标周围有几个雷
board(mine, show, x, y);//递归展开一片
//显示排查出的信息
DisplayBoard(show);
win++;
}
}
else
{
printf("坐标不合法,请重新输入\n");
}
}
if (win == row * col - mine_num)//全部都排查完了
{
printf("恭喜你,排雷成功!\n");
return 1;
}
return 0;
}
获取周围格子中雷的个数
?先展示代码:
//获取雷的个数
int get_mine_count(char** mine, 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 + 1] +
mine[x + 1][y] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
}
这里提一嘴 char** 型指针:
char** 型指针,只能指向 char* 型的对象
类似的,char* 型指针,只能指向 char 型的对象
近期我也想写一篇关于 指针 的文章,敬请关注!?
通过递归思想实现展开一片的功能
如题所示,通过递归,一切尽在代码中,自行体会🙏🙏
//递归展开一片
void board(char** mine, char** show, int x, int y)
{
//判断坐标是否越界
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
//判断是否已经被排除
if (show[x][y] != '*' && show[x][y] != '@')
{
return;
}
int count = get_mine_count(mine, x, y);//周围雷的个数
if (count > 0)
{
show[x][y] = count + '0';//数字再转换为字符
return;
}
//递归拓展地图
else if (count == 0)
{
show[x][y] = '0';
board(mine, show, x - 1, y);
board(mine, show, x - 1, y - 1);
board(mine, show, x, y - 1);
board(mine, show, x + 1, y - 1);
board(mine, show, x + 1, y);
board(mine, show, x + 1, y + 1);
board(mine, show, x, y + 1);
board(mine, show, x - 1, y + 1);
}
}
}
排行榜
上文已经讲过调用 clock 函数来获取玩家游戏时间、调用 qsort 函数排序从而实现排名的思想,这里就不再赘叙了,直接上代码:
获取时间:
int start = (int)clock();
int ret = FindMine(mine, show);//从mine中排查放到show中
Rank info;
strncpy(info.name, name, 20);
int end = (int)clock();
info.time = end - start;
比较时间实现排名:
int cmp(const void* a, const void* b)//传给qsort函数的参数比较函数
{
Rank* aa = (Rank*)a;
Rank* bb = (Rank*)b;
return aa->time > bb->time;//比较时间
}
//排行榜
void Update_Rank(Rank info)
{
Rank arr[6] = { 0 };//定义了一个结构体数组
for (int i = 0; i < 6; i++)
{
arr[i].time = LONG_MAX;//默认为int范围的最大值
}
FILE* fp1 = fopen("rank.bin", "ab+"); //防止打开失败
if (!fp1)
{
printf("open failed");
return;
}
fseek(fp1, 0, SEEK_SET);//文件位置指针回到文件开头
int num = fread(arr, sizeof(Rank), 5, fp1);//读文件
arr[num] = info;//将结构体变量info存入数组下标为num处
qsort(arr, num + 1, sizeof(Rank), cmp);//排序
//打印排名
if (num <= 4)
{
for (int i = 0; i <= num; i++)
{
printf("%-20s %-20d 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
else if (num >=5)
{
for (int i = 0; i <= 4; i++)
{
printf("%-20s %-20d 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
FILE* fp2 = fopen("rank.bin", "wb"); //不能用ab+
if (!fp2)
{
printf("open failed");
return;
}
num = num < 5 ? num + 1 : 5;//最多只有5个
fwrite(arr, sizeof(Rank), num, fp2);//写文件
//关闭
fclose(fp1);
fclose(fp2);
}
各功能的实现如上,根据逻辑组合就能得到一个体验还算是完整的简陋版扫雷游戏
源代码如下:
//game.h文件
#pragma once
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<string.h>
//全局变量
extern int row;//行数
extern int col;//列数
extern int mine_num;//类的个数
//定义了一个Rank结构体
typedef struct Rank
{
char name[20];//用户名
int time;//时间(代表游戏的成绩)
}Rank;
//初始化棋盘
void InitBoard(char** board, char set);
//打印棋盘
void DisplayBoard(char** board);
//布置雷
void SetMine(char**mine);
//标记雷
void SignMine(char** show);
//递归展开一片
void board(char** mine, char** show, int x, int y);
//获取雷的个数
int get_mine_count(char** mine, int x, int y);
//排查雷
int FindMine(char** mine, char** show);//从mine中排查放到show中
//获取排名函数
void Update_Rank(Rank info);
//game.c文件
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
//初始化
void InitBoard(char**board, char set)
{
int i = 0;
int j = 0;
//初始化
for (i = 0;i < row+2;i++)
{
for (j = 0;j < col+2;j++)
{
board[i][j] = set;
}
}
}
//打印棋盘
void DisplayBoard(char** board)
{
printf(" ");//考虑到y轴占两格
for (int j = 0; j < col; j++)//打印x轴坐标
{
printf(" %d ", j + 1);
}
printf("\n");
printf(" |");
for (int j = 0; j < col; j++)//打印棋盘封顶
{
printf("---|");
}
printf("\n");
for (int i = 1; i <= row; i++)
{
for (int j = 0; j <= col; j++)
{
if (j == 0)
{
printf("%2d|", i);//顺带打印y轴坐标
}
else
printf(" %c |", board[i][j]);//打印数据
}
printf("\n");
for (int j = 1; j <= col + 1; j++)//注意col应该+1,因为j==1的情况
{
if (j == 1)
printf(" |");
else
printf("---|");
}
printf("\n");
}
}
//设置雷
void SetMine(char** mine)
{
int count = mine_num;//雷的个数
while (count)
{
//生产随机的下标
int x = rand() % row + 1;//范围是1~row
int y = rand() % col + 1;//范围是1~col
if (mine[x][y] == '0')//避免重复设置雷
{
mine[x][y] = '1';//设置为雷
count--;
}
}
}
//标记雷
void SignMine(char** show)
{
while (1)
{
int input = 0;
printf("-----------------------\n");
printf("******你想标记地雷吗****\n");
printf("***** 1.标记 *******\n");
printf("***** 0.不标记 *******\n");
printf("-----------------------\n");
scanf("%d", &input);
if (0 == input)
{
break;
//不想标记就退出循环
}
else
{
int x = 0;
int y = 0;
printf("请输入你想标记的坐标: ");
scanf("%d %d", &x, &y);
//坐标合法性检验
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
show[x][y] = '@';//标记你认为的雷的位置为@
DisplayBoard(show);//显示标记的结果
}
else
{
printf("非法的坐标,请重新标记\n");
}
}
}
}
//获取雷的个数
int get_mine_count(char** mine, 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 + 1] +
mine[x + 1][y] +
mine[x][y + 1] +
mine[x - 1][y + 1] - 8 * '0';
}
//递归展开一片
void board(char** mine, char** show, int x, int y)
{
//判断坐标是否越界
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
//判断是否已经被排除
if (show[x][y] != '*' && show[x][y] != '@')
{
return;
}
int count = get_mine_count(mine, x, y);//周围雷的个数
if (count > 0)
{
show[x][y] = count + '0';//数字再转换为字符
return;
}
//递归拓展地图
else if (count == 0)
{
show[x][y] = '0';
board(mine, show, x - 1, y);
board(mine, show, x - 1, y - 1);
board(mine, show, x, y - 1);
board(mine, show, x + 1, y - 1);
board(mine, show, x + 1, y);
board(mine, show, x + 1, y + 1);
board(mine, show, x, y + 1);
board(mine, show, x - 1, y + 1);
}
}
}
//排查雷
int FindMine(char** mine, char** show)
{
//1.输入排查雷的坐标
//2.检查该坐标是不是雷
//(1)是雷 --> 很遗憾炸死了
//(0)不是雷 --> 统计坐标周围有几个雷-->存储排查雷的信息到show数组,游戏继续
int x = 0;
int y = 0;
int win = 0;
while (win < row*col - mine_num)//还没排查完就进入循环
{
SignMine(show);//标记雷
printf("请输入要排查的坐标: ");
scanf("%d %d", &x, &y);//x的范围是1~9,y的范围是1~9
//判断坐标的合法性
if (x >= 1 && x <= col && y >= 1 && y <= row)
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine);//把怎么被炸死的显现出来
break;
}
else
{
//不是雷的话统计(x,y)坐标周围有几个雷
board(mine, show, x, y);
//显示排查出的信息
DisplayBoard(show);
win++;
}
}
else
{
printf("坐标不合法,请重新输入\n");
}
}
if (win == row * col - mine_num)//全部都排查完了
{
printf("恭喜你,排雷成功!\n");
return 1;
}
return 0;
}
//test.c文件
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
int row = 0;//行
int col = 0;//列
int mine_num = 0;//雷的个数
int cmp(const void* a, const void* b)//传给qsort函数的参数比较函数
{
Rank* aa = (Rank*)a;
Rank* bb = (Rank*)b;
return aa->time > bb->time;//比较时间
}
//排行榜
void Update_Rank(Rank info)
{
Rank arr[6] = { 0 };//定义了一个结构体数组
for (int i = 0; i < 6; i++)
{
arr[i].time = INT_MAX;//默认为int范围的最大值
}
FILE* fp1 = fopen("rank.bin", "ab+"); //防止打开失败
if (!fp1)
{
printf("open failed");
return;
}
fseek(fp1, 0, SEEK_SET);//文件位置指针回到文件开头
int num = fread(arr, sizeof(Rank), 5, fp1);//读文件
arr[num] = info;//将结构体变量info存入数组下标为num处
qsort(arr, num + 1, sizeof(Rank), cmp);//排序
//打印排名
if (num <= 4)
{
for (int i = 0; i <= num; i++)
{
printf("%-20s %-20d 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
else if (num >=5)
{
for (int i = 0; i <= 4; i++)
{
printf("%-20s %-20d 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
FILE* fp2 = fopen("rank.bin", "wb"); //不能用ab+
if (!fp2)
{
printf("open failed");
return;
}
num = num < 5 ? num + 1 : 5;//最多只有5个
fwrite(arr, sizeof(Rank), num, fp2);//写文件
//关闭
fclose(fp1);
fclose(fp2);
}
void menu()
{
printf("**********************\n");
printf("***** 1.play *******\n");
printf("***** 0.exit *******\n");
printf("**********************\n");
}
void game()
{
int input = 0;
char name[20] = { 0 };
printf("请输入用户名: ");
scanf("%s", name);
printf("请选择游戏难度: \n");
printf("***** 1.easy *****\n");
printf("***** 2.normal *****\n");
printf("***** 3.hard *****\n");
scanf("%d", &input);
//选择难度
switch (input)
{
case 1:row = 4, col = 4, mine_num = 15; break;
case 2:row = 9, col = 9, mine_num = 10; break;
case 3:row = 11, col = 11, mine_num = 15; break;
default: printf("选择错误");
}
//创建数组
char** mine = (char**)malloc(sizeof(char*) *(row + 2));
char** show = (char**)malloc(sizeof(char*) *(row + 2));
for (int i = 0; i < row+2; i++)
{
mine[i] = (char*)malloc(sizeof(char) * (col + 2));
show[i] = (char*)malloc(sizeof(char) * (col + 2));
}
//初始化mine数组为全字符'0'
InitBoard(mine, '0');
//初始化show数组为全'*'
InitBoard(show, '*');
//打印棋盘
DisplayBoard(show);//只打印9*9的内容
//布置雷
SetMine(mine);
DisplayBoard(mine);
//这是不给玩家看到的
//排查雷
int start = (int)clock();
int ret = FindMine(mine, show);//从mine中排查放到show中
Rank info;
strncpy(info.name, name, 20);
int end = (int)clock();
info.time = end - start;
if (ret)
Update_Rank(info);
free(mine);
free(show);
}
void test()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择: ");
scanf("%d", &input);
switch (input)
{
case 1:
//扫雷
game();
break;
case 0:
break;
default:
printf("选择错误");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
文章长度可观,感谢阅读。
以后会继续分享学习的知识。
|