文章目录
- 1.扫雷游戏分析和设计
- 1.1 扫雷游戏的功能说明
- 1.2游戏界面:
- 1.3游戏的分析和设计
- 1.2.1 数据结构的分析
- 1.2.2 ⽂件结构设计
- 2.扫雷游戏的代码实现
- 3.代码讲解
1.扫雷游戏分析和设计
1.1 扫雷游戏的功能说明
- 使用控制台实现经典的扫雷游戏
- 游戏可以通过菜单实现继续玩或者退出游戏
- 扫雷的棋盘是9*9的格子
- 默认随机布置10个雷
- 可以排查雷
- 如果位置不是雷,就显示周围有几个雷
- 如果位置是雷,就炸死游戏结束
- 把除10个雷之外的所有非雷都找出来,排雷成功,游戏结束
1.2游戏界面:
初始界面 | 排雷界面 |
排雷失败界面 | 排雷成功界面 |
为了方便演示,我把排雷成功的设置成了2x2的,方便成功。
1.3游戏的分析和设计
1.2.1 数据结构的分析
我们先看一下扫雷的图片:
这一个个框框看着很像数组,事实上我们可以用数组来存放扫雷游戏需要的数据。
-
点击前:全是
*
-
点击后:有雷,我们就存放1,没有雷就存放0。
如果我们没有被雷炸到,那么我们点击的地方会显示周围有几个雷。
上面的表格里面,一开始我们不知道里面有啥,然后点了最中间的那个。
然后就出现了1(因为点击的周围有一个雷)。图2里面的雷和无雷是为了方便理解写出来的。
如果我们在[1][1]
处点击,那么就要统计[0][0],[0][1],[0][2],[1][0],[1][2],[2][0],[2][1],[2][2]
这8个地方一共有几个雷。
可如果在[2][2]
这个地方点击呢?只要统计3个地方了。
难道我们要分多钟情况去考虑吗?
当然不是,我们可以建造一个比实际需要的扫雷数组大一圈的数组。
例如我们只需要中间的3X3的数组用作扫雷游戏,那么我们可以准备一个5X5的数组,这样每个扫雷游戏的区域都可以采用一样的方法来统计周围有几个雷了。
同样的,我们要准备两个结构一样的数组,一个用来存放雷和非雷的数据,另一个用来显示。
可以理解为一个是前端,一个是后端。
-
然后我们让雷是1,非雷是0(后端)
-
未知的地方放
*
,已知的地方显示周围雷的数字(前端)
1.2.2 ⽂件结构设计
因为工程有点大,我们可以用3个文件来完成这个游戏。
test.c //文件中写游戏的测试逻辑
game.c //文件中写游戏中函数的实现等
game.h //文件中写游戏需要的数据类型和函数声明等
2.扫雷游戏的代码实现
game.h
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define EASY_COUNT 10
//初始化棋盘
void InitBoard(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 show[ROWS][COLS], int row, int col);
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//初始化棋盘
void InitBoard(char board[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++) {
board[i][j] = set;
}
}
}
//打印棋盘
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");
}
//布置雷
void SetMine(char board[ROWS][COLS], int row, int col) {
//布置10个雷
//生成随机的坐标,布置雷
int count = EASY_COUNT;
//横坐标:1-9;纵坐标:1-9
while (count) {
int x = rand() % row + 1;
int y = rand() % col + 1;
if (board[x][y] == '0') {
board[x][y] = '1';
count--;
}
}
}
//计算指定位置附近雷的个数
int get_mine_count(char board[ROWS][COLS], int x, int y) {
return (board[x - 1][y] +
board[x - 1][y - 1] +
board[x][y - 1] +
board[x + 1][y - 1] +
board[x + 1][y] +
board[x + 1][y + 1] +
board[x][y + 1] +
board[x - 1][y + 1] - 8 * '0');
//8个坐标的值要么是'0'要么是'1',加起来后减去8个'0'就可以得到他们的和了,也就是有几个雷
}
//排查雷
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) {
if (show[x][y] != '*') {
printf("该坐标被排查过了,不能重复排查\n");
}
else {
//如果是雷
if (mine[x][y] == '1') {
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL);
break;
}
//如果不是雷
else {
win++;
//统计mine数组中x,y坐标周围有几个雷
int count = get_mine_count(mine, x, y);
show[x][y] = count + '0';//转换成 数字字符
DisplayBoard(show, ROW, COL);
}
}
}
else {
printf("输入坐标非法,请重新输入\n");
}
}
if (win == row * col - EASY_COUNT) {
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL);
}
}
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
void menu() {
printf("************************************\n");
printf("******** 1. play 0. exit ********\n");
printf("************************************\n");
}
void game() {
char mine[ROWS][COLS] = { 0 };//存放布置好的雷的信息
char show[ROWS][COLS] = { 0 };//存放排查出的雷的信息
//初始化内容为指定的内容
//mine 数组在没有布置雷的时候,都是‘0’
InitBoard(mine, ROWS, COLS, '0');
//show 数组在没有排查雷的时候,都是‘*’
InitBoard(show, ROWS, COLS, '*');
//设置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//排查雷
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;
}
3.代码讲解
-
我们先找main()函数,在test.c文件里。
-
int input = 0;
这个定义了变量input用来让我们确认自己是玩游戏还是退出游戏。 -
srand((unsigned int)time(NULL));
这个是为了给rand 函数设定随机数种子。关于rand函数的详细解释在我之前发布的2.猜数字游戏
这个文档里有详细解释,这里就不多赘述了。time()函数需要用到
#include <time.h>
这个头文件。
rand函数的使用需要包含#include <stdlib.h>
这个头文件。 -
接下来进入do while循环,先进入menu()函数【这个函数在test.c里面】,打印
************************************ ******** 1. play 0. exit ******** ************************************
-
然后进入【test.c】35行,
printf("请选择:>");
打印:请选择:>
-
进入【test.c】36,输入选择,根据不同选择进入switch语句里的
case 1
,case 0
,default
-
进入
case 0
就打印退出游戏
,然后退出游戏 -
进入
default
就打印选择错误
,然后跳出switch语句,进入while的判断。因为input为0的时候才跳出循环,所以这里会继续进入循环。 -
进入
case 1
就进入game()函数【这个函数在test.c里面】 -
进入【test.c】11行
char mine[ROWS][COLS] = { 0 };
存放布置好的雷的信息(我们表面上看不到的后台的数据) -
进入【test.c】12行
char show[ROWS][COLS] = { 0 };
存放排查出的雷的信息(我们看到的部分,前台的图案) -
然后初始化内容为指定的内容,进入【test.c】15行
InitBoard(mine, ROWS, COLS, '0');
设置mine 数组(后台)在没有布置雷的时候,都是‘0’
-
这个
InitBoard()
函数在【game.c】里面第5行,用来初始化棋盘。void InitBoard(char board[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++) { board[i][j] = set; } } }
这里第4个参数设置成了
char set
,设置的很完美,这样不管是想在里面放'*'
还是'0'
都可以完成,而不需要设置两个结构一样的函数。 -
进入【test.c】17行
InitBoard(show, ROWS, COLS, '*');
设置show 数组(前台)在没有排查雷的时候,都是‘*’
-
进入【test.c】20行
SetMine(mine, ROW, COL);
用来设置雷 -
这个
SetMine()
函数在【game.c】里面第35行,用来布置雷//布置雷 void SetMine(char board[ROWS][COLS], int row, int col) { //布置10个雷 //生成随机的坐标,布置雷 int count = EASY_COUNT; //横坐标:1-9;纵坐标:1-9 while (count) { int x = rand() % row + 1; int y = rand() % col + 1; if (board[x][y] == '0') { board[x][y] = '1'; count--; } } }
因为实参是mine(mine 数组(后台)),所以这里设置的雷是给后台设置的。
-
进入
int count = EASY_COUNT;
,定义了一个变量count,这个count赋值为EASY_COUNT
,EASY_COUNT
在【game.h】里面第12行#define EASY_COUNT 10
相当于一个宏定义,定义
EASY_COUNT
就是10。这么做有个好处,就是如果想要更改雷的数量直接更改宏定义里面的10就可以了,否则的话,我们要去定义变量的地方更改,变量多了就容易分不清。 -
这里是定义了有10个雷,因为count=10,【之前的宏定义里面
EASY_COUNT
就是10】 -
然后进入while循环,进入第10行,
int x = rand() % row + 1;
这个是给x一个1-9的随机数【x是数组横坐标】。为什么
rand() % row + 1
是1-9的随机数?之前在我之前发布的
2.猜数字游戏
这个文档里有详细解释,我取关键的一段结论出来:如果我们想要生成
a~b
的随机数,a + rand()%(b-a+1)
这就得到了,
a~b
的值。这里面1-9就相当于:
1 + rand()%(9-1+1)
化简一下就是:
1 + rand()%(9)
前面【game.h】文件里还有四个宏定义
#define ROW 9//代表扫雷行号9行 #define COL 9//代表扫雷列号9列 #define ROWS ROW+2//代表11行 #define COLS COL+2//代表11列
这里的
ROWS
和COLS
是前面说过的建造一个比实际需要的扫雷数组大一圈的数组,来避免分类讨论带来的复杂情况
,所以加2,大了一圈。让我们回到
int x = rand() % row + 1;
这个row是形参,形参是由实参传递过来的。它对应的实参是ROW,也就是9。所以符合上面化简得结果。
-
和上一条类似,
int y = rand() % col + 1;
这个是给y一个1-9的随机数【y是数组纵坐标】。 -
然后:
if (board[x][y] == '0') { board[x][y] = '1'; count--; }
是一个简单的判断条件:如果我们生成随机数的地方是0(也就是没雷),那么就把0改成1。同时雷的个数就减1。然后重新进入while循环。
如果我们生成随机数的地方是1(也就是有雷),那么就不执行这个if语句,然后重新进入while循环。
-
然后让我们回到【test.c】文件的第23行,
DisplayBoard(show, ROW, COL);
这个会让我们进入【game.c】文件的15行,为了方便看,我把代码拿下来//打印棋盘 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"); }
show 数组(前台),show作为实参,将show数组传递给了board数组。
-
先定义前台数组的行号
i
和列号j
。然后打印-----------------扫雷游戏-----------------
这个行号就是我花红色方框的那行,列号是蓝色方框那列。
-
进入第一个for循环打印行号0-9,然后回车。
-
进入第二个for循环先打印列号1然后进入内嵌的for循环,将列号为1 的那行全打印
*
,然后进入外面那个for循环打印列号2然后进入内嵌的for循环,依此类推。 -
最后打印:
-----------------扫雷游戏-----------------
-
DisplayBoard(show, ROW, COL);
函数结束,进入FindMine(mine, show, ROW, COL);
来排查雷。 -
这个会让我们进入【game.c】文件的65行,为了方便看,我把代码拿下来:
//排查雷 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) { if (show[x][y] != '*') { printf("该坐标被排查过了,不能重复排查\n"); } else { //如果是雷 if (mine[x][y] == '1') { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, ROW, COL); break; } //如果不是雷 else { win++; //统计mine数组中x,y坐标周围有几个雷 int count = get_mine_count(mine, x, y); show[x][y] = count + '0';//转换成 数字字符 DisplayBoard(show, ROW, COL); } } } else { printf("输入坐标非法,请重新输入\n"); } } if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); DisplayBoard(mine, ROW, COL); } }
show 数组(前台),mine 数组(后台)
-
.先定义了数组横坐标x和纵坐标y,初始化为0。然后定义win为找到非雷的个数。
-
进入while循环,如果
win < row * col - EASY_COUNT
那么循环继续,也就是说只要玩家排查的非雷格子数量小于需要排查的总数,游戏的主循环就会继续运行。在这个循环中,玩家输入坐标来排查雷,如果排查到雷,游戏结束;如果排查到非雷格子, win 的值会增加,并且游戏会继续。如果运气非常好,把所有雷都排除了,并且把其他非雷的地方也全选了,那么就会让
win = row * col - EASY_COUNT
从而跳出while循环,执行下面的:if (win == row * col - EASY_COUNT) { printf("恭喜你,排雷成功\n"); DisplayBoard(mine, ROW, COL); }
打印:恭喜你,排雷成功,然后调用
DisplayBoard(mine, ROW, COL);
,将输入结果打印。 -
在没有把雷排完前会先在循环里,先打印:
请输入要排查的坐标:>
,然后输入自己选择的坐标,if (x >= 1 && x <= row && y >= 1 && y <= col)
这个是为了确认自己输入的坐标没有越界。然后进入内层判断,如果输入的坐标处已经不是
*
了,也就是之前这个坐标输入过了,就打印:该坐标被排查过了,不能重复排查
否则就进入else,然后进入内部的if,
//如果是雷 if (mine[x][y] == '1') { printf("很遗憾,你被炸死了\n"); DisplayBoard(mine, ROW, COL); break; }
如果输入的地方在mine数组(也就是后台)里面放的是1(地雷),就会输入:
很遗憾,你被炸死了
如果没被炸死就进入else,
//如果不是雷 else { win++; //统计mine数组中x,y坐标周围有几个雷 int count = get_mine_count(mine, x, y); show[x][y] = count + '0';//转换成 数字字符 DisplayBoard(show, ROW, COL); }
先给win加1,然后定义count来接受来自
get_mine_count(mine, x, y);
这个函数的返回值。这个函数在【game.c】文件的第53行,为了方便看,我把代码拿下来:
//计算指定位置附近雷的个数 int get_mine_count(char board[ROWS][COLS], int x, int y) { return (board[x - 1][y] + board[x - 1][y - 1] + board[x][y - 1] + board[x + 1][y - 1] + board[x + 1][y] + board[x + 1][y + 1] + board[x][y + 1] + board[x - 1][y + 1] - 8 * '0'); //8个坐标的值要么是'0'要么是'1',加起来后减去8个'0'就可以得到他们的和了,也就是有几个雷 }
返回的值是我们输入数组坐标的身旁8个位置的字符减去8个
'0'
。因为
'0'-'0'=0
,'1'-'0'=1
,8个位置依次完成这个操作就相当于8个位置加起来减去8个'0'
,得到的返回值就是输入坐标周围的雷的个数的值。 -
我们把这个值给count,然后把count的值加上
'0'
,就可以得到输入坐标周围的雷的个数的字符。(注意上面是值,这边是字符) -
然后用这个字符来替换数组输入位置原来的符号。
-
最后打印:
DisplayBoard(show, ROW, COL);
,将输入结果打印。 -
直到被炸死或者排雷成功游戏才会结束。