目录
一、扫雷游戏的分析和设计
二、扫雷游戏的文字描述
三、开始前的准备---多文件的创建
四、开始实操
步骤一:在test.c文件中进行操作一。
game.h文件
game.c文件
test.c文件
步骤二、在game.h 、game.c 、test.c中实现操作二。
game.h文件
game.c文件
test.c文件
步骤三、在game.h 、game.c 、test.c中实现操作三。
game.h文件
game.c文件
test.c文件
注释一:
五、扫雷游戏的扩展
1、可以新增的功能
2、实现扫除周围一片雷功能
开始正式的编程之前咱们先来熟悉一下扫雷游戏的游戏规则。
首先通过点击游戏内的小方框来实现雷的排查,
如果点击非雷位置那么在显示这个位置周围雷的个数的同时也会随机的扫出一片非雷位置(该功能需要利用递归思想),当所有非雷位置全部被找出时,排雷成功游戏结束,同时显示所有雷的位置。 (专门去给大伙做了个成功案例~)
如果点击到有雷位置,那么游戏结束,同时也显示所有雷的位置。
同时我们还可以选择游戏的难易程度:
简单 ---------- 9*9棋盘,10个雷
中等 ---------- 16*16棋盘,40个雷
困难 ---------- 30*16棋盘,99个雷
这次我们就先实现一下简单模式下的扫雷游戏(剩下的两种留着以后再更新),我们最后实现目标是这样的:
操作一:在执行代码后出现选择游戏菜单进行开始游戏和结束游戏的选项,选1游戏开始,选0游戏结束。
操作二:在游戏开始后显示游戏棋盘(这里就以最简单的9*9棋盘为例,以后可能会更新16*16和30*16的棋盘)。
操作三:依次输入要排查的坐标,如果是非雷位置就显示这个位置周围雷的个数;当全部非雷位置被排查完时游戏结束,同时显示所有雷的位置,并开始新的游戏菜单选项;当排查到雷时提示被炸死游戏结束,同时显示所有雷的位置,并开始新的游戏菜单选项。
在开始实现游戏的具体代码之前,为了更具有条理性的完成我们的任务,这次的代码将会被放在同一目录下的三个文件中进行编写,分别为:
geme.h ---------- 头文件
#pragma once
//头文件中除了要有要用到的头文件之外,还有要用到的数据的定义以及函数的声明等。
geme.c ---------- 函数实现文件
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h" //为了实现三个文件之间的互联需要令game.c和test.c都包含game.h头文件
//函数实现文件是对逻辑文件中的game()函数中内嵌的所有函数的具体实现。
test.c ---------- 整体逻辑文件
#define _CRT_SECURE_NO_WARNINGS 1 //这个只是我个人需要,当然你添上也么错
#include "game.h" //为了实现三个文件之间的互联需要令game.c和test.c都包含game.h头文件
//程序运行的逻辑
tips:在执行代码后出现选择游戏菜单进行开始游戏和结束游戏的选项,选1游戏开始,选0游戏结束。
//扫雷游戏菜单
void menu()
{
printf("**********************\n");
printf("***** 1、开始游戏*****\n");
printf("***** 0、结束游戏*****\n");
printf("**********************\n");
}
//开始游戏,game函数并不进行具体操作只是说明需要的函数以及它们的执行顺序,这些函数的具体实现要在 //geme.c中进行
void game()
{
}
//扫雷游戏主体逻辑
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
接着,为了能有一个9*9的棋盘供我们游玩,所有我们初步认为可能需要利用一个二维数组来存放这个棋盘并打印。同时我们假设,雷在数组中的元素为1,非雷为0。大概就是这样:
但是因为我们在操作三中还要在排出的非雷位置显示它周围雷的个数(排查个数的范围就是该非雷位置一周的八个坐标内)这个个数我们要先进行存储然后再打印出来(可能这点你看不懂但是不影响,你接着往下看就行),那么存储的时候这个数肯定是整型。
这时就会与我们之前规定的雷在数组中表示为1相冲突,所以我们在布置雷和非雷时采用字符'1'和字符'0'进行标记,这样就不会与存储的数字相冲突。
但是这时又会出现一个问题,又要显示周围雷的个数,又要进行雷和非雷的布置,甚至还要在最后显示所有雷的位置,那么这样的话一个数组用起来是不是感觉会很复杂?
所以,在这里我们想到了采用多个数组的办法,那么几个就足够了呢?
答案:两个足矣。
mine数组 ---------- 用来存放布置好的雷的信息。(只有在最后显示答案的时候我们才会看到mine数组)
show数组 ---------- 用来存放排查出雷的信息。(我们在执行后排查过程中看到的都是show数组,mine数组是在我们身后进行工作的)
mine和show进行信息传递时会发生这样的对话:
mine说:喂~,我这边已经排查过(1,2)这个位置了它是非雷位置
show说:好的~,我知道了。我会将这个位置周围的雷个数显示出来的。
然后我们通过代码来定义这两个字符型数组:
char mine[ROWS][COLS]; //定义mine数组,用来存放布置好的雷的信息
char show[ROWS][COLS]; //定义show数组,用来存放排查出的雷的个数
在这里,数组中用来表示行和列数量的ROWS和COLS可能又会有人不懂了,是都要设置成这样吗?直接写ROW和COL不行吗?
不行滴~,因为ROW和COL我们也要用。这又是为什么呢?
因为,假设我们排查(2,5)这个坐标时,我们排查的是周围⼀圈8个⻩⾊位置,统计周围雷的个数是1;假设我们排查(8,6)这个坐标时,我们访问周围的⼀圈8个⻩⾊位置,统计周围雷的个数时,最下⾯的3个黄色位置就会越界,为了防⽌越界,我们在设计的时候,将数组扩⼤⼀圈,雷还是布置在中间的9*9的坐标上,周围⼀圈不去布置雷就⾏,这样就解决了越界的问题。所以我们将存放数据的数组创建成11*11 是⽐较合适。
所以以ROWS和COLS表示行和列长度的数组表示的是一个11*11的数组它就相当于一个花园的整体,外层的围墙和里面的风景。而ROW和COL用来表示我们将来要看到的9*9数组,毕竟花园外的墙没人看,我们看的都是花园里面的风景。
为了代码的可重复性,我们可以在game.h头文件中对ROW、COL、ROWS、COLS进行数值的定义,以便于我们想要改变棋盘大小时直接在geme.h中更改定义时的数值大小即可,而不是还要在test.c中一个一个的去更改这四个的值。
#pragma once
//要包含的头文件 (这里我们就不多解释了知道实现本文中扫雷游戏需要包含这哥仨就行)
#include
#include
#include
//数值定义
#define ROW 9 //设定行的值为9
#define COL 9 //设定列的值为9
#define ROWS ROW+2 //设定ROWS = ROW + 2
#define COLS COL+2 //设定COLS = COL + 2
//!!!注意 COLS 和 COL+2之间没有等号,注释只是为了方便理解
//!!!注意 COLS 和 COL+2之间没有等号,注释只是为了方便理解
//!!!注意 COLS 和 COL+2之间没有等号,注释只是为了方便理解
//重要的事情说三遍~
最后我们还要对这两个数组进行初始化,那么我们就要声明一个函数:
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
切记,函数只有在声明后才能使用。
在我们声明过后就可以在test.c和game.c文件中围绕初始化棋盘函数进行相关操作。
步骤一完成后三个文件的内容会变成如下模样:
#pragma once
//要包含的头文件
#include
#include
#include
//数据定义
#define EASY_COUNT 10 //设定要布置雷的个数
#define ROW 9 //设定行的值
#define COL 9 //设定列的值
#define ROWS ROW+2 //ROWS = ROW + 2
#define COLS COL+2 //COLS = COL + 2
//函数声明
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//对geme()中提到的所有函数的具体实现
//定义棋盘函数(board是作为形参的一个新数组名但是里面的行名和列名不变1)
void InitBroad(char board[ROWS][COLS], int rows, int cols,char set) //(mine/show数组名, ROWS, COLS, 字符'0'/'*')
{
int i = 0;
for (int i = 0; i < rows;i++) //确定打印的行数
{
for (int j = 0; j < cols; j++) //确定打印的列数
{
board[i][j] = set; //根据要打印的行数和列数存储棋盘所要的字符
}
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//程序运行的逻辑
//扫雷游戏菜单
void menu()
{
printf("**********************\n");
printf("***** 1、开始游戏*****\n");
printf("***** 0、结束游戏*****\n");
printf("**********************\n");
}
//开始游戏,game并不进行具体操作只是说明需要的函数
void game()
{
//确定所要游玩的扫雷游戏难度
char mine[ROWS][COLS]; //定义mine数组,用来存放布置好的雷的信息
char show[ROWS][COLS]; //定义show数组,用来存放排查出的雷的个数
//游玩时在两个数组之间互相确认?
//将mine和show数组加入InitBroad函数中
//先进行
InitBroad(mine, ROWS, COLS,'0');//对mine数组进行初始化为全字符0 (这里的0就是一个字符)
//后进行
InitBroad(show, ROWS, COLS,'*');//对show数组进行初始化为全字符*
}
//扫雷游戏主体逻辑
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
tips:在游戏开始后显示游戏棋盘。
首先我们在game.h中声明一个用于打印棋盘的函数:
DisplayBoard(show, ROW, COL);
因为是打印棋盘,所以具体实现时采用的是show数组,以及ROW和COL,因为这俩才是我们在一个花园中想看到的风景。
//打印棋盘函数
void DisplayBoard(char board[ROWS][COLS], int row, int col) //(show , ROW ,COL)
{
int i = 0;
printf("--------扫雷游戏--------\n");
for (i = 0; i <= col; i++) //根据上面要求的列数打印行标0 1 2 3 4 5 6 ...
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++) //根据上面要求的行数打印列标1 2 3 4 5 6 ...
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)//打印行标的同时在该行同时打印设定的字符
{
printf("%c ",board[i][j]); //%c打印字符,board[i][j]数组已经在InitBoard函数中储存过了,现在需要的是将存储的东西在命令提示符中打印出来
}
printf("\n");
}
}
步骤二完成后三个文件的内容会变成如下模样:
#pragma once
//要包含的头文件
#include
#include
#include
//数据定义
#define EASY_COUNT 10 //设定要布置雷的个数
#define ROW 9 //设定行的值
#define COL 9 //设定列的值
#define ROWS ROW+2 //ROWS = ROW + 2
#define COLS COL+2 //COLS = COL + 2
//函数声明
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col);
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//对geme()中提到的所有函数的具体实现
//定义棋盘函数(board是作为形参的一个新数组名但是里面的行名和列名不变1)
void InitBroad(char board[ROWS][COLS], int rows, int cols,char set)//(mine/show数组名, ROWS, COLS, 字符'0'/'*')
{
int i = 0;
for (int i = 0; i < rows;i++) //确定打印的行数
{
for (int j = 0; j < cols; j++) //确定打印的列数
{
board[i][j] = set; //根据要打印的行数和列数存储棋盘所要的字符
}
}
}
//打印棋盘函数
void DisplayBoard(char board[ROWS][COLS], int row, int col) //(show , ROW ,COL)
{
int i = 0;
printf("--------扫雷游戏--------\n");
for (i = 0; i <= col; i++) //根据上面要求的列数打印行标0 1 2 3 4 5 6 ...
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++) //根据上面要求的行数打印列标1 2 3 4 5 6 ...
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)//打印行标的同时在该行同时打印设定的字符
{
printf("%c ",board[i][j]); //%c打印字符,board[i][j]数组已经在InitBoard函数中储存过了,现在许需要的是将存储的东西在命令提示符中打印出来
}
printf("\n");
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//程序运行的逻辑
//扫雷游戏菜单
void menu()
{
printf("**********************\n");
printf("***** 1、开始游戏*****\n");
printf("***** 0、结束游戏*****\n");
printf("**********************\n");
}
//开始游戏,game并不进行具体操作只是说明需要的函数
void game()
{
//确定所要游玩的扫雷游戏难度
char mine[ROWS][COLS]; //定义mine数组,用来存放布置好的雷的信息
char show[ROWS][COLS]; //定义show数组,用来存放排查出的雷的个数
//游玩时在两个数组之间互相确认?
//将mine和show数组加入InitBroad函数中
//先进行
InitBroad(mine, ROWS, COLS,'0');//对mine数组进行初始化为全字符0 (这里的0就是一个字符)
//后进行
InitBroad(show, ROWS, COLS,'*');//对show数组进行初始化为全字符*
//棋盘打印函数
DisplayBoard(show, ROW, COL);
}
//扫雷游戏主体逻辑
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
tips:依次输入要排查的坐标,如果是非雷位置就显示这个位置周围雷的个数;当全部非雷位置被排查完时游戏结束,同时显示所有雷的位置,并开始新的游戏菜单选项;当排查到雷时提示被炸死游戏结束,同时显示所有雷的位置,并开始新的游戏菜单选项。
最后我们整个扫雷游戏的三个文件内容应该是这样的:
#pragma once
//要包含的头文件
#include
#include
#include
//数据定义
#define EASY_COUNT 10 //设定要布置雷的个数
#define ROW 9 //设定行的值
#define COL 9 //设定列的值
#define ROWS ROW+2 //ROWS = ROW + 2
#define COLS COL+2 //COLS = COL + 2
//函数声明
//初始化棋盘
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);
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//对geme()中提到的所有函数的具体实现
//定义棋盘函数(board是作为形参的一个新数组名但是里面的行名和列名不变1)
void InitBoard(char board[ROWS][COLS], int rows, int cols,char set)//(mine/show数组名, ROWS, COLS, 字符'0'/'*')
{
int i = 0;
for (int i = 0; i < rows;i++) //确定打印的行数
{
for (int j = 0; j < cols; j++) //确定打印的列数
{
board[i][j] = set; //根据要打印的行数和列数存储棋盘所要的字符
}
}
}
//打印棋盘函数
void DisplayBoard(char board[ROWS][COLS], int row, int col) //(show , ROW ,COL)
{
int i = 0;
printf("--------扫雷游戏--------\n");
for (i = 0; i <= col; i++) //根据上面要求的列数打印行标0 1 2 3 4 5 6 ...
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++) //根据上面要求的行数打印列标1 2 3 4 5 6 ...
{
printf("%d ", i);
int j = 0;
for (j = 1; j <= col; j++)//打印行标的同时在该行同时打印设定的字符
{
printf("%c ",board[i][j]); //%c打印字符,board[i][j]数组已经在InitBoard函数中储存过了,现在许需要的是将存储的东西在命令提示符中打印出来
}
printf("\n");
}
}
//布置雷函数
void SetMine(char board[ROWS][COLS], int row, int col) //(mine, ROW , COL)
{
//布置10个雷
//⽣成随机的坐标,布置雷
int count = EASY_COUNT; //在头文件中设定要布置的雷的个数
while (count)
{
int x = rand() % row + 1; //根据行数和rand伪随机函数生成雷对应的行坐标x
int y = rand() % col + 1; //根据列数和rand伪随机函数生成雷对应的列坐标y
if (board[x][y] == '0') //因为开始引入的数组是全部为字符'0'的mine数组所以board[x][y]中的值肯定全为'0'
{
board[x][y] = '1'; //开始布置雷,这里规定雷的是字符'1';
count--; //每布置一个雷就减少一个要布置的雷的个数count
}
}
}
//返回排查位置周围八个位置的存储字符函数(本函数由于没有在game.h中添加,只是想让他在下面使用)
int GetMineCount(char mine[ROWS][COLS], int x, int y) //(mine ,x ,y)get因为这个函数是 //在find函数中嵌套的所以传入的是x ,y而非之前的那些col、row之类的
{
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');
//排查的位置为x,y
//'1'-'0' = 49 - 48 = 1
//x-1,y-1 x-1,y x-1,y+1
//x,y-1 x,y x,y+1
//x+1,y-1 x+1,y x+1,y+1
}
//排查函数
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col) //(mine,show,ROW,COL)
{
int x = 0;
int y = 0;
int win = 0;
while (win < row * col - EASY_COUNT) //一共有10个雷,如果win值大于等于9*9-10=71时,证明此时雷已经被排完,进行下面的if
{
printf("请输入要排查的坐标:>");
scanf_s("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col) //只有在x,y的值满足要求时才会进行排查
{
if (mine[x][y] == '1') //如果该位置是雷,那么被炸死游戏结束
{
printf("很遗憾,你被炸死了\n");
DisplayBoard(mine, ROW, COL); //被炸死后利用棋盘打印函数显示所有雷的位置
break;
}
else
{
//该位置不是雷,就利用getmine函数统计这个坐标周围有⼏个雷
int count = GetMineCount(mine, x, y); //用整型count来接收get统计的周围雷的个数
show[x][y] = count + '0'; //如有疑问,请跳转至注释一
DisplayBoard(show, ROW, COL); //将show[x][y]得到的字符型传递给打印棋盘函数从而显示排查位置周围雷的个数。
win++; //每排除一个雷我们的胜算就加一分,即win++
}
}
else //x,y的值不满足要求,请重新输入
{
printf("坐标非法,重新输⼊\n");
}
}
if (win == row * col - EASY_COUNT) //当win=71时,游戏胜利排除所有雷
{
printf("恭喜你,排雷成功\n");
DisplayBoard(mine, ROW, COL); //展示答案:最后打印所有雷
}
}
#define _CRT_SECURE_NO_WARNINGS 1
#include "game.h"
//程序运行的逻辑
//扫雷游戏菜单
void menu()
{
printf("**********************\n");
printf("***** 1、开始游戏*****\n");
printf("***** 0、结束游戏*****\n");
printf("**********************\n");
}
void game()
{
char mine[ROWS][COLS];//存放布置好的雷
char show[ROWS][COLS];//存放排查出的雷的信息
//初始化棋盘
//1. mine数组最开始是全'0'
//2. show数组最开始是全'*'
InitBoard(mine, ROWS, COLS, '0'); //!!!!注意是board不是broad注意与game.c文件中的函数名的一致。
InitBoard(show, ROWS, COLS, '*');
//打印棋盘
//DisplayBoard(mine, ROW, COL);
DisplayBoard(show, ROW, COL);
//1. 布置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//2. 排查雷
FindMine(mine, show, ROW, COL);
}
//扫雷游戏主体逻辑
int main()
{
int input = 0;
do
{
menu();
printf("请选择:>");
scanf_s("%d", &input);
switch (input)
{
case 1:
game();
break;
case 0:
printf("退出游戏\n");
default:
printf("选择错误,重新选择\n");
break;
}
} while (input);
return 0;
}
show[x][y] = count + '0';
因为我们在mine和show数组中初始化时采用的全都是字符'0' / '1' / '*'之类的,但是get返回的是一个整型,我们想让这个整型转化成这个整型对应的字符型(比如返回值为数字5,那么我们想要得到的就是字符'5')
根据ASCII码我们可以知道:
'1' - '0' = 49 - 48 = 1
'3' - '0' = 51 - 48 = 3
所以,一个数字加上字符'0'就可以得到它的字符型。即:
1 + '0' = 1 + 48 = 49 = '1'
3 + '0' = 3 + 48 = 51 = ‘3’
故采用 count+‘0’ 的方式让show数组(x,y)位置对应的字符为一个字符型。
我们在扫雷时会发现,当你点击一个非雷位置且该雷周围都没有雷时会炸开一大片没有雷的位置,直到该位置周围有雷时才会停止,同时显示该位置周围雷的个数时。实现这个功能我们需要运用递归的思想(关于递归的讲解请到另一篇文章《递归详解--青蛙跳楼梯和汉诺塔问题案例》中进行查看)
具体实现代码如下:
//定义递归展开排雷函数
void ChangeBroad(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col, int x, int y)
{
int count = GetBroad(mine, x, y); //获取x,y周围雷的个数
if (count == 0) //如果周围没有雷,展开周围没有雷的位置
{
show[x][y] = ' '; //如果x,y周围没有雷,就该位置设置为空
for (int i = x - 1; i <= x + 1; i++) //对x,y周围八个位置进行循环递归
{
for (int j = y - 1; j <= y + 1; j++)
{
if (show[i][j] == '*') //'*'代表该位置已经只有排查过,我们只排查不是'*'的坐
//标,然后才会进行递归
ChangeBroad(mine, show, row, col, i, j); //这里一定要写成i和j,因为我们是要在x,y周围进行扩散的,而这时候的i和j就相当于下次递归开始时的x和y,如果写成x和y会早场递归死循环
}
}
}
else //如果x,y周围有雷,就打印周围雷的个数
{
show[x][y] = count + '0';
}
}
最后我们还要将排查雷函数FindBroad进行修改:(当然头文件也需要但是就不多展示了)
void FindBroad(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = row * col - landmines_count;
while (win > 0)
{
printf("请输入要排查的坐标:>");
scanf_s("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("已排查,请重新输入\n");
}
else // 我们将原本的显示周围雷个数的GetBroad函数,放在了递归函数中
{
ChangeBroad(mine, show, row, col, x, y); //进行递归一次炸一大片雷
PrintfBroad(show, row, col); //递归完成后显示一次递归的结果
win--;
}
}
else
{
printf("输入非法,请重新输入\n");
}
}
if (win == 0)
{
printf("恭喜你答对了");
PrintfBroad(mine, ROW, COL);
}
}