目录
前言:
文本也是通过分文件写的,并且将每个小功能细化至各个函数,我会将我认为易错的点拿出来,大家也可以看看一些注释
设计思路:
1.定义、初始化数组
2.安插地雷
3.打印显示棋盘
4.范围性排查雷
5.计算周围雷的个数
6.添加、删除标记点
这是我封装的一些函数:
// 打印菜单
void Display_menu(void);
// 初始化棋盘
void Init_Board(void);
// 显示棋盘
void Display_Board(void);
// 运行游戏
void Run_Game(void);
// 设置雷
void Set_Mine(void);
// 排查雷
void Find_Mine(void);
//选择操作内容
int Select_Operate(void);
//设置标记的位置
void Mark_Place(void);
//删除标记的位置
void Delete_Place(void);
// 计算周围雷的个数
int Count_Around_Mine(int X, int Y);
// 判断输赢
int Is_Win(void);
// 爆炸展开棋盘
void Explosion_Spread(int X, int Y);
基本功能:
游戏主体:
// 运行游戏
void Run_Game()
{
Init_Board(); // 初始化棋盘
Display_Board(); // 显示棋盘
Set_Mine(); // 安插雷
Find_Mine(); //排查雷
printf("\n");
printf("Do you want to continue playing the game ?\n");
printf("-----------1.continue 0.break-----------\n");
printf("Your choose is:");
}
游戏菜单:
// 显示菜单
void Display_menu()
{
printf("**********************************\n");
printf("********** 1.palyer ************\n");
printf("********** 0.exit **************\n");
printf("**********************************\n");
}
定义数组(为什么是二维数组???):
首先,我们应该如何表示扫雷的雷盘及如何存放布雷、排雷的数据呢?当我们安插地雷后,那又该如何不将它们显示出来,并且还有不影响数据呢?
这里二维数组能够很好的解决这个问题,两个一模一样的板子,一个显示,一个背后控制,互不干扰。
但是注意,我们首先回忆一下扫雷的玩法,当我们输入一个坐标时,它会对周围九宫格都进行计算雷的个数,并且还有后面的范围性展开。
正如 位置 1 所示,它是能够对周围的九宫格进行处理的,但是 位置 2 呢?它在最边缘区域,如果再依次对它周围进行处理,这是就会导致栈溢出现象,使程序发生错误!!!
为了避免这一种情况,给控制棋盘的四周加上一圈,这一圈全都设置为' 0 ',这样就解决了栈溢出的问题(当然你也可以加上一些限制条件来解决这个问题,但是那样十分繁琐,并且代码可读性非常差!如下图:
注:最外围只是为了解决我们栈溢出问题,中间的格子才是我们的棋盘,我们将在这里面进行赋值操作
于是我们可以这样定义:
// 显示板子 行 和 列
#define ROW 2
#define COL 2
// 控制板子 行 和 列
#define ROWS ROW + 2
#define COLS COL + 2
char Show_board[ROW][COL] = {'\0'};
char Mine_board[ROWS][COLS] = {'\0'};
我们使用宏来定义雷盘的大小以及雷的个数,这样做的好处是当我们以后想使用更大的雷盘或者想增加扫雷的难度的时候,我们只需要改动这里一次即可,增加了代码的可维护性。
初始化数组:
// 初始化棋盘
void Init_Board()
{
int i = 0;
int j = 0;
// 初始化显示棋盘:
for (i = 0; i < ROW; i++)
for (j = 0; j < COL; j++)
Show_board[i][j] = '*';
// 初始化控制棋盘:
// 一定要初始化控制棋盘,不然在下一次循环玩游戏时就会出现地雷次数不一情况
for (i = 0; i < ROWS; i++)
for (j = 0; j < COLS; j++)
Mine_board[i][j] = '\0';
}
这里要对这两个数组都进行初始化,由于再玩扫雷时棋盘全部都是蒙蔽的所以要对显示数组进行全部赋值 '*' 增加神秘感,再对控制数组初始化为 '\0' 以便后面运算!
一定要初始化控制棋盘,不然在下一次循环玩游戏时就会出现地雷次数不一情况
显示棋盘:
这里增加了 行 和 列 的 显示方便用户查看坐标
// 显示棋盘 void Display_Board() { int i = 0; int j = 0; printf("\n"); system("cls"); printf("------------MineSweeping Game------------\n"); printf(" "); for (i = 1; i <= COL; i++) printf(" %d ", i); // 显示 列 坐标 printf("\n"); for (i = 0; i < ROW; i++) { printf("%d ", i + 1); // 显示 行 坐标 for (j = 0; j < COL; j++) printf(" %c ", Show_board[i][j]); printf("\n"); } printf("------------MineSweeping Game------------\n"); }
如下图所示:
安插雷:
// 困难程度(雷的个数)
#define Easy_Count (ROW * COL) / 4
// 安插雷
void Set_Mine()
{
srand((unsigned int)time(NULL));
int count = 0;
while (count != Easy_Count)
{
int X = rand() % ROW + 1; // 1 -- ROW
int Y = rand() % COL + 1; // 2 --COL
if (Mine_board[X][Y] == '\0') // 这个坐标没有任何东西时才能成功赋值一个雷
{ // 否则会出现重复安插雷(导致雷数不够)的情况
Mine_board[X][Y] = 'M';
count++;
}
}
}
这里有两个点:
1.rand()函数 和 srand() 函数的用法
2.只有这个坐标没有被赋值时,才会给它赋值,否则会出现少赋值雷的情况
还有这个赋值雷一定要给控制数组赋值,显示数组我们在游戏前都是保持神秘(未打开)的状态
重点部分:
排查雷:
思路设计:
当用户输入一个坐标时,首先要判断它的合法性,如果不合法一直输入,直至输入正确,输入正确时进入后面的范围性展开一片后打印一下棋盘,进行一下判断是否胜利,所以,这里要在一个循环里。
如果该位置有雷,打印一下棋盘,输出提示信息后结束!
输入坐标的一些可能性:
1.坐标正常
2.坐标已经被输入过
3.坐标超出范围
4.坐标被标记(这里不用管)
// 排查雷 void Find_Mine() { int X = 0; int Y = 0; int flag = 1; while (1) { if (Select_Operate() == 1) //判断所选操作是否为 排雷操作 { while (1) { printf("Please input your coordinate :"); scanf("%d%d", &X, &Y); // 该位置不为 '*' 有三种情况--> 1.坐标超出范围 2.坐标已被掀开 3.坐标已被标记 //这里是想没有被使用的全部涵盖了,所以我们应当将标记的情况排除在外 if (Show_board[X - 1][Y - 1] != '*' && Show_board[X - 1][Y - 1] != '!') { if ((X < 1 || X > ROW) || (Y < 1 || Y > COL)) // 坐标超出范围 printf("coordinate error!!!\n"); else // 坐标被占用 printf("The ccordinate already cover occupy!!!\n"); // 打印提示语句 printf("Please afresh input:"); scanf("%d%d", &X, &Y); //重新输入一下坐标 } else if (Mine_board[X][Y] == 'M') // 该位置为雷 { Show_board[X - 1][Y - 1] = Mine_board[X][Y]; // 将雷赋值 Display_Board(); // 游戏结束,显示一下棋盘 printf("There is alandmine in this location!!!\n"); return; //打印提示信息,直接结束该函数块 } else // 不满足以上情况就说明该坐标输入正确 { Explosion_Spread(X, Y); // 展开棋盘 Display_Board(); // 每次下完都刷新一下棋盘 if (Is_Win() == 1) // 判断输赢 { printf("Win!!!\n"); return ; } break; } } } } }
注意:这里有可能会问Select_Operate()是什么?Select_Operate()是为后面添加标签位置和删除标签位置而封装的一个选择操作函数,我们一会就会详细展开分析
判断是否胜利:
// 判断输赢
int Is_Win()
{
int i = 0;
int j = 0;
int count = 0;
for (i = 0; i < ROW; i++)
for (j = 0; j < COL; j++)
if (Show_board[i][j] != '*')
count++; // 统计还没有被掀开的格子
// 当还 没有被掀开 的格子 等于 所有格子减去雷的个数时 即为将雷全部排完,否则继续
if (count == ROW * COL - Easy_Count)
return 1;
else
return 0;
}
对数组进行遍历,当还 没有被掀开 的格子 等于 所有格子减去雷的个数时 即为将雷全部排完,否则继续。
计算周围的雷数:
// 计算周围雷的个数
int Count_Around_Mine(int X, int Y)
{
int x = -1;
int y = -1;
int Mine_count = 0;
// 循环遍历以该坐标为中心的九宫格
for (x = -1; x + X < X + 2; x++)
for (y = -1; y + Y < Y + 2; y++)
if (Mine_board[X + x][Y + y] == 'M') // 如果为 雷 个数加1
Mine_count++;
return Mine_count;
}
这里是用循环对周围进行判断计数,如果是雷就加一,最后返回个数,这里你也可以将它们周围八个点坐标都加起来,也很简单明了,这里方法不唯一。
范围性炸开一片:
当用户输入一个坐标时,如果周围都没有雷,那么就继续再对旁边的九宫格进行判断、展开,并将自身周围的雷数显示出来
所以,这里我们用递归的方式来实现这一功能
// 爆炸展开棋盘 void Explosion_Spread(int X, int Y) { int num = Count_Around_Mine(X, Y); // 获取周围雷的个数 if (num == 0) // 周围没有雷 { // 当 个数为 0 时以为周围没有地雷 --> 赋值空格 Show_board[X - 1][Y - 1] = ' '; int x = 0; int y = 0; // 递归 范围炸开部分: for (x = -1; x + X < X + 2; x++) { for (y = -1; y + Y < Y + 2; y++) // 判断所得的坐标是否在范围内,否则会造成栈溢出,也无法对周围数组进行雷数统计 if ((X + x > 0 && X + x <= ROW) && (Y + y > 0 && Y + y <= COL)) // 防止已经排查过过的位置再次排查,从而造成死递归 if (Show_board[X + x - 1][Y + y - 1] == '*' && Mine_board[X + x][Y + y] != 'M') return Explosion_Spread(X + x, Y + y); } } // 当 个数为不为0 时以为周围有地雷 --> 赋值所得个数 加上 '0'得到一个 数字字符 else Show_board[X - 1][Y - 1] = num + '0'; }
注意:
1.当我们对周围格子进行递归时,再进行判断、展开,会不会又会回到上一个递归它的那个格子里再这样无限循环呢?
所以,我们为了避免造成这样死递归的情况,应当判断它所要递归的位置是否被使用过,只有没有被使用我们才应当进行递归
2.在我们进行九宫格遍历时,还应当对越界这一情况进行限制,当进行到最边缘的时候,如果在判断其自身的九宫格就会产生越界现象,我们应该加上越界限制条件
3.这个函数,首先获取它周围的雷数,如果为0我们将进行周围的判断、展开,并将此时这个坐标的位置赋值一个空格 ’ ‘,否则不展开,给自身的位置赋值雷的个数 ,赋值在显示数组中。
因为,我们使用整型(int)进行统计的,而数组是字符型的,我们应该加上一个字符’0‘才能正确的显示其个数,如果你是用相加的方法(将周围八个格子都加起来)时,也应该减去一个字符'0'或一个相应的字符,来得到正确的字符个数并赋值
选择操作菜单:
在设置标记以前,我还封装了一个选择操作Select_Operate()函数,让用户来选择自己的操作
是否-->1.排雷 还是 2.添加标记 3.删除标记
// 选择操作内容
int Select_Operate()
{
int select = 0;
printf(" ----------------\n");
printf("|0.mine clearance|\n");
printf("|1.mark flag |\n");
printf("|2.Delete mark |\n");
printf(" ----------------\n");
printf("Pleasr select your oprate-->");
scanf("%d", &select);
do
{
switch (select)
{
case 0: //排雷操作
break;
case 1: //添加标记
Mark_Place();
Display_Board(); //显示棋盘
return 0;
case 2: //删除标记
Delete_Place();
Display_Board(); //删除棋盘
return 0;
default: //输入不对,重新输入
printf("select error!!!\n");
printf("Please afrsh input :");
scanf("%d", &select);
break;
}
} while (select);
return 1;
}
添加标记:
首先对输入的坐标进行判断-->1.已被掀开 2.超出范围 3.坐标合法但已被标记 不满足以上不成立条件之后即为符合题意,赋值一个标记字符 '!' ;或者简单粗暴一点,就直接判断输入的坐标的位置是否为隐初始化藏字符'*',如果是就赋值标记字符 '!'
// 设置标记位置 void Mark_Place() { int X = 0; int Y = 0; printf("Please input your coordinate :"); scanf("%d%d", &X, &Y); while (1) { if (Show_board[X - 1][Y - 1] != '*') { if ((X < 1 || X > ROW) || (Y < 1 || Y > COL)) // 坐标超出范围 printf("coordinate error!!!\n"); else if (Show_board[X - 1][Y - 1] == '!') // 已经被标记 printf("The coordinate have been marked!!!\n"); else // 被占用 printf("The ccordinate already cover occupy!!!\n"); // 打印提示语句 printf("Please afresh input:"); scanf("%d%d", &X, &Y); } else { Show_board[X - 1][Y - 1] = '!'; break; } } }
这个标记雷应该直接赋值给显示数组,并且因为玩家没有(0 0)坐标的概念,这里还应对横坐标(X)和纵坐标(Y)都进行减一操作,后面删除标记以及前面对显示素组进行判断时也一样,以后不过多赘述!
删除标记:
与上面一样,也是对坐标位置进行判断,你也可以直接判断是否为标记字符'!' 如果是就对该位置重新赋值'*'
// 删除标记 void Delete_Place() { int X = 0; int Y = 0; printf("Please input your coordinate :"); scanf("%d%d", &X, &Y); while (1) { if (Show_board[X - 1][Y - 1] != '*' && Show_board[X - 1][Y - 1] != '!') { if ((X < 1 || X > ROW) || (Y < 1 || Y > COL)) // 坐标超出范围 printf("coordinate error!!!\n"); else // 被占用 printf("The ccordinate already cover occupy!!!\n"); // 打印提示语句 printf("Please afresh input:"); scanf("%d%d", &X, &Y); } else { Show_board[X - 1][Y - 1] = '*'; break; } } }
https://github.com/HackerActivists/MineSweeping.git
这个是我的源码,大家有兴趣也可以看一下。
总结:
非常感谢大家能够看到这里,有很多细节的东西也写在了注释里,大家也可以看看注释。
这个程序本身不算太难,但细节太多,尤其必须捋好思路,在我后期添加标签功能的时候,我尽一时不是到在那添加,最后写出来也是差强人意,所以捋好思路很重要!
你也可以在这个基础上进行一些优化,以得到更好的显示效果,如果有什么问题欢迎在评论区积极讨论交流!!!