文章目录
- 一、扫雷游戏分析与设计
- 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-9
int 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点,不得不说真的收获满满,各位靓仔靓女喜欢主播文章不要忘记一键三连给予支持哦~