1.1 扫雷游戏的功能说明
• 使⽤控制台实现经典的扫雷游戏
• 游戏可以通过菜单实现继续玩或者退出游戏
• 扫雷的棋盘是9*9的格⼦
• 默认随机布置10个雷
• 可以排查雷
◦ 如果位置不是雷,就显示周围有几个雷
◦ 如果位置是雷,就炸死游戏结束
◦ 把除10个雷之外的所有雷都找出来,排雷成功,游戏结束
1.2 游戏文件结构的设计
为了养成良好的习惯,同时为以后在公司团队合作编程打下基础,该游戏采用多文件来进行编程。
test.c //⽂件中写游戏的测试逻辑
game.c //⽂件中写游戏中函数的实现等,是整个游戏的核心代码,一般不会对用户展示。
game.h //⽂件中写游戏需要的数据类型和函数声明等
2 扫雷游戏的具体功能及代码实现
2.1 菜单
代码:
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**********************\n");
}
在游戏开始前首先打印菜单供用户选择,输入1则开始游戏,输入0则退出游戏。
所以在主函数中应该还存在一个分支结构,这个结构负责对输入的结果进行判断,如果是1则运行游戏相关的函数,是0就退出游戏。
int main()
{
int input = 0;
srand((unsigned int)time(NULL));//用来生成随机数的种子,不理解可以先不管
do
{
menu();//打印菜单
printf("请选择:>");
scanf("%d", &input);
switch (input)//对用户输入的结果进行判断
{
case 1://结果为1,开始游戏
game();
break;
case 0://结果为0,退出游戏
printf("退出游戏\n");
break;
default://两个结果都不是,重新输入
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
注意:变量input要在do-while循环外定义,如果在内部定义的话while将无法识别input,导致编译错误。
2.2 核心代码
2.2.1 游戏数据结构的分析
从扫雷游戏的基本玩法可以知道:
1.如果排查的位置是雷,就被炸死了,游戏结束。
2.如果排查的位置不是雷,就显示这个坐标周围有几个雷。
那么在扫雷前,布置的雷和排查出的雷的信息都需要存储,所以需要一定的数据结构来存储这些信息。因为需要在9*9的棋盘上布置雷的信息和排查雷,那么首先想到的就是创建⼀个9*9的数组来存放信息。
需要布置雷的位置就存放1,不需要布置雷的位置就存放0。
假设排查(2,5)这个坐标时,访问周围的一圈8个黄色位置,统计周围雷的个数是1。
假设排查(8,6)这个坐标时,访问周围的⼀圈8个黄色位置,统计周围雷的个数时,最下面的三个坐标就会越界,为了防止越界,在设计的时候,给数组扩大一圈,雷还是布置在中间的9*9的坐标上,外围一圈不去布置雷即可,这样就解决了越界的问题。所以将存放数据的数组创建成 11*11 比较合适。
继续分析,在棋盘上布置了雷,棋盘上雷的信息(1)和非雷的信息(0),假设排查了某一个位置后,这个坐标处不是雷,这个坐标的周围有1个雷,那么需要将排查出的雷的数量信息记录存储,并打印出来,作为排雷的重要参考信息。
如果把雷的个数信息存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能产生混淆和打印上的困难。
如果雷和非雷的信息不使用数字,而使用某些字符,这样做会导致棋盘上除了有有雷和非雷的信息外,还有排查出的雷的个数信息,就比较混乱,不够方便。
比较好的方案是,专门给一个棋盘(对应一个数组mine)存放布置好的雷的信息,再给另外一个棋盘(对应另外一个数组show)存放排查出的雷的信息。这样就互不干扰,把雷布置到 mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考。
同时为了保持神秘,show数组开始时初始化为字符 ’ * ‘,为了保持两个数组的类型一致,从而可以使⽤同一套函数处理,mine数组最开始也初始化为字符’ 0 ‘,布置雷改成字符’ 1 '。如下:
为什么选择用字符’ 0 ‘和字符’ 1 '来表示雷和非雷而不是数字0和数字1或者其他别的符号,这里暂且不表,我们先接受这个设定。
对应的数组应该是:
char mine[11][11];//⽤来存放布置好的雷的信息
char show[11][11];//⽤来存放排查出的雷的个数信息
两个数组都采用字符类型,这样就可以用同一个函数来实现它们的打印。
接下来,就可以开始对核心代码进行实现。
2.2.2 game函数
在实现函数的初始阶段,我们可能会写出下面的代码:
void game()
{
char mine[11][11];
char show[11][11];
//棋盘初始化
InitBoard(mine, 11, 11,'0');
InitBoard(show, 11, 11,'*');
//布置雷
SetMine(mine, 9, 9);
//棋盘打印
DisplayBoard(show, 9, 9);
//排查雷
FindMine(mine, show, 9, 9);
}
这段代码虽然逻辑没有问题,最后其实也能正常运行,但如果后期我们想让雷盘更大一些,比如变成20*20的雷盘的时候,你会发现我们需要把上面设计雷盘大小的参数全部替换,这会非常不方便,所以我们可以在头文件 game.h 中定义一些常量的符号,需要使用这些符号的时候直接包含这个头文件即可,而需要更改雷盘大小也只需要在头文件中更改即可。
更加高效的代码:
//game.h
#include <stdio.h>
#include <Windows.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//test.c
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
//棋盘初始化
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'*');
//布置雷
SetMine(mine, ROW, COL);
system("cls");//用于清除刚才打印的菜单,使游戏界面美观整洁,调用需要引用头文件<Windows.h>
//棋盘打印
DisplayBoard(show, ROW, COL);
//DisplayBoard(mine, ROW, COL); //用于自己调试观察,比如观察雷是否被正确布置等,确认无误后注释掉即可
//排查雷
FindMine(mine, show, ROW, COL);
}
game函数中,对游戏设计的基本逻辑进行了展示,其中调用的具体功能的代码将在 game.c 中实现。
2.2.2.1 InitBoard函数
InitBoard函数用来实现棋盘的初始化。
写这个函数前,我们先在头文件中对其进行声明。
//game.h
void InitBoard(char board[ROWS][COLS], int rows, int cols);//初始化棋盘
然后,我们在 game.c 中对其进行实现。
在实现函数的初始阶段,我们可能会写出下面的代码:
//game.c
void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = '0';
}
}
}
但写完之后我们会发现,这个函数只能实现对雷盘初始化为字符’ 0 ‘的功能,如果要把雷盘初始化为’ * ',就需要再写一个函数,这就变麻烦了,我们正是因为想用同一个函数就可以解决问题,所以才把两个雷盘都设为字符类型。
如何实现把雷盘想初始化成什么就初始化成什么呢?
其实很好解决,我们只需要把想要初始化的字符作为参数传到函数中即可。
更加高效的代码:
//game.h
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);//初始化棋盘
//game.c
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;
}
}
}
2.2.2.2 DisplayBoard函数
将雷盘初始化后,我们可能想看一下打印出来的效果,这个时候我们就需要一个函数来实现对雷盘的打印,而棋盘的打印其实就是打印数组。
和前面一样,我们依然需要先对函数进行声明,然后再进行实现。
代码:
//game.h
void DisplayBoard(char board[ROWS][COLS], int row, int col);//打印棋盘
//game.c
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("----- 扫雷游戏 -----\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i);//打印行号
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//打印列号
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("----- 扫雷游戏 -----\n");
}
效果:
2.2.2.3 SetMine函数
SetMine函数用来实现布置雷。
实现布置雷的函数前,我们需要注意两个地方:
1.雷的布置要满足随机性,所以我们要用到生成随机数的函数,而用于随机生成坐标的rand函数的种子srand函数只需要在main函数中使用一次即可。
2.在布置雷的时候需要判断该位置是否已经布置过雷,以免重复。
为了方便修改雷的个数,我们在头文件中专门定义一个常量来表示雷的个数。
这里我们用while函数来实现布置雷的,每布置一个雷,待布置的雷就减一个,直到待布置的雷为0,函数也不再运行。
代码:
//game.h
#define EASY_COUNT 10//表示雷的个数为10个
//game.c
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;//表示待布置的雷
while (count)
{
//随机确认布置雷的坐标
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')//是字符'0'说明这个位置没有被布置过
{
mine[x][y] = '1';
count--;
}
}
}
2.2.2.4 FindMine函数
FindMine函数用来实现排查雷。
想象一下,查找雷的时候,我们需要先在mine数组中找到排查坐标处周围雷的信息,找到后还要放到show数组中对应的位置里去,所以,mine数组和show数组都要作为参数传到FindMine函数中去。
排查雷的时候我们首先需要让用户输入需要排查的坐标,然后判断坐标的合法性及该坐标是否被排查过,其次再判断该坐标是否有雷,如果没有,就继续排查,直到满足游戏胜利的条件,此处涉及到的循环我们用while函数来实现。
而不是雷的时候,我们需要收集周围雷的信息,所以我们还需要一个函数来计算坐标周围雷的个数,我们将其命名为GetMineCount。
GetMineCount统计出的雷的个数最终会被放到show数组中去,而show数组中存放的数字又都是字符的形式,这里就涉及到一个数值数字和字符数字转换的问题。
从ASCII码表中可以知道,字符数字0~9在表中的值的范围为48-57。
而我们发现,字符’ 1 ‘减去字符’ 0 ‘恰好就是数字1,同理,字符’ 2 ‘减去字符’ 0 ‘得到数字2,依此类推,我们就实现了字符数字和数值数字之间的转换,这也是之前我们为什么选择用字符’ 0 ‘和字符’ 1 '来表示雷和非雷的原因,如果用其他符号,这个地方想要实现把GetMineCount函数以及把统计出的雷的个数传到show数组中去就会变得麻烦。
要实现GetMineCount函数,我们还需要知道二维数组中一个坐标的周围坐标的表示形式,下图就很好地表示了坐标之间的相对关系。
这个时候实现GetMineCount函数和FindMine函数思路就比较清晰了:
//game.h
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷
//game.c
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1] +
mine[x - 1][y] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] - 8 * '0');//字符数字减去字符0得到对应的数值数字
//由于该函数是专门用于函数FindMine的,只在game.c内运行,
//所以不需要在game.h中声明
}
//game.c
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
printf("请输入要排查的坐标:>");
scanf("%d %d", &x, &y);
while (win < row * col - EASY_COUNT)//当win不再小于时,说明所有的雷都已排完,跳出循环
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
{
if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
{
printf("该坐标被排查过,重新输入坐标\n");
continue;
}
else if (mine[x][y] == '1')
{
system("cls");
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);//被炸死了就打印mine数组,让用户知道雷的正确信息
break;
}
else
{
int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
show[x][y] = count + '0';//数字加上字符0得到对应数字的字符
system("cls");
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
2.3 游戏完整代码
2.3.1 game.h
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
//把头文件都放在"game.h"里面,这样在其他文件直接包含"game.h"即可
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//函数的声明
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 mine[ROWS][COLS], int row, int col);//布置雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷
2.3.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;
printf("----- 扫雷游戏 -----\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i);//打印行号
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//打印列号
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("----- 扫雷游戏 -----\n");
}
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
//随机确认布置雷的坐标
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')//是字符'0'说明这个位置没有被布置过
{
mine[x][y] = '1';
count--;
}
}
}
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1] +
mine[x - 1][y] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] - 8 * '0');//字符数字减去字符0得到对应的数值数字
//由于该函数是专门用于函数FindMine的,只在game.c内运行,
//所以不需要在game.h中声明
}
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)//当win不再小于时,说明所有的雷都已排完,跳出循环
{
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')
{
system("cls");
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);//被炸死了就打印mine数组,让用户知道雷的正确信息
break;
}
else
{
int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
show[x][y] = count + '0';//数字加上字符0得到对应数字的字符
system("cls");
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
2.3.3 test.c
#include "game.h"
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**********************\n");
}
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
//棋盘初始化
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'*');
//布置雷
SetMine(mine, ROW, COL);
system("cls");//用于清除刚才打印的菜单,使游戏界面美观整洁,调用需要引用头文件<Windows.h>
//棋盘打印
DisplayBoard(show, ROW, COL);
//DisplayBoard(mine, ROW, COL); //用于自己调试观察,比如观察雷是否被正确布置等,确认无误后注释掉即可
//排查雷
FindMine(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();//结果为1,开始游戏
break;
case 0://结果为0,退出游戏
printf("退出游戏\n");
break;
default://两个结果都不是,重新输入
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
2.4 后续优化
2.4.1 ExplosionSpread函数
刚才游玩我们自己设计的扫雷时,可以很明显地感受到一个缺陷,就是我们每次只能排查一个坐标,也就是说就算每次都没有踩到雷,在雷的个数为10个的情况下,也要至少排查71次才能通过,这无疑是很影响游戏体验的。而通过游玩网页版的扫雷可以发现,当用户点击一个坐标,如果该坐标及其周围的坐标都没有雷,那么雷盘就会一次性展开一片。而我们可以利用递归的方式来实现这样的效果。
代码实现:
//game.c
void ExplosionSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw)
{
if (x >= 1 && x <= row && y >= 1 && y <= col) //判断输入的坐标是否在排查范围内,否则递归的坐标可能出到棋盘外
{
int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
if (count == 0)
{
show[x][y] = ' ';
(*pw)++;
int i = 0;
int j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
ExplosionSpread(mine, show, row, col, i, j, pw);
}
}
}
else
{
show[x][y] = count + '0';
(*pw)++;
}
}
}
代码实现后,ExplosionSpread函数就可以用在FindMine函数中来帮助我们提升排雷的效率。
//game.c
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)//当win不再小于时,说明所有的雷都已排完,跳出循环
{
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')
{
system("cls");
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);//被炸死了就打印mine数组,让用户知道雷的正确信息
break;
}
else
{
ExplosionSpread(mine, show, row, col, x, y, pw); //爆炸式展开
system("cls"); //清空屏幕
DisplayBoard(show, ROW, COL);//打印棋盘
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
2.4.2 MarkMine函数
在网页版的扫雷中我们还可以发现,如果我们确定一个坐标一定是雷时,我们可以利用标记功能来标识该坐标,方便我们后续的判断
本代码中,我们用字符 ! 来标识雷。
代码实现:
void MarkMine(char board[ROWS][COLS], int row, int col,int x,int y)
{
while (1)
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
{
if (board[x][y] == '*')//标记
{
board[x][y] = '!';
break;
}
if (board[x][y] == '!')//取消标记
{
board[x][y] = '*';
break;
}
else
{
printf("\n输入错误,请输入未被排查的坐标!\n");
continue;
}
}
else
{
printf("输入错误,请输入正确的坐标!\n");
}
}
}
想要执行MarkMine函数,我们应该以不同的输入方式来程序识别我们是否要标记。我们知道,scanf函数的返回值是返回成功读取的变量个数,那么我们可以利用这一点来让程序进行区分,如果我们输入的是两个数字表示的坐标,那么就说明我们是想排查那个坐标,而如果两个数字前还有一个字符,那么就表示我们要对那个坐标进行标记。如果想要严谨一点,我们可以添加一个执行判断的函数,只有输入了正确的字符,才会进行标记。在这里,为了省事,我们可以不限定输入的字符是什么。
把MarkMine函数应用在FindMine函数中,我们就可以实现雷的标记。
代码实现:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int* pw = &win;
char ch = 0;
int lose = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入要排查的坐标(如要标记雷,请在坐标前输入\"!\":>");
while ((ch = getchar()) != '\n');//输入数字后回车,这个回车会留在待读取的区域,需要用getchar吸收
if (scanf("%d %d", &x, &y) == 2)//排查雷
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
{
if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
{
printf("该坐标被排查过,重新输入坐标\n");
continue;
}
else if (mine[x][y] == '1')
{
system("cls");
printf("很遗憾,你被炸死了\n");
lose = 1;
DisplayBoard(mine, ROW, COL);
break;
}
else
{
ExplosionSpread(mine, show, row, col, x, y, pw);
//int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
//show[x][y] = count + '0';//数字加上字符0可以得到对应数字的字符
system("cls");
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标非法,重新输入\n");
}
if (win == row * col - EASY_COUNT)
{
system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
else if (scanf("%c %d %d", &ch, &x, &y) == 3)//标记雷
{
MarkMine(show, row, col, x, y);
system("cls");
DisplayBoard(show, ROW, COL);
continue;
}
}
}
2.5 优化后的完整代码
2.5.1 game.h
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <Windows.h>
//把头文件都放在"game.h"里面,这样在其他文件直接包含"game.h"即可
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//函数的声明
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 mine[ROWS][COLS], int row, int col);//布置雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);//排查雷
void ExplosionSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw);//爆炸式展开
2.5.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;
printf("----- 扫雷游戏 -----\n");
for (i = 0; i <= row; i++)
{
printf("%d ", i);//打印行号
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);//打印列号
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("----- 扫雷游戏 -----\n");
}
void SetMine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count)
{
//随机确认布置雷的坐标
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')//是字符'0'说明这个位置没有被布置过
{
mine[x][y] = '1';
count--;
}
}
}
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
return (mine[x - 1][y - 1] +
mine[x - 1][y] +
mine[x - 1][y + 1] +
mine[x][y - 1] +
mine[x][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] - 8 * '0');//字符数字减去字符0得到对应的数值数字
//由于该函数是专门用于函数FindMine的,只在game.c内运行,
//所以不需要在game.h中声明
}
void MarkMine(char board[ROWS][COLS], int row, int col,int x,int y)
{
while (1)
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
{
if (board[x][y] == '*')//标记
{
board[x][y] = '!';
break;
}
if (board[x][y] == '!')//取消标记
{
board[x][y] = '*';
break;
}
else
{
printf("\n输入错误,请输入未被排查的坐标!\n");
continue;
}
}
else
{
printf("输入错误,请输入正确的坐标!\n");
}
}
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int* pw = &win;
char ch = 0;
int lose = 0;
while (win < row * col - EASY_COUNT)
{
printf("请输入要排查的坐标(如要标记雷,请在坐标前输入\"!\":>");
while ((ch = getchar()) != '\n');//输入数字后回车,这个回车会留在待读取的区域,需要用getchar吸收
if (scanf("%d %d", &x, &y) == 2)//排查雷
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入的坐标是否在排查范围内
{
if (show[x][y] != '*')//判断输入的坐标是否已经被排查过
{
printf("该坐标被排查过,重新输入坐标\n");
continue;
}
else if (mine[x][y] == '1')
{
system("cls");
printf("很遗憾,你被炸死了\n");
lose = 1;
DisplayBoard(mine, ROW, COL);
break;
}
else
{
ExplosionSpread(mine, show, row, col, x, y, pw);
//int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
//show[x][y] = count + '0';//数字加上字符0可以得到对应数字的字符
system("cls");
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("坐标非法,重新输入\n");
}
if (win == row * col - EASY_COUNT)
{
system("cls");
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
else if (scanf("%c %d %d", &ch, &x, &y) == 3)//标记雷
{
MarkMine(show, row, col, x, y);
system("cls");
DisplayBoard(show, ROW, COL);
continue;
}
}
}
void ExplosionSpread(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y, int* pw)
{
if (x >= 1 && x <= row && y >= 1 && y <= col) //判断输入的坐标是否在排查范围内,否则递归的坐标可能出到棋盘外
{
int count = GetMineCount(mine, x, y);//统计坐标周围有几个雷
if (count == 0)
{
show[x][y] = ' ';
(*pw)++;
int i = 0;
int j = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
ExplosionSpread(mine, show, row, col, i, j, pw);
}
}
}
else
{
show[x][y] = count + '0';
(*pw)++;
}
}
}
2.5.3 test.c
#include "game.h"
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 0.exit ******\n");
printf("**********************\n");
}
void game()
{
char mine[ROWS][COLS];
char show[ROWS][COLS];
//棋盘初始化
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'*');
//布置雷
SetMine(mine, ROW, COL);
system("cls");//用于清除刚才打印的菜单,使游戏界面美观整洁,调用需要引用头文件<Windows.h>
//棋盘打印
DisplayBoard(show, ROW, COL);
//DisplayBoard(mine, ROW, COL); //用于自己调试观察,比如观察雷是否被正确布置等,确认无误后注释掉即可
//排查雷
FindMine(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();//结果为1,开始游戏
break;
case 0://结果为0,退出游戏
printf("退出游戏\n");
break;
default://两个结果都不是,重新输入
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
2.5.4 游戏效果演示
扫雷演示