如何手搓扫雷(待扩展)
文章目录
- 一、扫雷游戏分析与设计
- 1.1 扫雷游戏的功能说明
- 1.2 游戏的分析和设计
- 1.2.1 数据结构的分析
- 1.2.2 文件结构设计
- 二、扫雷游戏的代码实现
- 三、扫雷游戏的扩展
- 总结
一、扫雷游戏分析与设计
扫雷游戏网页版
1.1 扫雷游戏的功能说明
-
使用控制台(黑框框的程序,即类似的cmd窗口)实现经典扫雷游戏,因为作者目前的能力写一个界面还是有些困难。
-
游戏可以通过菜单实现反复玩(do while实现)或者退出游戏
-
扫雷游戏的棋盘是9×9的棋盘
-
默认随即布置10个雷
-
可以排查雷
-
- 如果位置不是雷,就显示周围有几个雷
- 如果位置不是雷,就显示周围有几个雷
-
- 如果位置是雷,就炸死游戏结束
- 如果位置是雷,就炸死游戏结束
-
- 把除10个雷之外的所有非雷找出来,排雷成功,游戏结束。
1.2 游戏的分析和设计
1.2.1 数据结构的分析
扫雷的过程中,布置的雷和排查出的雷的信息都需要存储,所以我们需要一定的数据结构来存储这些信息。
因为我的设想是在9×9的棋盘上布置雷的信息和排查雷,所以我们首先想的是创建一个9×9的二维数组来存放信息
接下来就是布置雷了,那应该怎么区分是雷还是不是雷呢?
如果这个位置布置雷,我们就存放1,没有布置雷就存放0
假设排查了(2,5)这个坐标的时候,还需要访问周围一圈8个坐标位置,统计周围雷的个数是1
再继续分析,我们在棋盘上布置了雷,棋盘上雷的信息(1)和非雷的信息(0),假设我们排查了某一个位置之后,这个坐标处不是雷,这个坐标周围有一个雷,那我们需要将排查出的雷的数字信息记录存储,并打印出来,作为排雷的重要参考信息。那么这个雷的个数信息存放在哪里呢?如果要存放在布置雷的数组里,这样雷的信息和雷的个数信息就可能产生混肴和打印困难。
有的兄弟就说了,显示周围雷的信息使用数字,而放雷的数组里用#表示雷,用*表示非雷,这样就避免雷和雷的信息的冲突了,但是这样做一个棋盘上就有雷和非雷的信息,还有排查出的雷的个数信息,想象一下,你的棋盘上有(# * 1 2 3 4 5…)这些数据,看起来非常混杂,而且不够方便。
这里还有另一种解决方案,可以分两个数组来干,给一个数组(mine)存放布置好的雷的信息,再给另一个棋盘(show)存放排查出的雷的信息。这样就互不干扰了,把雷布置到mine数组,在mine数组中排查雷,排查出的数组存放在show数组,并且打印show数组的信息给后期排查参考,这样这个问题就解决了。
但是这里还有一个问题,当我们排查(8,6)这个坐标的时候,我们访问周围一圈8个坐标位置,统计周围雷的个数时,最下面的三个坐标就会越界,为了防止越界,我们在设计的时候,给数组扩大一圈,雷还是布置在中间的9×9的坐标上,周围一圈不去布置雷就可以了,这样就解决了越界问题,所以我们存放数据的数组实际上是一个11×11的数组。
同时为了保持神秘,show数组开始时初始化为字符‘*’,为了保持两个数组的类型一致,可以使用同一套函数处理,mine数组最开始也初始化为字符‘0’,布置雷改成‘1’
如下:
show数组就是把里面的蓝色数字改为星号,当排查了一个位置后,该位置的星号就会显示周围雷的个数,注意这里显示的数字是字符,这样show数组就可以是一个字符数组,为了方便,mine也搞成字符数组,这样两个字符数组用一个函数就能实现打印了
所以对应的数组应该是
char mine[11][11]={0};
char show[11][11]={0};
这里我们就理清楚部分思路了。
- 需要2个数组
一个数组存放布置好的雷的信息
一个数组存放排查出的雷的信息 - 需要数组的大小是11×11
1.2.2 文件结构设计
用三个文件分别用来存放程序的主体,函数的声明和定义
- test_05_31_01.c(文件中写游戏的主体部分)
- game.c(文件中写游戏中函数的实现)
- game.h(文件中写游戏需要的数据类型和函数声明)
二、扫雷游戏的代码实现
这里我们用三个文件分别用来存放程序的主体,函数的声明和定义
- test_05_31_01.c(文件中写游戏的主体部分)
- game.c(文件中写游戏中函数的实现)
- game.h(文件中写游戏需要的数据类型和函数声明)
这里先把大致的程序主体部分(main)写出来:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
void menu()
{printf("***************************\n");printf("******** 1. play ********\n");printf("******** 0. exit ********\n");printf("***************************\n");
}int main()
{int input = 0;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;
}
再由前面的分析,以下是各文件中的初步代码
game.h中的代码
//函数声明
//初始化棋盘
void InitBoard(char board[11][11], int r, int c);
game.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void InitBoard(char board[11][11], int r, int c)
{int i = 0;for (i = 0;i < r;i++){int j = 0;for (j = 0;j < c;j++){board[i][j] = '0';}}
}
这里要初始化mine数组和show数组,所以使用board[][]来接收
test_05_31_01.c中的函数部分
#include"game.h"
void game()
{//完善扫雷游戏代码char mine[11][11] = { 0 };//存储布置好的雷的信息char show[11][11] = { 0 };//存储排查出的雷的信息//棋盘初始化//mine数组初始化为全'0'-表示还没有布置雷,所有位置都不是雷//show数组初始化为全'*'-表示所有位置都没有被排查InitBoard(mine, 11, 11);InitBoard(show, 11, 11);
}
问题总结
- 当我想要改变棋盘大小的时候,board[11][11]里的内容都要改
- 虽然是11×11的数组,但实际使用的是9×9的格子。
- 还有一个问题就是初始化如果写成board[i][j]=’0‘的话,对于mine的初始化是没有问题的,但是如果是show也会全初始化为0,这里难道是一个函数初始化两个数组的错吗?
那么有没有什么方法呢?
对应解决方法
- 可以使用#define在geme.h中规定好,用ROW代表行,代表列
- 可以定义两种样式,另一种为#define ROWS ROWS+2,对应的列也是一样的写法,这样就是11×11了,可以根据自己的需要选择对应的变量
- 可以在InitBoard(mine,ROWS,COLS,‘0’)后面指定传个0,show也一样,给自己传个*就可以了,传的参数不同,初始化的内容就不同,这时候就有4个参数了,当然声明里也要有4个参数,因为最后又加了一个字符,我这里加一个char类型的set,game.c里也要四个参数
改进加上打印的函数
game.h
#include<stdio.h>#define ROW 9
#define COL 9#define ROWS ROW+2
#define COLS COL+2
//函数声明
//初始化棋盘void InitBoard(char board[ROWS][COLS], int r, int c,char set);void DisplayBoard(char board[ROWS][COLS], int r, int c);
game.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void InitBoard(char board[ROWS][COLS], int r, int c,char set)
{int i = 0;for (i = 0;i < r;i++){int j = 0;for (j = 0;j < c;j++){board[i][j] = set;}}
}void DisplayBoard(char board[ROWS][COLS], int r, int c)
{int i = 0;printf("-------扫雷-------\n");for (i = 1;i <= r;i++){int j = 0;for (j = 1;j <= c;j++){printf("%c ", board[i][j]);}printf("\n");}
}
test_05_31_01.c
#define _CRT_SECURE_NO_WARNINGS
#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 };//存储排查出的雷的信息//棋盘初始化//mine数组初始化为全'0'-表示还没有布置雷,所有位置都不是雷//show数组初始化为全'*'-表示所有位置都没有被排查InitBoard(mine, ROWS, COLS,'0');InitBoard(show, ROWS, COLS,'*');//打印棋盘DisplayBoard(mine, ROW, COL);//因为这里只打印9×9的棋盘DisplayBoard(show, ROW, COL);//因为这里只打印9×9的棋盘}
上面代码有个细节就是把stdio.h这个头文件放到game.h里了,这样就不用每个文件都包含一次stdio.h这个头文件了,直接包含game.h就可以了。
如果我们还想加上行号列号便于观察
game.c加行号列号
出现这样的情况是因为打印行号的时候,把棋盘向右挤了一位,而列号是从1开始的,所以会空出来一位,只要打印列号的时候从0开始就可以了
棋盘已经基本完成了,接下来还有布置雷和排查雷,这两步依旧是通过函数来实现
在game.h里再加这两个函数声明
//布置雷
void SetMine(char mine[ROWS][COLS], int r, int c);//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int r, int c);
布置雷只需要在mine里布置就可以了,所以只要传mine的参数就可以了,而排查雷需要两个数组,mine查,查到的信息放到show里,传参就需要都传过去。
因为还是在9×9的棋盘里布置和排查,所以test_05_31_01.c里的函数依旧是ROW,COL
//布置雷
SetMine(mine, ROW, COL);//排查雷
FindMine(mine, show, ROW, COL);
game.c中的布置雷细节
void SetMine(char mine[ROWS][COLS], int r, int c)
{//随机布置10个雷int count = EASY_COUNT;while (count){//生成随机的坐标//x的范围是1-9,y的范围是1-9int x = rand() % r + 1;int y = rand() % c + 1;if (mine[x][y] == '0'){mine[x][y] = '1';count--;}}
}
这里因为使用了随机数,所以还要包含time.h和stdlib.h这两个头文件,x和y的计算就是%9的数为0-8,然后再+1,EASY_COUNT在game.c里用define定义了10。
game.c中的排查雷细节
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int r, int c)
{int x = 0;int y = 0;while (1){printf("请输入要排查的坐标:");scanf("%d %d", &x, &y);//坐标的合法性if (x >= 1 && x <= r && y >= 1 && y <= c){//判断是否为真if (mine[x][y] == '1'){printf("很遗憾,你被炸死了\n");DisplayBoard(mine, r, c);break;}else{//统计mine数组中,x,y坐标周围有几个雷int c = GetMineCount(mine, x, y);show[x][y] = c + '0';DisplayBoard(show, ROW, COL);}}else{printf("输入坐标非法,请重新输入\n");}}
在解释这串代码之前先要理解数字与对应的字符之间的关系
%c为以字符形式打印出来的字符1的值,%d是以整型形式打印出来的字符1的值,也就是字符1的ASCII值
数字只要+48就是字符
所以也就有了这种说法
任何数字只要加了’0‘就是它的字符
上代码中GetMineCount(mine, x, y);函数就是统计mine数组中x,y坐标周围雷的个数,show[x][y] = c + ‘0’;就是将统计到的雷的数字个数,转化为字符个数,给到show函数中的对应位置,再最后由DisplayBoard(show, ROW, COL);打印出来
那么在GetMineCount(mine, x, y);函数中,怎么实现统计x,y坐标周围雷的个数呢?先看一个九宫格
统计x,y坐标周围雷的个数实际上就是将该九宫格周围8个位置处的值相加,因为要求的是整型,而这8个位置上的值是字符型,根据前面字符数字减去字符0就是整型数字,就得到了下面一串代码
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{return 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] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0';
}
还有一种写法,这是使用循环的方式,类似的进行遍历了
int GetMineCount(char mine[ROWS][COLS], int x, int y)
{/*return 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] + mine[x + 1][y] + mine[x + 1][y + 1] - 8 * '0';*/int i = 0;int j = 0;int c = 0;for (i = -1;i <= 1;i++){for (j = -1;j <= 1;j++){if (mine[x + i][y + j] == '1')c++;}}return c;
}
稍微分析就能理解,不多作解释
这里我们再实现一下来看一下:
已经达到了我们想要的效果3行3列这里坐标附近的8个位置确实有3个雷
还有GetMineCount(mine, x, y);这里没有必要暴漏出去,所以就没有在头文件里声明,分装成一个函数在game.c里用就行了。
接下来程序就剩最后一个难题了,就是这里的game.c里排查雷的时候,while(1)这里是不断循环的,即不被炸死,就不停止排查,这显然与我们所设想的是不一样的。
我们所需要的功能是在找出所有非雷坐标后,游戏胜利并且结束,这里思路是这样的:
一共有9×9=81个格子,里面有10个雷,在排查的时候,只要排查了71个格子,且都是非雷,那么就游戏胜利了,基于此思路,开始搓代码
game.c中FindMine函数
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int r, int c)
{int x = 0;int y = 0;int win = 0;while (win < c * r - EASY_COUNT){printf("请输入要排查的坐标:");scanf("%d %d", &x, &y);//坐标的合法性if (x >= 1 && x <= r && y >= 1 && y <= c){//判断是否为真if (mine[x][y] == '1'){printf("很遗憾,你被炸死了\n");DisplayBoard(mine, r, c);break;}else{if (show[x][y] == '*'){//统计mine数组中,x,y坐标周围有几个雷int c = GetMineCount(mine, x, y);show[x][y] = c + '0';DisplayBoard(show, ROW, COL);win++;}}}else{printf("输入坐标非法,请重新输入\n");}}if (win == r * c - EASY_COUNT){printf("恭喜你,排雷成功\n");DisplayBoard(mine, r, c);}
}
最后进行这样的优化后,再将test_05_31_01.c文件里的mine注释掉(是写程序的时候参考看的,玩游戏的时候可不能把雷的位置放出来),就是完整的程序了。
附剩下两个文件的代码
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 r, int c,char set);//打印棋盘信息
void DisplayBoard(char board[ROWS][COLS], int r, int c);//布置雷
void SetMine(char mine[ROWS][COLS], int r, int c);//排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int r, int c);
test_05_31_01.c
#define _CRT_SECURE_NO_WARNINGS
#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 };//存储排查出的雷的信息//棋盘初始化//mine数组初始化为全'0'-表示还没有布置雷,所有位置都不是雷//show数组初始化为全'*'-表示所有位置都没有被排查InitBoard(mine, ROWS, COLS,'0');InitBoard(show, ROWS, COLS,'*');//布置雷SetMine(mine, ROW, COL);//打印棋盘//DisplayBoard(mine, ROW, COL);//因为这里只打印9×9的棋盘DisplayBoard(show, ROW, COL);//因为这里只打印9×9的棋盘//排查雷FindMine(mine, show, ROW, COL);
}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;
}
三、扫雷游戏的扩展
上面的代码基本达到了扫雷的程序需求,当然还是有很多可以扩展的地方
- 是否可以选择游戏难度
-
- 简单9×9棋盘,10个雷
-
- 中等16×16棋盘,40个雷
-
- 困难3.×16棋盘。99个雷
- 如果排查位置不是雷,周围也没有雷,可以展开周围的一片(依赖函数递归)
- 是否可以标记雷
- 是否可以加上排雷的时间显示
总结
主播今天真实从早上9点写到晚上9点,不得不说真的收获满满,各位靓仔靓女喜欢主播文章不要忘记一键三连给予支持哦~