前面带着大家进行了经典小游戏扫雷的代码实现,但是因为是简易版的,里面存在很多不足之处,比如无法选择难度模式,无法对地雷进行标记,无法对非地雷区进行展开等等有待改善的地方,导致扫雷游戏虽然初步完成,可是实际上可玩性极其差,很容易把玩家劝退,所以,这篇文章和大家一起把扫雷游戏进行优化升级。
目录
1.优化升级的方向
2.具体的优化升级方式
2.1展开功能的补充
2.2标记和取消标记功能
2.3排行榜,时间记录
2.4难度选择,模式丰富化
3.优化升级版完整代码如下图:
①没有实现展开功能,即不能点一个位置,清理附近无雷的地方。
②没有标志和取消标志功能,即对于存疑地方进行标注。
③没有时间记录,不知道完成游戏的用时,无最佳成绩,排行榜。
④没有自主选择难度的选项,玩法、模式太过单一。
我们在网页版扫雷游戏中,会出现展开一整片非雷区的情况,而我们自己想要实现这样的功能,需要我们怎么做呢?
大家想一下,如果我们排查了一个周围都没有雷的坐标,是不是就要排查这个坐标周围的八个坐标,然后这八个坐标如果其中还有周围都没有雷的坐标,我们还需要再排查这些坐标周围的没有被排查过的坐标,这很明显可以通过递归来实现,通过之前的学习我们知道,递归要有两个必要条件:
- 存在限制条件,当满足这个限制条件的时候,递归便不再继续。
- 每次递归调用之后越来越接近这个限制条件。
那么在这个递归函数中限制条件应该是什么呢?
1. 该坐标在游戏界面之中
2. 该坐标不是雷
3. 该坐标周围没有雷
4. 该坐标没有被排查过
以上的2和3可以合并为1个,1和4可以合并成一个所以我们只需要用两个判断语句就可以完成。
展开函数Search的代码如下图所示:
//查找该位置
void Spreadout(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
//递归限制条件1 4
if (x >= 1 && x <= ROW && y >= 1 && y <= COL)
{
win++;//win自增
int count = get_mine_count(mine, x, y);
if (count == 0)//该处周围八个格子都不是雷
{
show[x][y] = ' ';
int i = 0, j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*') //该处没有被展开过
{
Spreadout(mine, show, i, j);
}
}
}
}
else//该处周围八个格子有雷,就显示数字
{
show[x][y] = count + '0';//将数字count转化为字符count
}
}
}
特别注意:
展开条件:
(1)该坐标本身不是雷:mine[x][y] != '1' 该条件总是满足
(2)该坐标周围没有雷:GetMineCount(mine,x,y)==0 该条件不一定总是满足,所以要判断
(3)该坐标没有被展开过:show[x][y]=='*' 该条件不一定总是满足,所以要判断
每次调用展开函数,win都必须自增一,如果win只是排查地雷函数里的局部变量,展开函数是不认识这个局部变量的,这就是为什么win必须是全局变量。
如果该坐标的周围八个格子有雷,那么该坐标的格子就得显示周围有多少个雷;如果该坐标的周围八个格子没有雷,那么就递归调用展开函数,就能达到“展开”的效果。
这里的话就是通过递归从而达到从我们标记的位置开始一直向外展开,当然关于数字count转化为字符count值得大家认真一看。
我们在网页版扫雷中可以通过自己的分析判断哪些位置可能是雷,然后对其进行标注,使我们能够在接下来的游戏中对该位置先搁置不考虑,提高游戏的可玩性,降低游戏难度。
标记的话其实并没有涉及到太多需要考虑的点,只需要输入要标记的位置的坐标,然后在show中对其进行赋值变换,即可满足需求。
标记函数SignMine的代码如下图所示:
void SignMine(char show[ROWS][COLS])
{
int x = 0, y = 0;
int input = 1;
do
{
printf("请输入要标记的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= ROW && y >= 1 && y <= ROW && show[x][y] == '*')
{
show[x][y] = '?';
DisplayBoard(show, ROW, COL);
}
else
printf("坐标已被标记或者坐标越界,请重新输入\n");
printf("继续标记:1,跳过标记:0\n");
scanf("%d", &input);
switch (input)
{
case 1:
SignMine(show);
break;
default:
break;
}
} while (input);
}
取消标记函数ClearMine的代码如下图所示:
void ClearSign(char show[ROWS][COLS])
{
int x = 0, y = 0;
int input = 0;
do
{
printf("请输入要清除的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= ROW && y >= 1 && y <= ROW && show[x][y] == '?')
{
show[x][y] = '*';
DisplayBoard(show, ROW, COL);
}
else
printf("坐标已被清除标记或者坐标越界,请重新输入\n");
printf("继续清除:1,跳过清除:0\n");
scanf("%d", &input);
switch (input)
{
case 1:
ClearSign(show);
input = 0;//关键
break;
default:
break;
}
} while (input);
}
联机游戏最大的魅力莫过于排行榜上的名列前茅,而我们如果能添加排行榜和时间记录功能的话,相信可玩性会再度得到极大的提高。
排行榜函数Update_Rank的代码如下图所示:
//排行榜
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);
}
时间比较函数cmp如下图所示:
int cmp(const void* a, const void* b)//传给qsort函数的参数比较函数
{
Rank* aa = (Rank*)a;
Rank* bb = (Rank*)b;
return aa->time > bb->time;//比较时间
}
因为考虑到我们我们的各种功能都是建立在一个个函数基础之上,如果我们想要选择不同的难度模式,那么势必建立不同大小的二维数组作为棋盘,但是我们在简易版中参数的数据类型是固定的,所以,直接在简易版上进行修改就行不通了。那我们应该怎么办呢?
我们可以在game.h头文件中定义全局变量,把函数参数中数组大小的整型表达式用全局变量表示,同时难度模式就通过改变全局变量值来实现。
与此同时,会有新问题产生,就是简易版中数组的大小是固定的,无法改变,难度我们要建立多个数组,分分一一对应,但是那样的话,再想开辟新的难度模式不就又要重新建立数组了?
这里解决方法是用动态开辟内存空间,具体开辟的空间大小再和全局变量结合起来,这样问题就完美解决了。
因为这部分不是函数,所以就在完整代码中呈现了,希望大家仔细研究哦!
game.h
#pragma once
#include
#include
#include
#include
//全局变量
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);//这里也可以用for循环
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;
}
tect.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);
//选择难度
do {
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("选择错误,请重新输入\n");
break;
}
} while (input!=1&& input!= 2&& input != 3);
//创建数组
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;
}
由于个人原因,上面的完整代码并非由我本人创作,而是借用了一位博主朋友的代码,感谢朋友无私分享。原文链接如下:
C语言实现入门级小游戏——扫雷(排行榜+玩家自定义难度+递归展开一片+标记地雷)版本_c语言小游戏-CSDN博客
特别提醒,因为完整代码是该朋友的,所以完整代码和我们前面优化升级的代码名字等内容有些对不上,大家具体看该朋友后面两种是如何通过代码实现的,希望大家能有所收获。