需要创建包含以下3个文件如下:
- test.c-------用于测试游戏的运行
- game.h-------用于封装函数声明
- game.c------用于实现游戏所需要的函数
使用多个文件实现能方便代码的复用,调试时也能更加准确迅速找到对应的错误,并且能保证主程序简洁。
1.打印游戏主界面菜单以及玩家交互
进入游戏,首先需要打印一个与玩家交互的菜单,可供玩家自行选择,这里我们通过创建变量接收玩家选择项,这个选择具有多种性,并且可能会出现玩家选择错误的情况,这时要让玩家重新选择,这一过程需要用到循环,而do while循环比较适配这一过程,再配合可适配多选择性的switch语句,将能很好的完成这一流程,而system()函数可以使游戏选择界面更加简洁,以防玩家选错后,导致反复打印游戏主菜单,起到了清除的效果,使用需要引用头文#Include<Windows.h>
游戏主菜单打印
int input = 0;
do
{
menu();
printf("请选择:> ");
scanf("%d", &input);
system("cls");
switch (input)
{
case 0:printf("退出游戏\n");
break;
case 1:printf("游戏开始\n");
game();
break;
default:printf("输入错误,请重新输入:\n");
}
} while (input);
使用自定义函数menu()打印游戏主菜单
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 2.exit ******\n");
printf("**********************\n");
}
2.游戏进行
游戏棋盘
我们都知道,扫雷游戏是又一个n*m个格子所构成的,与棋盘的构成十分相似,所以我们直接采用三子棋中棋盘的布局方法来初始化,关于棋盘的布局,我们可以用二维数组Board[ROW][COL]来实现,分别用COW和COL来表棋盘的行数和列数,同时用#define的方式来定义ROW和COL大小。
两个棋盘的思想以及棋盘的扩大
两个棋盘
在扫雷时,我们需要在某几个格子下埋地雷,也就是在该点赋上一个特定的值来表示地雷,但是玩家在扫雷时并不能直接知晓改位置是否有地雷,而是一个未知量,也就是说,其实一个格子代表着两个量,一个是玩家所看到的未知的量,一个又是游戏开始就已经埋下的地雷,此时我们发现一个棋盘并不能做到这样的效果,于是我们不难产生使用两个棋盘的思想,一个棋盘用于存放地雷的相关信息,另一个棋盘用于玩家扫雷,随着玩家的选择,棋盘上的元素做出相应的改变。
棋盘的扩大
同样的,在后续扫雷时,我们还需要处理的一件事是,计算某点九宫格周围地雷的个数。我们知道,棋盘是由数组构成的,那么在统计某点周围地雷个数的时候就必然需要访问数组,而关于数组最让人头痛的事情就是数组访问的越界,关于中间元素的访问,我们并无担忧,但是作为数组最外层的访问,我们能够很明确的知道,此时的访问去统计地雷的个数就必然会导致数组越界,此时有人产生了给数组的行列都增加2的想法,再将中间的棋盘传递使用,此时能够完美的解决数组越界访问的问题,这时再使用#define定义ROWS和COLS分别表示ROW+2和COL+2,使的后续棋盘的行列便于改变。
棋盘的初始化
由于我们使用的是两个棋盘,并且,两个棋盘中要初始化成的元素不相同,所以我们可以通过传递要初始化为的元素来指定为棋盘中放哪些元素。通过函数InitBoard()来初始化棋盘中的元素。
对于棋盘元素的初始化,你可以初始化为任意的元素。这里我是将放有地雷信息的棋盘元素初始化为 '0' ,将展示给玩家的棋盘元素初始化为 '*'。
void InitBoard(char Board[ROWS][COLS], int row, int col, char p)//p为棋盘要初始化成的元素
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
Board[i][j] = p;
}
}
}
地雷的布置
在初始化完棋盘后,我们需要布置地雷的位置,由于地雷的位置是随机放置的,所以我们不难想到使用库函数rand()和srand()来产生随机数,赋值给二维数组的行和列元素的小标。
通过LayoutThunder()函数来更改对应点的元素,这里我将埋有地雷的点改为 '1' 来表示,同时使用#define定义要埋雷的个数THUNDER,同样是方便后续对埋雷个数的更改。
void LayoutThunder(char Board[ROWS][COLS], int row, int col)//初始化地雷的位置
{
int x = 0;
int y = 0;
int count = 0;//计算埋好雷的个数
while(count<THUNDER)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (Board[x][y] == '0')//避免同一点多次埋雷
{
Board[x][y] = '1';
count++;
}
}
}
打印棋盘
为了让棋盘更加好看,可以为棋盘加上一些边框来修饰,同时在棋盘最外层打印对应的行数和列数的信息,这样玩家在扫雷时能更加方便的找到相应的坐标位置,这一过程通过函数PrintBoard()来完成。效果如图下:
void PrintBoard(char Board[ROWS][COLS], int row, int col)
{
printf("----------------扫雷游戏---------------\n");
int i = 0;
for (i = 0; i <=row; i++)//打印列信息
{
if (row == i)
{
printf(" %d ", i);
}
else
{
printf(" %d |", i);
}
}
printf("\n");
for (i = 1; i <= row; i++)//打印行信息
{
int j = 0;
for (j = 0; j <= col; j++)
{
if (j == col)
{
printf("---");
}
else
{
printf("---|");
}
}
printf("\n");
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf("| %c ", Board[i][j]);
}
printf("\n");
}
printf("----------------扫雷游戏---------------\n");
}
扫雷游戏的开始
这里通过封装函数game()来使用包装扫雷时要使用到的函数,让主程序更加简洁。
void game()
{
char Mine[ROWS][COLS] = { 0 };
char Show[ROWS][COLS] = { 0 };
InitBoard(Mine, ROWS, COLS, '0'); //存储雷信息棋盘
InitBoard(Show, ROWS, COLS, '*');//玩家扫雷棋盘
PrintBoard(Show, ROW, COL);//打印棋盘
LayoutThunder(Mine, ROW, COL);//布雷
FindThunder(Mine, Show, ROW, COL);//扫雷
}
玩家扫雷
布置好雷的相关信息后,我们将正式进入扫雷游戏,调用函数FindThunder()来完成这一过程。通过玩家输入的坐标来访问对应的格子,判断扫雷结果。
void FindThunder(char Mine[ROWS][COLS],char Show[ROWS][COLS],int row, int col)//扫雷
{
int x;
int y = 0;
printf("请输入你要排雷的坐标:(两坐标之间用空格隔开)");
while (1)
{
scanf("%d%d", &x, &y);
if (x <= row && x >= 1 && y <= col && y >= 1)//判断坐标是否合法
{
if(Mine[x][y]=='1')//判断改点是否埋有雷
{
printf("很不幸,你被炸死了\n");
ShowThunder(Mine, Show, row, col);//打印埋有地雷的点
break;
}
else if (Mine[x][y] == '0' && Show[x][y] != '*')//判断是否坐标重复
{
printf("该位置已经排过雷,请重新输入其他坐标:");
}
else
{
UnfoldBoard(Mine, Show, row, col, x, y);//展开函数
PrintBoard(Show, row, col);//打印玩家扫雷后的棋盘
}
}
else
{
printf("输入坐标非法,请重新输入:");
}
if (Count(Show,row,col) == THUNDER)
{
printf("恭喜你,扫雷成功!\n");
ShowThunder(Mine, Show, row, col);
break;
}
}
}
3.扫雷的结果
-
扫雷点重复或者坐标非法
这个比较常见,玩家可能输入了错误的坐标点,关于这个,只需要判断玩家输入的坐标,通过循环让玩家重新输入一次坐标即可。
-
扫雷失败
最为容易判断的一个结果,我们可以通过玩家输入的坐标 x 和 y 访问储存雷信息的棋盘来判断改点是否为地雷,但是为了让玩家清楚自己踩了哪个雷,我们可以在玩家踩雷后打印出雷的分布点信息。这里通过ShowThunder()函数来完成,通过访问储存雷信息的棋盘,来得知雷的分布点,从而将展示给玩家的棋盘埋有地雷的点改为'@'(也可以改为其他任意字符),方便玩家更清楚的看到雷的分布点如图下效果:
void ShowThunder(char Mine[ROWS][COLS], char Show[ROWS][COLS], int row, int col)//显示布置雷的位置
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (Mine[i][j] == '1')
{
Show[i][j] = '@';//将为地雷的点改为@
}
}
}
PrintBoard(Show, row, col);//打印更改完后的棋盘
}
-
扫雷成功
这个也相对来说比较简单,我们可以通过自定义函数Count()来统计玩家扫雷棋盘中未知点的个数并返回,如果未知点的个数等于雷的的个数,则扫雷成功。
int Count(char Show[ROWS][COLS], int row, int col)
{
int i = 0;
int count = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (Show[i][j] == '*')//判断是否为未知点
{
count++;
}
}
}
return count;
}
-
扫雷继续(难点)
这个作为最常见的一个结果,但同时也是最为困难的。需要达到如图下的效果:
这个效果简单来说就是当扫雷点周围没有地雷的时候,向四周展开,直到周围有点存在地雷后,显示该点周围的地雷个数,那么应该怎么实现这个效果呢?
判断点周围雷的个数
首先我们先要完成判断某点周围地雷个数的函数,这个相对来说比较简单,我们通过函数CountThunder()传递改点行列的下标数来访问储存雷信息的棋盘,遍历改点周围的元素,从而统计到点周围雷的个数。
int CountThunder(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')//该点为'1'为地雷,count加1
{
count++;
}
}
}
return count;
}
向四周扩散的展开效果
通对于扫雷向四周扩散的展开效果,我们需要对点周围雷的数量进行讨论
- 点周围存在雷时:
当点的周围存在雷时,我们只需要标记出当前点雷的数量即可,通过调用接收CountThunder()函数的返回值来完成。值得注意的是,函数的返回值为整形变量,而棋盘中元素存放的是字符型变量
,我们可以通过让函数的返回值加上字符'0',即可得到改点雷的个数来标记改点。
- 点周围不存在雷时:
当点的周围不存在雷时,需要向四周扩散展开,直到点的周围存在雷,这一个过程十分适合使用递归来完成,我们知道递归的使用需要有限制条件,并且随着递归的展开,逐渐接近终止的条件,而这一过程则十分契合递归的使用条件。
我们可以把向四周扩散一次看作一次函数递归,每次递归会依次向周围八个点判断是否适用于递归条件,如果满足适用条件,则改点进行一次函数递归,再向它周围的八个点进行判断是否满足递归,依次循环如此,直到不满足递归条件。
看完上面的一小段话,应该多少了解了一点关于这个递归的思路,也有可能还有人还没弄明白怎么回事,那我们再来细捋一遍这一过程中还有一些需要我们注意的地方:
- 陷入死递归:
可能有人会说,递归的限制条件不就是周围没有雷吗,这个确实是,但不是唯一的条件,我们仔细想想,现随着递归的展开,它是否有可能陷入死递归呢?答案是肯定的,如果我们不对已经展开过一次递归的点做出修改,我们会发现,当某一点在进行一次递归后,其会被周围8个满足递归条件的点返回再次进行递归,如此反复的调用递归,就陷入了死递归。所有当某一点进行一次递归调用后,为了防止其进行多次递归,我们需要将该点元素改为空格,并且在递归前判断改点是否为不空格,如果为空格即使改点周围没有雷也跳过此次递归。
- 递归导致数组越界:
还有一个特别重要的条件可能大家所忽视,那就是递归导致的越界,我们要时刻谨记,在数组当中,越界是很有可能会发生的。仔细想想,我们不难发现,随着递归调用的展开,可能会出现递归到,棋盘最外层的情况,而虽然我们的棋盘行和列都要多2,但是由于棋盘是作为数组整体初始化的,所以棋盘最外层也是有元素的而且均不为地雷,因为我们的地雷是布置在中间9*9的棋盘,所以剩余两行两列都是不为雷的元素,这样一来最外层也满足了递归的条件,这就导致我们多分出来的两行两列并不能阻止数组越界,所以我们需限制要递归的另一个前提是递归的元素要是中间棋盘9*9中的元素。
void UnfoldBoard(char Mine[ROWS][COLS], char Show[ROWS][COLS], int row, int col, int x, int y)//展开棋盘
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//限制递归的展开空间
{
int count = CountThunder(Mine, x, y);//计算点周围雷的数量
if (count == 0)//周围没有雷,进行递归调用
{
Show[x][y] = ' ';//将要递归的点变为空格
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <=y + 1; j++)
{
if (Show[i][j] == '*')//限制递归的点,防止反复递归调用
{
UnfoldBoard(Mine, Show, row, col, i, j);
}
}
}
}
else//周围存在雷,显示该点雷的个数
{
Show[x][y] = count + '0';
}
}
}
结语
完整的代码在最后面,制作不易,如果有帮助的话还请点个赞支持一下鸭!有不好的地方还请指出,或者有更好的方法也可以分享出来。
test.c文件
#include "game.h"
void menu()
{
printf("**********************\n");
printf("****** 1.play ******\n");
printf("****** 2.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, '*');
PrintBoard(Show, ROW, COL);//打印棋盘
LayoutThunder(Mine, ROW, COL);//布雷
FindThunder(Mine, Show, ROW, COL);//扫雷
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请选择:> ");
scanf("%d", &input);
system("cls");
switch (input)
{
case 0:printf("退出游戏\n");
break;
case 1:printf("游戏开始\n");
game();
break;
default:printf("输入错误,请重新输入:\n");
}
} while (input);
return 0;
}
game.c文件
#include "game.h"
void InitBoard(char Board[ROWS][COLS], int row, int col, char p)//初始化数组元素
{
int i = 0;
for (i = 0; i < row; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
Board[i][j] = p;
}
}
}
void PrintBoard(char Board[ROWS][COLS], int row, int col)//打印数组元素
{
printf("----------------扫雷游戏---------------\n");
int i = 0;
for (i = 0; i <=row; i++)//打印行坐标位置
{
if (row == i)
{
printf(" %d ", i);
}
else
{
printf(" %d |", i);
}
}
printf("\n");
for (i = 1; i <= row; i++)//打印列坐标位置
{
int j = 0;
for (j = 0; j <= col; j++)
{
if (j == col)
{
printf("---");
}
else
{
printf("---|");
}
}
printf("\n");
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf("| %c ", Board[i][j]);
}
printf("\n");
}
printf("----------------扫雷游戏---------------\n");
}
void LayoutThunder(char Board[ROWS][COLS], int row, int col)//初始化雷的位置
{
int x = 0;
int y = 0;
int count = 0;
while(count<THUNDER)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (Board[x][y] == '0')
{
Board[x][y] = '1';
count++;
}
}
}
void FindThunder(char Mine[ROWS][COLS],char Show[ROWS][COLS],int row, int col)//扫雷
{
int x;
int y = 0;
printf("请输入你要排雷的坐标:(两坐标之间用空格隔开)");
while (1)
{
scanf("%d%d", &x, &y);
if (x <= row && x >= 1 && y <= col && y >= 1)//判断坐标是否合法
{
if(Mine[x][y]=='1')
{
printf("很不幸,你被炸死了\n");
ShowThunder(Mine, Show, row, col);
break;
}
else if (Mine[x][y] == '0' && Show[x][y] != '*')//判断是否坐标重复
{
printf("该位置已经排过雷,请重新输入其他坐标:");
}
else
{
UnfoldBoard(Mine, Show, row, col, x, y);
PrintBoard(Show, row, col);
}
}
else
{
printf("输入坐标非法,请重新输入:");
}
if (Count(Show,row,col) == THUNDER)
{
printf("恭喜你,扫雷成功!\n");
ShowThunder(Mine, Show, row, col);
break;
}
}
}
void ShowThunder(char Mine[ROWS][COLS], char Show[ROWS][COLS], int row, int col)//显示布置雷的位置
{
int i = 0;
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (Mine[i][j] == '1')
{
Show[i][j] = '@';//将为地雷的点改为@
}
}
}
PrintBoard(Show, row, col);
}
int CountThunder(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')//该点为'1'是为地雷,count加1
{
count++;
}
}
}
return count;
}
int Count(char Show[ROWS][COLS], int row, int col)
{
int i = 0;
int count = 0;//计算未知点个数
for (i = 1; i <= row; i++)
{
int j = 0;
for (j = 1; j <= col; j++)
{
if (Show[i][j] == '*')
{
count++;
}
}
}
return count;
}
void UnfoldBoard(char Mine[ROWS][COLS], char Show[ROWS][COLS], int row, int col, int x, int y)//展开棋盘
{
if (x >= 1 && x <= row && y >= 1 && y <= col)//限制递归展开空间
{
int count = CountThunder(Mine, x, y);//计算周围雷的数量
if (count == 0)//周围没有雷,进行递归调用
{
Show[x][y] = ' ';//将要递归的点变为空格
int i = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <=y + 1; j++)
{
if (Show[i][j] == '*')//限制递归的点,防止反复调用
{
UnfoldBoard(Mine, Show, row, col, i, j);
}
}
}
}
else//周围存在雷,显示该点雷的个数
{
Show[x][y] = count + '0';
}
}
}
game.h文件
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<Windows.h>
#define THUNDER 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
void InitBoard(char Board[ROWS][COLS], int row, int col, char p);//初始化数组元素
void PrintBoard(char Board[ROWS][COLS], int row, int col);//打印棋盘
void LayoutThunder(char Board[ROWS][COLS], int row, int col);//初始化雷的位置
void FindThunder(char Mine[ROWS][COLS], char Show[ROWS][COLS],int row, int col);//扫雷
void ShowThunder(char Mine[ROWS][COLS], char Show[ROWS][COLS], int row, int col);//显示布置雷的位置
int CountThunder(char Mine[ROWS][COLS], int x, int y);//计算周围雷的个数
int Count(char Show[ROWS][COLS], int row, int col);//计算未扫雷点个数
void UnfoldBoard(char Mine[ROWS][COLS], char Show[ROWS][COLS], int row, int col, int x, int y);//展开棋盘