文章目录
一、扫雷游戏整体设计思路
1.扫雷游戏功能说明
使⽤控制台实现经典的扫雷游戏
(1)游戏可以通过菜单实现继续玩或者退出游戏
(2)扫雷的棋盘是9*9的格⼦
(3) 默认随机布置10个雷
(4)可以排查雷
◦ 如果位置不是雷,就显⽰周围有⼏个雷
◦ 如果位置是雷,就炸死游戏结束
◦ 把除10个雷之外的所有⾮雷都找出来,排雷成功,游戏结束
2.游戏的分析和设计
扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要⼀定的数据结构来存储这些信息
因为我们需要在99的棋盘上布置雷的信息和排查雷,我们⾸先想到的就是创建⼀个99的数组来存放信息
如图,创建一个9 * 9的棋盘,我们这样定义,在这样一个数组中,字符0表示不是雷,字符1表示雷,当我们排查2,4这个坐标时,如果不是雷,就算出它周围8个坐标内有几个雷,然后显示在2,4这个位置,这就是排查雷
但是我们来看另一个坐标,如0,0这个坐标,我们发现要排查的有一部分区域超出边界了,会产生越界访问的情况,但是也不能专门写一个代码解决,因为除了这个0,0还有0,1、0,2·····等等的坐标会出现越界访问,如果每一个都写一个代码来解决就太麻烦了,怎么做呢?
我们可以在创建棋盘的时候扩大一圈,变成11 * 11的棋盘,但是只显示内部9 * 9的棋盘,如:
这样就有效避免了越界访问
再继续分析,我们在棋盘上布置了雷,棋盘上雷的信息(1)和⾮雷的信息(0),假设我们排查了某⼀个位置后,这个坐标处不是雷,这个坐标的周围有1个雷,那我们需要将排查出的雷的数量信息记录存储,并打印出来,作为排雷的重要参考信息的。那这个雷的个数信息存放在哪⾥呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能或产⽣混淆和打印上的困难,比如周围有1个雷,就要显示1,但是1原本是表示雷的,就导致了混淆
解决办法就是:我们专⻔给⼀个棋盘(对应⼀个数组hide)存放布置好的雷的信息,再给另外⼀个棋盘(对应另外⼀个数组show)存放排查出的雷的信息。这样就互不⼲扰了,把雷布置到hide数组,在hide数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查参考
同时为了保持神秘,show数组开始时初始化为字符 ‘*’,为了保持两个数组的类型⼀致,可以使⽤同⼀套函数处理,hide数组最开始也初始化为字符’0’,布置雷改成’1’,如图:
hide数组:
show数组:
3.文件结构设计:
我们之前学习了多⽂件的形式对函数的声明和定义,这⾥我们实践⼀下,我们设计三个⽂件:
test.c //⽂件中写游戏的测试逻辑
game.c //⽂件中写游戏中函数的实现等
game.h //⽂件中写游戏需要的数据类型和函数声明等
二 、主函数大致模型
主函数中的大致模型就和之前讲过的猜数字游戏相似,这里就不再过多赘述,直接上代码,后面的游戏制作才是重点
//文件test.c
void menu()
{
printf("********************\n");
printf("**** 1.开始游戏 ****\n");
printf("**** 0.退出游戏 ****\n");
printf("********************\n\n");
printf("提示:请输入1或0\n");
}
void game()
{
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("\n请输入:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("游戏退出成功\n");
break;
default:
printf("\n输入错误,请重新输入!\n\n");
break;
}
} while (input);
return 0;
}
三、创建棋盘
经过前面的分析,我们知道了需要11 * 11的数组,并且要创建两个,一个作为表面显示的棋盘,用于排雷,一个不让用户看见,用来布置雷,并且类型为char,代码如下:
void game()
{
//隐藏起来用于布置雷:
char hide[11][11] = {0};
//展示出来用于排查雷:
char show[11][11] = {0};
}
四、初始化棋盘
经过最上面的思路分析,我们最好将hide数组全部初始化为字符0,表示全部都不是雷,然后后续再对其加入雷(也就是字符1),将show数组全部初始化为字符*,增加神秘感
由于game函数中可能会有很多的代码,所以为了简明大方,我们将初始化棋盘以及后面的步骤都封装为函数,然后再game.h中进行声明,在game.c中实现
我们开始分析:
- 初始化棋盘的函数名:我推荐叫Initboard,init是initialise的缩写,含义是初始化,当然这个不用死记,函数名完全可以按照自己的想法命名,我这里只是做一个推荐
- 函数参数:我们要初始化棋盘,也就是初始化数组,自然要把数组传过去,由于要初始化,就要遍历数组,我们就要把行数和列数传过去,最后就是要把函数初始化为什么字符,我们可以把这个字符也传给函数,所以函数参数有:数组,行,列,初始化用到的字符
- 优化:当我们把代码写出来后,我们会发现我们写了很多11,后面也会有很多11和9,它们代表了棋盘的大小,试想一下,如果后面我觉得9 * 9的棋盘玩着不过瘾,想要把棋盘变大,就只能去把所有的11以及9改掉,太麻烦了,所以为了方便,我们这里可以把代表棋盘大小的数字用define定义一下,比如现在是显示9*9的棋盘,为了防止越界访问,实际上棋盘大小为11 * 11,后者比前者多2,因此我们可以用define来定义一个常数了,如:
//由于这是声明,我们写在game.h中
//显示的棋盘大小
#define Row 9
#define Col 9
//真正的棋盘大小
#define Rows Row+2
#define Cols Col+2
以后我们需要用到棋盘大小的地方就用定义的这些符号就行,当然,这些符号也是可以自行定义的,这里只是我的写法,然后如果以后想改棋盘大小就只需要把9改掉,就特别方便(记得把上面创建数组)
- 函数的声明:函数的声明要写在game.h中,由于很简单,这里只讲一次,后面讲到的函数就不会再讲到,自行推导就行,声明只需要写出函数的返回值,函数名,括号,形参,最后还有一个分号,如:
//声明初始化棋盘函数
void initboard(char board[Rows][Cols], int rows, int cols, char x);
- 函数的实现:函数的实现我们就放在game.c中,思路就是通过函数传过来的行列进行遍历数组,把每一个元素初始化为传过来的字符,代码如下:
//实现初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = x;
}
}
}
- 函数的使用:这个很简单,按照我们的形参,把实参写出来就行,如:
//初始化棋盘:
initboard(show, Rows, Cols, '0');
initboard(hide, Rows, Cols, '*');
五、打印棋盘
经过上面的初始化,我们现在还看不到棋盘,我们就先写一个打印棋盘的函数,将它们打印出来,看看我们初始化成功没有,步骤也和刚刚一样
我们需要这个函数帮我们把棋盘中间9 * 9的大小打印出来,并且有一个标识,设计步骤如下:
- 打印棋盘函数名:推荐printboard,直观简易,一眼看出来是打印棋盘,也可自行定义名称
- 函数参数:首先一定是要把数组传过去,由于数组的大小是真正棋盘的大小,所以后面跟的行和列是Rows和Cols,然后由于要遍历数组,所以要把行和列传过去,但是注意一点,我们打印数组的真正大小是11 * 11,但是真正显示的是中间9 * 9的部分,传参要传Row和Col而不是Rows和Cols,所以参数就是:数组,行,列
- 函数的声明:同初始化棋盘函数,此处不在赘述,代码如下:
//声明打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col);
- 函数的实现:
(1)在平常一样打印数组时,我们需要注意,每打印完一行就要进行一次换行
(2)最好标出行号和列号,这样玩家在玩的时候才方便输入坐标进行排雷
(3)我们可以在棋盘的上下做一点标识,让玩家更加醒目的看到棋盘
(4)虽然棋盘也就是数组的真正大小是11 * 11,但是真正显示的是中间9 * 9的部分,传参要传Row和Col而不是Rows和Cols
(5)代码:
//实现打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col)
{
int i = 1;
int j = 1;
//打印棋盘标志,让棋盘更显眼
printf("------ 扫雷 -------\n");
//打印列号:
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
//列号打印完进行换行
printf("\n");
for (i = 1; i <= row; i++)
{
//打印行号
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
//每打印完一行就要进行一次换行:
printf("\n");
}
//打印棋盘标志,让棋盘更显眼
printf("------ 扫雷 -------\n");
//棋盘打印完之后进行换行
printf("\n");
}
- 函数的使用:函数的一切都已经准备好了,这时我们只需要按照参数传参即可,我们游戏的要求只需要显示show数组,为了观察一下hide数组是否成功初始化可以使用一下
,随后进行注释操作,如:
//打印棋盘:
printboard(show, Row, Col);
//printboard(hide, Row, Col);
六、布置雷
经过第一部分的分析,我们可以知道,字符0代表不是雷,字符1表示雷,并且布置雷的对象是数组hide,之前我们把hide数组全部初始化为了字符0,这时我们只需要随机地在整个棋盘上布置雷,本质上就是随机地将一些字符0改成字符1,而随机数的产生要依靠rand函数和srand函数,以及时间戳,这些在猜数字游戏中有将到,如果忘了可以自行复习,后面直接贴上代码
- 布置雷函数的命名:我将其命名为setboard,也可自行命名
- 函数参数:首先要把hide数组传过去,随后我们会使用行和列,要将Row和Col传过去,然后就是要布置的雷的个数,由于雷的个数也是经常出现的常数,所以我们也可以用define定义一下,方便以后的更改,如:
//雷的个数:
#define Count 10
- 函数的声明:此处不做赘述,代码如下:
//声明布置雷函数:
void setboard(char board[Rows][Cols],int row,int col, int count);
- 函数的实现:
(1)首先我们需要产生1~Row的随机数,也就是1到雷的个数的随机数,只需要rand()%Row+1或者rand()%Col+1即可,rand如何产生随机数此处不在赘述,之前讲的猜数字游戏有,这里提醒一点就是记得将头文件放在game.h中
(2)随后就是布置雷,也就是将字符0改成字符1的过程,这个很简单,只需要用到一个if语句
(3)代码:
//实现布置雷函数:
void setboard(char board[Rows][Cols],int row,int col, int count)
{
while (count)
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
- 函数的使用:只需要将hide函数和雷的个数(即Count)传给函数即可,如:
//布置雷:
setboard(hide, Count);
- 注意,在用的时候,要把布置雷这个步骤写在打印棋盘前,如图:
七、排查雷
排查雷的重点就是,我们要看玩家输入的坐标位置是不是雷,也就是是不是字符1,如果是字符1,说明踩到雷了,游戏失败,如果是字符0,也就是不是雷,那么该处就显示为周围8个坐标的雷的个数,所以它既要涉及到show数组,又要涉及到hide数组,hide数组用来查看玩家输入的坐标是否是雷,show数组用于该坐标不是雷的时候,在该坐标处显示周围有几个雷
- 函数的命名:我将它命名为findboard,可以自行命名
- 函数的参数:由于既要涉及到show数组,又要涉及到hide数组,所以前两个参数就是两个数组,由于我们要判断用户输入的值是否合法,也就是是否在1~Row或者Col,所以我们需要将函数的Row和Col传过去,又因为游戏胜负和雷的个数有关,所以我们要将雷的个数传给函数
- 函数的声明:声明如下:
//声明排查雷函数
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row,int col,int count);
4.函数的实现:
(1)首先我们需要让用户输入一个坐标,并且要确保坐标正确,否则显示提示信息
(2)如果坐标正确,那么就判断坐标处是否是雷,如果是雷就直接炸死,游戏失败,当然,为了让玩家知道自己怎么失败的,我们可以将含有雷信息的数组hide打印出来
(3)如果不是雷,那么我们就想办法获取周围雷的个数,我们可以再创建一个函数getcount来解决,这个函数就帮我们统计周围雷的个数
(4)由于代表是否是雷的元素是字符0或者字符1,无法像整型一样直接相加,因为字符0代表的ascll码值是48,与整型的0不同,怎么解决呢?我们可以这样,让玩家周围的所有字符相加然后全部减去字符0,如果一个位置是字符1,减去字符0就成了真正的1,字符0减去字符0就成了真正的0,我们可以让坐标周围的八个坐标的字符相加,再减去8个字符0,就得到了雷的个数
(5)坐标周围雷的个数也最多就8个,也就是最多返回一个8的整数,我们怎么把它变成字符,存放在show数组的相应位置呢?此时我们又可以加上一个’0’,这时雷的个数就变成了字符
(6)如果玩家排雷成功要如何结束游戏呢?我们可以使用一个新的变量win,每次用户排雷就对它进行自增1的操作,直到win= =row * col-count,也就是循环条件是win<row*col-count,如果win= =row * col-count也就说明了玩家排雷成功,这时就可以输出扫雷成功的信息
(7)getcount函数实现:将其放在findboard函数前定义就行,不用在game.h进行声明,代码如下:
//如果坐标不是雷,获取坐标周围雷的个数
int getcount(char hide[Rows][Cols], int x, int y)
{
return hide[x][y-1] + hide[x][y+1] +
hide[x - 1][y] + hide[x-1][y-1] +hide[x-1][y+1]+
hide[x + 1][y]+ hide[x + 1][y-1] + hide[x + 1][y+1]-8*'0';
}
这是我觉得比较简单的思路,当然还有另一种思路,如:
//如果不是雷,获取坐标周围雷的个数
char getcount(char hide[Rows][Cols],int x,int y)
{
char num = 0;
int i = 0;
int j = 0;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
num += hide[x + i][y + j];
}
}
num = num - 8 * '0';
return num;
}
这样也可以直接返回字符,思路也不难,就是比较难写
(8)findboard函数实现:此处的代码使用的是第一个getcount的方法,代码如下:
//实现排查雷函数:
void findboard(char show[Rows][Cols], char hide[Rows][Cols],int row,int col, int count)
{
int x = 0;
int y = 0;
int win = 0;
printf("请输入空格或英文逗号隔开坐标!\n\n");
while (win<Row*Col-Count)
{
printf("请输入要排查的坐标:");
//这里需要用到之前学过的%*c,就是
//scanf的赋值忽略符*
//使用过后就可以输入空格或者英文逗号隔开坐标,但是不能用中文
//这里中文和英文逗号不能都使用
//所以我们最好在开始时给予一些提示
scanf("%d%*c%d", &x, &y);
printf("\n");
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (hide[x][y] == '1')
{
printf("很遗憾,你踩到雷了,游戏失败!\n\n");
printf("显示的0代表不是雷,1表示雷\n\n");
printboard(hide, Row, Col);
break;
}
else
{
win++;
int ret = getcount(hide, x, y);
show[x][y] = ret + '0';
printboard(show, Row, Col);
}
}
else
{
printf("输入有误,请重新输入!\n");
}
}
if (win == Row * Col - Count)
{
printf("恭喜你,扫雷成功!\n\n");
printf("显示的0代表不是雷,1表示雷\n\n");
printboard(hide, Row, Col);
}
}
八、源码
整个扫雷游戏的思路已经讲解完毕,接下来附上每个文件的源码,中间为了美观,可能会加了不少\n进行换行,但是不影响阅读代码,源码如下:
game.h:
//game.h
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//显示的棋盘大小
#define Row 9
#define Col 9
//实际的棋盘大小
#define Rows Row+2
#define Cols Col+2
//雷的个数:
#define Count 10
//声明初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x);
//声明打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col);
//声明布置雷函数:
void setboard(char board[Rows][Cols], int count);
//声明排查雷函数
void findboard(char show[Rows][Cols], char hide[Rows][Cols], int row,int col,int count);
game.c:
//game.c
#include "game.h"
//实现初始化棋盘函数:
void initboard(char board[Rows][Cols], int rows, int cols, char x)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = x;
}
}
}
//实现打印棋盘函数:
void printboard(char board[Rows][Cols], int row, int col)
{
int i = 1;
int j = 1;
//打印棋盘标志,让棋盘更显眼
printf("------ 扫雷 -------\n");
//打印列号:
for (i = 0; i <= col; i++)
{
printf("%d ", i);
}
//列号打印完进行换行
printf("\n");
for (i = 1; i <= row; i++)
{
//打印行号
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
//每打印完一行就要进行一次换行:
printf("\n");
}
//打印棋盘标志,让棋盘更显眼
printf("------ 扫雷 -------\n");
//棋盘打印完之后进行换行
printf("\n");
}
//实现布置雷函数:
void setboard(char board[Rows][Cols], int count)
{
while (count)
{
int x = rand() % Row + 1;
int y = rand() % Col + 1;
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
//如果坐标不是雷,获取坐标周围雷的个数
int getcount(char hide[Rows][Cols], int x, int y)
{
return (hide[x][y-1] + hide[x][y+1] +
hide[x - 1][y] + hide[x-1][y-1] +hide[x-1][y+1]+
hide[x + 1][y]+ hide[x + 1][y-1] + hide[x + 1][y+1] - 8*'0');
}
//实现排查雷函数:
void findboard(char show[Rows][Cols], char hide[Rows][Cols],int row,int col, int count)
{
int x = 0;
int y = 0;
int win = 0;
printf("请输入空格或英文逗号隔开坐标!\n\n");
while (win<Row*Col-Count)
{
printf("请输入要排查的坐标:");
//这里需要用到之前学过的%*c,就是
//scanf的赋值忽略符*
//使用过后就可以输入空格或者英文逗号隔开坐标,但是不能用中文
//这里中文和英文逗号不能都使用
//所以我们最好在开始时给予一些提示
scanf("%d%*c%d", &x, &y);
printf("\n");
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (hide[x][y] == '1')
{
printf("很遗憾,你踩到雷了,游戏失败!\n\n");
printf("显示的0代表不是雷,1表示雷\n\n");
printboard(hide, Row, Col);
break;
}
else
{
win++;
int ret = getcount(hide, x, y);
show[x][y] = ret + '0';
printboard(show, Row, Col);
}
}
else
{
printf("输入有误,请重新输入!\n");
}
}
if (win == Row * Col - Count)
{
printf("恭喜你,扫雷成功!\n\n");
printf("显示的0代表不是雷,1表示雷\n\n");
printboard(hide, Row, Col);
}
}
test.c:
#include "game.h"
void menu()
{
printf("********************\n");
printf("**** 1.开始游戏 ****\n");
printf("**** 0.退出游戏 ****\n");
printf("********************\n\n");
printf("提示:请输入1或0\n");
}
void game()
{
//不显示出来,用于布置雷
char hide[Rows][Cols];
//显示出来,用于排查雷
char show[Rows][Cols];
//初始化棋盘:
initboard(hide, Rows, Cols, '0');
initboard(show, Rows, Cols, '*');
//布置雷:
setboard(hide, Count);
//打印棋盘:
printboard(show, Row, Col);
//printboard(hide, Row, Col);
//排查雷:
findboard(show, hide,Row,Col, Count);
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("\n请选择:");
scanf("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("游戏退出成功\n");
break;
default:
printf("选择错误,请重新输入!\n");
break;
}
} while (input);
return 0;
}
九、如何把游戏分享给小伙伴
相信你已经尝试玩了这个扫雷游戏,是不是特别想分享给朋友室友玩一玩呢?跟着我来学习吧
首先打开VS,将上方的debug模式改为release版本,随后将代码运行一次,如图:
然后找到存放该文件的文件夹,找到x64文件夹,然后找到release文件夹,打开后找到扩展名为exe的可执行文件,直接将其发送给朋友就可以了,是不是非常简单呢!
十、扫雷进阶的一些思路
• 是否可以选择游戏难度
◦ 简单 9 * 9 棋盘,10个雷
◦ 中等 16 * 16棋盘,40个雷
◦ 困难 30 * 16棋盘,99个雷
• 如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚
• 是否可以标记雷
• 是否可以加上排雷的时间显⽰
如果小伙伴们有兴趣,后续还可以更新这些进阶扫雷需求,当然,如果排查位置不是雷,周围也没有雷,可以展开周围的⼀⽚,这个需要用到函数递归的知识点,后面讲到递归直接讲了,其它进阶需求出不出可以看小伙伴们的意愿,想看哪个直接评论区留言哦~
这篇关于扫雷的博客到此结束了,如果对你有帮助的话记得一键三连哦~