前言
扫雷游戏和三子棋游戏其实有很多相似的地方,都是在一个二维平面上进行操作,并且他们同为游戏,在很多逻辑上都是相通的,但若是要效果与真实的扫雷游戏接近,仍需要进行多处优化。
思路
与三子棋游戏相同:
整个游戏的代码我计划分为两个源文件实现,第一个源文件test.c用于实现整个游戏的大体流程,第二个源文件game.c用于实现游戏具体步骤的逻辑,也因此需要一个头文件game.h来简化game.c中的函数在test.c中的声明。
在打开游戏时需要打印一份菜单供玩家选择开始游戏还是结束游戏,当玩家完成一盘游戏后,还需要询问玩家是否需要重新开始,因此可以通过循环来实现。
而不同的是:
游戏的流程:布置好地雷-->展示操作平面-->玩家排雷-->展示操作平面-->玩家排雷-->...直到玩家被雷炸死或者将所有雷排查出来,游戏结束。
除却整个游戏的流程,因为我们需要实现的效果与真正的扫雷游戏接近,我们需要考虑到的一点是:若玩家输入的坐标不是雷,那么显示出周围雷的数量。因此我们需要访问到该坐标周围的数组元素。但如果该坐标刚好在边界处,需要访问的位置则会越界(如图1)。
图1
如何解决?
我们不妨将数组扩大一圈,但真正布置地雷、展示和提供给玩家操作的只是中间那一部分,这样我们就能规避上述问题。(如图2)
图2
游戏实现
头文件game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//define定义常量
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MINE 10
void InitBoard(char board[ROWS][COLS], int rows, int cols);
void DisplayBoard(char board[ROWS][COLS], int row, int col);
void SetMine(char mine[ROWS][COLS],char show[ROWS][COLS], int row, int col);
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
头文件game.h的作用在于声明函数、包含系统头文件、以及定义常量。
声明函数:简化在tect.c中声明game.c中函数的过程
包含系统头文件:两个源文件中只需包含game.h即可
定义常量:只需在game.h中改变常量即可改变棋盘大小
至于为何既定义ROW和COL,又定义ROWS和COLS,原因在于方便后续的表达,降低出错概率。
源文件test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu()
{
printf("扫雷游戏\n");
printf("*****************************************\n");
printf("*****************1.开始游戏**************\n");
printf("*****************0.结束游戏**************\n");
printf("*****************************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
printf("%d颗雷\n", MINE);
InitBoard(mine, ROWS, COLS);//初始化二维数组
InitBoard(show, ROWS, COLS);//初始化二维数组
DisplayBoard(show, ROW, COL);//打印棋盘
SetMine(mine, show, ROW, COL);//埋雷
//DisplayBoard(mine, ROW, COL);
FindMine(mine, show, ROW, COL);//玩家排雷
}
int main()
{
int input = 0;
srand((unsigned)time(NULL));
do
{
menu();//打印菜单
printf("请输入:>");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("游戏结束\n");
break;
default:
printf("输入错误,请重新输入\n");
}
} while (input);
return 0;
}
main函数与menu函数与三子棋游戏基本相同,本篇博客便不再赘述。
game函数首先创建两个字符数组,利用InitBoard函数将两个字符数组进行初始化,再使用SetMine函数布置雷,最后FindMine函数让玩家进行操作并判断输赢(此处也可以像三子棋游戏中通过FindMine函数的返回值在game函数中判断输赢)
源文件game.c
InitBoard函数
void InitBoard(char board[ROWS][COLS], int rows, int cols)
{
int i, j;
for (i = 0; i < rows; i++)//双层for循环访问到二维数组每一个元素
{
for (j = 0; j < cols; j++)
{
board[i][j] = ' ';
}
}
}
在InitBoard函数中,通过for循环的嵌套访问字符数组中的每一个元素并放置一个' '进行初始化。
DisplayBoard函数
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i, j;
printf("\n");
for (i = 0; i <= row; i++)//双层for循环打印二维数组的行和列
{
if(i == 0)
printf(" %d ", i);
else
printf(" %d |",i);
for (j = 1; j <= col; j++)
{
if (i == 0)
printf(" %d ", j);
else
printf(" %c |", board[i][j]);
}
printf("\n ");
for (j = 1; j <= col; j++)
{
printf("----");
}
printf("-\n");
}
}
类似于三子棋棋盘的打印,这里也是使用了for循环的嵌套访问到字符数组中展示给玩家的一部分并打印,同时为了方便操作,将每行每列对应的数字也展示了出来。
DisplayBoard函数效果
SetMine函数
void SetMine(char mine[ROWS][COLS],char show[ROWS][COLS], int row, int col)
{
int x, y;
while (1)
{
printf("首次排雷,请输入排查坐标(先输入行,再输入列,行和列用空格隔开):>");//防止第一次就被雷炸死
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{
mine[x][y] = '!';
int count = MINE;
while (count)
{
int i = rand() % row + 1;
int j = rand() % col + 1;
if (mine[i][j] == ' ')
{
mine[i][j] = '*';
count--;
}
}
mine[x][y] = ' ';
show[x][y] = MineCount(mine, x, y) + '0';
FindAround(mine, show, row, col, x, y);
DisplayBoard(show, row, col);
break;
}
else
printf("输入坐标非法,请重新输入\n");
}
}
在游玩扫雷函数时,玩家第一次排雷时是不会排查到地雷的,为了实现此功能,在布置雷之前让玩家进行第一次排雷,将mine字符数组中该元素置为'!',并在设置雷时,以布置的地雷数为while的判断条件,以if来控制是否能够设置地雷,每次设置完地雷就count--,直到count = 0时,预期个数的地雷全部布置完毕,while判断条件为假,跳出循环,结束地雷的布置。
MineCount函数
int MineCount(char mine[ROWS][COLS], int x, int y)
{
int i, j;
int add = 0;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (mine[i][j] == '*')
add++;
}
}
return add;
}
在扫雷游戏中,每排查一个位置,若不是地雷,那么该位置会显示周围地雷的个数,因此使用for循环嵌套访问周围八个元素,统计所排查位置周围的地雷个数,返回该数值,赋值给该位置的元素。
值得注意的是,创建的二维字符数组中应存放字符,但MineCount函数的返回值却为整型,因此在使用MineCount函数改变字符数组元素时需要将MineCount函数的返回值加上'0'(因为数字0-9所对应的ASCII码值是有序的、连续的,所以某个位数加上'0'就变成了该个位数作为字符时所对应的ASCII码值)。
MineCount函数效果
FindAround函数
void FindAround(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
if (x > 0 && x <= row && y > 0 && y <= col && show[x][y] == '0')
{
int i, j;
for (i = x - 1; i <= x + 1; i++)
{
for (j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == ' ' || show[i][j] == '#')
{
show[i][j] = MineCount(mine, i, j) + '0';
if (show[i][j] == '0')
FindAround(mine, show, row, col, i, j);
}
}
}
}
}
在实现了上述MineCount函数后,可以进一步实现扫雷中的展开,即当该位置周围没有地雷时,会自动排查周围的八个位置,若八个位置中仍有周围没有地雷的位置时,继续展开。
因为无法预知需要展开多少个位置,对各个位置检测是否需要展开又太过于繁琐,所以考虑是否能够大事化小。如果在排查周围位置并放置其周围地雷个数的同时,对元素为'0'的位置的周围再次排查,那么就能够实现展开一整片的效果,故此处使用函数递归。
FindAround函数效果
FindMine函数
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x, y, i;
int win = 1;
while (win)
{
printf("1.排查\n2.标记雷\n请选择:>");
scanf("%d", &i);
if (i == 1 || i == 2)
{
printf("请输入坐标:>");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{
if (show[x][y] == ' '|| show[x][y] == '#')
{
if (i == 1)
{
if (mine[x][y] == '*')
{
DisplayBoard(mine, row, col);
printf("很遗憾,你被炸死了,游戏结束\n");
break;
}
else
{
show[x][y] = MineCount(mine, x, y) + '0';
FindAround(mine, show, row, col, x, y);
DisplayBoard(show, row, col);
win = IsWin(mine, show, row, col);
}
}
if (i == 2)
{
show[x][y] = '#';
DisplayBoard(show, row, col);
}
}
else
printf("输入坐标已排查,请重新输入\n");
}
else
printf("输入坐标非法,请重新输入\n");
}
else
printf("输入错误,请重新输入\n");
}
}
在FindMine函数中实现的是玩家在第一次排雷后的所有操作,因为扫雷需要进行的操作不止一次,所以利用以变量win为判断条件的while实现多次操作,直到玩家被雷炸死或win被IsWin函数赋值为1时,游戏结束。
第一步:让玩家选择排查雷还是标记已推断出的雷的位置,对玩家输入的选项进行判断。若选项为1或0,继续输入坐标,否则进入下一循环重新输入。
第二步:让玩家输入想要操作的坐标,判断玩家输入的坐标是否合法。若坐标属于所展示的操作空间,进入下一步骤,否则进入下一循环重新输入。
第三步:判断玩家输入的坐标是否合理。若该坐标未被排查(被标记也属于未排查),则进入下一步骤,否则进入下一循环重新输入。
第四步:判断玩家输入坐标在mine字符数组中所对应的字符是否为'*'。若不为'*',将show字符数组中该元素置为附近的地雷数量(若数量为0,进行FindAround展开),再打印出操作后的操作面板,最后IsWin判断游戏是否需要继续进行,操作是否循环;若为'*',则将mine字符数组展示给玩家,并用break跳出循环,结束这一把游戏。
FindMine函数效果
IsMine函数
int IsWin(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int i, j;
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
if (mine[i][j] == ' ' && (show[i][j] == ' '|| show[i][j] == '#'))
return 1;
}
}
printf("恭喜你,扫雷成功!\n");
return 0;
}
在IsMine函数中,利用for循环嵌套使用检查字符数组中供给玩家操作的区域是否仍有某个位置在mine数组中为' '且在show数组中已被排查,若存在,则还有地雷未被排查,游戏继续,若不存在,则扫雷结束,玩家胜利。
结语
在我最初写出的扫雷游戏中所能实现的功能并不多,经过修修改改也算是逐渐接近真正的扫雷游戏的效果了,但仍然有优化的空间,如在每次玩家操作完就清空屏幕更加美观、改进算法减少空间占用以及加快运行速度等等。另外是我写文章截取效果图时发现的小bug,当地雷的数目为(row*col-1)时,因为第一次排查雷是在SetMine函数中实现的,没有使用IsWin函数进行判断,所以玩家必须执行第二次排雷并选择标记雷才能够获胜,但因为懒,但想着留个小bug给大家改正,促进大家思考也挺不错,大家有好的想法可以在评论区里分享分享。
这次的《C语言实现扫雷》其实很早就想写了,在国庆时就已经开始打稿了,但是迫于前段时间事务繁多,一直没能写完,自己在C语言上的学习也被搁置了很长时间。最近终于是把繁琐的事务都结束了,之后就会更加频繁地不定期更新一些博客,分享自己的程序、见解与经验。
感谢大家的支持,看到这儿的朋友要是喜欢就点个关注呀,现在关注以后就是老粉了^_^,你们的支持就是我更新的动力!