目录
前言
(省流:完整的代码在最后)本文将详细介绍如何用C语言实现扫雷游戏,这主要是对C语言的分支循环、数组、函数的综合训练,独立完成该游戏将很好的训练我们写代码的能力,除了基本游戏功能,还有展开一片游戏辅助的功能,如不了解该功能,我们可以通过在线扫雷游戏网站扫雷游戏网页版 - Minesweeper体验一下游戏
一、扫雷游戏逻辑分析
如图,我们把扫雷游戏看成一个二维数组,每个小方框位置看成由数组的行和列构成的坐标,我们需要排除所有不为雷的坐标,而每次排查一处后,如果该位置不是雷,就在该位置显示周围有几个雷的数字。排查的位置即为该坐标周围的8个坐标,如果排查的坐标在四角或边上那么需要排查的位置就为3个或5个,这样一来,边上与中间的坐标需要排查的数量不一致,分开写会导致代码异常复杂,为了方便遍历周围需排查的坐标,我们将棋盘扩大一圈,而最外圈并不作显示也不作排雷区,不设置雷,仅仅是为统一排查8个坐标而设,这样就不会造成数组的越界访问而导致程序崩溃以及方便了雷的数量统计
以上我只排查了左下角的位置,该位置没有雷,并且四周也没有雷,于是将四周没有雷的位置改为空坐标,并向周围同样四周没有雷的位置展开,四周有雷的位置返回周围雷的数量,以上即为展开一片的功能
因此为了能实现扫雷游戏,我们需要先布置雷,并且在开始前和每次排雷后都要展示棋盘,那么我们至少需要两个二维数组,一个用来放置雷,一个用来展示棋盘,并且用来展示和布雷的数组实际大小应比展示大小大一圈,例如9*9棋盘,我们需要创建11*11的二维数组,最外圈避免数组的越界访问和方便计算周围雷的数量
以上我们简单分析了一下游戏逻辑,另外为了方便管理代码,我们需要将整个游戏分为3个代码文件:一个是用来声明函数、库函数头文件以及一些预处理命令定义的符号常量的头文件game.h,一个是用来定义函数的源文件game.c,一个是main主函数的源文件test,c,我们先创建好这三个文件,然后开始实现游戏
二、扫雷游戏的实现
1.游戏菜单与选择
我们进入游戏一般都有菜单选择,在游戏开始前是选择玩游戏还是退出游戏,在游戏结束后再次选择是继续游戏还是退出游戏
首先我们需要打印一个游戏菜单:将其写在game.c文件里
注:以上为我展示的游戏菜单,我们也可以有其他样式的菜单,但必须包含选择1或者0进行开始或者结束游戏,我这里将其放在game.c里,因此需要声明该函数,也可以直接放在test.c里,就不需要再进行声明,因为该函数比较简短,故看个人喜好。
在game.h头文件里声明该函数:
另外,game.c和test.c我们也应该包含该头文件game.h:包含方式如下
注意:由于需要使用scanf函数,为了不报错,我们需要输入以下预处理指令:在头文件game.h中
往后我们需要用到的库函数头文件都可以写在game.h里
然后为了实现多次选择,以及至少执行一次的逻辑,我们选择do-while循环,以及使用switch分支根据选择执行不同的代码,整体实现如下:
int main()
{
int input = 0;//用来存储玩家的选择
do
{
menu();//打印菜单printf("请选择:\n");
scanf("%d", &input);switch (input)
{
case 1:
printf("开始游戏\n");
game();//game函数开始执行游戏代码
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入非法,请重新输入\n");
break;
}
} while (input);//等于0即可退出循环return 0;
}
效果如下:
2.game()函数
game()函数即为我们扫雷游戏实现的主要函数,类型为void,故不需要进行传参。
通过前面分析,我们需要两个二维数组,并且为字符数组,方便以'*'打印棋盘,一个用来布置雷的坐标,一个用来展示棋盘,我们将用来布置雷的二维数组取名为mine,用来展示的二维数组取名为show
以9*9扫雷棋盘为例:我们创建两个数组:
为了后续能方便后续能调整棋盘大小我们不用常量定义二维数组大小,而是通过预处理命令定义两个字符常量,如:
ROW表示二维数组需要用到的9行,COL表示二维数组需要用到的9列
ROWS表示二维数组实际行数,COLS表示二维数组实际列数
然后修改数组为:
注:我这里game函数放在test.c文件里,如果想要放在game.c里就需要在game.h里进行声明,因为该函数较简短,我将其放置在test.c里
3.InitBoard()函数
InitBoard函数为初始化两个二维数组的函数,以下为它需要的参数:
该函数声明写在game.h中
参数:
- board二维数组用来接收传入的二维数组
- rows用来接收行数,为方便与ROWS区分用小写
- cols用来接收列数,也是为区分COLS用小写
- set用来接收用来初始化的字符
下面来定义函数:
内容很简单,通过两个循环给二维数组赋值,mine数组全部赋值为字符0,show数组全部赋值为字符*
以下为调佣函数时传入的参数:
注意:因为要给数组所有元素赋值,因此传入实际大小字符常量11
4.DisplayBoard()函数
初始化之后我们就需要打印二维数组,而DisplayBoard函数就是用来展示数组,也就是展示棋盘,以下为它所需要的参数:
该函数声明写在game.h中
参数:
- board二维数组用来接收传入的二维数组
- row用来接收行数,方便管理循环结束条件
- col用来接收列数,方便管理循环结束条件
该函数的定义:
- 第一个打印是为了装饰游戏起到分隔棋盘的作用
- 往下第一个循环是打印棋盘的列数,好分辨列坐标
- 往下第二个嵌套循环,除了打印横坐标,还有打印整个棋盘
注意:每次打印函数里后面都有一个空格
以下为game函数中的调用传参:
注意:传入的是ROW、COL,因为这是实际打印给玩家看的棋盘
效果如下:
5.SetMine()函数
SetMine函数是用来布置雷的函数,因为初始化时将mine初始化为'0',因此我们随机将mine数组某处坐标修改为'1'表示雷,因为雷的坐标随机,因此我们需要随机数,代码声明如下:
该函数声明写在game.h中
参数:
- board二维数组用来接收传入的二维数组
- row用来接收行数
- col用来接收列数
代码定义如下:
首先定义雷count,其数量为10,但是为了后期方便调整,也使用预处理命令定义一个字符常量10
如下:在game.h头文件中定义EASY_COUNT
然后为了生成随机坐标进行布置雷,使用rand()函数随机生成一个数,rand()函数对9也就是row取余就能得到0至8的随机数,然后加上1就能得到1至9的随机数了,这样就可以随机生成x,y坐标了,
但是rand函数生成的是伪随机数,我们需要srand设置随机种子来得到真正的随机数,在主函数里加入以下代码即可:
另外使用rand函数需要包含头文件stdlib.h,使用time函数需要包含头文件time.h,在game.h中包含即可:
最后,在SetMine函数里需要加上一个条件判断,如果该坐标已布置雷就不再布置,布置成功后count--,直至所有雷被成功布置在二维数组mine的1至9坐标内
我们可以打印一下mine数组看看是否成功布置雷:
通过观察,10个雷被正常布置
6.Demine()函数
在雷布置好后,我们就需要进行排雷了,Demine函数就是用来排雷的,但是该函数也包含了统计雷数量的函数和展开一片功能的函数,我们首先看看函数声明:
该函数声明写在game.h中
参数:
- board1二维数组用来接收传入的mine数组
- board2二维数组用来接收传入的show数组
- row用来接收行数
- col用来接收列数
以下就为写在game.c中的函数定义:
void Demine(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = 0;
while (1)
{
printf("请输入你需要排查的坐标:\n");
scanf("%d %d", &x, &y);if (x >= 1 && y >= 1 && x <= row && y <= col && board2[x][y] == '*')
{
if (board1[x][y] == '1')
{
printf("很遗憾你踩到雷了,你炸了,游戏失败\n");
DisplayBoard(board1, ROW, COL);
return;
}
else
{
count = GetMineCount(board1, x, y);board2[x][y] = count + '0';
//展开一片
if (board2[x][y] == '0')
{
ExpendBoard(board1, board2, x, y);
}//改为空格
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (board2[i][j] == '0')
{
board2[i][j] = ' ';
}
}
}//打印棋盘
DisplayBoard(board2, ROW, COL);//判断是否胜出
int game_count = 0;
for (int i = 1; i <= row; i++)
{
for (int j = 1; j <= col; j++)
{
if (board2[i][j] == '*')
{
game_count++;
}
}
}
if (game_count == 10)
{
break;
}
}
}
else
{
printf("坐标非法,请重新输入:\n");
}
}
printf("恭喜你,排雷成功!\n");
}
GetMineCount()统计坐标周围雷数量的函数:(直接写在game.c中不需要声明)
int GetMineCount(char board[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
count += board[x + i][y + j] - '0';
}
}
return coun;
}
ExpendBoard()展开一片功能的函数:(也是直接写在game.c中不需要声明)
void ExpendBoard(char board1[ROWS][COLS], char board2[ROWS][COLS], int x, int y)
{
int i = 0;
int j = 0;
int count = 0;for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if ((x+i >= 1) && (x+i <= ROW) && (y+j >= 1) && (y+j <= COL) && (board2[x+i][y+j] == '*'))
{
count = GetMineCount(board1, x+i, y+j);
board2[x+i][y+j] = count + '0';
if (board2[x + i][y + j] == '0')
{
ExpendBoard(board1, board2, x + i, y + j);//函数递归
}
}
}
}
}
代码有点长,且听我一点一点的分析:
- 首先定义3个整型变量,x表示横坐标,y表示纵坐标,count变量为统计的周围雷的数量
- 然后输入要排查的坐标后,就需要判断这两个坐标是否处于show数组的1至9的范围中,以及该处是否为字符*,这样的排查坐标才合法,否则就错误需要重新输入,所以设置一个死循环进行输入判断
- 在坐标合法后,我们就需要继续判断改坐标是否为雷,此时就需要mine数组进行比较,如果该处坐标的mine数组值为字符1,我们就判断玩家踩到雷了,游戏结束,并且打印一份mine数组展示这局游戏中所有雷的位置,随后使用return直接结束该函数
- 如果玩家没有踩到雷,我们就需要统计该坐标周围8个坐标处一共有几个雷,此时就需要调用GetMineCount函数返回统计的雷的数量,并且用count去接收
- GetMineCount函数因此需要x,y坐标参数以及布置雷数组参数,因为在函数Demine中mine数组被board1数组接收,所以应该传入board1数组给GetMineCount函数
- GetMineCount函数的统计方法,通过两个循环,即可得到包括x,y坐标在内的9个坐标,我们可以通过图来观察这几个坐标的位置
- 我们可以得到的规律是第一行横坐标全为x-1,第二行全为x,第三行全为x+1,而列坐标,第一列全为y-1,第二列全为y,第三列全为y+1,因此可以得到以上的循环进行遍历,让每一个坐标减去一个字符0,累加起来就是统计的周围雷的个数,因为字符数字的ASCII码值减去字符0的ASCII码值就等于对应的整型该数字,将返回的整型数字再加上字符数字0就能赋值给board2数组了
- ExpendBoard函数是当被排查的坐标返回的值为0时,也就是周围没有雷的时候,就进入ExpendBoard函数进行展开一片功能,ExpendBoard函数因此需要两个二维数组参数进行接收雷数组和展示数组,还需要x,y坐标继续排查周围坐标的雷数量,ExpendBoard函数遍历周围每个坐标的方法和GetMineCount函数一样通过两个循环遍历,再调用GetMineCount函数返回统计雷数量的值,注意该处需要满足一定条件才能进行排查该处坐标,也就是被遍历的坐标应当满足x,y坐标都处于1-9之间,才能避免展开一片时将最外围不显示的一圈也进行了展开,这样就会造成一些bug,除了x,y坐标的条件外,还需要该处坐标未被排查,也就是board2该处位置为字符*,这样能避免重复计算
- 再然后就是递归函数了,如果再次排查到周围有坐标返回值为0,也就是borad2数组该坐标位置为字符0,就再次进入ExpendBoard函数进行排查周围坐标,如果周围又有坐标返回值为0,就继续调用ExpendBoard函数直到被排查的周围坐标不再返回0,这样就能实现函数的递归了
- 随后在Demine函数中使用循环将board2数组中的字符0全部改为字符空格,方便观察
- 最后就是判断输赢了,我们创建一个整型变量game_count用来统计board2中剩余的字符*,通过两个循环进行统计,如果game_count等于雷的数量也就是EASY_COUNT,我们就判断玩家胜利,执行break退出循环并执行打印函数
- 至此整个扫雷游戏得以实现
注意:以上的GetMineCount函数和ExpendBoard函数可直接写在game.c文件中,不需要进行声明,因为只有game.c中使用这两个函数,但是要注意函数定义的上下顺序,GetMineCount函数应定义在ExpendBoard函数上,ExpendBoard函数应定义在Demine函数上
效果展示:
总结
好累,终于写完了,哈哈,以上就是实现扫雷游戏以及展开一片功能的全部代码,需要完整的代码可以跳转到我的Gitee仓库下载全部代码
24_4_20/24_4_20/game.c · 心鹏编程/心鹏c语言仓库 - 码云 - 开源中国 (gitee.com)
24_4_20/24_4_20/game.h · 心鹏编程/心鹏c语言仓库 - 码云 - 开源中国 (gitee.com)
24_4_20/24_4_20/test.c · 心鹏编程/心鹏c语言仓库 - 码云 - 开源中国 (gitee.com)
以上的展开一片的功能实现仅为我个人的想法,如果大家有更好的方法欢迎提出来,另外文章中如果有哪里表述不清请一定提出来,我一定改。最后的最后,感谢大家的支持,希望对大家有所帮助