扫雷游戏分析和设计
扫雷游戏的功能说明
-
使用控制台实现经典的扫雷游戏
-
游戏可以通过菜单实现继续玩或者退出游戏
-
扫雷的棋盘是9*9的格子
-
默认随机布置10个雷
-
可以排查雷
如果位置不是雷,就显示周围有几个雷
如果位置是雷,就炸死游戏结束
把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束
游戏的分析和设计
因为我们学过多文件的使用,所以在这个游戏中我们创建3个文件:test.c(用来完成游戏的测试逻辑),game.c和game.h一起完成的是游戏逻辑的实现(.h里面放的函数的声明;.c里面放的是函数的实现)
大概代码逻辑:
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
void menu()
{
printf("*****************************\n");
printf("****** 1.play **********\n");
printf("****** 0.exit **********\n");
printf("*****************************\n");
}
void test()//这个函数的内部完成的是整个游戏的逻辑
{
int input = 0;
do
{
menu();//写个函数把菜单打印出来
printf("请选择:>");
scanf("%d", &input);//输入之后要判断情况
switch (input)//根据input进行处理
{
case 1:
printf("扫雷");
break;
case 0:
printf("游戏结束,退出游戏");
break;
default:
printf("选择错误,重新选择");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
数据结构的分析
在扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要一定的数据结构来存储这些信息。
因为我们需要在9×9的棋盘上布置雷的信息和排查雷,我们首先想到的就是创建一个9×9的数组来存放信息。
如果这个位置布置雷,我们就存放1,没有布置雷就存放0.
假设我们排查(2,5)(第2行,第5列)这个坐标时,我们访问周围的一圈8个黄色位置,然后统计周围雷的个数,发现有1个雷。
假设我们排查(8,6)这个坐标时,我们访问周围的一圈8个黄色位置,统计周围雷的个数时,最下面的三个坐标就会越界,为了防止越界,我们在设计的时候,给数组扩大一圈,雷还是布置在中间的9×9的坐标上,周围⼀圈不去布置雷就行(周围一圈都为0),这样就解决了越界的问题。所以我们将存放数据的数组创建成11×11是比较合适。
再继续分析,假设我们排查了某一个位置后,这个坐标处不是雷,但这个坐标的周围有1个雷,那我们需要将排查出的雷的数量在这个坐标上打印出来,用来提示周围有多少个雷。可是在布置雷的数组中1已经代表雷了,如果在(2,5)这个坐标上打印一个1的话,这个1到底是雷的个数还是雷呢?这就产生了混淆。
所以,这个雷的个数信息该存放在哪里呢?如果存放在布置雷的数组中,这样雷的信息和雷的个数信息就可能产生混淆和打印上的困难。
解决方法:
我们专门给一个数组mine存放布置好的雷的信息,排查出的信息为了避免和mine数组里的信息冲突了,我们再创建好另一个数组:show存放排查出的雷的信息,打印时也是打印这个数组的信息。
eg:我们在mine数组中排查(3,6)这个坐标,发现周围有1个雷,所以我们在show数组里面的(3,6)这个坐标显示一个1。
这样就互不干扰了,把雷布置到mine数组,在mine数组中排查雷,排查出的数据存放在show数组,并且打印show数组的信息给后期排查做参考。
在扫雷时,为了保持神秘,先把界面初始化为*
所以:show数组开始时初始化为字符 ‘*’,排查后的信息在相应的坐标上打印出来做提示,其余部分依旧为以下初始的字符,打印的时候也直接打印这个数组,让别人知道雷的信息。
我们发现mine数组与show数组元素个数一样(11×11),元素类型也一样,所以对数组的操作用同一套函数就可以了
文件结构设计
创建三个文件:
在头文件定义:
为什么还要定义ROW和COL为9呢?因为我们在做棋盘的时候要用的是9×9,为了防止越界,创建的时候要用的是11×11。所以既要11也要9.
创建这两个数组,我们发现有“未定义标识符”,这是因为ROWS和COLS这两个符号是定义在头文件中的,所以我们要包含头文件
写上头文件:
我们要将棋盘初始化,就写个初始化函数
再在头文件中写上初始化函数的声明
接在在game.c文件里实现这个函数
将棋盘初始化为0写完了,但是show数组得初始化为*,如果再写一个初始化函数就会显得冗余了,有没有其他的办法了呢?
我们可以多写一个参数:
调用两次初始化函数,这样就可以将两个数组都初始化想要的字符了
我们可以把棋盘打印出来看看效果是什么样的,因为平时玩游戏都只看9×9,所以此时只用打印9×9的棋盘就可以了,前面把数组设置成11×11是为了防止越界。
DisplayBoard在game.c文件里进行函数实现时注意:在11×11的棋盘里打印中间的9×9的棋盘时,行号与列号都是从1开始的,到9结束
所以函数实现时:
我们在测试打印mine棋盘时发现报错,这是因为我们只在test.c中包含了头文件<stdio.h>,在game.c中用了printf却没包含
因为game.c与test.c都包含了头文件game.h,所以我们可以将test.c中的<stdio.h>删掉放在game.h中,这样就相当于test.c和game.c都包含了<stdio.h>
mine棋盘测试成功:
接下里我们测试show棋盘:
打印成功
因为show数组是我们玩游戏时需要打印出来的,但是这样打印出来的show棋盘不太好看,如果在show棋盘上把行和列都显示出来的话,说定位的时候就会直观,所以我们将代码改一改
行号:
运行看效果:
因为列号在棋盘的最上方,所以在打印棋盘之前先写个for循环打印列号,打印完列号后要写个换行
我们发现列号错位了,第2列那里应该是第1列的,该怎么让1在对的位置呢?就在前面加上一个数字0.
这样的话坐标会非常的直观。我们将两个棋盘都打印出来看看:
打印出来的两个棋盘太拥挤了,可以加上一个分割的东西
因为mine数组中都是雷布置好的信息,要是打印出来就露馅了,所以我们要打印的是show数组。
当我们初始化棋盘后,应该是布置雷,打印暂时还用不上。
所以为什么上面写了初始化棋盘后写的就是打印棋盘呢?
因为只有打印了棋盘,我们才能知道初始化的对不对。所以接下来将打印棋盘暂时注释掉,写个SetMine函数开始布置雷。
因为mine数组中存放布置好的雷的信息,show数组中存放排查出的雷的信息,所以雷要布置到mine数组里面
在头文件中对SetMine函数进行声明
接下来在game.c文件中进行函数实现
首先在头文件中定义雷的个数
因为我们要利用的是中间9×9的棋盘,所以坐标范围要变为1~9
而生成随机数要用rand,并且在此之间还要调用一次srand,所以
包含它们相应的头文件:
在前面我们已经将坐标的范围决定好了,但在我们生成坐标时还要想一个问题:在布置雷时这个坐标会不会已经布置过雷了?
所以我们可以写个if语句判断一个,要是为字符0的时候就说明不是雷,此时可以进入这个if语句,在这个坐标上布置雷;如果这个坐标不是字符0(为1:雷),就进入不了if语句,接着重新生成坐标,一直循环,直到count为0循环结束。
这个代码循环次数应该大于等于10次的,因为没有走if的时候count是不减的,会一直循环。
现在我们打印以下看看有没有布置成功:
将雷布置好后,我们将整个流程还原:应该是先初始化棋盘,再布置雷,接着打印棋盘,最后排查雷
排查雷是在mine数组中排查,然后排查的信息放在show数组中,所以排查的过程中,这两个数组都会涉及到,即传参的时候这两个数组都要传。并且排查的过程中也只是关心中间的9×9,所以行为ROW,列为COL
同样的,在头文件中对这个函数进行声明:
注意:两个数组传参的时候,顺序要保持一致
在game.c文件中进行函数的实现
-
首先要输入坐标
-
在别人玩游戏中,万一输入的坐标不是我们所规定的棋盘的坐标范围,例如:(18,20).这样的话就越界了,所以在输入坐标后我们应判断坐标的有效性。
重新输入是再重新输入坐标,所以这里应该写个循环
-
当输入的坐标有效时就进入这个if语句了,有了坐标后我们就要判断这个坐标是不是雷,是雷就会被炸死,不是雷则要显示周围雷的信息
-
接着完成GetMineCount函数,如果我们不想GetMineCount这个函数放在头文件中,不想被其他人看到,可以直接写在game.c中。
-
那么该怎么直到这个坐标周围一圈雷的个数呢?
我们可以将这8个坐标找出来,然后对它们的值进行判断。如果是字符0,count不变,如果是字符1,count++。这样8个坐标统计完后就找出来了
还有另外一种方法:直接把周围8个坐标的值加起来,但是这个棋盘里的不是数字0和1,而是字符0和1.所以我们可以利用ASCII码值。
‘1’—49 ;
‘0’—48 ;
‘2’—50;
‘1’-‘0’=1;‘2’-‘0’=2
我们发现这样字符相减时会变成相应的ASCII码值相减,可以变成数字。
所以:字符0减去字符0可以等于数字0,字符1减去字符0=数字1.
即周围8个坐标所对应的字符都减去字符0,再将得到的数字再相加就可以了
如果想让 GetMineCount这个函数只在game.c这个文件中使用,可以在前面加上static
- 此时已经将该坐标周围雷的数量统计出来了,我们应该将count放进show数组里面去
但是不能直接将count放进show数组
因为我们打印棋盘的时候是按%c的格式打印的,而数字是用%d的格式打印的,如果这里将数字直接放上棋盘打印的话,会将该数字通过ASCII码值变成相应的字符打印出来,所以这个地方必须要变成字符数字。
那该数字这么变成字符数字呢?
通过ASCII码值我们可以发现:
‘0’+1=48+1=49=‘1’
字符0对应十进制数字是48,数字48加数字1等于数字49,十进制数字49也就是字符1.
这样就实现了数字与字符之间的相互转换。
再如:
4+‘0’=4+48=52=‘4’
所以假如统计有3个雷,返回到count的值为数字3,直接数字3+‘0’=3+48=51=‘3’
即我们要在count后面+一个字符0.
- 当我们排雷的时候应该显示那个棋盘
- 此时想看看统计的雷的数量对不对,我们可以将布置好雷的棋盘打印出来
将布置雷的棋盘打印出来后输入坐标,看统计的雷的个数正不正确
输入坐标:2 6
排查雷的数量正确,因为我们只需要向别人显示show棋盘,所以测试后将mine棋盘重新注释掉
此种方法代码的另一种写法:
虽然这种方法将9个坐标都遍历了,但是中间那个坐标一定不是雷,所以将它统计进去也不会影响最后的统计雷的数据。
count+=(mine[i][j]-‘0’)
count=count+(mine[i][j]-‘0’)
加static是因为想让这个函数只在game.c这个文件中使用,不想让别的文件看到,不让它暴露出去
此时我们发现,当我们在排查的过程中踩到雷的时候会被炸死结束游戏,而直到最后要是把不是雷的位置都已经排查完,只剩下雷的话,该怎么结束这个游戏呢?
雷是在9×9的棋盘内随机布置的,每行有9个坐标,一共有9行,所以总共有81个坐标。而这81个坐标里有10个坐标是雷,所以还剩71个坐标不是雷,在我们中途没踩到雷死掉的情况下,把这71个坐标全部找出来就是游戏结束的时候。
所以,接下来对这段代码进行修改:
当所输入的坐标不是雷时win就自增1,直到win刚好循环71次时,也就是排查了71个坐标,将所有不是雷的坐标都排查完了,此时游戏该结束了。
win<3循环了几次?
win等于0时,循环一次后win等于1
win等于1再循环,循环两次后win等于2
win等于2再循环,循环三次后win等于3
我们要排查71个坐标,所以要循环71次,循环71次后win等于71,为了不再循环所以while后面的表达式要写win<71(等于71的话会循环72次)
71个坐标全找出来后跳出循环,结束游戏:
此时我们也可以将雷的信息再打印出来
以上就是排雷的整个过程。
接下来我们可以测试一下,但是真的要把71个坐标全部排查完有点难,并且万一排查的过程中被雷炸死了的话,又得从头开始。所以我们可以把雷的个数改成80个,这样的话要排查的位置就只有一个了。
而且我们还可以把布置好雷的棋盘打印出来,让我们知道不是雷的位置在哪
结果:
测试完接着把雷的数量恢复10个,把Mine棋盘注释掉
还有个问题我们需要注意:就是已排查过的坐标就不要再重复输入了,在以上代码中我们只考虑了坐标的有效性,这点也需要注意,所以:
接着把代码补全:
扫雷游戏的代码实现
game.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ROW 9//ROW:行。这句代码的意思是定义一个符号ROW为9,也就是9行
#define COL 9//COL:列。定义一个符号COL为9,也就是9列
#define ROWS ROW+2//定义一个符号ROWS,内容为ROW+2,也就是11行
#define COLS COL+2//定义一个符号COLS,内容为COL+2,也就是11列
#define EASY_COUNT 10//表示定义10个(雷)
//棋盘初始化的函数声明
void InitBoard(char arr[ROWS][COLS],int rows,int cols,char set);
//打印棋盘函数声明
void 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);
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void InitBoard(char arr[ROWS][COLS], int rows, int cols,char set)
{
int i = 0;//行
for (i = 0; i < rows; i++)
{
int j = 0;//列
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
void DisplayBoard(char arr[ROWS][COLS], int row, int col)
{
int i = 0;
//打印列号
printf("--------扫雷游戏--------\n");
for (i = 0; i <= col; i++)
{
printf("%d ",i);
}
printf("\n");//换行
for (i = 1; i <= row; i++)
{
int j = 0;
printf("%d ",i);//在每一行打印之前,先打印i(行号)
for (j = 1; j <= col; j++)
{
printf("%c ",arr[i][j]);
}
printf("\n");
}
}
void SetMine(char arr[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;//10个雷
while (count)
{
//生成随机坐标
int x = rand()% row + 1;
int y = rand()% col + 1;
//任何数%9余的是0~8,再加上一个1就是1~9了
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
static int GetMineCount(char mine[ROWS][COLS],int x,int y)
{
int i = 0;
int count = 0;
for (i = x - 1; i <= x + 1; i++)
{
int j = 0;
for (j = y - 1; j <= y + 1; j++)
{
count += (mine[i][j] - '0');
}
}
return count;
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win<row*col- EASY_COUNT)//(win<71)
{
printf("请输入要排查的坐标:");
scanf("%d %d", &x, &y);
//判断坐标的有效性
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] == '*')
{
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");
}
}
else
{
printf("坐标不正确,请重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
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,'*');//这个数组为了保持神秘全部初始化为*
//布置雷
//就是在9*9的棋盘上随机布置10个雷
SetMine(mine,ROW,COL);
//DisplayBoard(mine,ROW,COL);
//打印棋盘
DisplayBoard(show, ROW, COL);
//排查雷
FindMine(mine,show,ROW,COL);
}
void test()//这个函数的内部完成的是整个游戏的逻辑
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();//写个函数把菜单打印出来
printf("请选择:>");
scanf("%d", &input);//输入之后要判断情况
switch (input)//根据input进行处理
{
case 1:
game();
break;
case 0:
printf("游戏结束,退出游戏");
break;
default:
printf("选择错误,重新选择");
break;
}
} while (input);
}
int main()
{
test();
return 0;
}
扫雷游戏的扩展
- 是否可以选择游戏难度
- 简单9*9棋盘,10个雷
- 中等16*16棋盘,40个雷
- 困难30*16棋盘,99个雷
- 如果排查位置不是雷,周围也没有雷,可以展开周围的一片
- 是否可以标记雷
- 是否可以加上排雷的时间显示
在线扫雷游戏:http://www.minesweeper.cn/