目录
整体设计思路
扫雷咋玩
还记得小时候玩扫雷可懵了,以为是个仅仅靠运气的游戏,没想到还是个益智类游戏。
现在应该也有好多小伙伴是不懂扫雷怎么玩的
扫雷游戏可以锻炼观察力、思考能力。
下面普及一下扫雷的玩法。
方法/步骤
扫雷模式一般分为初级,中级,高级与自定义。下面以高级模式为例。高级模式有16×30(即480)个方格,而地雷数是99.所以不懂玩法,光凭运气的不可能将这99颗雷全部找出来的。
玩扫雷,第一步,随便用鼠标左键点开任意方格。这一步完全是看运气。很可能会出现两种情况。1.点了好几个方格都没开出空白区域。2.没点开空白区域就以及点到雷。(额,认命吧,重新开始)
第二步,点开空白区域以后。可以看到有很多数字1,2,3,甚至还会出现4,5,6,这些数字代表的意思是,与数字相邻的几个方格(如果数字在四个角为三个方格,在四条边为五个,其他地方都为八个)中必有而且只有x个雷,所以说,数字1 的周围就会有一个雷,数字2的周围就会有两个雷,以此类推。
第三步,标记。如果确定某一格为雷后只要点鼠标右键(点一下)标记就可以了,这时会看到那个上面会有一个小旗。说明已经标记成功,如果某一个格你不确定是不是雷,也是可以有鼠标右键(点两次)标记的,这时方格上会出现一个问号。如果发现标记错误,就继续用右键点击该方格即可取消。
如果地雷已经全部被标记,而且不存在未点开的方格,那么,恭喜你,你已经成功完成了本次扫雷。
简言之:就是点开的一个数字就是周围八个格子内雷的数量。鼠标右键标记完全确定是雷的位置,全部标记出即可。
test.c 用来存放游戏的整体逻辑。
实现游戏菜单
首先在test.c中建立一个主函数,主函数中是一个选择语句,游戏的最初逻辑。给定一个变量input,当input为1时,开始扫雷游戏。当input为0时,退出游戏。
void menu()//菜单
{
printf("*************************\n");
printf("*********1.play**********\n");
printf("*********0.exit**********\n");
printf("*************************\n");
}
int main()
{
srand((unsigned int)time(NULL));//后面随机放地雷时会用到这个随机值种子
int input = 0;//给定一个变量input
do
{
menu();
scanf("%d", &input);
switch (input)
{
case 1:
printf("扫雷游戏\n");
game();//进入扫雷游戏的函数
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("输入错误,请重新输入\n");
break;
}
} while (input);
return 0;
}
游戏主逻辑game函数
- 游戏主逻辑存放在上面代码中提到的game函数里,扫雷的棋盘其实就是一个二维数组。假设数组中1为雷,0为非雷,但是排雷时又需要在数组中显示数据来表示某个坐标附近有几颗雷。这样的话一个数组会严重混淆我们,并且无法正常游戏。所以我们建立两个数组,一个数组是mine用来存放地雷信息。另一个是show,用来展示给玩家。
扫雷标准是9行9列,但是由于考虑到靠边的坐标计算周围的地雷数时,还需要不包括越界的地方,所以将两个数组都扩大一圈,就不会有这方面的困扰了,即变为了11行11列的二维数组,下文中会提到使用宏定义的方式分别定义ROW 9,COL 9,ROWS 11,COLS 11,以及设置的地雷数EASY_COUNT
- 然后我们需要给两个数组(棋盘)进行初始化,说白了就是地雷数组中先全部放
'0'
(就是暂时还没雷呢,后面再埋雷),show数组中放星号'*'
,用来表示玩家还没排查过的迷雾区域。 - 然后我们需要给地雷数组中埋地雷了
- 最后就是玩家们排雷的操作了,当然在排雷前先把show数组打印一下。
以上如初始化数组,放置地雷,排雷,都做成单独的函数模块。game函数中只存放逻辑。以下是代码
创建数组中的ROW,COL,ROWS,COLS.是提前define定义的常量
因为这些常量后面会经常用到,且能增加代码可拓展性。如随时改变棋盘大小。后面说
void game()
{
char mine[ROWS][COLS] = { 0 };//创建用来存放地雷的棋盘
char show[ROWS][COLS] = { 0 };//创建用来展示给玩家的棋盘
//初始化数组
Initboard(mine, ROWS, COLS, '0');
Initboard(show, ROWS, COLS, '*');
//放置雷
setmine(mine, ROW, COL);
//排雷,排雷前先把show数组打印一下。方便玩家选坐标。
Showboard(show, ROW, COL);
findmine(mine, show, ROW, COL);
}
game.h 函数声明和符号定义
包含一些头文件,和自定义的常量,以及后面会用到的函数定义。
注:项目中其他文件要引用一下这个头文件
#include "game.h"
#pragma once
#include <stdio.h>
#include <windows.h>
#include <time.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//初始化二维数组(棋盘)
void Initboard(char arr[ROWS][COLS], int rows, int cols, char set);
//打印棋盘的定义
void Showboard(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 扫雷游戏各项功能函数具体实现
棋盘初始化
没啥说的,遍历整个数组,往里塞。
mine里面先塞满0
show里面塞满星号
//初始化棋盘, 给定棋盘大小 行数 列数 要塞进去的字符
void Initboard(char arr[ROWS][COLS], int rows, int cols, char set)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = set;
}
}
}
布置地雷
随机给mine数组中我们需要的空间9*9中埋雷,假定1
就是地雷,所以我们随机存放1
.
存EAST_COUNT
个(头文件中定义的常量)
//放置地雷函数的实现
void setmine(char arr[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY_COUNT;
while (count)
{
x = rand() % row + 1; //随机获得1-9的数字
y = rand() % col + 1;
if (arr[x][y] == '0')
{
arr[x][y] = '1';
count--;
}
}
}
记得在提前用一下·
srand((unsigned int)time(NULL));
随机放地雷时需要这个随机值种子
放在主函数就可以了
玩家扫雷
雷放置好了之后,就可以开始扫雷了。
我的逻辑是这样的:
先创建个win
变量,初始情况下win
变量就是当前安全格子的数量,棋盘9*9=81个,雷的数量10个,那么win
就是71.
开始扫雷之后首先输入1
或者2
来选择是清理地雷还是标记地雷,
- 清理地雷就是展开show数组中存放的
*
号,每展开一个,win
就减1。 - 标记地雷就是将show数组中未展开的
*
区域中某个坐标,变为M就是我们假定M就是标记这里有雷了,每标记一次win也减1,但是标记数量不能大于雷的总数。 - 当win降为0后,代表着我们已排雷区域加上标记区域已经充满了所有无雷区域,玩家就胜利了。当然也可以不使用标记功能,全部排完也是可以胜利的(^-^)V。
- 但是如果你输入了雷的坐标,就是踩到雷了额,我们的胜利判断标准win就会瞬间变成
748
,然后玩家就无了。
void findmine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int input = 0;
int win = row * col - EASY_COUNT;//判断胜利的参数,win代表剩余未排查的个数。
int* pwin = &win;
do
{
printf("请输入(1或2),1为排雷,2为标记 => ");
scanf(" %d", &input);
switch (input)
{
case 1:
printf("清理地雷\n");
//带有递归展开的清理地雷功能的函数,把胜利参数地址加进去,每递归一次,胜利判断的参数就--,直到为0
clear_mine(mine, show, ROW, COL, pwin);//进入递归展开功能
//判断胜利或者失败
if (win == 748)//win变成了748,那就去死吧
{
goto die;//跳转到死亡界面(最下面)
}
else if (win == 0)//win变成了0,我们赢了
{
goto cleard;//跳转到胜利界面(最下面)
}
break;
case 2:
printf("标记地雷\n");
//把胜利参数地址加进去,每标记一次,胜利判断的参数就--
mark_mine(mine, show, ROW, COL, pwin);//进入标记功能
if (win == 748)
{
goto die;
}
else if (win == 0)
{
goto cleard;
}
break;
default:
printf("输入错误,请从新输入\n");
}
} while (1);
die:
printf("被炸死了炸死了死了了。。。。。。。。。。。。\n");
Showboard(mine, ROW, COL);
return;
cleard:
printf("排雷完毕(^-^)V!!!\n");
return;
}
递归展开功能(附加胜利判断)
- 首先我们要知道如果不递归,每次输入坐标只展开一个方块,这个方块要显示什么。这个方块应该显示的是附近8个块的地雷数量,如果附近没有地雷,那么这个方块我们让它显示空格。所以我们需要一个函数,输入某个坐标,然后这个函数去寻找这个坐标附近8个坐标的地雷数量,并且能够有返回值把地雷数量返回。
下图代码中,因为mine数组中存放的都是字符0或者1,而字符在没内存中是以ASCII码的形式存储的字符0的ASCII码值是48,字符1是49.所以我们把这个坐标附近8个坐标内存储的ASCII值相加再减去8个字符0的值就是附近存在的地雷数量。我们以int形式返回。
static int get_mine_count(char mine[ROWS][COLS], int x, int y)
{
return mine[x - 1][y - 1] +
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] - 8 * '0';
}
- 不过递归才能让玩家有更好的游戏体验
我们需要用到递归来实现在扫雷中点一下炸开一片无雷空间的功能。
但是递归一定要存在限制条件,当满足这个限制条件的时候,递归便不再继续。
并且每次递归调用之后越来越接近这个限制条件。(这个好理解,展开一次,棋盘就少一块,棋盘是有数的)
但是限制条件是什么呢(O_o)??
boom展开一片,无非就是这个坐标附近的8个坐标没有地雷的话,再以其他8个坐标为中心,再遍历这8个坐标各自附近的8个坐标,这样延伸下去。所以限制条件可以这样判断
- 首先判断坐标是否有效,就是不能超过我们的棋盘大小,x和y要在1-9之间。
- 这个坐标不能是地雷
- 这个坐标不能是之前展开过的,不然就死递归了。
所以代码如下
//递归爆炸式展开
static void boom(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* p)
{
//坐标有效性判断
if ((x >= 1 && x <= ROW) && (y >= 1 && y <= COL))
{
//是否地雷判断
if (mine[x][y] != '1')
{
//是否已清理判断,只有show数组中是星号才是没清理过的
if (show[x][y] == '*')
{
int count = get_mine_count(mine, x, y);
if (count != 0)
{
show[x][y] = count + '0';
(*p)--;//每展开一下离胜利又近一分
}
else
{
show[x][y] = ' ';
(*p)--;//每展开一下离胜利又近一分
boom(mine, show, x - 1, y - 1, p);
boom(mine, show, x - 1, y, p);
boom(mine, show, x - 1, y + 1, p);
boom(mine, show, x, y + 1, p);
boom(mine, show, x + 1, y + 1, p);
boom(mine, show, x + 1, y, p);
boom(mine, show, x + 1, y - 1, p);
boom(mine, show, x, y - 1, p);
}
}
}
}
}
标记功能( 附加胜利判断)
标记很简单的,直接输入一个坐标你,把show数组中这个坐标变成M,然后胜利标准减一下。就OK了,对了还要加一个限制,标记数量不能超过雷的数量,不然把所有地方都标记为雷也能胜利,这样摆烂式排雷法简直是太妙了,但是违背了我们的设计初衷。
//标记功能
void mark_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int* p)
{
int x = 0;
int y = 0;
int count = 0;
again:
printf("请输入坐标,先输入行坐标再输入列坐标,两个坐标之间隔一个空格。(已标记过的用M表示)\n");
printf("请输入=> ");
scanf(" %d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col && show[x][y] == '*')
{
if (count < EASY_COUNT)
{
show[x][y] = 'M';
(*p)--;
Showboard(show, ROW, COL);
}
else
{
printf("标记不可超过雷的数量\n");
}
}
else
{
printf("坐标越界或坐标已被排查,请重新输入\n");
goto again;
}
}
尾声
搞到这里c语言扫雷基本上是做完了,但这并不是最终版,还有很多地方可以优化。比如地雷的埋放的顺序放到玩家第一次排雷之后,排除玩家的坐标后其他坐标随机摆放。这样可以保证玩家第一次排雷的时候不会被炸死。等等一些其他的细节。
代码我上传到Gitee中了 点击进入Gitee链接https://gitee.com/wei-leyuan666/for_green_dot.git
第一次写这么长的文章。
(‘-ωก̀ )好困