和光同尘_我的个人主页
扫雷
🔒前言
扫雷,那段全神贯注的时光,追逐着隐藏在方块下的雷,是我回不去的童年记忆。如今,岁月不曾停留,回忆依然鲜活。终于到了有一天,自己也能实现简单的扫雷,以另一种方式找回童年的美好。
1. 准备
首先,对于此类功能函数较多的项目,我们最好使用模块化编程方式,即分别将函数的声明、实现以及功能测试放在mine.h mine.c test.c中,使代码更具可读性、更便于维护👍
- 确定游戏功能与逻辑
- 打印菜单,选择是否进行游戏
- 布置雷,并打印出棋盘信息
- 玩家选择位置信息,进行排查
- 检查坐标,不为雷:展开同样不为雷的格子;旁边有雷:显示个数;为雷结束游戏
- 重复3-4步骤
- 成功排雷结束游戏
- 函数功能设计
为了实现以上功能,我们一共需要两个二维数组,一个为show
,用于展示给用户,没有选中检查时打印‘*’
,旁边有雷时打印周围雷的个数,另一个为mine
,用于放置雷
函数 | 功能 |
---|---|
InitBoard | 初始化棋盘 |
DisplayBoard | 打印棋盘 |
SetMine | 随机设置雷 |
FindMine | 输入坐标,判断是否为雷 |
GetMineCount | 获得坐标周围雷的数量 |
ExposeMine | 检查坐标周围没有雷时,展开周围同样不为雷的格子 |
定义雷的数量
#define MINE_NUM 10
2. 函数功能实现
2.1. 菜单的打印与选择
- 这个比较简单,通过 printf 和 switch 即可实现,
- 唯一值得注意的就是,使用while循环,在游戏结束后重新进入菜单页面,重新进行选择是否进行游戏
void menu()
{
printf("**************************\n");
printf("******** 1.play ********\n");
printf("******** 0.exit ********\n");
printf("**************************\n");
}
int main()
{
srand((unsigned int)time(NULL));
int sel = 0;
do
{
menu();
printf("请选择->");
scanf("%d", &sel);
switch (sel)
{
case 1:
printf("开始游戏\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
}while (sel);
return 0;
}
2.2. InitBoard:初始化棋盘
需要注意的是,由于我们在统计检查格子周围雷的数量是,需要遍历旁边一圈的格子,把雷的数量相加,而如果检查格子在数组边缘,就无法读取周围的数据,相比于通过特殊情况来控制,我们可以通过创建一个行为ROW+2,列为COL+2
的二维数组,而在打印时,只打印中心区域,即行为ROW,列为ROW
的区域,就可以解决这个问题
因此,我们可以在“game.h”
文件通过#define
定义以下内容,便于后期修改与维护
用于打印二维数组的大小
#define ROW 10
#define COL 10
用于创建二维数组的大小
#define ROWS ROW+2
#define COLS COL+2
- 由于我们需要初始化
show
与mine
两个棋盘,而这两个棋盘大小一样,仅在初始化内容上有区别,所以我们在设计函数时,可以多设置一个参数,用于传递用于初始化的数据,初始化show
就传递*
,初始化mine
就传递0
.
二维数组的初始化,两层循环遍历数组并赋值即可
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.3. DisplayBoard:打印棋盘
- 前面提过,我们创建的棋盘
行为ROW+2,列为COL+2
,打印的棋盘行为ROW,列为ROW
,所以打印函数设计时,参数既要传递行为ROW+2,列为COL+2
的二维数组,也要传递ROW
和COL
用于打印
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
printf(" %c ", board[i][j]);
}
printf("\n");
}
}
运行结果:
- 此时我们会发现,数据确实打印出来了,但是缺少分割线,不太美观;同时如果没有一个坐标指引,不方便下一步选择坐标。我们稍稍改进一下
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("|");
for (i = 0; i <= row; i++)
{
printf("-----|");
}
printf("\n|");
for (i = 0; i <= row; i++)
{
printf(" %2d |", i );
}
printf("\n|");
for (i = 0; i <= row; i++)
{
printf("-----|");
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("| %2d |", i);
for (j = 1; j <= col; j++)
{
printf(" %c |", board[i][j]);
}
printf("\n|");
for (j = 0; j <= col; j++)
{
printf("-----|");
}
printf("\n");
}
}
- 这样打印出来就比较美观了
*同时,我们在打印show棋盘的同时,在这一步也可以打印mine棋盘,看看mine棋盘是否成功初始化
2.4. SetMine: 随机设置雷
-
随机数的设置我们可以使用
rand
生成随机数,不过单独使用rand
函数的话我们每次执行这段代码生成的随机数都是一样的,所以还要使用srand
函数初始化随机种子,使每次执行生成不一样的随机数
srand
函数的调用应当放在主函数中并包含头文件<time.h>
-
主函数
int main()
{
srand((unsigned int)time(NULL));
int sel = 0;
do
{
menu();
printf("请选择->");
scanf("%d", &sel);
switch (sel)
{
case 1:
printf("开始游戏\n");
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
}while (sel);
return 0;
}
- 由于我们创建的二维数组
行为ROW+2,列为COL+2
,而在打印时,只打印行为ROW,列为ROW
的区域,所以我们调用rand函数生成的随机数%row+1就可以让随机数生成的范围限定在[1,ROW]中,即我们要打印的区域下标范围
- 另外,我们在布置雷时,要避免在同一位置重复布雷,所以在随机生成雷的坐标后,要对坐标进行检测,如果为
0
,就说明不为雷,将此坐标赋值为1
;如果为1
,就说明这个坐标已经有雷了,我们跳过
void SetMine(char board[ROWS][COLS], int row, int col)
{
//创建num等于事先定义的雷的数量
int num = MINE_NUM;
//循环num次,布置雷
while (num)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
//判断坐标是否布置过雷
if (board[x][y] == '0')
{
board[x][y] = '1';
num--;
}
}
}
2.4. GetMineCount:获得检查坐标周围雷的数量
由于我们在设计函数时已经为获得坐标周围雷的数量,多给二维数组加了一圈,并且在mine
二维数组布雷时,有雷的格子赋值为1
,所以获得检查坐标周围雷的数量,只需要遍历mine
二维数组坐标周围8个格子,求和即可
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
int a = mine[x - 1][y - 1] + mine[x - 1][y] + mine[x][y - 1]
+ mine[x + 1][y] + mine[x][y + 1] + mine[x - 1][y + 1]
+ mine[x + 1][y - 1] + mine[x + 1][y + 1]-8*'0';
return a;
}
2.5. FindMine:输入坐标,判断是否为雷
这个函数主要实现3个功能,我们一个个讲下
1. 输入坐标:对于输入的坐标,我们要进行范围检查,即判断坐标是否在[1,ROW]
区间内
2. 判断是否为雷:如果输入坐标对应的mine
二维数组上的位置为雷(即值为1
),结束游戏,打印所有雷的位置;不为雷,显示周围雷的数量
3. 游戏结束的条件判断:定义一个win
变量,每次成功找到一个不为雷的格子,win--
,直到win==格子总数-雷的数量
,游戏胜利
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 - MINE_NUM)
{
printf("输入查找坐标");
scanf("%d %d", &x, &y);
//检查坐标范围
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//判定踩到雷,游戏结束,打印所有雷的位置
if (mine[x][y] == '1')
{
printf("失败,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
//判定不为雷,win++,显示雷的数量
else if (mine[x][y] == '0')
{
win++;
show[x][y] = GetMineCount(mine, x, y) + '0';
DisplayBoard(show, ROW, COL);
}
}
else
printf("坐标非法,重新输入\n");
}
if (win == ROW * COL - MINE_NUM)
{
printf("成功\n");
DisplayBoard(mine, ROW, COL);
}
}
- 不过仅仅这样设置的话,一次只能显示一个格子周围雷的数量,那如何实现一次性展开周围所有不为雷的格子呢,这就用到了接下来递归展开。
2.6. ExposeMine:检查坐标周围没有雷时,展开周围同样不为雷的格子
递归的思路也很简单,只有遍历坐标周围的8个格子,依次计算每个格子周围雷的数量,如果有雷,把雷的数量赋值到show
二维数组对应的位置;如果没有雷,就把同样周围没有雷的格子再作为参数传入ExposeMine
函数,直到周围没有雷的格子都被展开。而后记得没调用一次ExposeMine
函数,win--
一次,这样才能正确判断游戏胜利
void ExposeMine(char mine[ROWS][COLS],char show[ROWS][COLS], int x, int y, int* win)
{
if (x >= 1 && x <= ROW && y >= 1 && y <= COL)
{
int i = x;
int j = y;
int a = GetMineCount(mine, i, j);
if (a == 0)
{
show[i][j] = ' ';
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*')
ExposeMine(mine, show, i, j, win);
}
}
}
else
show[i][j] = a + '0';
(*win)++;
}
}
这样我们就可以实现展开了
🗝️总结
- 这样我们就实现了一个简单的扫雷,希望大家看完文章后能自己进行实践和改进,还有一些功能可以试着自己去实现,例如如何保证第一次输入坐标不被炸死、对已经判断为雷的格子进行标记👍
本节完~~,如果你在实现过程中遇到任何问题,欢迎在评论区指出或者私信我!💕 |
新人博主创作不易,如果有收获可否👍点赞✍评论⭐收藏一下?O(∩_∩)O
THANKS FOR WATCHING |