扫雷游戏
引言
在C语言中,有一个很经典的小游戏——扫雷,这个游戏涉及到了数组和函数两大知识点,如果小伙伴们对这两个知识点有所了解,对于这篇文章会了解的更加深刻。btw 结尾附上完整代码,可以点击目录跳转直接运行游玩。
扫雷游戏的分析与设计
在这里我会带着大家对于扫雷游戏的结构框架有一个整体的认识,帮助我们在后期的代码实现的更加效率。
1.扫雷游戏的功能说明
- 扫雷游戏要基于控制台实现
- 游戏可以通过菜单实现 继续游玩 还是 退出游戏
- 扫雷的棋盘应该是9*9的格子
- 默认的简单难度要随机布置10个雷
- 可以排查雷
1.如果所选位置不是雷,就显示周围有几个雷
2.如果所选位置是雷,就被炸死游戏结束
3.把除10个雷之外的所有非雷都找出来,就算排雷成功,游戏结束
下面是游戏的界面,以及我的酋长血统(排的第一个坐标就是雷:>)。
2.数据结构的分析——最重要的一个部分
要想实现扫雷游戏,我们就需要一个数据结构来存储我们的棋盘,因此我们采用一个 11x11(内置9x9的棋盘)的二维数组来存储。 如果这个地方没有雷我们就放入一个'0'
,如果有雷我们就要放入一个'1'
。
为什么我们要采用一个 11x11(内置9x9的棋盘)的二维数组来存储呢? 因为如果是一个9x9的棋盘,我们访问边界的点位时,需要查看他周边一圈是否有雷。这个时候就有可能造成越界访问。因此我们在9x9的棋盘的外面加上一圈,并且不去布置雷,就可以做成一个11x11的棋盘,然后让玩家用到的还是9x9的棋盘就可以解决了。
接下来,我们要设置两个棋盘来运行游戏,一个棋盘(对应数组mine)我们专门用来存放布置好的雷的信息,另一个棋盘(对应另一个数组show)存放排查出雷的信息。 这样可以避免一个数组无法分辨此坐标是雷还是周边有雷的情况了。
我们只需要把雷布置到mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给玩家。
同时为了保持神秘,show数组开始时初始化为字符'*'
,为了保持两个数组的类型一致,可以使用同一套函数处理,mine数组最开始也初始化为'0'
,布置雷的点位初始化为'1'
。见下图
对应的数组为
char mine[11][11] = {0};//存放布置好的雷的信息
char show[11][11] = {0};//存放排查出的雷的个数信息
3.扫雷游戏的代码实现
在这里我将会手把手带你逐行分析代码如何实现扫雷游戏。请打起精神,这里将是本篇的核心。
首先我设计了3个文件用来实现,后面只会讲到game.c文件和test.c文件,game.h仅用来做各种声明。
test.c //文件中写游戏的测试逻辑,以及各种函数的使用
game.c //文件中写游戏中函数的实现等,以及各种函数的定义
game.h //文件中写游戏需要的数据类型和函数声明等
game.h文件——需要大家看一下,知道每个变量和数据类型对应的是什么
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//文件中写游戏需要的数据类型和函数声明等
#define EASY_COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//各种函数的声明——声明是要加;
//初始化棋盘函数
void initBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//接下来的操作是在逻辑数组上完成的
//然后是布置雷
void SetMine(char board[ROWS][COLS],int row,int col);
//最后是排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
接下来是代码实现的讲解——每一段函数所属的文件我都会在代码块的第一行给大家标注出来
我们首先需要一个菜单来供大家选择
//test.c文件
void menu()
{
printf("**************************\n");
printf("******** 1.扫雷 ********\n");
printf("******** 0.exit ********\n"); //用0是因为也可以将选择的0作为假条件跳出循环
printf("**************************\n");
}
如果玩家选择1,则进入游戏,如果玩家选择0,则退出游戏。
然后使用do while循环和switch语句——做到菜单可以多次供你选择。
//test.c文件
int main()
{
int input = 0;
srand((unsigned int)time(NULL)); //初始化随机数
do
{
//先将菜单显示出来供你选择
menu();
printf("请选择:");
scanf("%d",&input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while(input);
return 0;
}
srand((unsigned int)time(NULL)); //初始化随机数
这一段代码是用来创建随机数的,现在可以先不用管,后面随机点位布置雷的时候会讲到。
这段代码的意思是:首先让玩家输入一个值,并把这个值给到dowhile循环和switch分支的条件,他们接收到1和0,都会有对应的措施。
这个时候我们的初始框架就有了。接下来我们要实现game()
函数。
game()
中
首先第一步初始化棋盘
//game.c文件中定义
void initBoard(char board[ROWS][COLS],int rows,int cols ,char set)
{//参数需要传一个数组,以及这个数组的长度——数组的长度不要用宏,还有要放置的元素。
//使用for循环的嵌套来实现初始化
int i = 0;
for (i=0;i<rows;i++)
{
int j = 0;
for (j = 0;j<cols;j++)
{
board[i][j] = set;
}
}
}
简单的for循环嵌套,在棋盘中放置'0'
'*'
。
//test.c文件中使用
initBoard(mine,ROWS,COLS,'0');
initBoard(show,ROWS,COLS,'*');
第二步,打印棋盘
//game.c文件中定义
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
//依旧是使用两个for循环来打印数组
int i = 0;
//打印棋盘框架
printf("----------扫雷游戏----------\n");
for (i=0;i<=col;i++)
{
printf("%d ",i); //打印棋盘的横坐标
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ",i); //打印棋盘的纵坐标
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
通过for循环打印棋盘的横坐标,然后用for循环的嵌套,打印棋盘的纵坐标以及棋盘上的信息。
//test.c文件中使用
DisplayBoard(show, ROW, COL);//这里向玩家展示是9x9的棋盘,所以用ROW和COL
接下来的操作是在逻辑数组(mine)上完成的
第三步,布置雷
//game.c文件中定义
void SetMine(char board[ROWS][COLS], int row, int col)
{
//布置10个雷
//生成随机的坐标,布置雷
int count = EASY_COUNT; //可以在这里根据难度的不同来设置雷的数量
while (count)
{
//制定x y的随机值
int x = rand() % row + 1;
int y = rand() % col + 1;
//如果这个地方是'0',就放设置一个雷,并用count计数
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
通过while循环和rand()函数来生成随机的坐标放置雷,并用count来计数。
//test.c文件中使用
SetMine(mine, ROW, COL);
最后一步,排查雷——也是扫雷游戏的核心逻辑
//game.c文件中定义
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
//坐标为x , y 形式
int win = 0;
while (win < row * col - EASY_COUNT)
{
int x = 0;
int y = 0;
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
int c; // 防止非法格式输入坐标
while ((c = getchar()) != '\n' && c != EOF); // 因为在缓冲区中,非法格式的坐标不能被读取出来,数据就一直在缓冲区中。
// 每一次循环的scanf,都会直接去缓冲区拿数据,每次都是非法字符,就会造成死循环。
if (x >= 1 && x <= row && y >= 1 && y <= col) //因为我们的棋盘实际上是11*11的,但是玩家所看到的9*9的,所以我们需要让坐标在合法范围内
{
//开始判定是否是雷
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//该位置不是雷,就统计这个坐标周围有几个雷
int count = GetMineCount(mine, x, y); //统计周围有几个雷
show[x][y] = count + '0'; //将信息同步到玩家的棋盘上
DisplayBoard(show, ROW, COL);
win++; //统计扫了几个点位了
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col -EASY_COUNT)//row*col为棋盘中所有的点位,EASY_COUNT为棋盘中所有的炸弹数,当win==两位相减就证明已经排除了所有的雷
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL); //打印棋盘
}
}
首先函数参数传入两个数组用来判定和展示,然后设置一个变量win来记录查找的点位数量,如果点位数量等于了全部点位减去全部雷位(也就是所有安全点位),则证明挑战成功。其次通过第一个外循环来接收玩家选择的点位以及通过if语句来判断是否通关。在外循环中内置一个if - else
结构,来判断玩家输入的是否是正确的合法坐标。嵌套的if - else
结构来判定是否有雷,以及不是雷的话,统计坐标周围有几个雷。并将棋盘信息展示给玩家。
//game.c文件中定义以及使用
//计算周围有几颗雷
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
//计算所选坐标的周围8个坐标有几个 '1'——雷
return (mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}
其中涉及到的统计炸弹函数GetMineCount(mine, x, y)
在这里,此函数把所选点位旁边的8个点位相加,如果都不是雷则相加值减去8个'0'
,就得0。相反同理。
//test.c文件中使用
结尾——完整代码
game.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//文件中写游戏中函数的实现等
//各种函数的定义
//初始化棋盘函数
void initBoard(char board[ROWS][COLS],int rows,int cols ,char set) //参数需要传一个数组,以及这个数组的长度——数组的长度不要用宏,还有要放置的元素。
{
//使用for循环的嵌套来实现初始化
int i = 0;
for (i=0;i<rows;i++)
{
int j = 0;
for (j = 0;j<cols;j++)
{
board[i][j] = set;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
//依旧是使用两个for循环来打印数组
int i = 0;
//打印棋盘框架
printf("----------扫雷游戏----------\n");
for (i=0;i<=col;i++)
{
printf("%d ",i); //打印棋盘的横坐标
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ",i); //打印棋盘的纵坐标
int j = 0;
for (j = 1; j <= col; j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
}
//接下来的操作是在逻辑数组上完成的
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
//布置10个雷
//生成随机的坐标,布置雷
int count = EASY_COUNT;
while (count)
{
//制定x y的随机值
int x = rand() % row + 1;
int y = rand() % col + 1;
//如果这个地方是'0',就放设置一个雷,并用count计数
if (board[x][y] == '0')
{
board[x][y] = '1';
count--;
}
}
}
//计算周围有几颗雷
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{
//计算所选坐标的周围8个坐标有几个 '1'——雷
return (mine[x - 1][y] + mine[x - 1][y - 1] + mine[x][y - 1] + mine[x + 1][y - 1] + mine[x + 1][y] + mine[x + 1][y + 1] + mine[x][y + 1] + mine[x - 1][y + 1] - 8 * '0');
}
//排查雷——————游戏的主体
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
//坐标为x , y 形式
int win = 0;
while (win < row * col - EASY_COUNT)
{
int x = 0;
int y = 0;
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
int c; // 防止非法格式输入坐标
while ((c = getchar()) != '\n' && c != EOF); // 因为在缓冲区中,非法格式的坐标不能被读取出来,数据就一直在缓冲区中。
// 每一次循环的scanf,都会直接去缓冲区拿数据,每次都是非法字符,就会造成死循环。
if (x >= 1 && x <= row && y >= 1 && y <= col) //因为我们的棋盘实际上是11*11的,但是玩家所看到的9*9的,所以我们需要让坐标在合法范围内
{
//开始判定是否是雷
if (mine[x][y] == '1')
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
else
{
//该位置不是雷,就统计这个坐标周围有几个雷
int count = GetMineCount(mine, x, y); //统计周围有几个雷
show[x][y] = count + '0'; //将信息同步到玩家的棋盘上
DisplayBoard(show, ROW, COL);
win++; //统计扫了几个点位了
}
}
else
{
printf("坐标非法,重新输入\n");
}
}
if (win == row * col -EASY_COUNT)//row*col为棋盘中所有的点位,EASY_COUNT为棋盘中所有的炸弹数,当win==两位相减就证明已经排除了所有的雷
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL); //打印棋盘
}
}
game.h文件
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
//文件中写游戏需要的数据类型和函数声明等
#define EASY_COUNT 10
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
//各种函数的声明——声明是要加;
//初始化棋盘函数
void initBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//接下来的操作是在逻辑数组上完成的
//然后是布置雷
void SetMine(char board[ROWS][COLS],int row,int col);
//最后是排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
test.c文件
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//文件中写游戏的测试逻辑
//各种函数的使用
//先写一个菜单的函数
void menu()
{
printf("**************************\n");
printf("******** 1.扫雷 ********\n");
printf("******** 0.exit ********\n"); //用0是因为也可以将选择的0作为假条件跳出循环
printf("**************************\n");
}
//再写一个扫雷的函数——使用扫雷要用到的函数
void game()
{
//首先要创造两个数组,一个是逻辑数组——用来存放雷,和判定周围一圈有几个雷.一个是展示数组——用来向玩家展示扫雷棋盘的情况
//首先第一步初始化棋盘
//1.mine的数组最开始是全 '0 '
//2.show的数组最开始全是 '* '
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的信息
//写一个函数来初始化棋盘
initBoard(mine,ROWS,COLS,'0');
initBoard(show,ROWS,COLS,'*');
//其次打印棋盘
DisplayBoard(show, ROW, COL);//这里向玩家展示是9x9的棋盘,所以用ROW和COL
//接下来的操作是在逻辑数组上完成的
//然后是布置雷
SetMine(mine, ROW, COL);
//最后是排查雷
FindMine(mine,show, ROW, COL);
}
int main()
{
//做一个选择菜单界面——使用do while循环和switch语句——做到菜单可以多次供你选择。
int input = 0;
srand((unsigned int)time(NULL)); //初始化随机数
do
{
//先将菜单显示出来供你选择
menu();
printf("请选择:");
scanf("%d",&input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,重新选择\n");
break;
}
} while(input);
return 0;
}
到这里扫雷游戏的讲解就全部结束了,祝大家玩得愉快~