一,编写代码先问自己几个问题?
1.你玩过扫雷游戏吗?
如果你没有玩过扫雷游戏的,可以不用往下看了,先去玩一下游戏再回来,因为讲了你也听不懂,看了你也看不懂。
在线扫雷游戏:https://www.msn.cn/zh-cn/play/microsoft-minesweeper/cg-msminesweeper
2.你玩过扫雷游戏,并且你知道扫雷上的数字,1,2,3代表的意义是什么?
在这里我还是需要普及一下扫雷游戏规则,照顾一些可能真没有玩过扫雷这种单机的低端游戏的童鞋们!
2.1 数字1:意味着1的四周除1意外的8个空格内有1个地雷。
2.2 数字2:意味着2的四周除2意外的8个空格内有2个地雷。
2.3 数字3:意味着3的四周除3意外的8个空格内有3个地雷。
如果你确认这个区域是雷区,那么就给他一个插上胜利的红旗!
今天你就是旗手!
OK,讲的有点跑偏了,今天我们重点是来写代码的,不是来玩游戏的。
讲重点!
二,如何实现9*9的棋盘格子,进行扫雷。
1.这是一张需要进行排雷的9x9的棋盘
默认在棋盘上布置10个雷!
2.排雷规则
2.1 如果排查位置不是雷,就显示数字(1或2或3),数字代表其周围有多少雷,作为提示。
2.2 如果排查位置是雷,则就炸死结束游戏。
2.3 如果将布置的10个雷全部排查出来,那么则排雷成功,结束游戏。
3.设计你专属的游戏界面
这种?
还是这种?
4.排雷游戏的分析和设计
4.1 问题1:如何布置雷,布置的雷如何存储?
4.2 问题2:如何排查雷,排查的雷如何存储?
因此,我们需要设计数据结构来进行存储,那么我会想到设计一个9x9的数组来存放雷的位置。
如果这个位置布置一个雷,那么我们就放置一个1,否则我们就放置0,现布置10个1,代表1个雷
4.3 布置雷完成之后,假设我们开始排雷的坐标时候。
4.3.1 排查坐标(2,2)
当我们排查(2,2)这个坐标的时候,我们访问周围的的8个黄色位置,统计周围的雷的个数是1个。(为什么我们访问的是8个黄色位置,因为根据扫雷游戏的规则,我们查看雷的个数,我们会在9个格子里面放置雷,放置雷是随机的,现在如果以中间绿色的区域向外排雷,那么右下角的有一个雷,也就是在8个黄色的区域中有一个雷)
4.3.2排查坐标(5,8)
当我们排查(5,8)这个坐标的时候,我们访问周围的一圈8个黄色位置,当我们统计雷的个数时候,我们发现最下面的三个坐标就会出现越出边界,也就是设计的数组长度不够,那么为了防止查询越界,我们在设计的时候就给数组增加一圈。
4.3.3 雷和非雷的坐标地址
现在,我已经可以知道雷和非雷的坐标地址,这个地址就算他们的一个身份信息。可是,我们排查了一个位置之后,就会知道聪这个位置(例如(2,2)这个坐标的周围的8个坐标有一个雷)。那么我们需要将雷的信息进行存储起来,并且也需要打印在显示屏上面共玩家查看,因为这个信息是供玩家参考的。
有同学说,排查出来雷的数据存储在原来布置雷的数组里面,原来的数组存储数据本身没有问题,但是这样雷的信息和雷的个数就会产生信息的重叠和阅读的困难。
那有没有更好的办法呢?
这里我想到使用两个数组,一个数组用于存储布置好的雷的信息mine[] ,另一个数组用来存储排查出来的雷的信息print[]。
有了mine[]和print[]两个数组,那么存放的雷的信息和排查出来的雷的信息就不会互相干扰了,如下图示意。
由于我们加了边界,那么实际数组的长度是
4.3.4 如何增加代码可读性
为了游戏可读性,我们定义两个数组用于存储信息:
char mine[11][11]={0}; //用来存放需要排查的雷的信息
char display[11][11]={0};//用来存放已经排查出来的雷的个数信息
5.如何进行程序框架设计
为了将不同功能存放到对应的文件中,我们可以设计多个文件来进行
test.c //用来存放扫雷游戏的逻辑部分功能的处理
game.c //用来存放游戏中功能函数
game.h //用来存放游戏中需要使用的数据类型和函数声明
5 如何编写代码的棋盘和游戏规则
5.1 定义一个菜单,玩家可以选择1或者0
void menu()
{
printf("*****************************\n");
printf("****** 1 play **************\n");
printf("****** 0 exit **************\n");
printf("*****************************\n");
}
5.2 为了更方便移动值,初始化棋盘的值,我们会进行宏定义声明
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
然后对数组mine和display进行初始化
char mine[ROWS][COLS] = {0};//'0'
char display[ROWS][COLS] = { 0 };//'*'
初始化数组之后,那我们应该怎么样才能将字符’0‘和’*‘存储到棋盘中
这个时候,我们就想到我们需要定义一个函数来将mine数组和display数组进行初始化,给每一个行列上代表的坐标存储一个数值,如下图是使用函数进行存储的字符。
要存储两种字符,我们只需要用一个函数就可以实现
因此,我们是怎么只使用一个函数来进行初始化实现呢?
void board_init(char board[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++)
{
board[rows][cols] = set;
}
}
}
因此,我们定义了字符:char set
然后将字符可以传递过去,虽然使用的是同一个函数,但是我们只要遵循谁先调用,那谁来决定初始化mine[][] 或者是display[][]。
具体想初始化什么内容,是’0‘还是’*‘可以由你来决定。
board_init(mine, ROWS, COLS,'0');
board_init(display, ROWS, COLS,'*');
5.3 棋盘的打印
那我们需要写一个打印的函数,不过由于我们这边设置了边界增加了ROW+2,COL+2
因此,我们在这里打印输出就不需要打印边界,只需要将中间的9*9打印出来就可以。
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
函数的实现如下:
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 1; i<=row; i++)
{
int j = 0;
for (j = 1; j<=col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
}
然后调试打印一下输出结果,现在mine数组初始化为0,display数组初始化为*
5.4 但是从这个棋盘我们感觉,似乎这个不直观,我们是不是加上行和列
那我们的函数稍作修改,就能够实现行,列数字的一一对应
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
for (i = 0; i <= row; 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");
}
}
运行一下
5.5 抑制雷的坐标屏蔽
从软件开发的角度来说,我们正常是不会将雷的这个棋盘告诉玩家的,因此,我们可以选择将雷的打印这个屏蔽。
//DisplayBoard(mine, ROW, COL);
DisplayBoard(display, ROW, COL);
5.6 布置雷 ,怎么布置雷呢?
5.6.1 雷的存放
我们需要把布置的雷布置在中间的9*9的格子里面,也就是在mine[][]的数组里面。
5.6.2 如何随机布置雷
我们需要随机布置雷,就需要用到随机数,要求只在9*9的格子里面进行布置雷。由于我们需要的随机数字行列是在1到9之间,因此,我们对生成的随机数进行模row,如rang()%row+1 ,rang 模上row=9之后的数字是0~8,那么加1之后就算1~9
5.6.3 布置雷,需要考虑该坐标是否布置过雷
拿到随机数之后,我们还需要关注一个地方就是,要对未知坐标进行布置雷,那么首先要判断改坐标没有布置过雷。
而且我们已知的是mine[][]的坐标,一开始初始化为0,因此如果查询到mine[x][y]='0',那就可以布置一个雷。
函数实现如下:
void Setmine(char mine[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;
while (count) //布置一个雷之后,conut就会--,直到布置完
{
int x = rand() % row + 1;
int y = rand() % col + 1;
if (mine[x][y] == '0')
{
mine[x][y] = '1';
count--;
}
}
}
运行数一下,布置雷布置成功,只有10个雷
5.7 排查雷
如何排查?
需要设计一个排查雷的函数:包含如下两部分
5.7.1 找出排查坐标周围的8个坐标雷的个数
其中这个函数需要包含雷的个数,那就需要调用mine数组内已经布置好的雷,输入一个坐标将自己认为是雷的坐标进行排查。那就需要调用mine[][]数组。
5.7.2 找出排查坐标周围的8个坐标雷的个数并展示出来
通过5.7.1 选中一个排查的坐标之后,如果排查的是雷,那么就需要展现打印出来,
我们设计一个函数:FindMineCount(char mini)
那么首先你需要知道,你排查的做标中心点四周的8个坐标,然后再去判定这8个坐标里雷的个数有几个,然后将雷的个数返回出来,并记录到排查的坐标内。
5.7.3 那么我们的程序如何实现呢?
以X,Y坐标为中心点往四周查看,那么我们需要查看,
第一列:(x-1,y-1),(x-1,y),(x-1,y+1)
第二列:(x,y-1),(x,y+1)
第三列:(x+1,y-1),(x+1,y),(x+1,y+1)
既然已经把需要排查的坐标穷举出来,那么我们需要对这么坐标进行判断其是否是雷。
5.7.4 如何判断该坐标是雷?
-->因为我们已经知道,当我们在布置雷的时候,我们回顾一下,我们布置雷的坐标放置的字符‘1’,不布置雷的坐标放置‘0’,下图就是我们一开始设计程序的时候进行的一个布置。
因此,我们判断一个做板是否为雷。
首先我们来了解一下:字符‘1’的ASCLL码值是49,字符‘2’的ASCLL码值是50,字符‘3’的ASCLL码值是51。
那么我们来看字符‘0’的ASCLL码值是48
好了,字符‘1’减去字符‘0’就相当于49减去48得到的数值。
‘1’-‘0’=49-48 = 1
‘2’-‘0’=50-48 =2‘3’-‘0’=51-48=3
那么只需要取出改坐标的字符与字符‘0’ 比较大小,将待比较坐标与字符‘0’相减,
例如将做板坐标(2,1)内放置的字符是‘0’减去字符‘0’,那么结果是0,也就说明该坐标非雷。
例如将做板坐标(3,3)内放置的字符是‘1’减去字符‘0’,那么结果是1,也就说明该坐标是雷。
-->通过以上的分析,我们只需要将5.7.2图所示的其他8个坐标分别与字符‘0’进行相减,那么就可以得知排查的做板(x,y)周围的八个坐标有多少个雷了。
在代码实现如下,8个坐标减去8个字符'0'后的数值作为排查雷的返回个数:
//函数作用,用来计算排查的坐标的四周除自己坐标之外的8个坐标内有几个是雷
//函数返回值,返回雷的个数
int GetMineCount(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][y + 1] +
mine[x + 1][y - 1] +
mine[x + 1][y] +
mine[x + 1][y + 1] - 8 * '0');
}
int count = GetmineCount(mine, x, y);
display[x][y] = count + '0';
这个int count是我们用来存放有多少个雷的个数,这个个数是int 类型的,而要将这个int 类型转成字符,那么我们只需要对该数字加上字符‘0’就可以转换成对应的字符了。
同时我们排查这个坐标的周围有几个雷进行打印出来:
DisplayBoard(display, ROW, COL);
那个真个查找雷的函数代码如下:
void Findmine(char mine[ROWS][COLS], char display[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
while(1)
{
printf("请输入要排查的坐标!");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,被炸死,游戏结束!");
DisplayBoard(mine, ROW, COL);//打印雷的位置,让玩家死得其所
break;
}
else
{
int count = GetmineCount(mine, x, y);
display[x][y] = count + '0';//数字加上字符’0‘之后就可以转成字符count对应的数字字符了
DisplayBoard(display, ROW, COL); //排查完之后,再次打印Display的棋盘供玩家参考
//获取雷的个数,在X,Y周围有几个雷,如果X,Y附件有几个雷,那就放1,2,3放到则个排查的坐标里面
//’0‘的asc值是48
//’1‘的ass是49
//’2‘的ass是50
//’1‘-’0‘ =1 字符转成数字
//’2‘-’0‘ =2
//字符1需要放到display数组里面,就算计算出来的雷的个数,放到display数组里面去。
}
}
else
{
printf("非法输入,重新输入!\n");
}
}
//思路,上来之后就排查,使用printf,请输入要排查的坐标,如坐标(2,5)
//输入的值的坐标需要在9*9范围内
//如果输入值的坐标不在范围内,提升输入非法,重新输入
//因为要多次输入,因此这是一个循环,使用while()
//如果坐标合法的情况下,那就要判断这个坐标是不是雷
//如果这个坐标是雷,也就是坐标是’1‘,那就被炸死,打印雷的坐标,游戏结束,
//如果不是雷,则继续游戏
}
接下来,我们进行调试排查雷的个数,并输入一个坐标2 7:
查看坐标2 7 周围有几个雷:
输入坐标2 7 之后,我们可以看到程序排查出了2个雷:
继续排查一下坐标2 4,看他周围有几个雷,排查结果是0个雷。
程序到这里,还没有写完,我们玩游戏,以玩家的角度,我们总是想要赢的对吧!
5.7.5 如何实现全部雷的排查
那么我们在排查雷的时候,如果while(1)里面放置的一直是1,那么就是在死循环的排雷。
因此,我们需要需要把所有的雷排查出来之后,才认为游戏结束。
那么,我们需要统计一下雷的个数,并觉得游戏结束的条件:
那么我们定义一个整数来存放剩余雷的个数,int win =0;
如果win < ROW*COL-EASY_COUNT
在game.h的头文件里面,我们进行了宏定义EASY_COUNT=10;
ROW=9,COL=9
因此win只要小于<9*9-10=71,那么就算排雷成功
#pragma once
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
void board_init(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 display[ROWS][COLS], int row,int col);
int GetMineCount(char mine[ROWS][COLS], int row, int col);
int GetMineCount(char mine[ROWS][COLS], int row, int col);
但是考虑到排查雷的时间会很久,我们这里延时的时候,就一开始将雷布置79个,这个只剩下来个地方待待排查。
#define EASY_COUNT 79
放置79个雷,可以知道,现在只有2个坐标3 8以及8 2需要排查。
代码这部分我们增加win的判断:
void Findmine(char mine[ROWS][COLS], char display[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
while (win<row*col-EASY_COUNT) //EASY_COUNT=10,EASY_COUNT是我们一开始布置雷的个数
{
printf("请输入要排查的坐标!");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (mine[x][y] == '1')
{
printf("很遗憾,被炸死,游戏结束!");
DisplayBoard(mine, ROW, COL);//打印雷的位置,让玩家死得其所
break;
}
else
{
int count = GetmineCount(mine, x, y);
display[x][y] = count + '0';//数字加上字符’0‘之后就可以转成字符count对应的数字字符了
DisplayBoard(display, ROW, COL); //排查完之后,再次打印Display的棋盘供玩家参考
//获取雷的个数,在X,Y周围有几个雷,如果X,Y附件有几个雷,那就放1,2,3放到则个排查的坐标里面
//’0‘的asc值是48
//’1‘的ass是49
//’2‘的ass是50
//’1‘-’0‘ =1 字符转成数字
//’2‘-’0‘ =2
//字符1需要放到display数组里面,就算计算出来的雷的个数,放到display数组里面去。
win++;//排查成功一个雷,win加上一个
}
}
else
{
printf("非法输入,重新输入!\n");
}
if (win == row * col - EASY_COUNT) //当win等于71的时候,也就是所有的雷都排查出来,排雷成功
{
printf("恭喜你,排雷成功!");
DisplayBoard(mine, ROW, COL); //展示那个做板放置了雷
//为了演示需求,我们只放置一个两个雷来进行排查,这样会快一点
//因此,我们可以将EASY_COUNT宏定义声明为79
}
}
输入待排查的坐标3 8 ,8 2
当排查完这两个坐标之后,在游戏的最后,显示排雷成功!!!
6 玩游戏
6.1.玩家规则
玩家肯定是不知道一开始有多少雷,因此,我们需要将棋盘进行隐藏,我们只需要将DisplayBoard(mine, ROW, COL); 这个函数进行隐藏注释掉即可。
void game()
{
char mine[ROWS][COLS];//'0'
char display[ROWS][COLS];//'*'
board_init(mine, ROWS, COLS,'0');
board_init(display, ROWS, COLS,'*');
//谁来调用,就谁来初始化,根据想初始化什么就初始化什么
//打印一下棋盘,只打印中间的9个就可以了,从1到9行
//DisplayBoard(mine, ROW, COL);
DisplayBoard(display, ROW, COL);
//布置雷
Setmine(mine,ROW,COL);
//DisplayBoard(mine, ROW, COL); //棋盘雷的个数展示
//
//排查雷
Findmine(mine, display, ROW, COL);
//从mine数组里面找雷,找到之后放到display数组里面去
}
隐藏之后,我们看到的棋盘是这样子的
6.2 那么开始玩游戏吧!
随机输入一个坐标2 3,很幸运,你竟然没有被炸死。
再输入个3 4 ,很幸运还是没死!!
再输入个4 6
很遗憾被炸死了
为了让你死得不冤,所以这里我们再打印一下你输入排查的坐标到底是不是雷:
至此,整个扫雷游戏的分析结束!
7 讨论区
7.1 大家觉得扫雷游戏应该怎么写!
不知道各位看懂了没有,如果有不懂的地方,可以问我哦!
我一定知无不言,言无不尽!!!
需要源代码的可以私信关注我们之后,联系我拿!
最后,请各位发财的金手指,帮忙点点赞和关注!
💁♂️💁♀️🙋🙋♂️🙋♀️🧏🧏♂️一赞三连🧏♀️🙇🙇♂️🙇♀️🤦🤦♂️🤦♀️🤷🤷♂️🤷♀️
💁♂️💁♀️🙋🙋♂️🙋♀️🧏🧏♂️一赞三连🧏♀️🙇🙇♂️🙇♀️🤦🤦♂️🤦♀️🤷🤷♂️🤷♀️
💁♂️💁♀️🙋🙋♂️🙋♀️🧏🧏♂️一赞三连🧏♀️🙇🙇♂️🙇♀️🤦🤦♂️🤦♀️🤷🤷♂️🤷♀️