哈喽小伙伴们大家好,欢迎来到数组进阶特别篇二,扫雷的讲解,如果是没有基础的小伙伴建议优先阅读三子棋的讲解,弄懂了三子棋,扫雷就会变得容易懂得多,不然直接看扫雷不太容易看懂。废话不多说,让我们开始本期的内容,扫雷详解。
还是从思路上入手,相信大家玩过扫雷哈,我这里就简单的介绍一下,就是在一个n乘n的棋盘里,里面有一些位置是地雷,我们点击一个格子,如果是地雷我们就游戏扫雷失败,如果不是地雷,会显示该格子周围9宫格内地雷的个数,根据这一线索,找出所有地雷位置即为游戏胜利。我们还是先写一个大框,游戏流程为:先选择是否开始游戏,进入游戏后循环的让玩家选择排雷,标记或标记取消,直至游戏胜利或者失败。例如下代码:
//菜单
void menu()
{
printf("************************************************************************************************************************");
printf("******************************************* 扫雷游戏 ********************************************");
printf("******************************************* 1.Play 0.Exit ********************************************");
printf("************************************************************************************************************************");
printf("************************************************************************************************************************");
}
//输入错误
void Error()
{
printf("请根据菜单正确选择!\n");
}
//游戏函数
void game()
{
int a;
do
{
//清空缓存(初始菜单选择时留下一个“\n”没有被读取)
getchar();
//选择菜单
printf("请选择操作:1排雷/2标记雷/3取消标记\n");
a = getchar();
switch (a)
{
case '1':
{
printf("排雷\n");
break;
}
case '2':
{
printf("标记\n");
break;
}
case '3':
{
printf("标记取消\n");
break;
}
default:
{
Error();
break;
}
}
//胜利或失败条件达成把a置为0跳出循环
} while (a);
}
//测试函数
void test()
{
int a;
do
{
printf("请根据菜单选择!\n");
scanf("%d", &a);
switch (a)
{
case 1:
{
game();
break;
}
case 0:
{
printf("游戏退出!\n");
break;
}
//输出指令不是菜单中的
default:
{
Error();
break;
}
}
} while (a);
}
//程序入口main函数
int main()
{
menu();
test();
return 0;
}
这样游戏的大框我们就有了,运行效果如图:
接下来就是增加函数来构建游戏规则,看了上期三子棋的小伙伴应该很容易想到,我们要先构建棋盘,然后初始化棋盘,然后再写出打印棋盘函数,与三子棋不同的是,扫雷时我们没有排查的位置应该显示为空,但是实际上这个位置还要有一个数字用来表示这个格子周围9宫格雷的个数,也就是同一个位置应该有两种意义,我们“扫”这个位置之前一种,直接显示;这个位置周围有几个雷也是一种,作为预备显示 。这样实际上要实现的话一个二维数组就不够用了,我们要用到两个二维数组,一个“棋盘”,一个“雷盘”。这里的行和列我们还是采用上期给大家说的全局定义#define,这里有一个问题哈,就是坐标合法性问题,上期忘了给大家说了,因为三子棋查同无论如何都是要判定坐标是否合法的(一个坐标的横竖斜只要相同就得一直查下去),但是对于扫雷来说显然并不需要,因为我们只排查一个位置的9宫格,如果让所有位置周围9宫格都是合法坐标,就可以直接不用判定是否合法了。比如我们写一个9乘9的棋盘,我们直接创建数组的时候创建11乘11的数组,这样即使时9乘9棋盘边缘的位置的9宫格也都是合法坐标,这样就不用判定是否合法了。增加函数如下代码:
void game()
{
//创建模板
char borad[ROWS][COLS] = {0};
//创建雷板
char mineborad[ROWS][COLS] = {0};
//初始化模板
Initializeborad(borad, ROW, COL);
//初始化雷盘
Initializeborad(mineborad, ROW, COL);
//打印模板
Diskplay(borad, ROW, COL);
//随机生成雷
Minerand(mineborad, ROW, COL);
{
......
省略
}
}
分别实现:
//初始化数组,由于数组创建是11乘11,使用是9乘9,所以略有变换
void Initializeborad(char borad[ROWS][COLS], int row, int col)
{
int i = 0;
int j = 0;
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
//双重for循环所有borad值变为空格
borad[i][j] = ' ';
}
}
}
大家可能在里面发现一些费解的代码啊,其实都是为了输出结果好看 ,并非原则上的必须。
//打印函数
void Diskplay(char borad[ROWS][COLS], int row, int col)
{
//清屏
system("cls");
int i = 0;
int j = 0;
//打印开头
Head();
//双重for循环打印游戏面板
for (i = 1; i <= row; i++)
{
for (j = 1; j <= col; j++)
{
//每行第一个元素前打印一堆空格美观
if (j == 1)
{
printf(" ");
}
//第一列打印j作为列标
if (i == 1)
{
//行标1到9前加一个空格后加两个空格补位
if (j < 9)
{
printf(" %d ", j);
}
//行标大于等于9前后加一个空格补位
else
{
printf(" %d ", j);
}
}
//不是第一列打印“---”
else
{
printf("---");
//最后一列不在打印“|”
if (j == col)
{
break;
}
printf("|");
}
}
//换行
printf("\n");
for (j = 1; j <= col; j++)
{
//每列第一个元素前打印一堆空格美观
if (j == 1)
{
printf(" ");
//1到10打印数字加空格,10以后只打印数(空格为了占位,两位数本身就2位,不用空格补位)
if (i+1 <= 10)
{
printf("%d ", i);
}
else
{
printf("%d", i);
}
}
printf(" %c ", borad[i][j]);
if (j == col)
{
break;
}
printf("|");
}
printf("\n");
}
End();
}
运行结果如下:
//生成雷函数
void Minerand(char borad[ROWS][COLS], char mineborad[ROWS][COLS], int row, int col)
{
int x, y, z;
z = 0;
while (z < NUM)
{
{
//随机生成坐标
x = (rand()%row) + 1;
y = (rand()%col) + 1;
if (mineborad[x][y] == ' ')
{
mineborad[x][y] = '*';
z++;
}
}
}
//直接算出所有格子的值
for (int i = 1; i <= row; i++)
{
for (int j = 0; j <= col; j++)
{
if (mineborad[i][j] != '*')
{
Countmine(borad, mineborad, i, j, ROW, COL);
}
}
}
}
//数九宫格雷个数
void Countmine(char borad[ROWS][COLS], char mineborad[ROWS][COLS], int a, int b, int row, int col)
{
//定义一个值表示地雷个数,初始为0
int mine = 0;
//依次判定9宫格除自己以外每个位置是否为雷,因为Ismine函数判定是雷返回1,不是返回0,所以直接mine += Ismine进行累计
mine += Ismine(mineborad, a, b+1, ROW, COL);
mine += Ismine(mineborad, a+1, b, ROW, COL);
mine += Ismine(mineborad, a+1, b+1, ROW, COL);
mine += Ismine(mineborad, a, b-1, ROW, COL);
mine += Ismine(mineborad, a-1, b, ROW, COL);
mine += Ismine(mineborad, a-1, b-1, ROW, COL);
mine += Ismine(mineborad, a+1, b-1, ROW, COL);
mine += Ismine(mineborad, a-1, b+1, ROW, COL);
//数组是字符类型的,把得到的“数字”转化为“字符”
if (mine == 0)
{
mineborad[a][b] = '0';
}
else if (mine == 1)
{
mineborad[a][b] = '1';
}
else if (mine == 2)
{
mineborad[a][b] = '2';
}
else if (mine == 3)
{
mineborad[a][b] = '3';
}
else if (mine == 4)
{
mineborad[a][b] = '4';
}
else if (mine == 5)
{
mineborad[a][b] = '5';
}
else if (mine == 6)
{
mineborad[a][b] = '6';
}
else if (mine == 7)
{
mineborad[a][b] = '7';
}
else if (mine == 8)
{
mineborad[a][b] = '8';
}
}
现在数据已经生成好了,继续细化game函数,扫雷功能:
case '1':
{
int x, y;
printf("请输入坐标:\n");
//获取玩家的输入坐标
scanf("%d%d", &x, &y);
//定义ret1接收有返回值的扫雷函数,并根据返回值写结果
int ret1 = Demine(borad, mineborad, x, y, ROW, COL);
if (ret1 == 1)
{
//返回值为1,即该位置是雷,打印雷盘并调用失败函数
Diskplay(mineborad, ROW, COL);
Defeat();
a = 0;
}
if (ret1 == 2)
{
//返回值为2说明这个坐标已经排查过了
printf("该作坐标已经排查过了!\n");
}
if (ret1 == 0)
{
//返回值为0说明该位置不为0,游戏继续,打印棋盘
Diskplay(borad, ROW, COL);
}
break;
}
//排雷函数
int Demine(char borad[ROWS][COLS], char mineborad[ROWS][COLS], int x, int y, int row, int col)
{
//坐标位置不是空,表示该位置已经排查过了
if (borad[x][y] != ' ')
{
return 2;
}
//该位置为*表示该位置是0,把棋盘相应位置改为*,返回1表示游戏失败
if (mineborad[x][y] == '*')
{
borad[x][y] = mineborad[x][y];
return 1;
}
//该位置不为雷,把雷盘上的数字赋值给棋盘
else
{
borad[x][y] = mineborad[x][y];
return 0;
}
}
继续细化game函数,标记功能:
case '2':
{
int x, y;
printf("请输入坐标:\n");
//获取玩家的输入坐标
scanf("%d%d", &x, &y);
//定义ret2接收有返回值的标记函数,并根据返回值写结果
int ret2 = Markmine(borad, mineborad, x, y);
if(ret2 == 3)
{
//返回值为3表示所有位置雷都已经找到,打印棋盘并调用胜利函数
Diskplay(borad, ROW, COL);
Win();
//把a置为0跳出switch语句
a = 0;
}
else if (ret2 == 1)
{
//返回值为1表示该位置已经做过标记了
Diskplay(borad, ROW, COL);
printf("该位置已做过标记啦!\n");
}
else if (ret2 == 2)
{
返回值为2表示该位置已经显示为数字,不是雷
Diskplay(borad, ROW, COL);
printf("该位置已确定不是雷啦!\n");
}
else
{
//排除以上情况,表示游戏正在进行,没分出输赢,打印棋盘表示继续
Diskplay(borad, ROW, COL);
}
break;
}
//标记雷函数
int Markmine(char borad[ROWS][COLS], char mineborad[ROWS][COLS], int x, int y)
{
//定义静态局部变量来存储标记对的个数
static int count = 0;
//如果棋盘该坐标位置为空,就把他标为*,并且雷盘上该位置如果真的为*,count加1表示正确标记了一个雷的位置
if (borad[x][y] == ' ')
{
borad[x][y] = '*';
if (mineborad[x][y] == '*')
{
count++;
}
//如果某一次标记完标记对的个数count等于NUM(即雷的个数),说明游戏胜利,返回3
if (count == NUM)
{
count = 0;
return 3;
}
return 0;
}
//该位置为*表示已经标记过
else if (borad[x][y] == '*')
{
return 1;
}
//不是空也不是*表示该位置是数字
else
{
return 2;
}
}
继续细化game函数,取消标记功能:
case '3':
{
int x, y;
printf("请输入要取消的标记:\n");
//获取玩家的输入坐标
scanf("%d%d", &x, &y);
//定义ret3接收有返回值的取消标记函数,并根据返回值写结果
int ret3 = Unmark(borad, x, y);
//返回值为1,表示该位置并没有被标记,无法取消
if (ret3 == 1)
{
Diskplay(borad, ROW, COL);
printf("该位置不是标记位置!\n");
}
//除上中情况表示游戏应该正常继续,打印棋盘
else
{
Diskplay(borad, ROW, COL);
}
break;
}
//取消标记
int Unmark(char borad[ROWS][COLS], int x, int y)
{
//该位置是玩家标记的*,把它变回空格
if (borad[x][y] == '*')
{
borad[x][y] = ' ';
return 0;
}
//该位置不是标记位置,返回1
else
{
return 1;
}
}
好了,这个程序到这里就是完全写完了,运行效果如下(为了方便用了3乘3,1个雷):
当然这只是一个简单的扫雷,目的还是数组的讲解,事实上我们还可以加入相当多的小功能,比如玩家第一个输入的坐标即使是雷也应该把这个雷“挪走”,不能让玩家一上来就炸死了,实现方法就是定义个变量作为状态位赋初值比如0表示是第一次排雷,当玩家输入一个坐标时,只要状态位是0,则检验该位置是不是雷,如果是,则重新调用生成雷函数然后重新检验,直至该坐标位置不为雷,则把状态位置1(游戏输赢时记得重新置为0激活第一次排雷保护机制)。
还有例如当排雷坐标为0时连锁显示相邻的所有是0的位置和位置是0的相邻一个位置的数字。具体实现采用递归,即当一个坐标是周围雷个数是0时,调用数雷函数,参数为相邻的8个坐标,注意这样递归时要把数雷函数最开始加一个判定即该位置是空格时才进行数雷,否则会栈溢出(比如数(2.2)位置是0时,递归调用(2.1)时如果是0则又会递归调用去数(2.2)这样无限循环导致栈溢出)。
再比如记分功能,写个函数计算已经显示的不是雷的数字的位置的个数。
等等之类,不过已经不属于数组的范畴了,大家感兴趣可以自行研究。
好了,这期内容就到这里啦,后面会发一个完整源码的扫雷程序,包含保护机制和连锁排雷。。如果觉得这篇文章对你有帮助欢迎点赞转发评论区交流,关注小白阿g,让小白不再白学,亲爱的小伙伴们下期见。