前言
🎈大家好,我是何小侠🎈
🌀大家可以叫我**小何或者小侠🌀**
🔴我是一名普通的博客写作者🔴
💐希望能通过写博客加深自己对于学习内容的理解💐
🌸也能帮助更多人理解和学习🌸
🍃我的主页: 何小侠的主页🍃
这篇博客我们一起来学习如何用C语言所学知识来实现扫雷,另外我们还将学习一个难点TNT式展开。希望能帮助大家理解和学习
🍊
引子🍊
扫雷最原始的版本可以追溯到1973年一款名为“方块”的游戏。
不久,“方块”被改写成了游戏“Rlogic”。在“Rlogic”里,玩家的任务是作为美国海军陆战队队员,为指挥中心探出一条没有地雷的安全路线,如果路全被地雷堵死就算输。两年后,汤姆·安德森在“Rlogic”的基础上又编写出了游戏“地雷”,由此奠定了现代扫雷游戏的雏形。
1981年, 微软公司的 罗伯特·杜尔 和 卡特·约翰逊 两位工程师在Windows
3.1系统上加载了该游戏,扫雷游戏才正式在全世界推广开来。 扫雷很快在全球范围内流行开来,成为Windows操作系统中最受欢迎的游戏之一。许多人在办公室和家庭中都玩这个游戏来消磨时间。**
随着移动设备的普及,扫雷也进入了移动设备领域。现在,扫雷已经成为了一种经典的游戏,被广泛地应用于各种平台和设备中。
游戏思路🍊
我们看一下我们的扫雷游戏的图片
这就是我们熟悉的扫雷游戏,虽然我们现在没有能力将它100%的复刻出来,但是大部分的逻辑在我们的努力下是能够做到的。
首先我们看一下简单的逻辑
我们点击这个数字为2的方格后,这个方格显示了2的数字,这就是告诉我们在这个九宫格内,一共有两个雷。
我点击随机点击这个九宫格,直达有雷,最后我们看到确实是这样。(当然我们后面也会介绍TNT式展开)
现在我们应该就能大致理解这个游戏了,大概的思路是:
- 首先要有雷。
- 然后再排雷。
- 如果排到了雷就游戏结束,如果没有排到,就应该显示一下周围有几个雷,然后继续排。
- 游戏胜利——我们将所有的雷找出来,或者拍完所有非雷坐标。
但是我们肯定需要东西来存储扫雷的数据,用什么呢?
对, 应该是数组而且是二维数组,由于我们后面主要将9x9,所以我们就需要一个9x9的二维数组,但是这其中还是有一点细节的。下面我们将会探讨。
我们在给二维数组初始化的时候,如果用‘1’表示雷,‘0’表示非雷。
由于‘0’太多了我就不画完了。
我们会发现一个问题如果在九宫格里只有一个雷的时候,我们这个‘1’就存在歧义了,
但是有人想说,那我就不用‘1’‘0’表示雷和非雷不就行了吗?我用‘’和‘#’表示雷与非雷就行
但是如果这样我们的数组的内容就很杂乱,我们和我们玩的扫雷就存在很大的差距,我们在电脑上玩的扫雷游戏总体上只有三种内容,要么是空格,要么是数字,要么是雷。那我们应该怎么办呢?
我们就可以用两个大小相同的二维数组,一个专门放置雷,一个就专门用来展示有关于雷的信息,这个信息也是从放置雷的数组得来的。
下面给出参考
我们在放置雷的数组中只用‘1’,‘0’。在展示给玩家的数组中,没有被排查的位置全部置为‘’保持神秘感,然后排查的位置就用与九宫格雷数相等的字符数字来表示
所以我们定义两个数组 char mine[ 9 ][ 9 ], char show[ 9 ][ 9 ]
但是我们现在任然存在一个问题。
我们在mine数组检测玩家点击的方格时,如果玩家点击到的是最外层的方格,我们就会越界访问。
怎么解决呢?
那我们就直接把数字开大一点就行char mine[ 11 ][ 11 ],我特意画了一个图来展示。
可以看到即使我们在9x9棋盘的边缘查找,也不会越界。为了保证mine数组和show数组是严格对应的,我们也把show数组开大一点,变成show[ 11 ][ 11 ]。
这就是我们大概的游戏思路,接下来我们开始实现。
定义和初始化
void game()
{
//定义二维数组
char mine[ROWS][COLS];
char show[ROWS][COLS];
······
}
我们暂时先给出一点代码,避免干扰思路,
注意看我们用的常量ROWS,和COLS来定义。
这是我们在头文件用#define定义的,为什么我们还非要写ROW,和COL呢?因为我们有时候不需要操作整个数组,只需要操作9x9就行。
然后是我们的初始化->
void game()
{
//定义二维数组
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化二维数组
init_arr(mine, ROWS, COLS, '0');
init_arr(show, ROWS, COLS, '*');
·······
//初始化二维数组
void init_arr(char arr[ROWS][COLS], int rows, int cols, char ch)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = ch;
}
}
}
可以看到我们传的最后一个参数,我们可以根据你要初始化什么字符来使用这个函数,也是这个初始化比较好的部分。
这个还有一个要注意的地方,我们写形参的时候不能写char arr[ROW][COL],因为我们传的是整个数组。我们后来用小写 rows,cols表示ROWS,COLS增加可读性,避免一直用大写ROWS,COLS,当然用也没什么问题,但是我感觉小写看着舒服一点。
打印棋盘
打印棋盘没有太多要讲的,格式大家可以多调一调。
void game()
{
//定义二维数组
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化二维数组
init_arr(mine, ROWS, COLS, '0');
init_arr(show, ROWS, COLS, '*');
//打印二维数组
display (show, ROW, COL);
··········
void display(char arr[ROWS][COLS], int row, int col)
{
printf("·················扫雷·················\n");
int i = 0;
int j = 0;
for (i = 0; i <= row; i++)
{
printf(" %d ", i);
if (i > 0)
printf(" ");
}
printf("\n");
printf("\n");
for (i = 1; i <= row; i++)
{
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf(" %c ", arr[i][j]);
if (j <= col -1)
printf("|");
}
printf("\n");
if (i <= row - 1)
{
printf(" ");
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
printf("·················扫雷··················\n");
}
有过要注意的点,我们打印的数组下标是从1开始,因为最外围是我们不需要用的,我上面有一张图可以看,这里就不再插入图片了。
如果有人看不太懂可以去看看我三子棋的博客>:链接: link
打印出来效果就是这样。我感觉看起来比起一堆数字还是舒服一点。但是这个提示的几行几列数字不是很好打印,要多调整调整。当然我们也可以打印show数组,打印出来就全是‘*’
布置雷
我们想要布置雷,那我们一定是要在mine数组,我们只需要在9x9的领域布置,那我们就只需要传ROW,COL。我们也还有设定一下雷的数量,埋完就不埋了。
void game()
{
//定义二维数组
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化二维数组
init_arr(mine, ROWS, COLS, '0');
init_arr(show, ROWS, COLS, '*');
//打印二维数组
display (show, ROW, COL);
//埋雷
set_mine (mine, ROW, COL);
······
}
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (x > 0 && x <= row && y > 0 && y <= col && mine[x][y]!='1')
{
mine[x][y] = '1';
count--;
}
}
}
如果有小伙伴看不懂rand()这个函数的用法可以看看我的这篇博客.
链接: link
这里我就直接用EASY表示简单模式了,只埋10个雷。
排查雷🍊
我们在一开始就说过,我们会在mine数组根据玩家输入的坐标排查周围的雷,然后把雷的数量返回到show数组,所以我们在传参的时候两个数组都应该传。
void game()
{
//定义二维数组
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化二维数组
init_arr(mine, ROWS, COLS, '0');
init_arr(show, ROWS, COLS, '*');
//打印二维数组
display (show, ROW, COL);
//埋雷
set_mine (mine, ROW, COL);
//display(mine, ROW, COL);
//排查雷
find_mine(mine, show, ROW, COL);
}
这就是我们整个游戏的过程了。
int mine_count(char mine[ROWS][COLS], int x, int y)
{
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';
/*for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
也可以用这种方式
}
}*/
}
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int sign = EASY;
int input = 0;
printf("请选择排查(1)或者标记(2)O(∩_∩)O\n");
scanf("%d", &input);
switch (input)
{
case 1:
while (win < row * col - EASY)
{
printf("排查\n");
printf("请输入你要排查的坐标\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{ //判断合法
if (mine[x][y] == '1')
{ //如果是雷
printf("很遗憾,艺术就是派大星\n");
display(mine, ROW, COL);
break;
}
else //不是雷
{
show[x][y] = mine_count(mine, x, y) + '0';
display(show, ROW, COL);
win++;
}
}
else
{
printf("坐标不合法,请重新输入\n");
}
}
system("cls");
if (win == row * col - EASY)
{
printf("恭喜通关了\n");
display(show, ROW, COL);
}
break;
case 2:
if (sign != 0)
{
printf("标记\n");
printf("请输入你要标记的坐标\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{ //判断合法
show[x][y] = '#';
sign--;
display(show, ROW, COL);
break;
}
else
{
printf("坐标不合法,请重新输入\n");
}
}
else
{
printf("标记已经用完了\n");
break;
}
}
}
现在来看看这个查找雷的函数,
我写了两个功能,一个是标记,一个是排查(排查里面有TNT式展开,我们下一个标题讲,大家就当没看到 ^ _ ^ )
我们先讲排查:我先定义了一个sign表示可标记的数量,因为只有10个雷,所以你只能用10个标记,用完了就提示一下,我们也要判断你要标记的坐标是否合法,合法就把这个坐标变成’#'表示被标记了,当然不要忘记再打印一遍show数组。
我们再说最重要的排查,还有要你输入一个坐标,然后判断是否合法,合法之后就判断你这个位置的坐标是不是雷,是雷就提示你爆炸了,表示就在show数组的对应坐标上显示雷的数量。
但是我们会有一个排查的上限,我们81个坐标有10个雷也就是说我们最多只能排查71个位置,所以用了一个int win 来记录已经被排查的非雷的区域,当win到达71了就说明你排完了所有雷。
再来仔细说说怎么查找那周围8个坐标的雷的个数,当然我们还是要用函数来实现,int mine_count(char mine[ROWS][COLS], int x, int y), 这个函数就是看有几个雷,那就用8个位置的雷数减去8个位置都没有雷,结果就是有几个雷,也就是-8*‘0’的含义
大家看这个函数的返回值,我们想要的是显示字符数字但是这个函数返回的是整形那怎么办呢?
show[x][y] = mine_count(mine, x, y) + ‘0’;这就是转换的方法
当一个整型数值与一个字符型的 ASCII 码值相加时,
会自动将整型数值转换为字符型例如,
如果 mine_count(mine, x, y) 的值为 3,
那么 mine_count(mine, x, y) + ‘0’ 的结果就是字符型的 ‘3’。
TNT式展开🍊
这是我们没有展开的排查雷的一个部分,我在上个标题删去了展开,方便大家理解。
else //不是雷
{
show[x][y] = mine_count(mine, x, y) + '0';
display(show, ROW, COL);
win++;
}
else //不是雷
{
expend(mine, show, x, y, &win);//递归展开
/*show[x][y] = mine_count(mine, x, y) + '0';*/
display(show, ROW, COL);
win++;
}
这个才是我们真正的展开代码。但是想完成这个代码,我们需要仔细研究一下展开的这个过程。再说一句这其实是一个递归的过程,我们要靠向递归模式思考
我们还是简单的分析一下这个过程,我们点击的红色坐标周围如果没有雷,就再从这个坐标周围的八个坐标分别开始递归,然后就再递归······。
我们先简单分析然后我们来继续细致的分析。
思考一🍊
由于画太多影响观感,我就只画了三个。
我们先点击红色区域,然后排查周围八个坐标,我们可以蓝,绿,橙,这三个区域的中心坐标都是用空格表示的,为什么呢?因为在蓝,绿,橙三个区域内没有一个雷,所以中心区域才能变成空格。
然后我们再仔细观察这个过程,我们在检查红色区域的时候会检查到(x-1,y),我们在蓝色区域检测的时候我们还是会检测(x,y),如果是这个的话,当又到(x,y)的时候不是又要检测一次吗?再检测(x,y)就又要检查
到(x-1,y),那这样不就是会一直死循环下去吗?
所以我们要设置一下我们的递归的第一个条件,这个坐标不能为空格,或者说只有没有被排查的坐标才可以被递归,可以想一下,我们如果是空格的坐标说明已经检查过它周围的坐标了,也就是不需要再检测。
思考二🍊
如果我们只是用上面的条件去实现递归是不可能成功的。
为什么?
我就直接讲了,因为还是越界访问(数组)。
还是请大家看图方便理解。
我们大块的绿色区域就是我们9x9的棋盘,我们之前在构思的时候确实解决了,在检查周围8个坐标的越界问题,但是现在是递归了,递归不会管你的二维数组有多大,它会一直递归下下去,大家看蓝色区域,蓝色区域之后还会有很多很多区域这里就不画出来了。
总结,递归要限制在我们的绿色区域内。
递归展开代码
void expend(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* win)
{
if (mine_count(mine,x,y) == 0)
{
show[x][y] = ' ';
(*win)++;
int i, j;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (x + i >= 1 && x + i <= ROW && y + j >= 1 && y + j <= COL &&
show[x+i][y+j]=='*')
{
expend(mine, show, x + i, y + j, win);
}
}
}
}
else
{ //如果九宫格有雷就算数
show[x][y] = mine_count(mine, x, y) + '0';
}
}
解释:首先还是没有雷才开始递归,再把我们选择的坐标赋值为space,再把(*win)++,说明你里胜利又进了一步,然后我们就用两个for循环开始递归, 当然递归的条件是在9x9棋盘内和该坐标没有被排查过。
test.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
void menu()
{
printf("********************\n");
printf("***** 1.play *****\n");
printf("***** 0.exit *****\n");
printf("********************\n");
}
void game()
{
//定义二维数组
char mine[ROWS][COLS];
char show[ROWS][COLS];
//初始化二维数组
init_arr(mine, ROWS, COLS, '0');
init_arr(show, ROWS, COLS, '*');
//打印二维数组
display (show, ROW, COL);
//埋雷
set_mine (mine, ROW, COL);
//display(mine, ROW, COL);
//排查雷
find_mine(mine, show, ROW, COL);
}
int main()
{
int input = 0;
srand((unsigned int)time(NULL));
do
{
menu();
printf("请输入—>\n");
scanf("%d", &input);
switch (input)
{
case 0:
printf("退出游戏\n");
break;
case 1:
game();
break;
default:
printf("输入错误请重新输入\n");
break;
}
} while (input);
return 0;
}
game.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"game.h"
//初始化二维数组
void init_arr(char arr[ROWS][COLS], int rows, int cols, char ch)
{
int i = 0;
int j = 0;
for (i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
arr[i][j] = ch;
}
}
}
void display(char arr[ROWS][COLS], int row, int col)
{
printf("·················扫雷·················\n");
int i = 0;
int j = 0;
for (i = 0; i <= row; i++)
{
printf(" %d ", i);
if (i > 0)
printf(" ");
}
printf("\n");
printf("\n");
for (i = 1; i <= row; i++)
{
printf(" %d ", i);
for (j = 1; j <= col; j++)
{
printf(" %c ", arr[i][j]);
if (j <= col -1)
printf("|");
}
printf("\n");
if (i <= row - 1)
{
printf(" ");
for (j = 0; j < col; j++)
{
printf("---");
if (j < col - 1)
printf("|");
}
printf("\n");
}
}
/*for (i = 0; i <= row; i++)
{
printf("%d ", i);
}
printf("\n");
for (i = 1; i <= row; i++)
{
printf("%d ", i);
for (j = 1; j <= col; j++)
{
printf("%c ", arr[i][j]);
}
printf("\n");
}*/
printf("·················扫雷··················\n");
}
void set_mine(char mine[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int count = EASY;
while (count)
{
x = rand() % row + 1;
y = rand() % col + 1;
if (x > 0 && x <= row && y > 0 && y <= col && mine[x][y]!='1')
{
mine[x][y] = '1';
count--;
}
}
}
int mine_count(char mine[ROWS][COLS], int x, int y)
{
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';
}
void expend(char mine[ROWS][COLS], char show[ROWS][COLS], int x, int y, int* win)
{
if (mine_count(mine,x,y) == 0)
{
show[x][y] = ' ';
(*win)++;
int i, j;
for (i = -1; i <= 1; i++)
{
for (j = -1; j <= 1; j++)
{
if (x + i >= 1 && x + i <= ROW && y + j >= 1 && y + j <= COL &&
show[x+i][y+j]=='*')
{
expend(mine, show, x + i, y + j, win);
}
}
}
}
else
{//如果九宫格有雷就算数
show[x][y] = mine_count(mine, x, y) + '0';
}
//当一个整型数值与一个字符型的 ASCII 码值相加时,
//会自动将整型数值转换为字符型例如,
//如果 mine_count(mine, x, y) 的值为 3,
//那么 mine_count(mine, x, y) + '0' 的结果就是字符型的 '3'。
}
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col)
{
int x = 0;
int y = 0;
int win = 0;
int sign = EASY;
int input = 0;
printf("请选择排查(1)或者标记(2)O(∩_∩)O\n");
scanf("%d", &input);
switch (input)
{
case 1:
while (win < row * col - EASY)
{
printf("排查\n");
printf("请输入你要排查的坐标\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{ //判断合法
if (mine[x][y] == '1')
{ //如果是雷
printf("很遗憾,艺术就是派大星\n");
display(mine, ROW, COL);
break;
}
else //不是雷
{
expend(mine, show, x, y, &win);//递归展开
/*show[x][y] = mine_count(mine, x, y) + '0';*/
display(show, ROW, COL);
win++;
}
}
else
{
printf("坐标不合法,请重新输入\n");
}
}
system("cls");
if (win == row * col - EASY)
{
printf("恭喜通关了\n");
display(show, ROW, COL);
}
break;
case 2:
if (sign != 0)
{
printf("标记\n");
printf("请输入你要标记的坐标\n");
scanf("%d %d", &x, &y);
if (x > 0 && x <= row && y > 0 && y <= col)
{ //判断合法
show[x][y] = '#';
sign--;
display(show, ROW, COL);
break;
}
else
{
printf("坐标不合法,请重新输入\n");
}
}
else
{
printf("标记已经用完了\n");
break;
}
}
}
game.h
#pragma once
#define ROW 9
#define COL 9
#define ROWS 9+2
#define COLS 9+2
#define EASY 10
#include<stdio.h>
#include<stdlib.h>
#include<time.h>
#include<windows.h>
//初始化
void init_arr(char arr[ROWS][COLS], int row, int col, char ch);
//打印二维数组
void display(char mine[ROWS][COLS], int row, int col);
//埋雷
void set_mine( char mine[ROWS][COLS], int row, int col);
//排查雷
void find_mine(char mine[ROWS][COLS], char show[ROWS][COLS], int row, int col);
总结🍊
这篇博客我们系统的介绍了扫雷以及扫雷的TNT展开,我自己感觉我的代码还是有很多不足之处的,如果有兴趣的小伙伴可试一试。
最后如果这篇博客有帮助到你,欢迎点赞关注加收藏
如果本文有任何错误或者有疑点欢迎在评论区评论