目标
一、实现一个带有菜单的扫雷游戏
二、在成功排查所有非地雷位置或排查到地雷后显示地雷的分布情况
三、实现自动扩展一片的功能
设计思路
首先为了能够把代码逻辑区分清楚,我们把代码拆分进三个文件中,所有的函数说明和头文件放入game.h文件中,所有的函数实现放入game.c文件中,游戏的菜单和实现逻辑放入test.c文件中。
在确定游戏运行的逻辑前,我们要先确定所需要的结构,因为扫雷游戏时在一个M×N大小的网格内进行的,这个网格和我们所学过的二维数组有很大的相似性,所以我用二维数组来存放我们的游戏数据,为了方便操作数组,我们同时定义两个数组,一个用来存放地雷的位置,另一个用来展示扫雷的游戏棋盘;同时为了简化代码,我们应当把两个数组的类型定义为同一个类型,这样同一个函数就可以对两个数组进行操作。
游戏的具体实现
一、确定棋盘的具体大小、地雷的数量和所用的数组类型
这里我们以9×9的棋盘为例,我们可以在定义数组大小的时候直接定义一个9×9大小的二维数组,如果是这样的话,将来我们要改变这个棋盘的大小或者地雷的数量时,就要将代码中所有的数组大小都改一遍,因此为了方便以后的操作,我们采用定义宏的方式来确定数组的大小,这样以后要改变棋盘的大小时,我们只需要在game.h文件中改变我们定义的宏就一次性实现了。
#define ROW 9
#define COL 9
#define MineNUMS 10
我们采用'*’的方式表示这个位置还未被排查,当这个位置被排查后,则会显示周围的八个格子内有多少个地雷。为了达到这个效果我们把用来存放地雷的数组定义为mine[ROW][COL];用来展示棋盘的数组我们定义为show[ROW][COL]。
但像这样定义时会发生一个问题,那就是四个边上的位置在计算周围有多少个地雷时,会出现函数越界的情况。
为了防止这个情况我们就要单独写一个函数来判断是否在我们棋盘的范围内,这个大大降低了程序的运行效率,因此我们采用第二种方法,将数组的行和列各扩大两格,那就是我们在计算边缘的数据时,也不用担心会出现越界的问题,大大提高了程序的效率。
因此我们在定义两个宏用来表示数组的行和列
#define ROWS ROW+2
#define COLS COL+2
此时我们的数组变成了mine[ROWS][COLS] 和show[ROWS][COLS] 。
我们要用'*'来表示未被排查的位置,所以我们把数组的类型定义为char类型。
二、设计菜单
当我们输入1时开始游戏,当我们输入0时,退出程序,同时这个代码要至少执行一次,所以要用到do while循环,具体如下:
void menu()
{
printf("*******************************\n");
printf("********* 1、开始游戏 *********\n");
printf("********* 0、退出游戏 *********\n");
printf("*******************************\n");
}
int main()
{
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);
return 0;
}
三、数组的初始化
二维数组的初始化用两个while循环来实现,我们需要把我们展示棋盘用的数组全部初始化为'*',因为我们只需要初始化我们所要展示的部分,所以只要告诉函数我们所要初始化数组的函数和列数即可,具体如下:
void InitBoard(char arr[ROWS][COLS], int row, int col)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
arr[i][j] = ‘*’;
}
}
}
如果通过以上方式初始化,那我们对存放地雷的函数初始化时又要通过一个新的函数,但如果我们不把初始化的内容“写死”,而是用一个参数来替代,而我们能够明确知道我们想把这个函数里的内容初始化为什么,所以我们可以通过添加一个参数char set来告诉函数我们所要初始化的内容,这样就能提高代码的利用率,具体如下:
void InitBoard(char arr[ROWS][COLS], int row, int col, char set)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
arr[i][j] = set;
}
}
}
四、打印棋盘
我们可以通过打印棋盘的方式告诉玩家,有哪些位置已经被排查过,哪些位置还未被排查,如果我们只是单纯的把数组里的内容打印出来不仅很丑,而且在每次要排查某个位置的时候,都要数一遍一遍,很不方便,因此我们可以在打印数组内容的同时,给棋盘加上行数和列数,具体如下:
DisPlayBoard(char arr[ROWS][COLS], int row, int col)
{
printf("------扫雷游戏-----\n");
//在打印内容之前先打印一行列数
for (int i = 0; i <= row; i++)
{
printf("%d ",i);
}
//打印完列数换行
printf("\n");
for (int i = 1; i <= row; i++)
{
//在打印每行的内容时,i代表所在的函数,因此在打印具体内容前先打印i就可以打印出行数
printf("%d ", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
//每打完一行换行
printf("\n");
}
}
五、设置地雷
我们要达到每次玩游戏地雷的位置不同的效果,就要使用到rand()函数和srand()函数给rand()函数设置随机数的种子,我们只需要用一次srand()函数所以,我们在main函数中设置一次就行,要达到每次放的位置都不同的效果,我们可以让srand()函数生产的随机数随时间的变化而变化,具体如下:
srand((unsigned int)time(NULL));
把给time()函数传入一个空指针,同时将函数的放回值强制类型转化为unsigned int就可以实现rand()函数产生的随机数随时间的变化而变化。
我们要把地雷设置在我们9×9的方格内,我们就需要把rand产生的随机数进行处理,变成1~9之间的数字,同时为了防止在同一个位置重复设置地雷,我们还要设置判断的条件,具体如下:
void SetMine(char arr[ROWS][COLS], int row, int col)
{
//MineNUMS是我们设置的地雷的个数
int count = MineNUMS;
//当count不为0时则代表地雷的个数还没有达到我们设置的要求,循环就会继续执行,直到地雷个数到达要求为止
while (count)
{
int x = rand() % row + 1;//rand() % row可以把随机数转化为0~8的数加1就可以变成1~9的数字
int y = rand() % col + 1;//同上
//只有在x,y这个位置未被设置地雷时,才会把这个位置的设置为地雷,同时把count的数字减1,表示地雷的数量增加了1个
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
六、统计周围地雷个数
我们在计算x,y位置周围的地雷个数时,不就是统计x-1,y-1 x-1,y-1 x-1,y+1 x,y-1 x,y+1 x+1,y-1 x+1,y x+1,y+1这八个位置的地雷个数吗?
同时我们在初始化存放地雷的数组时,我们把没有地雷的位置初始化为字符'0',把地雷设置为字符'1',字符1~9和数字1~9的ASCII码值相差了48,而字符‘0’的ASCII码值为48,那我们把这九个位置的内容分别减去一个字符‘0’再相加,不是就可以得到周围所有地雷的个数了吗,具体如下:
int GetMineCount(char arr[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
count += (arr[i][j]-'0');
}
}
return count;
}
七、排查地雷
首先我们要从键盘上读取一个我们要排查的位置的坐标,同时我们要验证这个输入的坐标是否在我们棋盘内,若不在棋盘内则提示玩家该坐标不合规;然后我们要验证该坐标是否未被排查,若被排查过则提示玩家已被排查,若未被排查则读取我们存放地雷的数组中的数据,若这个位置存放了地雷,则提示游戏结束,并且把地雷的分布情况打印出来告诉玩家,若这个位置没有存放地雷,则统计周围的地雷的个数并显示在这个位置上;当这个位置周围的地雷数量为‘0’时,我们则会触发展开一片的功能,具体如下:
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int win)
{
int x = 0;
int y = 0;
//win在未被排查前的值为0 每排查一个位置 win就会++ 当win的数值小于总格数减去地雷的数量时 循环结束
while(win<row*col-MineNUMS)
{
printf("请输入要排查的坐标>:\n");
scanf("%d %d", &x, &y);
//判断坐标是否合法
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
//判断坐标是否已经被排查过
if (show[x][y] != '*')
{
printf("该坐标已被排查过!\n");
}
//判断坐标是否为地雷
if (mine[x][y] == '1')
{
printf("你被炸死了!\n");
DisPlayBoard(mine, ROW, COL);
break;
}
else
{
//当该坐标不为地雷时,计算周围共有多少个地雷
int count = GetMineCount(mine,x,y);
show[x][y]=count+'0';
win++;
//当周围没有地雷时,将周围一圈都自动排查一遍
if (show[x][y] == '0')
{
ExpandAround(mine, show,x,y,win);
}
}
}
else
{
printf("不存在该坐标!请重新输入 >:\n");
}
DisPlayBoard(show, ROW, COL);
}
//当循环结束并且排除过的位置数量等于总格数减去地雷的个数才算胜利
if(win== row * col - MineNUMS)
{
printf("恭喜你找出全部地雷!\n");
DisPlayBoard(mine, ROW, COL);
}
}
八、展开一片
当被我们排查的位置周围的地雷数量为0是,我们要对周围一圈进行展开,为了达到这个效果可以通过一个函数递归来实现,只要在计算周围地雷数量为0以后就可以对周围的所有位置进行排查,当没有被排查过时,就可以实现这个位置周围的地雷个数,若这个位置周围的地雷个数还为0,就可以继续展开他周围的方格,具体如下:
void ExpandAround(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y,int win)
{
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
//判断周围的位置是否合法并且未被排查过 用循环判断周围的所有位置
if (i>= 1 && i <= ROW && j >= 1 && j <= COL && show[i][j] == '*' )
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
win++;
//当展开的位置周围的地雷数量还为0时 则继续展开
if(show[x][y]=='0')
ExpandAround(mine, show, i, j,win);
}
}
}
}
总结
这个游戏就是对二维数组的操作,重点就在于数组大小的选择、数组类型的选择和如何方便操作减少代码量上。
总的代码
game.c如下:
#include"game.h"
void InitBoard(char arr[ROWS][COLS], int row, int col, char set)
{
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col; j++)
{
arr[i][j] = set;
}
}
}
DisPlayBoard(char arr[ROWS][COLS], int row, int col)
{
printf("------扫雷游戏-----\n");
for (int i = 0; i <= row; i++)
{
printf("%d ",i);
}
printf("\n");
for (int i = 1; i <= row; i++)
{
printf("%d ", i);
for (int j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}
}
void SetMine(char arr[ROWS][COLS], int row, int col)
{
int count = MineNUMS;
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
int GetMineCount(char arr[ROWS][COLS], int x, int y)
{
int count = 0;
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
count += (arr[i][j]-'0');
}
}
return count;
}
void ExpandAround(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y,int win)
{
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (i>= 1 && i <= ROW && j >= 1 && j <= COL && show[i][j] == '*' && mine[i][j]=='0')
{
int count = GetMineCount(mine, x, y);
show[x][y] = count + '0';
win++;
if(show[x][y]=='0')
ExpandAround(mine, show, i, j,win);
}
}
}
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int win)
{
int x = 0;
int y = 0;
while(win<row*col-MineNUMS)
{
printf("请输入要排查的坐标>:\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标已被排查过!\n");
}
if (mine[x][y] == '1')
{
printf("你被炸死了!\n");
DisPlayBoard(mine, ROW, COL);
break;
}
else
{
//当该坐标不为地雷时,计算周围共有多少个地雷
int count = GetMineCount(mine,x,y);
show[x][y]=count+'0';
win++;
//当周围没有地雷时,将周围一圈都自动排查一遍
if (show[x][y] == '0')
{
ExpandAround(mine, show,x,y,win);
}
}
}
else
{
printf("不存在该坐标!请重新输入 >:\n");
}
DisPlayBoard(show, ROW, COL);
}
if(win== row * col - MineNUMS)
{
printf("恭喜你找出全部地雷!\n");
DisPlayBoard(mine, ROW, COL);
}
}
game.h如下:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define MineNUMS 10
//初始化棋盘
void InitBoard(char arr[ROWS][COLS], int row, int col,char set);
//打印棋盘
DisPlayBoard(char arr[ROWS][COLS], int row, int col);
//设置地雷
void SetMine(char arr[ROWS][COLS], int row, int col);
//找地雷的位置
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col,int win);
//统计周围地雷的数量
int GetMineCount(char arr[ROWS][COLS], int x, int y);
//展开周围的位置
void ExpandAround(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y,int win);
test.c如下:
#include"game.h"
void menu()
{
printf("*******************************\n");
printf("********* 1、开始游戏 *********\n");
printf("********* 0、退出游戏 *********\n");
printf("*******************************\n");
}
void game()
{
int win = 0;
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*' );
SetMine(mine, ROW, COL);
DisPlayBoard(show, ROW, COL);
FindMine(mine, show, ROW, COL,win);
}
int main()
{
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);
return 0;
}