我们如果想要实现扫雷游戏,首先要打印出来一个棋盘。在此棋盘中,玩家看不到雷(mine)的分布,玩家可以通过输入坐标的方式来进行排雷。对于我们程序员来说,也要同时布置一个棋盘,与玩家不同的是我们可以清晰的看到雷区的分布,以此来判断玩家是否踩雷。于是我们就有了目标,想要初始化两个棋盘。先创建两个二位数组,然后初始化的时候我们可以将雷视为字符’1’,将非雷视为字符’0’。当然了想要随机设置雷就要编写代码来具体实现,文章后面会有详细讲解。其打印结果如下:
接下来就是玩家开始找雷,当玩家输入二维数组的坐标时,我们来判断此位置是否为雷’1’,如果非雷’0’则玩家继续排查,若踩到雷则玩家失败。想必看到这篇文章的同学都是玩过扫雷的,但是到现在为止,我们还是没有真正的感受到扫雷的乐趣,我们的代码到底差在哪里呢?代码还可以如何优化呢?欲知后事如何,请看下文分析。
在讲解之前首先要明确一个模块化编写代码的思路,就是通过源文件、头文件相结合的方式来实现的。
这里有test.c和game.c文件,以及game.h头文件
1.为了整体代码的功能和美观我们可以先编写一个菜单提示栏(menu)
void menu()
{
printf("**************************\n");
printf("******* 1.play ********\n");
printf("******* 2.exit ********\n");
printf("**************************\n");
}
2.编写扫雷游戏前要写出菜单栏信息,让玩家可以实现基本的游戏/退出游戏的操作,此操作可以放在test函数中。如果玩家选择游戏则调用game函数开始游戏,反之则退出游戏。
void test()
{
int temp = 0;
srand((unsigned)time(NULL)); //后文会讲到,定位到 三、随机布置周围雷的个数
do
{
menu();
printf("请选择:>");
scanf("%d",&temp);
switch (temp)
{
case 1:
game();
break;
case 2:
printf("游戏退出\n");
break;
default:
printf("选择错误!请重新输入!\n");
}
}while(temp);
}
game中实现扫雷的基本功能,其中包括两个棋盘的初始化(InitBoard)、打印(DisplayBoard)、随机设置雷(SetMine)、玩家找雷(FindMine)函数。这是扫雷游戏的基本流程。
❓ 什么是初始化? 答:向二位数组中添加内容,这里是加字符’0’或’1’。
❓ 什么是打印棋盘? 答:用printf()来输出二位数组里的内容。
void game()
{
char mine[ROWS][COLS] = { 0 };//布置好的雷的信息
char show[ROWS][COLS] = { 0 };//排查出雷的信息
printf("游戏开始\n");
//初始化两个棋盘
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'*');
//打印棋盘
DisplayBoard(show, ROW, COL);
//随机设置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//玩家开始查找雷
FindMine(mine, show, ROW, COL);
}
这里的ROWS和COLS是在game.h头文件中设定的值12,ROW和COL同样也是在game.h头文件中设定好的值9;
❓ 这里有人会问到为什么我们打印 9 *9 棋盘但是却要初始化 11 *11 的棋盘呢???不要慌,带着疑问看下去可能你的理解会更加深刻(答案在 :四、计算该位置周围雷的个数)
设置x和y来接收随机函数(rand)的数值。如果想要接收 1~9 之间任意一个数,需要先把rand函数取余(%)9这样会得到 0~9之间的数,再加上1就会得出 1~9之间的数。
❓什么是srand()??,怎么用呢???
答: 为了得到随机数,在此之前我们需要先初始化随机种子srand().不过为了防止随机数每次重复,常常使用系统时间来初始化 即: srand((unsigned int) time(NULL));如果想在一个程序中生成随机数序列,需要至多在生成随机数之前设置一次随机种子。即:只需在主程序开始处调用srand((unsigned)time(NULL)); 后面直接用rand就可以了。不要在for等循环放置srand((unsigned)time(NULL));
void SetMine(char board[ROWS][COLS], int row, int col)
{
int n = count;
while (n)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y]=='0')
{
board[x][y] = '1';
n--;
}
}
}
以图中所打印的二位数组为例,若我们想计算该位置周围雷的个数,就需要对周围8个元素逐个判断。
到这里我们不难想到可以把8个字符加起来,再转换成数字就可以判断该位置周围雷的个数。代码如下:
int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
int tmp = 0;
for (int i=-1;i<=1;i++)
{
for (int j=-1;j<=1;j++)
{
tmp+= mine[x + i][y + j]-'0';
}
}
return tmp;
}
❓ 那么问题来了,我要是判断如下位置的元素应该怎么办呢?
还记得上文我创建的 11 *11 的二维数组吗,这里只是显示的是 9 *9,实际上也是11 *11。由于判断如上元素周围雷的个数不好判断,所以我们才创建了11 *11的二维数组,这时候我们就可以其轻而易举的算出周围雷的个数。
⚙️ 根据我们玩扫雷的多年经验可知,我们点下任意位置,若该位置周围没有雷则它就会向周围展开一圈,若果该位置的周围有雷它则会停止并显示周围雷的个数。我们可以这样理解,电脑会一直重复这个展开操作,直到不能展开(周围位置有雷)为止。如下图所示展开的位置都没有雷,但是在边界的周围有雷。
再来对代码进行想象,这段代码会是一段循环,循环体内会有判断。判断条件就应该是:该位置周围如果没有雷就直接展开,反之则显示该位置周围雷的个数。这里我们需要借助get_mine_count函数的思想,对该位置周围8个进行判断,但与之不同的是,这里需要不断循环的往下递归,不断的对新位置周围8个元素进行判断吗,所以这里还需要调用到get_mine_count函数。
具体代码的编写思想出来了,剩下的就是添加限制条件:1.如果该位置被判断过则停止递归 2.若果该位置超过9 *9二位数组的边界则递归停止。由此我们就可以写代码喽。
//递归展开
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS],int x,int y)
{
if (x == 0 || y == 0 || x == ROWS - 1 || y == COLS - 1)
{
return ;
}
//判断是否已经被排除
if (show[x][y] != '*')
{
return ;
}
int n = get_mine_count(mine, x, y); //周围有几个炸弹
if (n>0) //周围如果没有炸弹,则展开
{
show[x][y] = n+'0';
}
else if (n==0)
{
show[x][y] = ' ';
for (int i=-1;i<=1;i++)
{
for (int j=-1;j<=1;j++)
{
Unfold(mine, show, x+i, y+j);
}
}
}
}
这里我们用到mine和show数组,这里的代码编写起来比较简单,大多数用到的就是循环条件和判断条件。玩家标记了一个未被判断过的位置,若此位置为炸弹则炸弹总数量减1,反之则炸弹总数量不变。相信读者大大仔细阅读下列代码都会明白。
//标记或取消标记
int sign_mine(char mine[ROWS][COLS],char show[ROWS][COLS])
{
int n = 0,a=0,b=0;
int win = 0;
while (1)
{
//美化界面
printf("\n****************\n");
printf(" 1.mark_mine(标记)\n 2.cancel_mine(取消标记)\n 3.continue(跳过)\n");
printf("****************\n请选择:>");
scanf("%d", &n);
if (n == 1)
{
printf("请输入要标记的坐标:>");
scanf("%d%d", &a, &b);
if (show[a][b] == '*')
{
show[a][b] = 'Y';
win += 1;
DisplayBoard(show, ROW, COL);
continue;
}
else
{
printf("标记错误,请重新标记!!\n");
continue;
}
}
else if (n == 2)
{
printf("请输入要取消标记的坐标:>");
scanf("%d%d", &a, &b);
if (show[a][b]=='Y')
{
show[a][b] = '*';
DisplayBoard(show, ROW, COL);
win -= 1;
continue;
}
else
{
printf("操作错误,请重新输入!!\n");
continue;
}
}
else
return win;
}
}
这个模块就是玩家输入坐标查询,1.若该位置被查询过则请玩家重新输入 2.若该位置为雷则游戏失败 3.若该位置非雷则调用(Unfold)函数展开同时打印棋盘 4.若该位置超过边界则输入错误。
最后就是判断输赢,在标记雷(sign_mine)函数中,若标记正确则雷的总数减1,直到总数为0时则排雷成功。
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = count;
while (win>0)
{
printf("\n请输入想要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标被排查过了\n");
continue;
}
if (mine[x][y] == '1')
{
printf("抱歉!你已经被炸死了!\n");
DisplayBoard(mine, ROW, COL);
break;
}
if (mine[x][y] == '0')
{
Unfold(mine, show, x, y); //利用递归展开非雷区域
printf("\n");
DisplayBoard(show, ROW, COL); //打印棋盘
int n=sign_mine(mine, show); //标记雷或取消标记雷
win = win - n;
printf("\n");
printf("还剩%d个雷\n",win);
}
}
else
printf("坐标非法!\n");
}
if (win==0)
{
printf("恭喜你,排雷成功!!!!!\n");
//DisplayBoard(mine, ROW, COL);
Distribution_mine(mine,ROW,COL);
}
}
#define _CRT_SECURE_NO_WARNINGS 1;
#include "game.h"
void menu()
{
printf("**************************\n");
printf("******* 1.play ********\n");
printf("******* 2.exit ********\n");
printf("**************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };//布置好的雷的信息
char show[ROWS][COLS] = { 0 };//排查出雷的信息
printf("游戏开始\n");
//初始化两个棋盘
InitBoard(mine, ROWS, COLS,'0');
InitBoard(show, ROWS, COLS,'*');
//打印棋盘
DisplayBoard(show, ROW, COL);
//随机设置雷
SetMine(mine, ROW, COL);
//DisplayBoard(mine, ROW, COL);
//玩家开始查找雷
FindMine(mine, show, ROW, COL);
}
void test()
{
int temp = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请选择:>");
scanf("%d",&temp);
switch (temp)
{
case 1:
game();
break;
case 2:
printf("游戏退出\n");
break;
default:
printf("选择错误!请重新输入!\n");
}
}while(temp);
}
int main()
{
test();
return 0;
}
#pragma once
#include
#include
#include
#define ROW 9
#define COL 9
#define ROWS 11
#define COLS 11
#define count 5 //雷的数量
//初始换棋盘
void InitBoard(char board[ROWS][COLS],int rows, int cols,char ret);
//打印棋盘
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"
//初始化棋盘
void InitBoard(char board[ROWS][COLS], int rows, int cols, char ret)
{
for (int i = 0; i < rows; i++)
{
for (int j = 0; j < cols; j++)
{
board[i][j] = ret;
}
}
}
//打印棋盘
void DisplayBoard(char board[ROWS][COLS], int row, int col)
{
printf("\n--------扫雷--------\n");
for (int i=0;i<=row;i++)
{
printf("%d ",i);
}
printf("\n");
for (int i=1;i<=row;i++)
{
printf("%d ",i);
for (int j=1;j<=col;j++)
{
printf("%c ",board[i][j]);
}
printf("\n");
}
printf("--------扫雷--------\n");
}
//随机设置雷
void SetMine(char board[ROWS][COLS], int row, int col)
{
int n = count;
while (n)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y]=='0')
{
board[x][y] = '1';
n--;
}
}
}
//周围雷的个数
int get_mine_count(char mine[ROWS][COLS],int x,int y)
{
int tmp = 0;
for (int i=-1;i<=1;i++)
{
for (int j=-1;j<=1;j++)
{
tmp+= mine[x + i][y + j]-'0';
}
}
return tmp;
}
//递归展开
void Unfold(char mine[ROWS][COLS], char show[ROWS][COLS],int x,int y)
{
if (x == 0 || y == 0 || x == ROWS - 1 || y == COLS - 1)
{
return ;
}
//判断是否已经被排除
if (show[x][y] != '*')
{
return ;
}
int n = get_mine_count(mine, x, y); //周围有几个炸弹
if (n>0) //周围如果没有炸弹,则展开
{
show[x][y] = n+'0';
}
else if (n==0)
{
show[x][y] = ' ';
for (int i=-1;i<=1;i++)
{
for (int j=-1;j<=1;j++)
{
Unfold(mine, show, x+i, y+j);
}
}
}
}
//标记或取消标记
int sign_mine(char mine[ROWS][COLS],char show[ROWS][COLS])
{
int n = 0,a=0,b=0;
int win = 0;
while (1)
{
//美化界面
printf("\n****************\n");
printf(" 1.mark_mine(标记)\n 2.cancel_mine(取消标记)\n 3.continue(跳过)\n");
printf("****************\n请选择:>");
scanf("%d", &n);
if (n == 1)
{
printf("请输入要标记的坐标:>");
scanf("%d%d", &a, &b);
if (show[a][b] == '*')
{
show[a][b] = 'Y';
win += 1;
DisplayBoard(show, ROW, COL);
continue;
}
else
{
printf("标记错误,请重新标记!!\n");
continue;
}
}
else if (n == 2)
{
printf("请输入要取消标记的坐标:>");
scanf("%d%d", &a, &b);
if (show[a][b]=='Y')
{
show[a][b] = '*';
DisplayBoard(show, ROW, COL);
win -= 1;
continue;
}
else
{
printf("操作错误,请重新输入!!\n");
continue;
}
}
else
return win;
}
}
//成功后打印最终雷区分布
void Distribution_mine(char mine[ROWS][COLS],int row,int col)
{
for (int i=0;i<=col;i++)
{
printf("%d ",i);
}
printf("\n");
for (int i=1;i<=row;i++)
{
printf("%d ", i);
for (int j=1;j<=col;j++)
{
if (mine[i][j]=='1')
{
mine[i][j] = 'Y';
printf("%c ",mine[i][j]);
}
else if (mine[i][j]=='0')
{
mine[i][j] = ' ';
printf("%c ",mine[i][j]);
}
}
printf("\n");
}
printf("\n");
}
void FindMine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = count;
while (win>0)
{
printf("\n请输入想要排查的坐标:>");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标被排查过了\n");
continue;
}
if (mine[x][y] == '1')
{
printf("抱歉!你已经被炸死了!\n");
DisplayBoard(mine, ROW, COL);
break;
}
if (mine[x][y] == '0')
{
Unfold(mine, show, x, y); //利用递归展开非雷区域
printf("\n");
DisplayBoard(show, ROW, COL); //打印棋盘
int n=sign_mine(mine, show); //标记雷或取消标记雷
win = win - n;
printf("\n");
printf("还剩%d个雷\n",win);
}
}
else
printf("坐标非法!\n");
}
if (win==0)
{
printf("恭喜你,排雷成功!!!!!\n");
//DisplayBoard(mine, ROW, COL);
Distribution_mine(mine,ROW,COL);
}
}