目录
首先我们来看一下流程图,因为为什么布置两个棋盘等基础信息大家在网上可以随便找到,所以在这里就不过多解释直接从流程图开始,然后逐步讲解实现
所有代码均在visual studio2022上编译完成!
1 游戏功能简介
初级版
• 使用控制台实现经典的扫雷游戏
• 游戏可以通过菜单实现继续玩或者退出游戏
• 扫雷的棋盘是9*9的格⼦
• 默认随机布置10个雷
• 可以排查雷
◦ 如果位置不是雷,就显⽰周围有⼏个雷
◦ 如果位置是雷,就炸死游戏结束
◦ 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束
升级版
• 可以选择游戏难度
◦ 简单 9*9 棋盘,10个雷
◦ 中等 16*16棋盘,40个雷
◦ 困难 30*16棋盘,99个雷
• 如果排查位置不是雷,周围也没有雷,展开周围的⼀⽚
• 可以标记雷
• 加上排雷的用时显示
• 加上排行榜
2.流程图
首先我们来看一下流程图,因为为什么布置两个棋盘等基础信息大家在网上可以随便找到,所以在这里就不过多解释直接从流程图开始,然后逐步讲解实现
3.代码实现
- 游戏逻辑的实现
结合上面的功能解释,我们可以知道菜单的功能有以下几个:
- 游戏的进入
- 游戏的退出
- 打印排行榜
void menu()
{
printf("*****************************\n");
printf("******* 0. exit *******\n");
printf("******* 1. play *******\n");
printf("*****************************\n");
}
int main()
{
int i = 0;
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请输入你的选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();//将所有与游戏实现有关的函数都放在这个函数里面
break;
case 2:
open_rank();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
system("pause");
return 0;
}
由于这里一进入游戏界面就要看到菜单,所以这里选择使用do......while();的结构配合上switch分支来实现对菜单的选择,input作为do......while();的条件,当输入1或者2时,游戏需要继续进行,此时while()里的条件为真,继续打印菜单,当输入0时,需要停止退出游戏,此时while()里的条件为假,游戏退出。
当输入1时,就要进入到游戏实现的函数game()了
2.游戏主体的实现
- 棋盘的创建
我们知道,需要创建两个大小相同的数组mine和show一个用来放雷,一个用于排雷
//创建数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
- 初始化棋盘
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;
}
}
}
我们知道,两个棋盘初始化的内容不一样,我们只需要在函数传参时做出一点小改变即可
//函数调用如下:
initboard(mine, ROWS, COLS, '0');
initboard(show, ROWS, COLS, '*');
- 埋雷
void set_mine(char board[ROWS][COLS], int row, int col)//埋雷
{
int count = easycount;//(easycount为雷的个数)
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;//这里随机生成坐标
if (board[x][y] == '0')//这里避免坐标重复
{
board[x][y] = '1';
count--;//控制循环结束
}
}
}
- 打印棋盘
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("%2d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%2d ", i);
for (j = 1; j <= col; j++)
{
printf("%2c ", board[i][j]);
}
printf("\n");
}
printf("-------扫雷游戏-------\n");
}
在上面我们创建了两个数组分别为mine数组和mine数组,其中mine数组是用来埋雷的,而show数组是用于展示给玩家的,所以这里只打印show数组就可以
- 排雷
关于排查雷,我们是这样实现的:
输入排查雷的坐标
检查该坐标是不是雷
(1)是雷 --> 很遗憾炸死了
(0)不是雷 --> 统计坐标周围有几个雷-->存储排查雷的信息到show数组,游戏继续 。
这里我们用了 getminecount 和 FindMine 两个函数
int getminecount(char board[ROWS][COLS], int x, int y)//统计mine中xy坐标对应八个相邻的坐标的1的个数
{
/*int i = 0;;
int j = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (mine[x + i][y + j] == '1')
{
count++;
}
}
}
return count;*/
//也可以用循环从-1开始到1如上面所示
return(board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] - 8 * '0');
//'1'-'0'= 1要注意字符1不等同于数字一’0'-'0' = 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-easycount)
{
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);
break;
}
//如果不是雷
else
{
//统计mine中xy坐标对应八个相邻的坐标的1的个数
int count = getminecount(mine, x, y);
show[x][y] = count + '0';//转化为字符
displayboard(show, ROW, COL);
}
}
}
else
{
printf("输入的坐标错误,请重新输入:>");
}
if (win == row * col - easycount)
{
//测试可以将easycount变为八十
printf("恭喜你排雷成功!\n");
displayboard(mine, ROW, COL);
}
}
}
在game.h中定义的easycount为雷的个数,这里的win其实就是非雷的个数,每排一个坐标,不是雷win就加1,直到win和非雷的个数相等,排雷成功游戏重新回到菜单。
在这个findmine函数中,输入坐标判断该坐标不是雷的话,要统计周围八个坐标的个数,所以这里分装了一个getminecount函数,判断该位置合法且不是雷时调用来获取雷的个数
这里要注意字符’0’不等同于数字0,字符’1’也不等同于数字1,让我们来看一下ASCII码表:
我们可以发现'0' '1' '2'这些字符的ASCII码值都是连续的,我们想显示出排查格子周围雷的个数,需要先将字符'0','1','2'...转化为数字 0,1 ,2,3...
我们只需要把每个字符减去一个'0',就可以了,所以上面getminecount函数中返回值形式如下:
return( board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] - 8 * '0');
4.升级功能
- 展开
在上面的排雷函数中,我们知道当输入的坐标合法并且该位置不是没有被排查过也不是雷时,会统计出该坐标周围雷的个数,玩过扫雷的都知道,当排查的位置不是雷,并且周围的八个坐标也都没有雷时就会展开一篇,展开的位置变成空格,直到不满住展开的条件,那么我们也可以利用函数的递归实现这个功能
我们自定义一个unfold函数来实现这个功能:
递归详解:就像上面我说到,坐标附近的八个坐标,无非就是加1,0,-1,就可以遍历完,所以这里也是一样,两个for将周围八个的坐标排查完,这里没排查一次,就会重新调用unfold这个函数,又将附近八个坐标以这种方式展开,就像如果我们排查坐标 5 5,调用函数时会将该坐标附近的八个坐标依次传给unfold函数,每个坐标传过去又会以自己为中心排查附近八个坐标,因为这里的判断条件为show[x][y] == ’*’,才能进入,所以避免了重复循环和死递归
void unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* count)
{
//这里展开功能的解释:扫雷展开是要当前坐标前后左右八个坐标都没有雷时才展开,所以这里写成了if,else结构,如果当前位置附近没有雷时,才进行递归展开
int i = 0;
int j = 0;
if (1 <= x && x <= ROW && 1 <= y && y <= COL && mine[x][y] == '0' && show[x][y] == '*')//坐标合法性判断
{
if (getminecount(mine, x, y))
{
//getmincount获取坐标附近雷的个数
show[x][y] = getminecount(mine, x, y) + '0';//因为上面初始化数组时第一行第一列已经被用来打印数字了,所以就从1开始就可以
(*count)--;
//这里count也要减减
}
else
{
show[x][y] = ' ';//这里周围八个坐标都不是雷,才变成空格
(*count)--;//这里没进来一次count都要--
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)//附近的八个坐标无非就是不变加一和减一,所以这样写递归即可
{
unfold(mine, show, x + i, y + j, count);//递归
//函数每次调用都会以自身为中心遍历周围八个坐标遍历完八个停止
}
}
}
}
}
- 难度选择
在上面我们提到,扫雷有三个难度
◦ 简单 9*9 棋盘,10个雷
◦ 中等 16*16棋盘,40个雷
◦ 困难 30*16棋盘,99个雷
那我们想其实在创建数组时将数组创建为38*16的就可以了,然后我们再通过数组传参来控制初始化的棋盘的范围和雷的个数,即可实现难度选择功能,我们在game函数中开始的位置添加一个选择难度的函数selectmy(),同时打印一个新的菜单menu1():
void menu1()
{
printf("********请选择你的难度*******\n");
printf("******* 1.easy *******\n");
printf("******* 2.normal *******\n");
printf("******* 3.difficult *******\n");
printf("*****************************\n");
}
void selectmy()
{
int choice = 0;
do
{
menu1();
printf("请输入你的选择:>");
getchar();
scanf("%d", &choice);
switch (choice)
{
case 1:
ROW = 9;
COL = 9;
easycount = 1;//9*9 十个雷
break;
case 2:
ROW = 16;
COL = 16;
easycount = 40;//16*16 40个雷
break;
case 3:
ROW = 30;
COL = 16;
easycount = 99;//30*16 99个雷
break;
default:
printf("输入错误,请重新输入\n");
}
} while (choice != 1 && choice != 2 && choice != 3);//这里判断输入合法性除了输入1 2 3都要重新循环,所以这里我们的条件就写成这样子
}
这里ROW,COL,easycount改为了在game.h中定义的全局变量,不是define定义的常量了,所以可以直接更改
- 扫雷用时
这个功能相对简单,我们只需要利用clock函数,在开始游戏时我们利用clock函数获取程序运行到此刻的时间
然后finish-start就是扫雷成功用时,但此时的单位不是秒,所以我们除上上图所示的一串英文(其实代表的就是1000这个整数),就可以将时间转化为秒
- 标记,取消标记功能
在这里,我自定义了两个函数,markmine,cancelmine,分别执行标记和取消标记的功能,在这里我们将’!’视为标记,要标记时就将show数组对应位置变为’!’,取消标记就将show数组上对应的位置变回’*’。
markmine:
void markmine(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j, int* appear)//mine 不用传
{
int x = 0;
int y = 0;
//标记有两种情况
//1. 坐标合法 ,此处无标记,标记成功
//2. 坐标合法,但是该位置已经被标记
while (1)
{
printf("请输入要标记的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= i && y >= 1 && y <= 9 && show[x][y] == '*')//第一种情况
{
show[x][y] = '!';//判断输入坐标合法性后,将show数组上的对应位置
break;
}
else if (x >= 1 && x <= i && y >= 1 && y <= j && show[x][y] == '!')
{
printf("该位置已经被标记!请重新输入\n");
}
else
{
printf("输入错误请重新输入\n");
}
}
*appear += 1;
//标记成功appear要加1
}
cancelmine:
void cancelmine(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j,int* appear)
{
int x = 0;
int y = 0;
//取消标记两种情况
//1. 输入合法,该位置有标记
//2. 输入合法,该位置没有标记
while (1)
{
printf("请输入要取消标记坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= i && y >= 1 && y <= j && show[x][y] == '!')//第一种情况
{
show[x][y] = '*';
break;
}
else if (x >= 1 && x <= i && y >= 1 && y <= j && show[x][y] == '*')//第二种情况
{
printf("该位置还未被被标记!请重新输入。\n");
}
else
{
printf("输入错误请重新输入\n");
}
}
*appear -= 1;
//因为appear是用来统计标记的个数的,所以这里到最后也要减减
}
这里的appear是标记的个数,所以每标记一次,appear就要加1,每取消标记一次,appear就要减1,我们在展开函数调用后面位置,加上了一个do....while()循环,打印了菜单供玩家选择,再配合上switch分支实现:
//如果不是雷
else
{
//展开一片的功能
unfold(mine, show, x, y, &count);
//展开完打印一下棋盘
displayboard(show, ROW, COL);
if (count != 0)//在打印菜单时要判断游戏是否结束
{
do
{
printf("******************************\n");
printf("******* 1. 标记 *******\n");
printf("******* 2. 取消标记 *******\n");
printf("******* 0.不需要标记 *******\n");
printf("******************************\n");
getchar();//读空格
printf("请输入你的选择:>");
scanf("%d", &z);
switch (z)
{
case 1:
markmine(mine, show, row, col, &appear);
displayboard(show, ROW, COL);
break;
case 2:
{
if (appear >= 1)//取消标记肯定要有标记才行,所以这里要判断棋盘上是否有标记
{
cancelmine(mine, show, row, col,&appear);
//这里传appear的地址,可以在被调用函数中修改值
displayboard(show, ROW, COL);
break;
}
else
{
printf("棋盘上还没有标记,请标记后再选择此功能\n");
z = 5;//给z赋值为5,使得可以再次进入循环
}
}
case 0:
break;
default:
printf("输入错误,请重新输入。\n");
}
} while (z != 1 && z != 2 && z != 0);
//这里当然也可以一直执行功能,等到输入0才结束这个功能,只需要把条件前两个删掉即可。
}
}
这里要注意细节,要判断棋盘上是否有标记,没有标记就要提示并且重新打印菜单,上面在提示没有标记后将z改为5,目的是重新进入循环,因为我这里写的是执行一个功能就又回到排雷,所以选择取消雷时,如果没有标记就会回到排雷,如果是不输入0都一直循环的就不用赋值给z。
- 排行榜
实现这个功能,这里定义了一个函数Update_Rank来实现这个功能,同时我们在game.h里定义一个结构体变量Rank,并且通过typedef重命名。
//定义了一个Rank结构体
typedef struct Rank//typedef重命名
{
char name[20];//用户名
int time;//时间(代表游戏的成绩)
}Rank;
这里我们就可以把用时功能得到的时间保存在time里面,将用户名保存在name里面,这时候我们就创建一个结构体变量info,和一个结构体数组arr[6],info用来暂时存放输入的名字和用时,完成后将info中的内容保存到arr数组中,实现姓名成绩的保存,利用文件操作将arr中的内容输入到文件中,打印排行榜时从文件输入到arr数组中,然后打印即可具体实现看代码:
int cmp(const void* a, const void* b)//传给qsort函数的参数比较函数
{
Rank* aa = (Rank*)a;//强转类型为Rank*,并且赋给aa
Rank* bb = (Rank*)b;
return aa->time - bb->time;//比较时间,这里aa为指针可以直接使用->,找到时间,相减,qsort接受的返回值不同,执行功能
}
//排行榜
void Update_Rank(Rank info)
{
int i = 0;
int j = 0;
Rank arr[6] = { 0 };//定义了一个结构体数组
for (int i = 0; i < 6; i++)
{
arr[i].time = INT_MAX;//默认为int范围的最大值
}
FILE* fp1 = fopen("rank.txt", "ab+"); //防止打开失败,打开成功会返回文件的首地址,失败返回空指针
if (fp1 == NULL)
{
perror("rank");//perror函数打印错误信息
return;
}
fseek(fp1, 0, SEEK_SET);//文件位置指针回到文件开头,库函数
int num = fread(arr, sizeof(Rank), 5, fp1);//读文件看看有几个内容了,因为是按大小顺序来写进文件的,fread()函数会返回读取到的个数,我们将保存在info中的内容保存在读取到的内容个数的后一个即可,即下标为num处,这里设置好了只读五个所以不会出现数组越界的情况
arr[num] = info;//将结构体变量info存入数组下标为num处
qsort(arr, num + 1, sizeof(Rank), cmp);//排序,会根据cmp函数的返回值执行功能
//打印排名
if (num <= 4)
{
for (int i = 0; i <= num; i++)
{
printf("%-20s 用时:%5d秒 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
else if (num >= 5)
{
for (int i = 0; i <= 4; i++)
{
printf("%-20s 用时%5d秒 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
FILE* fp2 = fopen("rank.txt", "wb"); //不能用ab+
if (fp2 == NULL)
{
perror("rank");
return;
}
num = num < 5 ? num + 1 : 5;//最多只有5个
fwrite(arr, sizeof(Rank), num, fp2);//写文件
//关闭
fclose(fp1);
fclose(fp2);
fp1 = NULL;
fp2 = NULL;
}
void game()
{
//创建一个字符数组来保存输入的名字
char name[20] = { 0 };
printf("请输入用户名: ");
scanf("%s", name);
selectmy();
//创建数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
//初始化,打印,埋雷,排雷
//初始化数组
initboard(mine, ROWS, COLS, '0');
initboard(show, ROWS, COLS, '*');
set_mine(mine, ROW, COL);//(埋雷)
//打印数组里面的内容,一般mine数组就不打印,因为埋好雷怎么可能让别人看见呢
displayboard(show, ROW, COL);
//displayboard(mine, ROW, COL);
//排查雷
start = clock();//开始时间,这里计算程序开始到这一步的时间
int ret = findmine(mine, show, ROW, COL);//ret接受返回值,1代表扫雷成功,0代表扫雷失败
Rank info = { 0 };//创建一个结构体变量info,保存当前输入的名字和用时
strncpy(info.name, name, 20);//库函数
info.time = (finish - start) / CLOCKS_PER_SEC;//转化为秒
if (ret)
Update_Rank(info);
}
- 打印排行榜
void print_rank(void)
{
int i = 0;
Rank arr1[6] = { 0 };//定义一个结构体数组arr1
FILE* p = fopen("rank.txt", "ab+");//以二进制追加的方式打开文件
if (p == NULL)//判断打开是否成功
{
perror("rank");//perror函数打印错误信息
return;
}
fseek(p, 0, SEEK_SET);//文件位置指针回到文件开头,库函数
int num = fread(arr1, sizeof(Rank), 6, p);
//输出数据到arr1中,并返回读取成功的个数
if (num == 0)
{
printf("没有数据,打印失败\n");
}
else
{
printf("****************排行榜********************\n");
printf("姓 名 用 时 排名\n");
for (i = 0; i < num; i++)
{
printf("%s %3d秒 %2d\n", arr1[i].name, arr1[i].time, i+1);
}
printf("****************排行榜********************\n");
}
//if-else实现有无数据的打印
p = NULL;//将p置为空指针
}
5.源代码
基础版
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void menu()
{
printf("*****************************\n");
printf("******* 1. play *******\n");
printf("******* 0. exit *******\n");
printf("*****************************\n");
}
//初始化,打印,埋雷,排查雷
void game()
{
//创建数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
//初始化数组
initboard(mine, ROWS, COLS,'0');
initboard(show, ROWS, COLS,'*');
set_mine(mine, ROW, COL);
//打印数组里面的内容,一般mine数组就不打印,因为埋好雷怎么可能让别人看见呢
displayboard(show, ROW, COL);
//displayboard(mine, ROW, COL);这里测试程序的时候,可以打印,便于快速测试功能
//初始化,打印,埋雷,排雷
//排查雷
findmine(mine,show,ROW,COL);
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
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;
}
game.c
#define _CRT_SECURE_NO_WARNINGS
#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;
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 board[ROWS][COLS], int row, int col)//布置雷
{
int count = easycount;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
int getminecount(char board[ROWS][COLS],int x, int y)
{
//也可以用循环从-1开始到1
return(board[x-1][y] +
board[x-1][y-1] +
board[x][y-1] +
board[x+1][y-1] +
board[x+1][y] +
board[x+1][y+1] +
board[x][y+1] +
board[x-1][y+1] -8*'0');
//'1'-'0'= 1要注意字符1不等同于数字一’0'-'0' = 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-easycount)
{
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')//布置的时候1为雷
{
printf("你被炸死了\n");
displayboard(mine, ROW, COL);
break;
}
//如果不是雷
else
{
//展开一片的功能,待添加
win++;
//统计mine中xy坐标对应八个相邻的坐标的1的个数
int count = getminecount(mine, x, y);
show[x][y] = count + '0';//因为上面初始化数组时第一行第一列已经被用来打印数字了,所以就从1开始就可以
displayboard(show, ROW, COL);
}
}
}
else
{
printf("输入的坐标错误,请重新输入:>");
}
if (win == row * col - easycount)
{
//测试可以将easycount变为八十
printf("恭喜你排雷成功!\n");
displayboard(mine, ROW, COL);
}
}
}
game.h
#pragma once
#include<stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define easycount 10
#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 set_mine(char board[ROWS][COLS], int row, int col);
//排查雷
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row,int col);
int getminecount(char board[ROWS][COLS], int x, int y);
升级版
test.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
int cmp(const void* a, const void* b)//传给qsort函数的参数比较函数
{
Rank* aa = (Rank*)a;//强转类型为Rank*,并且赋给aa
Rank* bb = (Rank*)b;
return aa->time - bb->time;//比较时间,这里aa为指针可以直接使用->,找到时间,相减,qsort接受的返回值不同,执行功能
}
//排行榜
void Update_Rank(Rank info)
{
int i = 0;
int j = 0;
Rank arr[6] = { 0 };//定义了一个结构体数组
for (int i = 0; i < 6; i++)
{
arr[i].time = INT_MAX;//默认为int范围的最大值
}
FILE* fp1 = fopen("rank.txt", "ab+"); //防止打开失败,打开成功会返回文件的首地址,失败返回空指针
if (fp1 == NULL)
{
perror("rank");//perror函数打印错误信息
return;
}
fseek(fp1, 0, SEEK_SET);//文件位置指针回到文件开头,库函数
int num = fread(arr, sizeof(Rank), 5, fp1);//读文件看看有几个内容了,因为是按大小顺序来写进文件的,fread()函数会返回读取到的个数,我们将保存在info中的内容保存在读取到的内容个数的后一个即可,即下标为num处,这里设置好了只读五个所以不会出现数组越界的情况
arr[num] = info;//将结构体变量info存入数组下标为num处
qsort(arr, num + 1, sizeof(Rank), cmp);//排序,会根据cmp函数的返回值执行功能
//打印排名
if (num <= 4)
{
for (int i = 0; i <= num; i++)
{
printf("%-20s 用时:%5d秒 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
else if (num >= 5)
{
for (int i = 0; i <= 4; i++)
{
printf("%-20s 用时%5d秒 您的排名是:%d\n", arr[i].name, arr[i].time, i + 1);
}
}
FILE* fp2 = fopen("rank.txt", "wb"); //不能用ab+
if (fp2 == NULL)
{
perror("rank");
return;
}
num = num < 5 ? num + 1 : 5;//最多只有5个
fwrite(arr, sizeof(Rank), num, fp2);//写文件
//关闭
fclose(fp1);
fclose(fp2);
fp1 = NULL;
fp2 = NULL;
}
void menu()
{
printf("*****************************\n");
printf("******* 0. exit *******\n");
printf("******* 1. play *******\n");
printf("******* 2. 打印排行榜 *******\n");
printf("*****************************\n");
}
//初始化,打印,埋雷,排查雷
void menu1()
{
printf("********请选择你的难度*******\n");
printf("******* 1.easy *******\n");
printf("******* 2.normal *******\n");
printf("******* 3.difficult *******\n");
printf("*****************************\n");
}
void selectmy()
{
int choice = 0;
do
{
menu1();
printf("请输入你的选择:>");
getchar();
scanf("%d", &choice);
switch (choice)
{
case 1:
ROW = 9;
COL = 9;
easycount = 1;//9*9 十个雷
break;
case 2:
ROW = 16;
COL = 16;
easycount = 40;//16*16 40个雷
break;
case 3:
ROW = 30;
COL = 16;
easycount = 99;//30*16 99个雷
break;
default:
printf("输入错误,请重新输入\n");
}
} while (choice != 1 && choice != 2 && choice != 3);//这里判断输入合法性除了输入1 2 3都要重新循环,所以这里我们的条件就写成这样子
}
void game()
{
//创建一个字符数组来保存输入的名字
char name[20] = { 0 };
printf("请输入用户名: ");
scanf("%s", name);
selectmy();
//创建数组
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
//初始化,打印,埋雷,排雷
//初始化数组
initboard(mine, ROWS, COLS, '0');
initboard(show, ROWS, COLS, '*');
set_mine(mine, ROW, COL);//(埋雷)
//打印数组里面的内容,一般mine数组就不打印,因为埋好雷怎么可能让别人看见呢
displayboard(show, ROW, COL);
//displayboard(mine, ROW, COL);
//排查雷
start = clock();//开始时间,这里计算程序开始到这一步的时间
int ret = findmine(mine, show, ROW, COL);//ret接受返回值,1代表扫雷成功,0代表扫雷失败
Rank info = { 0 };//创建一个结构体变量info,保存当前输入的名字和用时
strncpy(info.name, name, 20);//库函数
info.time = (finish - start) / CLOCKS_PER_SEC;//转化为秒
if (ret)
Update_Rank(info);
}
void print_rank(void)
{
int i = 0;
Rank arr1[6] = { 0 };//定义一个结构体数组arr1
FILE* p = fopen("rank.txt", "ab+");//以二进制追加的方式打开文件
if (p == NULL)//判断打开是否成功
{
perror("rank");//perror函数打印错误信息
return;
}
fseek(p, 0, SEEK_SET);//文件位置指针回到文件开头,库函数
int num = fread(arr1, sizeof(Rank), 6, p);
//输出数据到arr1中,并返回读取成功的个数
if (num == 0)
{
printf("没有数据,打印失败\n");
}
else
{
printf("****************排行榜********************\n");
printf("姓 名 用 时 排名\n");
for (i = 0; i < num; i++)
{
printf("%s %3d秒 %2d\n", arr1[i].name, arr1[i].time, i+1);
}
printf("****************排行榜********************\n");
}
//if-else实现有无数据的打印
p = NULL;//将p置为空指针
}
int main()
{
int i = 0;
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请输入你的选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 2:
print_rank();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
system("pause");
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS
#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;//这里我们将要初始化的字符传了过来,用char set来接受,所以这里直接等于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("%2d ", j);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%2d ", i);
for (j = 1; j <= col; j++)
{
printf("%2c ", board[i][j]);
}
printf("\n");
}
printf("-------扫雷游戏-------\n");
}
void set_mine(char board[ROWS][COLS], int row, int col)//埋雷
{
int count = easycount;//(easycount为雷的个数)
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;//这里随机生成坐标
if (board[x][y] == '0')//这里避免坐标重复
{
board[x][y] = '1';
count--;//控制循环结束
}
}
}
int getminecount(char board[ROWS][COLS], int x, int y)//统计mine中xy坐标对应八个相邻的坐标的1的个数
{
/*int i = 0;;
int j = 0;
int count = 0;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (mine[x + i][y + j] == '1')
{
count++;
}
}
}
return count;*/
//也可以用循环从-1开始到1如上面所示
return(board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] - 8 * '0');
//'1'-'0'= 1要注意字符1不等同于数字一’0'-'0' = 0
}
void unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* count)
{
//这里展开功能的解释:扫雷展开是要当前坐标前后左右八个坐标都没有雷时才展开,所以这里写成了if,else结构,如果当前位置附近没有雷时,才进行递归展开
int i = 0;
int j = 0;
if (1 <= x && x <= ROW && 1 <= y && y <= COL && mine[x][y] == '0' && show[x][y] == '*')//坐标合法性判断
{
show[x][y] = '2';//这里先让show[x][y] = '2',目的是让下面的递归展开时,避免重复执行,避免死递归
if (getminecount(mine, x, y))
{
//getmincount获取坐标附近雷的个数
show[x][y] = getminecount(mine, x, y) + '0';//因为上面初始化数组时第一行第一列已经被用来打印数字了,所以就从1开始就可以
(*count)--;
//这里count也要减减
}
else
{
show[x][y] = ' ';//这里周围八个坐标都不是雷,才变成空格
(*count)--;//这里没进来一次count都要--
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)//附近的八个坐标无非就是不变加一和减一,所以这样写递归即可
{
unfold(mine, show, x + i, y + j, count);//递归
}
}
}
}
}
void markmine(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j, int* appear)//mine 不用传
{
int x = 0;
int y = 0;
//标记有两种情况
//1. 坐标合法 ,此处无标记,标记成功
//2. 坐标合法,但是该位置已经被标记
while (1)
{
printf("请输入要标记的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= i && y >= 1 && y <= 9 && show[x][y] == '*')//第一种情况
{
show[x][y] = '!';//判断输入坐标合法性后,将show数组上的对应位置
break;
}
else if (x >= 1 && x <= i && y >= 1 && y <= j && show[x][y] == '!')
{
printf("该位置已经被标记!请重新输入\n");
}
else
{
printf("输入错误请重新输入\n");
}
}
*appear += 1;
//标记成功appear要加1
}
void cancelmine(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j,int* appear)
{
int x = 0;
int y = 0;
//取消标记两种情况
//1. 输入合法,该位置有标记
//2. 输入合法,该位置没有标记
while (1)
{
printf("请输入要取消标记坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= i && y >= 1 && y <= j && show[x][y] == '!')//第一种情况
{
show[x][y] = '*';
break;
}
else if (x >= 1 && x <= i && y >= 1 && y <= j && show[x][y] == '*')//第二种情况
{
printf("该位置还未被被标记!请重新输入。\n");
}
else
{
printf("输入错误请重新输入\n");
}
}
*appear -= 1;
//因为appar是用来统计标记的个数的,所以这里到最后也要减减
}
int findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int z = 0;
int appear = 0;//标记个数
int count = row * col - easycount;//非雷个数,为0就扫雷成功
while (count)
//这里count是用来评判游戏结束的;count为总棋子数减去雷的个数,当没有被雷炸死的情况出现,conunt为0的时候游戏就结束,在unfold函数里递归都执行了*count--这一操作
{
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y] == '*' || show[x][y] == '!')//这里判断输入坐标的合法化
{
if (show[x][y] != '*' && show[x][y] != '!')//排查是否重复排查
{
printf("输入坐标重复,该位置已经排查过雷\n");
}
else if (show[x][y] == '!')
{
printf("该位置为标记,请取消标记后再排查该位置\n");
}
//如果是雷
else
{
if (mine[x][y] == '1')
{
finish = clock();//取结束时间
printf("用时%d 秒\n\n", (int)(finish - start) / CLOCKS_PER_SEC);//这里的(int)为强制类型转换
printf("你被炸死了\n");
displayboard(mine, ROW, COL);
break;
}
//如果不是雷
else
{
//展开一片的功能
unfold(mine, show, x, y, &count);
//展开完打印一下棋盘
displayboard(show, ROW, COL);
if (count != 0)//在打印菜单时要判断游戏是否结束
{
do
{
printf("******************************\n");
printf("******* 1. 标记 *******\n");
printf("******* 2. 取消标记 *******\n");
printf("******* 0.不需要标记 *******\n");
printf("******************************\n");
getchar();//读空格
printf("请输入你的选择:>");
scanf("%d", &z);
switch (z)
{
case 1:
markmine(mine, show, row, col, &appear);
displayboard(show, ROW, COL);
break;
case 2:
{
if (appear >= 1)//取消标记肯定要有标记才行,所以这里要判断棋盘上是否有标记
{
cancelmine(mine, show, row, col,&appear);
//这里传appear的地址,可以在被调用函数中修改值
displayboard(show, ROW, COL);
break;
}
else
{
printf("棋盘上还没有标记,请标记后再选择此功能\n");
z = 5;//给z赋值为5,使得可以再次进入循环
}
}
case 0:
break;
default:
printf("输入错误,请重新输入。\n");
}
} while (z != 1 && z != 2 && z != 0);
//这里当然也可以一直执行功能,等到输入0才结束这个功能,只需要把条件前两个删掉即可。
}
}
}
}
else//这里当输入错误时提示然后重新循环
{
printf("输入的坐标错误,请重新输入!\n");
}
}
if (count == 0)
//这里通过对count的操作,判断最后排雷成功的提示,如果count进行判断后count--为0,就进入到这里
{
//测试可以将easycount变为八十
finish = clock();//取结束时间,程序开始到现在的时间,相减即可
printf("用时%4d 秒\n", (int)(finish - start) / CLOCKS_PER_SEC);
printf("恭喜你排雷成功!\n");
displayboard(mine, ROW, COL);
return 1;
}
return 0;
}
game.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <time.h>
#include <conio.h>
#include <Windows.h>
#define ROWS 32
#define COLS 18
#define _CRT_SECURE_NO_WARNINGS
#include<stdlib.h>
#include<string.h>
double start, finish;
int ROW;
int COL;
int easycount;
//定义了一个Rank结构体
typedef struct Rank//typedef重命名
{
char name[20];//用户名
int time;//时间(代表游戏的成绩)
}Rank;
//数组初始化
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 board[ROWS][COLS], int row, int col);
//排查雷
int findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
//雷的个数
int getminecount(char board[ROWS][COLS], int x, int y);
//展开
void unfold(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* count);
//标记
void markmine(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j, int* appear);
//取消标记
void cancelmine(char mine[ROWS][COLS], char show[ROWS][COLS], int i, int j, int* appear);
//排行榜
void Update_Rank(Rank info);
//传给qsort函数的参数函数
int cmp(const void* a, const void* b);
//打印排行榜
void print_rank(void);