有志者、事竟成,破釜沉舟,百二秦关终属楚;苦心人、天不负,卧薪尝胆,三千越甲可吞吴。
1 扫雷游戏简介
《扫雷》是一款大众类的益智小游戏,于1992年发行。游戏目标是在最短的时间内根据点击格子出现的数字找出所有非雷格子,同时避免踩雷,踩到一个雷即全盘皆输。
2 扫雷游戏的设计思路
我们从上图可知,扫雷游戏类似于棋盘游戏,游戏进行中的每一步操作精确在每一个小的方格中,由于游戏的过程中玩家需要频繁的点击其中的每一个小方格实现排雷,所以我们可以考虑到用一个二维数组去存储玩家点击每一个小方格的信息。另外,游戏还需要将布置的炸弹记录下来,并且玩家是不可见的,既要存储炸弹位置还要存储玩家修改方格的信息,所以我们这里考虑用两个二维数组分别存储。这里的二维数组其实也有巧妙的地方,由于我们后面需要统计边缘区域的四周的炸弹数量,所以我们可以将所需要的二维数组的大小比所设计的扫雷棋盘大小大一圈,这样我们后面考虑边界的时候就不用担心越界会造成什么影响的问题。
现在我们已经知道了扫雷棋盘的布局之后还需要知道什么呢?接下来当然是思考一颗颗地雷和玩家的点击信息该如何存储了呀。首先是地雷的存储,地雷的存储,无非就是在其中的一个二维数组里面随机位置放置n个炸弹,那么该如何表示放置的炸弹呢?
这里,小王建议用‘1’和‘0’来区分炸弹的有无,‘1’:此处有炸弹,‘0’:此处无炸弹,为什么这样设计呢?这里我们就先卖个关子,咱们后面就会大彻大悟了!😏😏
玩家点击信息的存储与雷的存储类似,照猫画老虎,这里我就不赘述了😝😝。
知道了雷的布置和玩家游戏这些信息之后我们就要思考那么扫雷这个游戏该怎样运行呢?首先扫雷是游戏呀,就会有开始和结束吧。那么什么情况游戏才会结束呢?
①当我们排查到雷了之后,我们就会被炸死导致游戏失败。
②如果我们把所有的方块全排查完了,并且没有踩到雷,那么游戏就会以胜利告终。
3 代码实现
为了保持代码的可读性和美观性,代码的实现依然分文件分模块编写。
3.1 菜单
菜单的打印很简单,设计合理就行。
void menu()
{
printf("**********************************\n");
printf("********** 1.play **********\n");
printf("********** 0.exit **********\n");
printf("**********************************\n");
}
3.2 二维数组初始化
这一步目的是为了将雷区布置的二维数组和玩家点击信息的数组初始化,方便后面随机布置雷区和玩家游戏的进行。这里我们把布置雷区的二维数组初始化为全‘0’,玩家所能操作的二维数组初始化为全‘*’。
void Initboard(char board[ROWS][COLS], int rows, int cols,char set)
{
int i,j;
for (int i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
效果如图:上面为玩家操作的数组,下方为雷区布置的数组。因为游戏全程玩家都是通过数组的下表去进行游戏操作,所以我们可以在区域边缘输出一串连续的数字方便玩家读取坐标。
3.3 布置雷区
由于雷区布置需要具有随机性,所以我们可以随机生成两个不越界的n组数将雷区二维数组的每一位从‘0’变成‘1’。
void Setboom(char board[ROWS][COLS], int row, int col,int boom)
{
while (boom--)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
}
}
}
以十个雷为例:
3.4 排查雷
排查雷,意思就是让玩家在所给的数组中去操作,如果踩到了一个点对应了雷区数组中的雷的位置,则游戏失败,如果没有对应到雷则游戏继续或者游戏胜利。
3.4.1 周围雷的统计
到这,游戏中出现的数字就对于我们玩家就尤为的重要,这关系着我们是否会不会排雷。那么这些数字到底代表什么意思呢?其实这些数字就代表以这个数字为中心的方块的四周一个存在多少个雷,那这个数字我们用什么思路和方法去得到呢。这个其实很简单,我们可以用一个变量去存储找到的雷,令这个变量初始化为0,然后机械地去找以这个方块为中心的各个方块是不是雷,如果是的话就变量加一,直到遍历完以这个方块为中心区域,这种思维简单,可能代码偏麻烦一点。
所以我们换个思路,我们知道数字字符减去字符0就得到了对应的整数数字。所以最开始令炸弹为字符1,没有炸弹为字符0就有了妙用,我们将以这个方块为中心的周围的方块内的字符加起来减去个数乘以字符0最后我们就能得到周围炸弹的数量。
3.4.2 连锁展开
当我们去玩扫雷的时候,第一步我们总会随机点击一个点碰一碰运气,运气好的话他就会一下子展开很大的空白区域,这样是不是就增加我们排雷的效率了呢,我是觉得这样非常爽,你们觉得呢?
那么这样的连锁展开是怎样实现的呢?我们可以看到,一条由数字组成的分界线将空白区域与位置区域隔开,所以我们可以想到如果我们点击了一个区域,而这个区域四周的方块都不是雷,那么这一片区域是不是就都是空白区域,一直往外展开下去,直到遇到了周围有雷的一个小方块的时候展开就停止。
所以根据这个思路,我们总是在玩家要排雷的位置处往四周展开,而且在展开的过程中,对于每一个小方块都是进行着同样的步骤,所以我们可以利用递归,以所选中的小方块为圆心向中间四周去判断到底满不满足以自己为中心并且四周都没有雷的点的情况。
void ChainRec(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col,int x,int y)
{
int Num_boom = AroundBoom(board1, x, y);
if(Num_boom!=0) board2[x][y] = Num_boom+'0';
if (((board2[x][y] == '0') ||(board2[x][y]=='*')) && Num_boom == 0 && x >= 1 && x <= row && y >= 1 && y <= col)
{
board2[x][y] = ' ';
ChainRec(board1, board2, row, col, x - 1, y - 1);
ChainRec(board1, board2, row, col, x - 1, y);
ChainRec(board1, board2, row, col, x - 1, y + 1);
ChainRec(board1, board2, row, col, x , y - 1);
ChainRec(board1, board2, row, col, x , y + 1);
ChainRec(board1, board2, row, col, x + 1 , y - 1);
ChainRec(board1, board2, row, col, x + 1, y + 1);
ChainRec(board1, board2, row, col, x + 1, y);
}
}
这里需要说明一个情况,因为对于每一个以它为中心的小方块都是做着同样的操作,所以会导致之前已经被递归检查的方块再次被递归检查,所以会导致死递归,这里我使用的是将满足条件的中心方块直接设置为空,并且进入递归的只能是玩家操作的二维数组的‘*’或者'0',这样就避免了重复递归的死递归状态。方法不唯一,可根据自己的想法设置条件。
经过这个步骤我们就得到了由数字分隔开的神秘区域与空白区域,是不是很神奇!!
3.4.3 标记可疑雷
在实际游戏中,我们可以在游戏进行中用小红旗去标记我们觉得此处是雷的点。同样,在我们设计的扫雷游戏中也可以添加这种功能,实现玩家排雷中去标记自己觉得可能是雷的点。
所以,在我的扫雷游戏设计中,利用玩家输入的点将玩家操作的二维数组内所对应的数组符号改成‘ !’意味着这里有雷。
因为在此处设计玩家输入标记坐标和输入排查雷的坐标可能会涉及到分支,所以我就简单写了一个标注,如果想标记的话输入坐标为0,0即可进入标记界面。
void Flagboom(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col)
{
int x, y;
system("cls");
Displayboard(board2, ROW, COL);
printf("输入标记坐标=>");
scanf_s("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col && board2[x][y] == '*')
{
board2[x][y] = '!';
system("cls");
Displayboard(board2, ROW, COL);
}
else
{
printf("标记非法,请重新输入\n");
}
}
3.4 游戏结束
这一部分当然就是游戏的输赢判断啦,当我们踩到雷或者将除开雷的其他所有区域的神秘面纱全解开的时候游戏就宣告结束,当然,踩到雷,游戏就失败,没踩到游戏结束的话游戏就是胜利。
void play(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col)
{
int x, y;
char flag;
int count = 0;
while (ROW*COL-BOOM-count)
{
printf("请输入坐标(如需标记地雷坐标输入0,0)=>");
scanf_s("%d %d", &x, &y);
if (x == 0 && y == 0)
{
Flagboom(board1, board2, ROW, COL);
}
else
{
if (x >= 1 && x <= row && y >= 1 && y <= col && board2[x][y] == '*'||board2[x][y]=='!')
{
if (board1[x][y] == '0')
{
count++;
int s = AroundBoom(board1, x, y);
board2[x][y] = s + '0';
system("cls");
ChainRec(board1, board2, row, col, x, y);
Displayboard(board2, ROW, COL);
}
else
{
system("cls");
printf("你被炸死了,游戏结束\n");
break;
}
}
}
}
if (count == ROW * COL - BOOM)
printf("恭喜你,排雷成功!!!");
Displayboard(board1, ROW, COL);
}
4 完整代码
4.1 game.h
#pragma once
#include <stdio.h>
#define ROW 9
#define COL 9
#define ROWS ROW+2
#define COLS COL+2
#define BOOM 10 //炸弹数量
//初始化棋盘
void Initboard(char board[ROWS][COLS], int rows, int cols, char set);
//打印棋盘
void Displayboard(char board[ROWS][COLS], int row, int col);
//随机炸弹
void Setboom(char board[ROWS][COLS], int row, int col, int boom);
//玩家排雷
void play(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col);
//标记雷
void Flagboom(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col);
//连锁
void ChainRec(char board[ROWS][COLS], char board2[ROWS][COLS], int row, int col,int x,int y);
4.2 game.c
#include "game.h"
void Initboard(char board[ROWS][COLS], int rows, int cols,char set)
{
int i,j;
for (int i = 0; i < rows; i++)
{
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void Displayboard(char board[ROWS][COLS], int row, int col)
{
printf("-------扫雷--------\n");
int i, j;
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 ",board[i][j]);
}
printf("\n");
}
}
void Setboom(char board[ROWS][COLS], int row, int col,int boom)
{
while (boom--)
{
int x = rand() % 9 + 1;
int y = rand() % 9 + 1;
if (board[x][y] == '0')
{
board[x][y] = '1';
}
}
}
int AroundBoom(char board1[ROWS][COLS], int x, int y)
{
return (board1[x - 1][y - 1]
+ board1[x - 1][y]
+ board1[x - 1][y + 1]
+ board1[x][y - 1]
+ board1[x][y + 1]
+ board1[x + 1][y - 1]
+ board1[x + 1][y]
+ board1[x + 1][y + 1] - 8 * '0');
}
void play(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col)
{
int x, y;
char flag;
int count = 0;
while (ROW*COL-BOOM-count)
{
printf("请输入坐标(如需标记地雷坐标输入0,0)=>");
scanf_s("%d %d", &x, &y);
if (x == 0 && y == 0)
{
Flagboom(board1, board2, ROW, COL);
}
else
{
if (x >= 1 && x <= row && y >= 1 && y <= col && board2[x][y] == '*'||board2[x][y]=='!')
{
if (board1[x][y] == '0')
{
count++;
int s = AroundBoom(board1, x, y);
board2[x][y] = s + '0';
system("cls");
ChainRec(board1, board2, row, col, x, y);
Displayboard(board2, ROW, COL);
}
else
{
system("cls");
printf("你被炸死了,游戏结束\n");
break;
}
}
}
}
if (count == ROW * COL - BOOM)
printf("恭喜你,排雷成功!!!");
Displayboard(board1, ROW, COL);
}
void Flagboom(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col)
{
int x, y;
system("cls");
Displayboard(board2, ROW, COL);
printf("输入标记坐标=>");
scanf_s("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col && board2[x][y] == '*')
{
board2[x][y] = '!';
system("cls");
Displayboard(board2, ROW, COL);
}
else
{
printf("标记非法,请重新输入\n");
}
}
//连锁
void ChainRec(char board1[ROWS][COLS], char board2[ROWS][COLS], int row, int col,int x,int y)
{
int Num_boom = AroundBoom(board1, x, y);
if(Num_boom!=0) board2[x][y] = Num_boom+'0';
if (((board2[x][y] == '0') ||(board2[x][y]=='*')) && Num_boom == 0 && x >= 1 && x <= row && y >= 1 && y <= col)
{
board2[x][y] = ' ';
ChainRec(board1, board2, row, col, x - 1, y - 1);
ChainRec(board1, board2, row, col, x - 1, y);
ChainRec(board1, board2, row, col, x - 1, y + 1);
ChainRec(board1, board2, row, col, x , y - 1);
ChainRec(board1, board2, row, col, x , y + 1);
ChainRec(board1, board2, row, col, x + 1 , y - 1);
ChainRec(board1, board2, row, col, x + 1, y + 1);
ChainRec(board1, board2, row, col, x + 1, y);
}
}
4.3 test.c
#include "game.h"
void menu()
{
printf("**********************************\n");
printf("********** 1.play **********\n");
printf("********** 0.exit **********\n");
printf("**********************************\n");
}
void game()
{
char mine[ROWS][COLS] = { 0 };
char show[ROWS][COLS] = { 0 };
//初始化
Initboard(mine, ROWS, COLS, '0');
Initboard(show, ROWS, COLS, '*');
//随机炸弹
Setboom(mine, ROW, COL,BOOM);
//打印棋盘
Displayboard(show, ROW, COL);
Displayboard(mine, ROW, COL);
//玩家排雷
play(mine, show, ROW, COL);
标记地雷
//Flagboom(mine, show, ROW, COL);
}
int main()
{
int input;
srand((unsigned int)time(NULL));
do {
menu();
scanf_s("%d", &input);
switch (input)
{
case 0:
printf("退出游戏\n");
break;
case 1:
printf("游戏开始\n");
game();
break;
default:
printf("非法输入,请重新输入\n");
break;
}
} while (input);
return 0;
}
代码已上传Gitee,有兴趣可自行下载。