大家好,这一期跟大家聊聊如何用C语言实现扫雷这款十分有趣的游戏,这代码背后的逻辑以及我们如何思考去一步一步实现和完善的。
1、扫雷游戏的功能说明
-
扫雷游戏需要一份菜单来供玩家选择继续玩游戏和退出游戏
-
扫雷棋盘是9*9的格子
-
默认随机布置10个雷
-
玩家可以排查雷
如何排查雷呢?如果位置不是雷,就显示周围有多少个雷
如果位置是雷,就显示被炸死了,游戏结束跳转菜单
只有把除十个雷之外的所有不是雷的位置找出来,排雷才算成功,游戏结束跳转菜单 -
我们使用控制台来实现经典的扫雷游戏。
那何为控制台呢?
这就是Windows控制台命令窗口,这个扫雷游戏我们将通VS的控制台窗口来实现扫雷。下面是在控制台上的游戏界面
2、扫雷游戏的分析和设计
2.1 数据结构的分析和设计
扫雷的过程中,我们需要数据结构去存储我们所布置的雷以及我们排查出的雷的信息和周围雷的个数,那我们应该设计怎样的数据结构去存储呢?
我们可以想到扫雷游戏是需要在一个9*9的棋盘去布置雷的信息以及排查雷,那我们需要去定位到棋盘中的每一个格子,这时候我们会用到坐标。当看到坐标时,我们也不难想到我们可以用二维数组这样的一种结构来存放我们的信息。
那我们又如何来表示我们的信息呢?如何表示有雷和无雷呢?
有和无,真和假,1和0,我们也就想到把有雷置为1,无雷置为0,这样看来似乎很合理。
那当我们进一步去思考下一步的功能时,我们就发现了一个问题了。我们都知道扫雷游戏在扫雷过程中如果我们选择某个不是雷的位置时,它会显示以该位置为中心的九宫格中雷的数量来提示玩家附近有多少个雷,那当我们九宫格中有一个雷时,是不是该位置需要显示1,如果我们都将雷和排雷的提示信息放在同一个数组中,那这个1究竟是雷还是附近有1个雷呢?这会导致混淆而实现不了效果,后续打印也会很困难,那有什么解决办法吗?
要不我们将雷定义为字符的1和0,而附近雷的个数定义为整数?
这种方法确实可以解决冲突,但是如果把他们放在一个数组中,既有雷有无信息和附近雷个数的信息,两者混合在一起,对后续我们要打印出来给玩家呈现最终雷的排布信息很困难,不方便,观感差。
那不如我们用两个棋盘,也就是两个数组,一个存放布置好的雷的信息,一个存放以所指位置为中心的九宫格中雷的数量?
这种方法是最好的,两者互不干扰,在一个数组中排雷,在另一个数组中记录排雷时排查的数据,在排雷过程中可以打印排查数据这个数组供玩家参考来继续排雷,而在玩家踩雷或排雷成功后打印雷的信息这个数组来直观呈现雷的布局,使玩家玩得明明白白。
接下来我们都知道在最开始时游戏界面都是空白的,将雷隐藏于背后来保持神秘感,所以我们会想到用空白来遮挡,但是个人觉得空白在控制台上的视觉效果并不是很好,所以用了*来实现,我们也可以根据自己的喜好选择不同的字符。所以很显然我们数组所用的类型就是字符,也为了方便我们对两个棋盘进行初始化、打印等处理,我们将个数和雷的信息都设置为字符’0’,'1’等等字符数字。
当我们将数组定义为9行9列,并进一步思考排雷的过程时,我们会在排查棋盘边缘雷的个数时遇到一些问题,我们以最后一行或最后一列某个位置为中心的九宫格去排查时,都会发生数组越界的情况,那应该如何呢?
当我们看到这个棋盘时,我们不难想到,我们如果给数组扩大一圈,也就是1111的棋盘,雷还是布置在99的棋盘里面,只要初始化时都初始化为字符0就不会影响我们的排雷,且不会发生越界,并且打印时我们也只需要打印9*9即可。
这样随着我们的一步一步思考,存储的数据结构也就设计出来了。
这样就可以设计出来两个二维数组
2.2文件结构的设计
扫雷游戏分为三个文件
1.test.c //文件中写游戏的测试逻辑
2.game.c //文件中写游戏中函数的实现等
3.game.h //文件中写游戏需要的数据类型和函数声明等
2.3扫雷游戏的代码的设计和具体实现
我们开始着手写代码时,考虑玩家刚开始进入的界面应该是菜单,所以我们main函数里最先需要一个菜单(menu)函数,不需要返回值,只需要打印菜单。
#include <stdio.h>
void menu()
{
printf("*******************\n");
printf("**** 1.play ****\n");
printf("**** 0.exit ****\n");
printf("*******************\n");
}
int main()
{
menu();
return 0;
}
代码运行成功,当我们继续写,就需要一个变量(input)来存放玩家输入的0和1,0则退出,1则开始扫雷,这样想我们就可以使用switch语句来实现,我们游戏要实现玩家可以一直玩,而不是只能玩一局,这样我们会想到循环,而且我们要求开局就会立刻显示菜单,这时我们就会想到do-while循环语句了,我们把input作为while判断的依据,这样就可以实现我们要的效果了。
如果玩家选错了,没有选0和1,那系统又会如何处理呢?
我们可以给个default来打印选择错误,重新选择。
int main()
{
int input = 0;
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
printf("开始扫雷\n");
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
代码运行成功,达到我们的效果,我们把最基础的界面实现了。
接下来就是要去实现真正的扫雷游戏了。我们需要在case1那里插入一个game()函数,来实现扫雷游戏,而game函数中也是存放实现游戏功能的函数,而具体函数的实现在game.c中完成,game.h存放写游戏所需要的数据类型和函数声明等,所以test.c和game.c都需要包含game.h。
开始游戏设计,我们首先是需要创建两个二维数组作为棋盘(mine[][]和show[][])并进行初始化(因为我们定义时只能初始化为全数字0,而不能是全字符0和*),这个时候我们就需要一个初始化棋盘函数InitBoard(),以便于我们对两个棋盘初始化,参数自然需要二维数组,行,列这三个参数,但是我们两个棋盘初始化的数据并不相同,mine数组用于存放雷,我们将其初始化为全字符0(无雷),show数组用于存放排雷的信息,呈现给玩家进行排雷,为了保持神秘,我们会将其初始化为全*,这时我们就需要第四个参数来存放初始化的数据了,这样就可以着手写初始化函数了
#include "game.h"
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
}
#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;
}
}
}
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
或许看到上面的代码会有一些疑问,比如为什么我们不把数组直接写上11而是在头文件进行宏定义呢?
原因很简单,为了后续我们对游戏进行改进更简便,我们要实现例如12*12这样的棋盘,只需修改宏定义背后的9,而不需要将整个代码中的11和9进行修改,提高了代码的质量。
当我们完成上述代码后,想验证初始化的棋盘是不是我们所想要的效果,我们就需要将其打印出来,所以接下来我们写一个打印函数来验证棋盘的初始化。
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
DisplayBoard(mine, ROWS, COLS);
DisplayBoard(show, ROWS, COLS);
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
printf("%c", board[i][j]);
}
printf("\n");
}
}
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
我们可以看到我们初始化的结果是正确的。
我们发现game.c和test.c都需要用到printf函数,所以将stdio.h头文件放在game.h中。当我们看到这个结果时我们会发现很难看,想找几行几列很难找,所以我们可以设计将行号和列号打印出来,并且我们也只需要呈现9*9的棋盘,这也是为何我们宏定义需要定义ROW和COL。代码改进如下:
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
}
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
printf("------开始扫雷-------\n");
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
这样就优化了界面,使玩家更好的观感和体验。
初始化之后就是布置雷了,我们要在棋盘中随机布置10个雷,需要一个SetMine()函数,那我们首先考虑如何在81个格子中随机的找10个格子放置雷呢?自然就是生成两个随机数来表示行和列,就随机锁定一个格子,将雷埋在其中就可以了。这个过程中我们需要在数组下标范围为1到9的十个格子中埋雷,所以我们要生成1-9的随机数。再者,我们还要考虑到可能再次产生的随机数形成的位置我们已经埋雷了,所以还需要进行判断。所以到这里我们就可以去实现我们的代码了。
int main()
{
int input = 0;
srand((unsigned int)time(NULL));//种子随机一次即可
do
{
menu();
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
}
} while (input);
return 0;
}
void game()
{
char mine[ROWS][COLS] = { 0 };//存放布置好的雷
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
/*DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);*/
//布置雷
SetMine(mine, ROW, COL);
DisplayBoard(mine, ROW, COL);
}
void SetMine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY_COUNT;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define EASY_COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化棋盘
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 board[ROWS][COLS], int row, int col);
我们可以看到我们布置的雷每次生成的位置是不一样的,达到了我们想要的效果,雷埋完之后,下一步就是玩家的排雷和踩雷设置了。首先我们需要获取玩家所要排雷的位置,也就是需要玩家输入一个坐标,那玩家输入坐标之后我们就要去排查该位置是雷还不是雷,是雷就打印很遗憾,你被炸死了。自然我们也要玩家被炸的明明白白,所以还需要打印雷的布置给玩家。如果不是雷,我们需要在这个位置打印出以这个位置为中心的九宫格所隐藏的雷。也就是还需要打印雷的信息棋盘,将改成对应的数字然后打印,所以参数需要两个数组,而且我们还需要设计一个统计周围雷的数量的函数。玩家需要将其他71个格子找出才算胜利,所以我们需要一个循环来实现,当踩雷则退出循环,游戏结束,如果不是雷则将71一次一次的减,则到为0则打印扫雷成功。
但是我们可以再想的更全面一点,万一玩家坐标输错一个超出范围的坐标呢,万一我们再次查同一颗雷呢,那应该如何处理?
那给出判断,当输入错误时,不满足条件,打印输入错误重新输入,这样就可以避免输入错误而无法正常运行游戏。当重复时,判断show中是不是,是则打印坐标已被排查,重新输入。
那让我们再想想如何设计统计九宫格内周围雷的个数呢?
我们可以从x-1到x+1循环再从y-1到y+1这个循环中去判断如果是1,就加一最后统计多少个;也可以在循环中将将他们加起来,但他们是字符并不是数字,所以我们要的到数字需要每个字符都减去字符0,就能得到相应的整数,再相加得到雷的个数。例如字符0的ASCI码是48,字符1的是49,字符2的是50,减后都能得出相应的数字;我们也可以将位置周围顺时针八个相加再减去八个字符0,同样也能得到。
下面是关于排雷实现的代码:
void game()
{
char mine[ROWS][COLS] = { 0 };//存放布置好的雷
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化棋盘
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
/*DisplayBoard(mine, ROW, COL);*/
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(mine, ROW, COL);
/*DisplayBoard(mine, ROW, COL);*/
//排查雷
FindMine(mine, show, ROW, COL);
}
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 + 1][y + 1] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x][y - 1] - 8 * '0';
}
//int GetMineCount(char mine[ROWS][COLS], int x, int y)
//{
// int i = 0;
// int count = 0;
// for (i = x - 1; i <= x + 1; i++)
// {
// int j = 0;
// for (j = y - 1; j <= y + 1; j++)
// {
// count += (mine[i][j] - '0');
// }
// }
// return count;
//}
//int GetMineCount(char mine[ROWS][COLS], int x, int y)
//{
// int i = 0;
// int count = 0;
// for (i = x - 1; i <= x + 1; i++)
// {
// int j = 0;
// for (j = y - 1; j <= y + 1; j++)
// {
// if (mine[i][j] == '1')
// count++;
// }
// }
// return count;
//}
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)
{
printf("请输入要排查的坐标:>");
scanf("%d%*c%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标已被排查,重新输入\n");
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define EASY_COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化棋盘
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 board[ROWS][COLS], int row, int col);
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
这样我们整个最基础的扫雷游戏就实现了。
3、扫雷游戏的拓展
游戏难度选择
- 简单9*9,10个雷;
- 中等16*16,40个雷;
- 困难30*16,99个雷
如果排查不是雷,是否可以展示周围没有雷的那一片
雷的标记设计
排雷时间显示设计
这些功能可以使扫雷游戏更加有趣,设计也比较复杂,这一期我们先从简单入手,期待我们后面对其的改进。
下面展示扫雷游戏的全部代码
#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, '*');
//打印棋盘
DisplayBoard(show, ROW, COL);
//布置雷
SetMine(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();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
#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 <= col; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
void SetMine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY_COUNT;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (board[x][y] == '0')
{
board[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 + 1][y + 1] + mine[x + 1][y] + mine[x + 1][y - 1] + mine[x][y - 1] - 8 * '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 - EASY_COUNT)
{
printf("请输入要排查的坐标:>");
scanf("%d%*c%d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
DisplayBoard(show, ROW, COL);
win++;
}
}
else
{
printf("坐标已被排查,重新输入\n");
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define EASY_COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//初始化棋盘
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 board[ROWS][COLS], int row, int col);
//排雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
感谢大家的观看,不好之处请大家指正!