一、扫雷游戏功能
- 使⽤控制台实现经典的扫雷游戏
- 游戏可以通过菜单实现继续玩或者退出游戏
- 扫雷的棋盘是9 * 9的格子:9 * 9 的二维数组存放数据
- 默认随机布置10个雷
- 排查雷
(1) 如果是雷,就炸死了,游戏结束。
(2) 如果不是雷,显示该位置周围有几个雷。
(3) 把所有不是雷的位置全部找出来,剩余的就是雷,游戏就结束了,排雷成功。
二、项目准备
(1)项目创建:在VS2019创建新项目Minesweeper,里面创建3个文件,分别为:
test.c —— 包含main函数,主要用来测试游戏的主逻辑
game.h —— 函数的声明
game.c —— 函数的实现
(2)扫雷游戏要求控制台输出以及菜单控制继续玩或者退出游戏,可以参考之前的 手把手教你实现C语言】猜数字游戏 的游戏菜单来实现。
test.c 文件中:
运行测试一下:
(3)将实现扫雷游戏的过程封装成game()
函数。
test.c 文件中:
三、扫雷游戏分析
1. 游戏实现流程
打开 在线扫雷游戏:https://www.minesweeper.cn/,我们会发现扫雷游戏的界面是一个 9 * 9 的棋盘格。
随机点击一个格子,它会显示数字。以下图为例,以红框圈中的1为圆心,它周围8个格子(黄框中)有1个雷。
扫雷游戏的玩法就是选中雷被炸死,没选中雷,则继续选择,直至排雷成功。
2. 几点思考
(1)利用2个二维数组,存放布置雷的信息和排查出的雷的信息。
-
玩扫雷游戏时,根据游戏界面,我们可以利用9 * 9 的格子随机布置10个雷,1表示雷,0表示非雷。
-
当我们布置好雷时,可以开始排雷。
但是当我们将这个1存放到这个棋盘上,就会产生歧义,让我们不知道1代表的是布置的雷还是排查出它周围雷的个数是1。
- 解决方法:
方法1:
利用2个二维数组存放,1个存放布置雷的信息(用字符 ’ 1 ’ 表示雷,字符 ’ 0 ’ 表示非雷),另1个将排查出雷的个数放在对应的位置上(用字符 ’ * ’ 表示)。
方法2:
有雷的地方用“ # ”表示,无雷的地方用“ * ”表示,这样就不会和排查出的雷的数量搞混了。
这两种创建数组的方法都可行。
- 而后,玩家排查雷的时候,需要根据雷的坐标进行排查,所以我们需要打印出扫雷棋盘的行和列,以便确定雷所在的坐标位置。
- 综上,我们可以创建两个二维数组 ,一个用来存放布置的雷的信息,一个用来打印并储存排查出来的雷的信息。
(即考虑到打印的问题,使用上述问题中的第1种方法更好。)
(2)将9 * 9 棋盘扩大一圈,变成11*11的棋盘,好排查边界的雷。
- 当我们排雷的时候,我们要根据玩家给出的坐标像四周拓展来统计雷的个数。但如果这个坐标在表格的边缘时,我们再向四周拓展的时候就会造成越界。
- 那么我们该如何解决呢?
一般我们可以分情况讨论:
先判断玩家给出的坐标在哪个位置,如果在中间,就向四周拓展计算雷的个数;如果在边缘处,就根据在哪个边缘分情况拓展计算。
但是这种方法听起来就好麻烦……除了上下左右四条边,还有角落处的四个位置都要另外分情况计算。
- 最好的解决方法是给棋盘扩大一圈,变成11*11的棋盘。
- 但是为了让存放雷的信息的位置坐标和布置雷的位置坐标一样,所以我们也要把存放雷信息的格子设计为
11*11
的数组。
(3)上面的2个数组创建为字符数组。
上面的数组要利用字符进行填充,所以创建为字符数组类型。
四、扫雷游戏实现整体流程
扫雷游戏实现流程:
(1)控制台输出和菜单选择继续玩还是退出游戏
(2)初始化2个二维数组,一个布置雷(用字符),一个排查雷(存放雷的位置信息)
(3)这期间,可以打印两个数组看看效果
(4)随机布置10个雷(1随机生成雷的坐标rand1~9,保证位置合法;2不是雷,才布置雷)
(5)排查雷(1看排查的坐标是否越界,x、y在范围内<没越界,看是不是雷;越界,输入坐标不合法>;
2看是否是雷<是雷,就炸死;不是雷,就存show数组>)
(6)看看排查雷是否排查完,谁赢了
五、代码实现
1. 控制台输出和菜单控制在 二(2)中。
- test.c 中:
运行一下,看看效果~
2. 创建 11 * 11 的2个二维数组。
(1) 封装game()
函数,创建2个字符数组(一个布置雷,一个排雷)
- test.c文件中:
(2) 在game.h 文件中利用 #define 定义数组的行和列
若下次使用30*30的数组,可以不用在test.c文件中逐个更改。
- game.h文件中:
因为在game.h中声明函数和定义变量,所以在test.c中记得引用game.h哦~
test.c文件中:
(3)初始化数组
- test.c 文件中:
- game.h 文件中:声明
InitBoard
函数
- game.c 文件中:定义
InitBoard
函数
3. 打印看看棋盘效果
在初始化之后,我们想看一下初识化棋盘的效果,所以这里我们需要写一个打印函数。
- test.c文件中:
- game.h文件中:声明
DisplayBoard
函数
- game.c 文件中:定义
DisplayBoard
函数
下面运行,看看效果吧~(为了美观,打印出了棋盘的行号、列号以及“--------扫雷--------”提示符)
4. 随机布置10个雷
- test.c 文件:调用布置雷的函数
SetMine
-
game.h文件中:声明
SetMine
函数
-
game.c文件: 定义
SetMine
函数,随机布置10个雷
(a. 生成雷的随机坐标,保证雷的位置合法 b.不是雷,才布置雷)
随机生成雷中 :
(a) game.h文件中定义布置雷的个数 EASY_COUNT
(b) srand在test.c 文件中的main函数中
(c)game.h文件包含所需头文件
最后,在test.c 中调用
DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
看看效果。
5. 排查雷
判断mine数组中雷坐标的合法性,如果坐标不合法,就重新输入
如果坐标合法,则要判断玩家输入的坐标处是否有雷,是雷就炸死
不是雷,就统计它周围的8个坐标雷的个数(GetMineCount函数),并存到show数组
- test.c 文件:调用
FindMine()
函数排查雷
mine数组排查雷,排查的信息放到show数组,排查的范围是9*9
- game.h文件中:声明
FindMine()
函数
- game.c文件: 实现
FindMine()
函数
(1) 首先,我们需要玩家输入一个坐标,然后,我们要判断坐标的合法性,如果坐标不合法则重新输入;如果坐标合法,则要判断玩家输入的坐标处是否有雷,如果有雷,则玩家提示玩家被炸死,游戏结束。
game.c 文件中:
运行一下,看看结果。
(2)如果不是雷,就需要统计此坐标位置周围的8个坐标中雷的个数,然后放到show数组中,显示出来(用GetMineCount
函数)。
但我们会发现,统计雷的个数,得到的是一个数,而我们存放在show数组中的元素是一个字符,这里就涉及到了字符和整型数字之间的转换:
在ASCII码表中,每个字符对应一个整型数字,所以如果我们把两个字符相减,就能得到一个整形数字。如:‘9’ - ‘0’ = 9。同样地,把9+‘0’,就能得到字符‘9’。
GetMineCount
函数统计周围雷个数(game.c 文件中的GetMineCount函数):
统计出的雷的个数,放到show数组中(game.c 文件中的FindMine()函数):
运行一下,结果如下。
6. 设计游戏结束条件(game.c 文件中的FindMine函数)
由于排雷的循环条件是while(1),所以游戏会一直排雷,不能结束;除非被炸死,才能结束游戏。
回头看,我们是在9*9的棋盘上,布置了10个雷,所以有71个格子非雷。意味着我们排除71个不是雷的位置,游戏结束。
定义排查次数 win
不要忘记在game.h文件中设置布置雷的个数EASY_COUNT哦~
不是雷,可以提示玩家还需要排查多少个位置。
不是雷,并且排雷次数用完后,打印“排雷成功”。
测试一下,我们不妨把上面的EASY_COUNT设为80,说明只有一个位置不是雷。
执行一下,看看:
六、全部代码
1. test.c
#define _CRT_SECURE_NO_WARNINGS 1
//test.c --- 包含main函数,主要是来测试游戏的主逻辑
#include "game.h"
void menu()//菜单
{
printf("************************\n");
printf("******** 1.play ********\n");
printf("******** 0.exit ********\n");
printf("************************\n");
}
//完成扫雷游戏的测试
void game()
{
char mine[ROWS][COLS] = { 0 };//用来存放布置好的雷的信息 --- '0'
char show[ROWS][COLS] = { 0 };//用来存放排查出的雷的信息 --- '*'
//a.下面的函数都是函数的调用
//1.初始化数组
InitBoard(mine, ROWS, COLS, '0');
InitBoard(show, ROWS, COLS, '*');
//2.打印棋盘(只是看一下棋盘打印是否正确,还轮不到打印)
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
//3.布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//DisplayBoard(show, ROW, COL);
//4.排查雷
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();
//printf("开始扫雷游戏\n");
break;
case 0:
printf("退出游戏\n");
break;
default:
printf("选择错误,请重新选择!\n");
break;
}
} while (input);
return 0;
}
2. game.h
#pragma once
//game.h --- 函数的声明
//game.h和game.c负责游戏逻辑的实现,然后在test.c中调用即可
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
//#define定义符号
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//b.下面的函数都是函数的声明
//1.初始化棋盘
void InitBoard(char board[ROWS][COLS],int row,int col,char set);
//2.打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
//3.布置雷
void SetMine(char board[ROWS][COLS], int row, int col);//传过来的行、列参数依然为11*11,只不过只用9*9
//4.排查雷
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
3. game.c
#define _CRT_SECURE_NO_WARNINGS 1
//game.c -- 函数的定义
//game.h和game.c负责游戏逻辑的实现,然后在test.c中调用即可
#include "game.h"
//c.下面的函数都是函数的定义
//1.初始化棋盘:mine数组全为'0',show数组全为'*'
void InitBoard(char board[ROWS][COLS], int row, int col,char set)
{
int i = 0;
int j = 0;
for (i = 0; i < row; i++)
{
for (j = 0; j < col; j++)
{
board[i][j] = set;
}
}
}
//2.打印棋盘:将初始化好的'0'布置雷的棋盘和'*'排查雷的棋盘,打印出来
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
printf("------ 扫雷 ------\n");
for (j = 0; j <= col; j++)//显示棋盘的列号
{
printf("%d ", j);
}
printf("\n");
//打印棋盘的元素
for (i = 1; i <= row; i++)
{
printf("%d ", i);//显示棋盘的行号
for (j = 1; j <= col; j++)
{
printf("%c ", board[i][j]);
}
printf("\n");
}
printf("------ 扫雷 ------\n");
}
//3.布置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
int count = EASY_COUNT;//布置10个雷
while (count)
{
//(1)生成雷的随机下标,保证雷位置的合法性
//随机雷的行x:1~9
//随机雷的列y:1~9
int x = rand() % row + 1;
int y = rand() % col + 1;
//(2)布置雷:不是雷,才布置雷
if (board[x][y] != '1')
{
board[x][y] = '1';
count--;
}
}
}
int GetMineCount(char mine[ROWS][COLS], int x,int y)//统计排查的雷,周围8个坐标中雷的个数
{
return 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] + mine[x - 1][y + 1] - 8 * '0';
}
//4.排查雷
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)
{
printf("请输入要排查的坐标: ");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断mine数组中雷坐标是否合法
{
if (mine[x][y] == '1')//是雷
{
printf("很遗憾,此处是雷,被炸死了\n");
DisplayBoard(mine, ROW, COL);//打印布置好的雷的信息
break;
}
else//不是雷
{
win++;
printf("还需要排查%d个位置\n", row * col - EASY_COUNT - win);
//就统计x,y周围的8个坐标中雷的个数
int c = GetMineCount(mine,x,y); //调用GetMineCount函数统计周围雷的数量
show[x][y] = c + '0'; //统计出来的数据转化为字符,存到show数组中
DisplayBoard(show, ROW, COL);
}
}
else
{
printf("输入的坐标有误,请重新输入\n");
}
}
if (win == row * col - EASY_COUNT)
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}