扫雷游戏的实现和三子棋的实现方式从差不多
也是分成三个模块实现
主函数实现---test.c
里面包含主逻辑运行函数,以及菜单函数,主游戏逻辑实现
具体游戏函数代码实现---game.c
里面包含游戏实现过程的函数以及逻辑运行
头文件引用---game.h
里面包含库函数引用,字符定义,函数调用声明
一、主函数实现
主函数的功能也就是进入游戏的界面选择逻辑,分为玩游戏,退出游戏,以及输入非法三部分
//主逻辑函数
void test()
{
//生成随机数只需要在主函数中调用一次即可
srand((unsigned int)time(NULL));
int input = 0;//存放输入的数据
do
{
menu();//打印菜单
scanf("%d", &input);
switch (input)
{
case 1://玩游戏
game();
break;
case 0://退出游戏
printf("退出游戏\n");
break;
default://其他情况
printf("输入错误,请重新输入\n");
break;
}
} while(input);
}
//主函数创建
int main()
{
test();
return 0;
}
其中包含目录函数
//菜单
//进行选择,是开始游戏还是退出游戏
void menu()
{
printf("***********************\n");
printf("******* 扫雷 *******\n");
printf("***********************\n");
printf("******* 1.play *******\n");
printf("******* 0.exit *******\n");
printf("***********************\n");
}
二、字符定义
对一些将来可能需要更改的字符进行定义,放便将来更改
//字符定义
#define ROW 9 //棋盘使用行数
#define COL 9 //棋盘使用列数
#define ROWS ROW+1 //实际创建的棋盘行数
#define COLS COL+1 //实际创建的棋盘列数
#define BOMB 10 //雷的个数
在game.c文件中还定义了一些全局变量进行调用
int count1 = BOMB;//全局变量,最大标记次数
int count2 = 0;//全局变量,用来计算正确标记雷的数量
char ch = 0;//拿走多余字符
三、游戏函数实现
一个扫雷游戏的完整逻辑应该是,创建棋盘,对棋盘进行初始化,打印棋盘,埋雷,选择是排雷还是标记雷,输入坐标,判断是否为雷,显示游戏结果。
//主游戏函数
//在这里实现游戏的运行
void game()
{
//1.创建棋盘
char mine[ROWS][COLS] = { 0 };//埋雷的数组
char show[ROWS][COLS] = { 0 };//展示信息的数组
//2.初始化棋盘函数
//由于想用一个函数对两个棋盘都可以进行初始化,但是棋盘的初始化内容不同
//所以将内容传给初始化函数,就可以实现棋盘不同的初始化
InitBoard(mine, ROWS, COLS, '0');//对埋雷的数组进行初始化,不是雷的地方为‘0’是雷为‘1’
InitBoard(show, ROWS, COLS, '*');//对展示信息的数组进行初始化
//3.打印棋盘函数(此函数在排雷过程中调用)
//将棋盘进行打印,在排查雷的时候调用,可以直观看到布置情况
//DisplayBoard(mine, ROW, COL);//只需要打印实际应用的范围就可以
//4.布置雷
SetMine(mine, ROW, COL);//在埋雷的数组中进行布置
//DisplayBoard(mine, ROW, COL);
//5.调用函数打印出棋盘
DisplayBoard(show, ROW, COL);
//6.扫雷过程(1.排查 2.标记 3.取消标记)
ModeSelection(mine, show, ROW, COL);//模式选择
}
接下来进行细化分析。
(一)棋盘创建
本质还是二维数组的创建,与三子棋相差不大,但是这次我们为了方便代码实现,避免一些不必要的歧义,我们需要创建两个大小完全一样的二维数组,一个用来布置雷,一个用来记录信息,并且打印出来。
还要注意一点,棋盘的创建要比实际应用的棋盘整体大一圈,也就是说我们实际使用的棋盘大小为9*9的棋盘,但是创建的二维数组应该为11*11的大小。
这样做的原因在后文中会解释。
(二)棋盘初始化
由于想用一个函数对两个棋盘都可以进行初始化,但是棋盘的初始化内容不同所以将内容传给初始化函数,就可以实现棋盘不同的初始化。
对布置雷的棋盘初始化为全 '0' ,埋雷的时候将其更改为 '1' 即可
对展示信息的棋盘初始化为 ' * '。
使用两个for循环将其进行初始化,初始化内容为函数参数set
//1.初始化棋盘
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;//初始化内容为set接收的内容。
}
}
}
(三)棋盘打印
由于扫雷游戏的棋盘比较大,为了方便玩家输入坐标是易于观看坐标,因此我们打印上行号和列号
我们在每一次打印棋盘之前进行分割符号打印,方便区分每一次的扫雷结果。
棋盘大小日后可能更改,分割符号也需要随着棋盘的大小进行调整,所以我们用棋盘大小来控制分隔符号大小。
打印的棋盘内容为实际使用的棋盘大小,即9*9大小的棋盘,由于我们创建的为11*11的二维数组,所以在使用for循环的时候,控制变量应该从1开始,而不是0。
//2.打印棋盘
//打印棋盘的时候我们希望能够打印出来行标和列标,这样可以方便玩家锁定坐标进行输入
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0;i <= 2 * col;i++)//打印分隔符,根据棋盘大小来打印不同长度的分隔符
{
if (i == col)
{
printf("扫雷");
}
else
printf("-");
}
printf("\n");
for (i = 0;i <= row;i++)//打印列号
{
printf("%d ", i);
}
printf("\n");//换行
for (i = 1;i <= row;i++)//i从1开始,因为打印的是数组下标1-9的内容
{
printf("%d ",i);//打印行号
int j = 0;
for (j = 1;j <= col;j++)
{
printf("%c ", board[i][j]);
}
printf("\n");//换行
}
for (i = 0;i <= 2 * col;i++)//打印分隔符,根据棋盘大小来打印不同长度的分隔符
{
if (i == col)
{
printf("扫雷");
}
else
printf("-");
}
printf("\n");
}
(四)布置雷
棋盘创建完毕后,需要对埋雷的数组mine[ ] [ ] 进行布置雷的操作,就是生成随机坐标,并且判断此坐标是否为‘ 0 ’,如果为‘ 0 ’就将其设置成 ‘ 1 ’。
切记随机坐标的生成必须要放到循环内部,如果放在循环外,随机坐标只产生一次,然后每次循环都将使用同一个坐标,无法布置雷。
在布置雷的过程中用计数器来计算布置雷的数量,并且控制循环。布置成功后,数量减一,当计数器变为0时,循环也停止了。
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = BOMB;//用计数器来存放雷的个数,布置成功一个,计数器减一
while (count)//count为0时跳出,雷布置完毕
{
//产生随机坐标,坐标范围为棋盘大小范围,必须要放到循环里
x = rand() % row + 1;
y = rand() % col + 1;
if (board[x][y] == '0')//判断坐标是否为'0',只有坐标为'0'才布置
{
board[x][y] = '1';//'1'代表雷
count--;
}
}
}
(五)扫雷具体过程
在完成棋盘创建和雷的布置后,我们正式进入扫雷游戏的主要环节:排查雷,标记雷,判断输赢。
扫雷过程的选择:
对其进行分析发现,主要包含以下方面:
排查雷:输入排查坐标,如果不是雷,游戏继续,是雷,游戏结束。
标记雷:输入标记坐标,对其进行标记,区分自己识别出来的雷
取消标记:输入坐标,取消掉自己标记的雷
判断输赢:每完成一次操作,判断是否扫雷成功
//扫雷过程(排查雷,标记雷,取消标记)
void ModeSelection(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int input = 0;
int flag = 0;
int win = 1;//win=0的时候排雷成功
while (win)
{
printf("请选择:> 1.排查雷 0.返回\n");
printf(" 2.标记雷 3.取消标记\n");
scanf("%d", &input);
while (ch = getchar() != '\n')//拿走多于字符
{
;
}
switch (input)
{
case 1:
{
FindMine(mine, show, ROW, COL);//排查雷
win = IsWin(mine, show, ROW, COL);
if (win== 0)//判断是否扫雷成功
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
else
{
DisplayBoard(show, ROW, COL);
}
break;
}
case 2:
{
MarkMine(mine, show, ROW, COL);//标记雷
win = IsWin(mine, show, ROW, COL);
if (win == 0)//判断是否扫雷成功
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
else
{
DisplayBoard(show, ROW, COL);
}
break;
}
case 3:
{
UnMark(mine, show, ROW, COL);
win = IsWin(mine, show, ROW, COL);
if (win == 0)//判断是否扫雷成功
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
else
{
DisplayBoard(show, ROW, COL);
}
break;
}
case 0:
goto flag;
default:
printf("输入错误,请重新输入");
break;
}
}
flag:
;
}
函数主体采用while循环和switch语句进行。控制while循换的变量为win,win接收判断输赢函数的返回值,返回0,则扫雷成功,循环终止,游戏结束。
1、排查雷
选择1进入排查雷之后,输入排查坐标。x ,y
如果mine[x][y]=='1'? 说明坐标为雷,游戏结束,否则进入展开函数,判断周围情况,然后对
show[ ][ ]? 函数进行信息反馈。
//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
printf("请输入排查坐标:>\n");
scanf("%d %d", &x, &y);
if (1 <= x && x <= row && 1 <= y && y <= col)//判断坐标是否合法
{
if (mine[x][y] == '1')//坐标为雷,游戏结束
{
printf("很遗憾,你被炸死了!\n");
DisplayBoard(mine, ROW, COL);//打印出棋盘
}
else//坐标不为雷,显示周围信息,并且展开
{
SpreadMine(mine ,show,x,y);
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
2、获取排查信息函数
本质上是对mine[x][y]??坐标周围的8个坐标进行排查,由于mine中不是雷的地方存放的是‘0’,是雷的地方存放的是‘1’,因此只需要对其周围的坐标相加然后减去八个‘0’的ASCII码值即可。
为什么要创建11*11的数组呢?
因为如果数组为9*9,在排查最外面一行和最外面一列的坐标周围元素情况的时候会出现越界访问,如果大一圈,这个问题将被解决。由于这一圈中存放的都是‘ 0 ’,不会影响结果。
//计算雷数
int GetMine(char mine[ROWS][COLS], int x, int y)//周围8个元素
{
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';
}
3、展开函数
展开函数就是利用递归将那些周围一圈都没有雷的坐标进行展开,直到周围出现雷停下,这样可以增加游戏的可玩性,避免一些无用信息的出现。
展开的限制条件是show数组中坐标为‘ * ’才会展开,如果周围都没有雷,这个坐标就打印空格,如果有雷,则打印雷的信息,递归停止。
//展开雷
void SpreadMine(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y)
{
int i = 0;
int sum = 0;
sum = GetMine(mine,x,y);//接收坐标周围信息
if (sum == 0)
{
show[x][y] = ' ';
int m = 0;
for (m=x-1;m<= x+1;m++)
{
int n = 0;
for(n=y-1;n<=y+1;n++)
{
if (show[m][n] == '*')//只有为‘*’的时候才进行调用展开函数
SpreadMine(mine, show, m, n);
}
}
}
else
{
show[x][y] = sum+'0';
}
}
4、标记雷
当玩家找出雷后,为了方便和其他的坐标进行区分,则选则2进入标记状态
输入坐标标记雷
标记的次数是和雷的数量一致的
当数量一致时,将无法进行标记
标记一次,最大标记次数减一,然后进行判断,判断标记坐标是否为雷,如果为雷,则正确标记数目加一。
//标记雷
void MarkMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
if (count1 == 0)
{
printf("已达到最大标记数目,无法标记\n");
}
else
{
printf("请输入标记坐标:>\n");
scanf("%d %d", &x, &y);
if (1 <= x && x <= row && 1 <= y && y <= col)//判断坐标是否合法
{
if (show[x][y] == '*')//可以标记
{
show[x][y] = '#';//标记符号为'#'
count1--;//最大标记次数减1
if (mine[x][y] == '1')//判断是否为正确标记
{
count2++;
}
}
else if (show[x][y] == '#')
{
printf("已经被标记,无法重复标记\n");
}
else
{
printf("坐标被排查,无法标记\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
}
5、取消标记
取消标记和标记的逻辑基本相反,取消成功一次,最大标记数目加一,然后进行真确标记判断,判断是否取消的为正确标记,如果是,正确标记数目减一。
//取消标记
void UnMark(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
printf("请输入取消的标记坐标\n");
scanf("%d %d", &x, &y);
if (1 <= x && x <= row && 1 <= y && y <= col)//判断坐标是否合法
{
if (show[x][y] == '#')//判断是否为标记处
{
show[x][y] = '*';
count1++;
if (mine[x][y] == '1')//判断是否为正确标记
{
count2--;
}
}
else
{
printf("非标记处,无法取消,请重新输入\n");
}
}
else
{
printf("坐标非法,请重新输入\n");
}
}
6、判断输赢函数
扫雷成功有两种:
1、show数组上剩余的‘? *? ’号是否等于雷的个数,等于则成功,否则游戏继续。
2、正确标记数是否为雷的个数,是,则扫雷成功,否则扫雷失败。
//判断是否扫雷成功
int IsWin(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int count = 0;
int i = 0;
for (i = 1;i <= row;i++)
{
int j = 0;
for (j = 1;j <= col;j++)
{
if (show[i][j] == '*')
{
count++;
}
}
}
if (count == BOMB)//剩余的未排查坐标数等于雷数
{
return 0;
}
if (count2 == BOMB)//标记出均为雷
{
return 0;
}
return 1;
}
|