扫雷游戏的功能说明:
●
使用控制台实现经典的扫雷游戏
●
游戏可以通过菜单实现继续玩或者退出游戏
●
扫雷的棋盘是9*9的格子
●
默认随机布置10个雷
●
可以排查雷
○
如果位置不是雷,就显示周围有几个雷
○
如果位置是雷,就炸死游戏结束
○
把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束
●
游戏的界面:
一、扫雷游戏代码的函数声明
1.格式化输出printf(),格式化输入scanf(),初始化随机数的生成器函数srand(),时间函数time()需要用到下面的三个头文件。
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
2.宏定义棋盘的行数ROW和列数COL,而ROWS和COLS是在原来棋盘行列的基础上再增加一行和一列,这里宏定义的目的是方便后面要对棋盘的行和列数进行修改时,从这里就可以修改。
#define ROW 9
#define COL 9
#define ROWS ROW+1
#define COLS ROW+1
3.宏定义要布置在棋盘当中的雷的数量。(宏定义其实就是对文本内容的替换,如下面宏定义的意思是在预处理程序时,会将程序中所有Mine_num用10来替换)
#define Mine_num 10
4.下面是实现扫雷功能的所有函数的函数声明。
void InitBoard(char arr[ROWS][COLS],int row,int col,char set);
void DisplayBoard(char Board[ROWS][COLS], int row, int col);
void SetMine(char board[ROWS][COLS], int row, int col);
void ClearMine(char Mine[ROWS][COLS],char Show[ROWS][COLS], int row, int col);
上面InitBoard()函数是用来初始化棋盘的,DisplayBoard()函数是用来展示棋盘的,SetMine()是用来对初始化的棋盘进行布雷的函数,最后一个ClearMine()是玩扫雷游戏的函数。我们将函数的声明、函数的定义及玩扫雷游戏的菜单选项分成三个文件来写,上面包含的头文件、宏定义和扫雷功能函数的声明我们统一放在.h的头文件中,命名为game.h,以后在.c文件中就可以包含这个头文件来使用里面的函数声明。
二、扫雷功能所有函数的定义:
1.用来初始化棋盘的函数InitBoard()定义如下:
void InitBoard(char board[ROWS][COLS],int row,int col,char set)
{
int i = 0;
for (i = 0; i <row ; i++)
{
int j = 0;
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
把给定的二维数组(棋盘)所有的数组元素初始化为自定义的字符,给定的行和列实参为ROWS和COLS。如果是初始化布置雷的棋盘mine(后台隐藏的棋盘),则用字符0初始化棋盘,字符0表示无雷,之后进行布雷时,用字符1表示雷。如果是初始化展示的棋盘show(给玩家看的棋盘),则数组元素全部初始化为字符*,表示还没有扫雷时的一张张方块:
2.用来展示棋盘的函数DisplayBoard()定义如下:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
int j = 0;
for (j = 1; j <= row; j++)
{
int k = 0;
printf("%d ", j);
for (k = 1; k <= col; k++)
{
printf("%c ", board[j][k]);
}
printf("\n");
}
}
上面的函数从上往下第一个for循环是用来打印列号在屏幕上;第二个for循环里又嵌套了一个for循环,是用来展示棋盘的信息的,其中外层的for循环实现要打印几行,而且包含了打印行号,内层的for循环实现要打印几列。这样棋盘上有行和列号,就方便我们观察格子的位置信息,并用来输入要扫雷的格子坐标。这里能发现棋盘(二维数组)上扫雷的格子坐标(数组下标)行和列都是从1开始并且小于等于相应的行和列号,这是因为棋盘的大小实际是ROWS*COLS的,而我们要在屏幕上输出的棋盘大小是ROW*COL的,数组的下标是从0开始的,不管是行还是列,我们跳过了棋盘的第一行和第一列,就相当于跳过了下标为0的行和列,而从下标为1的元素开始打印,这样就和真实的坐标相对应起来。所以这里传的行列实参要是ROW和COL。
3.用来布置雷的函数SetMine()的定义如下:
void SetMine(char board[ROWS][COLS],int row,int col)
{
int count = Mine_num;
while (count)
{
int i = rand() % row + 1; //表示数组的行下标
int j = rand() % col + 1; //表示数组的列下标
if (board[i][j] != '1')
{
board[i][j] = '1';
count--;
}
}
}
当布雷的棋盘被初始化以后,棋盘上全是一样的字符0(非雷),要对其进行布雷,这里是随机的在棋盘上布置雷,所以要用到rand()函数,使用rand函数之前要先调用srand()函数来设置rand函数生成随机数的时候的种子,在整个程序中srand函数只需要被调用一次即可。上面rand()%row (表示rand()函数生成的随机数除以row以后得到的余数) 表示得到的数的范围在0~row-1之间,所以rand()%row+1表示得到的数的范围在1~row之间(包括1和row),这样就生成了一个随机的行坐标i,同样的就可以得到一个随机的列坐标j,范围在1~col之间,然后对这个随机的坐标进行布雷。布了雷以后的格子要显示为字符1(雷),这里我们编写的是一个9*9格的扫雷游戏,布雷数量为Mine_num(10)。上面的程序中while循环的循环条件为count,而count要什么情况下才减小呢?要顺利地布置我们设置的雷数,就要考虑到之后随机生成的坐标会不会和以前随机生成的坐标相同,也就是前面已经在这次生成的坐标中布置了雷,则就不能在相同的坐标上重复布雷,要先判断输入的坐标位置上是否有雷,没有雷才在该坐标上布雷,循环的条件count也才随之减小。这样才能完整的布置我们设置的雷数。
4.玩扫雷游戏的函数ClearMine()的定义如下:
void ClearMine(char Mine[ROWS][COLS],char Show[ROWS][COLS], int row, int col)
{
int x = 0;//棋盘的横坐标
int y = 0;//棋盘的纵坐标
int NotMine_count = 0;
while (NotMine_count < row * col - Mine_num)
{
printf("请输入要排查的坐标(x y):");
again:
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (Mine[x][y] == '1')
{
printf("很遗憾,你被炸死了!\n");
DisplayBoard(Mine, row, col);
printf("\n");
break;
}
else
{
if (Show[x][y] == '*')
{
Show[x][y] = AroundMine(Mine, x, y) + '0';
DisplayBoard(Show, row, col);
NotMine_count++;
}
else
{
printf("坐标已排查,请重新输入排查坐标(x y):");
goto again;
}
}
}
else
{
printf("非法坐标,请重新输入:");
goto again;
}
}
if (NotMine_count == row * col - Mine_num)
{
printf("恭喜你排雷成功!\n");
DisplayBoard(Mine, row, col);
printf("\n");
}
}
当布雷的棋盘已经准备好以后,接下来就要进入玩扫雷游戏的阶段,首先是你通过show棋盘选择一个坐标,然后在mine棋盘上进行判断,你选的坐标上是否有雷,如果有雷,你就被炸死了,之后展示出布雷的棋盘mine,游戏结束,回到菜单选项;如果不是雷,则就要在mine棋盘上统计你选的坐标周围8格的雷的总数,然后在show棋盘上在你选择的坐标处替换'*'为该坐标周围雷的数量,但要注意这个数量要用字符的数字,不能是整型的数字,因为棋盘是一个二维的字符数组,不能存储整型元素。那要怎么统计你选择的坐标周围的雷的数量呢?我们包装一个函数用来统计坐标周围的雷的数量,函数命名为AroundMine(),通过这个函数返回坐标周围8格雷的数量。这个函数具体怎么实现放到后面来介绍;当返回 你选择的坐标周围雷的数量后,在show棋盘上展示出来,你就可以进行下一步的坐标选择,如果你一直没有扫到雷,那总会把雷排干净,所以要设置游戏胜利的条件,这要根据你布置雷的数量和棋盘的大小,定义变量NotMine_count用来计数你没有扫到雷的次数(格子数),这里设置的棋盘大小为ROW*COL,布雷的数量为Mine_num(10),那么没有雷的格子有ROW*COL-Mine_num个,当你扫雷的次数达到ROW*COL-Mine_num次,说明你已经把所有的雷区排查出来了,排雷成功,然后展示出布雷的棋盘mine,游戏结束,回到菜单选项;所以上面的while循环的循环条件就为NotMine_count<ROW*COL-Mine_num。但是在输入坐标后还需要完善的一点是,你输入的坐标是不是合法的,也就是行列坐标有没有在棋盘的范围以内,所以要提前进行判断是不是在排查的坐标范围以内。还有一个就是如果你多次排查,选到了以前你选过的坐标,那就是重复排查了,这个问题也需要提前进行判断,所以在上面的代码中有三重if else语句的嵌套。
5.AroundMine()函数的定义及补充:
int AroundMine(char board[ROWS][COLS],int x, int y)
{
int i = 0;
int MineNum = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
if (i == x - 1 || i == x + 1)
MineNum += board[i][j];
else if (j == y - 1 || j == y + 1)
MineNum += board[i][j];
}
}
return (MineNum-8*'0');
}
AroundMine函数是用来统计mine棋盘上坐标为(x,y)的格子周围雷的数量,就是除了(x,y)坐标以外,周围8格的雷的数量都要计算。这里有一个整型与字符进行转换的方法,要把一个字符格式的数字转化为整型数字时,可以用该字符数字减去字符0就得到了整型的数字(如char a = '7',int b = a-'0',则b = 7)。因为字符在计算机中以二进制数存储,字符与整型的四则运算也是通过字符的ASCII值与整型进行运算的。从字符0到字符9的十进制ASCII值是连续递增的,所以要得到整型的阿拉伯数字,可以用字符的数字减去字符0就会得到整型的数字,同理,整型的数字加上字符0就会得到字符的数字(如int a = 7,char b = a+'0',b = '7'),所以AroundMine函数的返回值是整型数字,加上一个字符0就得到了字符的数字,这样就可以知道选择的坐标周围有多少颗雷。当然上面还有更直接的方法,不用for循环,直接return(返回)选择的坐标周围8个格子的和减去8*'0'以后的值即可。玩的棋盘大小是ROW*COL的,为什么要增加一行一列变成ROWS*COLS呢?如果棋盘的大小只是ROW*COL的话,如下图,如果我们访问(2,6)这个坐标时,周围一圈8个黄色位置的雷数为2,当我们访问(8,4)这个坐标时,周围一圈8个黄色位置,统计周围雷的个数时,最下面的三个坐标会越界,所以为了防止越界,设计棋盘时,给数组扩大一圈,雷还是布置在中间的ROW*COL的棋盘中,最外围一圈不需要布雷也不需要展示出来全部初始化为字符0即可,这样就不会出现数组越界的问题了。而且还可以发现,扩大了一圈后的棋盘,中间棋盘的坐标跟真实坐标相对应,没有带0的坐标,就是因为数组扩大了一圈,跳过了第一行和第一列。
原棋盘
周围加上一圈后的棋盘
标准I码
以上功能函数的定义统一写在一个.c文件中,文件命名为game.c,当然在以上函数的形参中,数组长度使用了宏定义的内容,所以要在game.c文件的开头中包含game.h的头文件。
三、扫雷的菜单选项
在定义完所有扫雷的功能函数以后,将这些功能函数包装成一个完整的游戏逻辑代码,我们将上面的功能函数放在一个名为game()的函数里,在game函数里对上面的功能函数进行调用,即可进行扫雷游戏:
#include "game.h"
void menu(); //菜单函数的声明
void game();
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:");
again:
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏!\n");
break;
default:
printf("输入错误,请重新选择:");
goto again;
}
} while (input);
return 0;
}
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'); //初始化存放雷的棋盘为全'0'
InitBoard(show, ROWS, COLS, '*'); //初始化展示的棋盘为全'*'
SetMine(mine, ROW, COL); //对初始化的存放雷的棋盘进行布雷
//DisplayBoard(mine, ROW, COL); //展示已经布置雷的棋盘
DisplayBoard(show, ROW, COL); //显示棋盘上没有扫中雷时的周围雷的信息
ClearMine(mine, show, ROW, COL); //进行扫雷游戏的函数
}
程序运行起来从main函数进入,跳出菜单选项,选择1以后,调用game函数进行扫雷游戏,如果选择0,则退出游戏;要是不小心输入了其他大于1的数,则需要重新输入。最核心的就是调用game函数以后,就开始扫雷游戏了,在game函数中先是创建了两个二维的字符数组(棋盘),一个是用来存放雷的棋盘,另一个是用来展示给玩家看的棋盘,之后就是对两个棋盘进行初始化,然后对初始化后的存放雷的棋盘进行布雷,然后玩家就可以开始扫雷,玩家输入一个坐标,程序对输入的坐标进行逻辑判断。大概的思路框架就是这样。将上面代码写在一个.c文件中,命名为test.c,就是用来写扫雷游戏的测试逻辑的,上面的代码一开头就包含了game.h这个头文件,在game.h这个头文件中包含了我们声明定义的所有函数类型,就是要在这个test.c文件中进行调用。
四、程序的不足
上面只是一个初步的扫雷程序,还可以对此程序进行扩展,如:
●是否可以选择游戏难度
1.简单 9*9 棋盘,10个雷
2.中等 16*16棋盘,40个雷
3.困难 30*16棋盘,99个雷