1.游戏介绍:
扫雷的玩法:在一个9×9(初级)、16×16(中级)、30×16(高级)或自定义大小的方块矩阵中随机布置一定量的地雷(初级为10个,中级为40个,高级为60个),再由玩家逐个翻开方块,以找出所有地雷为最终游戏目标。如果玩家翻开的方块有地雷,则游戏结束。
简单说来:
1. 在已经准备好的棋盘中,找出所有没有设有雷的位置,找出所有位置后,既获得胜利。
2. 该游戏设有三种难度等级,分别是简单、中级和困难模式,每种模式的棋盘大小和雷的数目都不一样。
3. 踩到雷即游戏失败。
2.算法解析:
2.1.声明:
本文使用多文件撰写代码,以提高可读性与逻辑性。
game.h-宏定义,头文件与函数声明。
game.c-函数的实现。
test.c-主函数(游戏大致过程)
2.2. 难点解析:
2.2.1.难度选择:
玩家可对难度进行选择,要求数组中元素个数应当随着玩家的需求而改变。行数,列数,雷的数量应该按照合适的比例提前确定,雷过多或过少都会对体验造成较大影响。
2.2.2.三个区域:
若以某个位置周围的位置个数划分区域,可划分成“三区”:“角”,“边”,“中”。分别如下图橙、黄、绿,三区所示。
角块周围有三个区域。
边块周围有五个区域。
中块周围有八个区域。
若分成以上三种情况,实属繁琐。那么我们应当想办法化”三“为“一”。
我们可以在该雷盘外再加一圈雷区(不对改雷区进行对其进行布雷扫雷操作),行列数分别加2,并且完全初始化为‘0’,那么原雷区的‘’边块‘’和‘’角块‘’都化为了‘’中块‘’。如下:
2.2.3.递归展开:
为优化玩家体验,我们应实现点击一块无雷的区域便成片展开的功能。这个功能也是阉割版扫雷与较完整的进阶版扫雷的主要区别,那么,我们应该如何实现?
排雷时,点击一块区域,若周围八块有雷,则显示雷的个数,若没有雷,则向四面展开。
递归条件:不“出界”(要符合区域范围),不“重复”(已经展开的不再展开)。
递归出口:即(x,y)周围八个方向。
递归终点:(x,y)周围八个区域雷个数非零,则显示返回雷的个数。
2.3.步骤简概:
使用工具
vs2022
3.实现过程:
3.1.主函数创建:
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请输入(0/1)\n");
scanf("%d", &input);
switch (input)
{
case 1:
{
LEVEL();
break;
}
case 0:
{
printf("退出游戏\n");
break;
}
default:
{
printf("输入非法,请重新输入\n");
break;
}
}
} while (input);//这里的输入非零即为真,与上case巧妙相对应
}
第一次循环判断条件需为真/至少进行一次循环。需用do while循环或其他类型循环(须正确选定跳出循环的条件)
3.1.1.menu菜单函数创建:
void menu()
{
printf("**************************\n");
printf("*******1.play 0.exit******\n");
printf("**************************\n");
}
3.1.1.1预期效果:
3.1.2.level难度选择函数创建:
这里就需要在头文件中定义各难度对应的宏常量。(game.h)
ROW表示游戏行数,COL标示游戏列数,ROWS表示总行数,COLS表示总列数,count表示雷的数量。
EASY_、MID_、DIF_分别表示难度等级。
两者进行组合便表示相应难度下相应元素的数量。
#define EASY_ROW 9
#define EASY_COL 9
#define EASY_COUNT 10
#define MID_ROW 16
#define MID_COL 16
#define MID_COUNT 40
#define DIF_ROW 30
#define DIF_COL 16
#define DIF_COUNT 60
#define EASY_ROWS EASY_ROW+2//设置+2的原因如2.2.2所述
#define EASY_COLS EASY_COL+2
#define MID_ROWS MID_ROW+2
#define MID_COLS MID_COL+2
#define DIF_ROWS DIF_ROW+2
#define DIF_COLS DIF_COL+2
还需在test.c中定义全局变量如下:
int COLS = 0;//全局变量最好不放在.h文件中,如果定义于其中,便要使用extern进行重定义,显得臃肿。
int ROWS = 0;
int COL = 0;
int ROW = 0;
int count = 0;
LEVEL函数,主要思路与菜单选择实现相同。
void LEVEL()
{
int level=0;
int input = 0;
printf("请选择难度(1/2/3):\n");
printf("1.简单难度");
printf("2.普通难度");
printf("3.疯人院难度");
printf("0.EXIT\n");
do
{
scanf("%d", &level);
switch (level)
{
case 1://简单难度
{
ROW = EASY_ROW;
COL = EASY_COL;
ROWS = EASY_ROWS;
COLS = EASY_COLS;
count = EASY_COUNT;
game();//游戏体函数
input = 0;//进行了游戏会跳出循环
system("cls");//清屏
break;
}
case 2:
{
ROW = MID_ROW;
COL = MID_COL;
ROWS = MID_ROWS;
COLS = MID_COLS;
count = MID_COUNT;
game();
input = 0;//进行了游戏会跳出循环
system("cls");
break;
}
case 3:
{
ROW = DIF_ROW;
COL = DIF_COL;
ROWS = DIF_ROWS;
COLS = DIF_COLS;
count = DIF_COUNT;
game();
input = 0;//进行了游戏会跳出循环
system("cls");
break;
}
case 0:
{
printf("退出\n");
break;
input = 0;//选择退出游戏也会跳出循环
}
default:
{
printf("选择非法,请重新选择\n");
break;
input = 1;//选择非法时,满足循环条件,不跳出,继续输入。
}
}
} while (input);
}
注意:在判断选择状态时引入input变量,在不同情况下分别赋值(1/0),以控制循环。进行了游戏会跳出循环,选择退出游戏也会跳出循环,只有选择非法时,满足循环条件,不跳出,继续输入。
3.1.2.1.预期效果:
3.2.game游戏体函数创建:
1.创建两个字符型二维数组,一个存放雷的信息,一个用于打印和展示排查出的雷的信息。
char mine[DIF_ROWS][DIF_COLS] = { 0 };
char show[DIF_ROWS][DIF_COLS] = { 0 };
由于vs2022不支持C99标准的变长数组(gcc则支持),这里的二维数组行列均放入程序中行列最大值[DIF_ROWS][DIF_COLS],以防越界出错。
注:函数的创建(实现)均放在后文,函数的声明都放在4.1中,不于正文部分展示
2.调用初始化雷盘函数,在存雷盘放上字符‘0’,在展示信息盘放上字符‘*’。
initboard(mine, ROWS, COLS, '0');//边界也要初始化,故传ROWS
initboard(show, ROWS, COLS, '*');
3.调用随机布雷函数,进行雷的布置,在雷盘中‘字符1’表示雷。
setmines(mine, ROW, COL, count);//记得将雷的个数传过去
4.调用展示雷信息盘函数,给玩家展示操作界面。
displayboard(show, ROW, COL);
5.调用排雷函数,进行排雷操作(整个工程中最重要的部分)
findmine(mine,show,ROW,COL,count);
完整game函数:
void game()
{
char mine[DIF_ROWS][DIF_COLS] = { 0 };
char show[DIF_ROWS][DIF_COLS] = { 0 };
initboard(mine, ROWS, COLS, '0');
initboard(show, ROWS, COLS, '*');
setmines(mine, ROW, COL, count);
//displayboard(mine, ROW, COL);//不能将其显示,仅供自行调试使用。
displayboard(show, ROW, COL);
findmine(mine,show,ROW,COL,count);
}
3.2.1.initboard初始化数组函数创建:
void initboard(char board[DIF_ROWS][DIF_COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;//每个元素放上相应初始化元素
}
}
}
细节:j放在第一层for循环内创建,节省内存占用。
3.2.2.setmines随机埋雷函数创建:
void setmines(char board[DIF_ROWS][DIF_COLS], int row, int col,int count)
{
int i = 0;
i = count;
while(i)//等于0为假时跳出
{
int x = rand() % row +1;
int y = rand() % col +1;
if (board[x][y] == '0')//不重复布置雷
{
board[x][y] = '1';//‘1’表示雷,放在雷盘
i--;//每成功布置雷一次计数器减一
}
}
}
注意:这里使用了随机数生成函数rand(),要记得设置初始化时间起点,并在.h文件中调用相应库函数。
3.2.3.displayboard打印雷盘函数创建:
void displayboard(char board[DIF_ROWS][DIF_COLS], int row, int col)
{
int a = 0;
int i = 0;
int j = 0;
printf("————--------扫雷--------————\n");
for (a = 0; a <= col; a++)//打印列号
{
printf("%2d ", a);//,%2d两字符右对齐
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 1;
printf("%2d ", i);//打印行号
for (j = 1; j <= col; j++)
{
printf("%2c ", board[i][j]);
}
printf("\n");
}
printf("————--------扫雷--------————\n");
}
注:打印时应该打印上行列号以便选择。
3.2.3.1.预期结果:
3.2.3.findmine排雷函数创建:
void findmine(char mine[DIF_ROWS][DIF_COLS], char show[DIF_ROWS][DIF_COLS], int row, int col,int count)
{
while (1)//排雷是个重复操作
{
int x = 0, y = 0,js=0;
printf("请输入要排查的坐标\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)//判断输入坐标是否合法
{
if (show[x][y] != '*')//已排查
{
printf("该坐标已排查,请重新输入\n");
continue;
}
else if (mine[x][y] == '1')//踩到雷游戏结束,并打印雷盘让玩家”瞑目“
{
printf("很遗憾,您踩到雷,被炸死了\n");
displayboard(mine, row, col);
break;
}
else //未踩到雷,进行展开操作
{
spread(mine,show,x,y,row,col);//如下文
displayboard(show, row, col);
}
}
else//非法
{
printf("输入坐标非法,请重新输入\n");
}
for (x = 1; x <= row; x++)//累加未排查未展开的区域的个数
{
for (y = 1; y <= col; y++)
{
if (show[x][y] == '*')
js++;
}
}
if (js == count)//若未排查未展开的区域的个数与雷个数相等,则玩家获胜。
{
printf("针不戳,你赢了\n");
break;//退出游戏
}
}
注意:对雷信息盘操作后要调用展示函数打印雷盘。
3.2.3.1.spread展开函数创建:
1.由于字符’0‘和字符’1’的ASCII码差值为1,所以对周围八个位置进行雷计数时减去八个字符’0‘便能得到雷的个数。
2.周围雷的数量为0、坐标在规定范围内、该位置没有被排查过(雷信息盘为‘*’即为未排查),三个条件同时成立即可展开。(开始向八个方向递归)
3.在展开的位置,我们放上‘ ’(空格字符)
4.周围雷数量不为0的时候便将数字(字符型)放进该位置。
void spread(char mine[DIF_ROWS][DIF_COLS], char show[DIF_ROWS][DIF_COLS], int x, int y,int row,int col)
{
int num = 0;
num = getminenum(mine,x,y);//注意这里是:雷的个数(整形)
if (num == 0&&x>=1&&x<=row&&y>=1&&y<=col && show[x][y] == '*')
{
show[x][y] = ' ';
spread(mine, show, x-1, y-1, row, col);
show[x][y] = ' ';
spread(mine, show, x-1, y, row, col);
show[x][y] = ' ';
spread(mine, show, x-1, y+1, row, col);
show[x][y] = ' ';
spread(mine, show, x, y-1, row, col);
show[x][y] = ' ';
spread(mine, show, x, y+1, row, col);
show[x][y] = ' ';
spread(mine, show, x+1, y-1, row, col);
show[x][y] = ' ';
spread(mine, show, x+1, y, row, col);
show[x][y] = ' ';
spread(mine, show, x+1, y+1, row, col);
show[x][y] = ' ';
}
else if(num!=0)
{
show[x][y] = num+'0';//为了统一,都化为字符型
}
}
周围雷个数用getminecount函数获取。
int getminenum(char mine[DIF_ROWS][DIF_COLS], int x, int y)
{
int num = 0;
num = 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';//减去八个字符‘0’
return num;
}
一个较为完整的扫雷程序到此结束。
试运行如下:
4.完整代码:
4.1.game.h
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<time.h>
#include<stdlib.h>
#include<windows.h>
#define EASY_ROW 9
#define EASY_COL 9
#define EASY_COUNT 10
#define MID_ROW 16
#define MID_COL 16
#define MID_COUNT 40
#define DIF_ROW 30
#define DIF_COL 16
#define DIF_COUNT 60
#define EASY_ROWS EASY_ROW+2
#define EASY_COLS EASY_COL+2
#define MID_ROWS MID_ROW+2
#define MID_COLS MID_COL+2
#define DIF_ROWS DIF_ROW+2
#define DIF_COLS DIF_COL+2
void initboard(char board[DIF_ROWS][DIF_COLS], int rows, int cols, char set);
void setmines(char board[DIF_ROWS][DIF_COLS], int row, int col,int count);
void displayboard(char board[DIF_ROWS][DIF_COLS],int row,int col);
void findmine(char mine[DIF_ROWS][DIF_COLS], char show[DIF_ROWS][DIF_COLS], int row, int col,int count);
4.2.game.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
void initboard(char board[DIF_ROWS][DIF_COLS], int rows, int cols, char set)
{
int i = 0;
for (i = 0; i < rows; i++)
{
int j = 0;
for (j = 0; j < cols; j++)
{
board[i][j] = set;
}
}
}
void setmines(char board[DIF_ROWS][DIF_COLS], int row, int col,int count)
{
int i = 0;
i = count;
while(i)
{
int x = rand() % row +1;
int y = rand() % col +1;
if (board[x][y] == '0')//不重复布置雷
{
board[x][y] = '1';
i--;
}
}
}
void displayboard(char board[DIF_ROWS][DIF_COLS], int row, int col)
{
int a = 0;
int i = 0;
int j = 0;
printf("————--------扫雷--------————\n");
for (a = 0; a <= col; a++)
{
printf("%2d ", a);
}
printf("\n");
for (i = 1; i <= row; i++)
{
int j = 1;
printf("%2d ", i);
for (j = 1; j <= col; j++)
{
printf("%2c ", board[i][j]);
}
printf("\n");
}
printf("————--------扫雷--------————\n");
}
int getminenum(char mine[DIF_ROWS][DIF_COLS], int x, int y)
{
int num = 0;
num = 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';
return num;
}
void spread(char mine[DIF_ROWS][DIF_COLS], char show[DIF_ROWS][DIF_COLS], int x, int y,int row,int col)
{
int num = 0;
num = getminenum(mine,x,y);//雷的个数(整形)
if (num == 0&&x>=1&&x<=row&&y>=1&&y<=col && show[x][y] == '*')
{
show[x][y] = ' ';
spread(mine, show, x-1, y-1, row, col);
show[x][y] = ' ';
spread(mine, show, x-1, y, row, col);
show[x][y] = ' ';
spread(mine, show, x-1, y+1, row, col);
show[x][y] = ' ';
spread(mine, show, x, y-1, row, col);
show[x][y] = ' ';
spread(mine, show, x, y+1, row, col);
show[x][y] = ' ';
spread(mine, show, x+1, y-1, row, col);
show[x][y] = ' ';
spread(mine, show, x+1, y, row, col);
show[x][y] = ' ';
spread(mine, show, x+1, y+1, row, col);
show[x][y] = ' ';
}
else if(num!=0)
{
show[x][y] = num+'0';//整成字符型
}
}
void findmine(char mine[DIF_ROWS][DIF_COLS], char show[DIF_ROWS][DIF_COLS], int row, int col,int count)
{
while (1)
{
int x = 0, y = 0,js=0;
printf("请输入要排查的坐标\n");
scanf("%d %d", &x, &y);
if (x >= 1 && x <= row && y >= 1 && y <= col)
{
if (show[x][y] != '*')
{
printf("该坐标已排查,请重新输入\n");
continue;
}
else if (mine[x][y] == '1')
{
printf("很遗憾,JJ踩到雷,被炸死了\n");
displayboard(mine, row, col);
break;
}
else
{
spread(mine,show,x,y,row,col);
displayboard(show, row, col);
}
}
else
{
printf("输入坐标非法,请重新输入\n");
}
for (x = 1; x <= row; x++)
{
for (y = 1; y <= col; y++)
{
if (show[x][y] == '*')
js++;
}
}
if (js == count)
{
printf("针不戳JJ,你赢了\n");
break;
}
}
}
4.3.test.c
#define _CRT_SECURE_NO_WARNINGS
#include"game.h"
int COLS = 0;//全局变量最好不放在.h文件中,还是要使用extern进行重定义,显得臃肿。
int ROWS = 0;
int COL = 0;
int ROW = 0;
int count = 0;
void menu()
{
printf("**************************\n");
printf("*******1.play 0.exit******\n");
printf("**************************\n");
}
void game()
{
char mine[DIF_ROWS][DIF_COLS] = { 0 };
char show[DIF_ROWS][DIF_COLS] = { 0 };
initboard(mine, ROWS, COLS, '0');
initboard(show, ROWS, COLS, '*');
setmines(mine, ROW, COL, count);
displayboard(mine, ROW, COL);//不能将其显示
displayboard(show, ROW, COL);
findmine(mine,show,ROW,COL,count);
}
void LEVEL()
{
int level=0;
int input = 0;
printf("请选择难度(1/2/3):\n");
printf("1.简单难度");
printf("2.普通难度");
printf("3.疯人院难度");
printf("0.EXIT\n");
do
{
scanf("%d", &level);
switch (level)
{
case 1:
{
ROW = EASY_ROW;
COL = EASY_COL;
ROWS = EASY_ROWS;
COLS = EASY_COLS;
count = EASY_COUNT;
game();
input = 0;
system("cls");
break;
}
case 2:
{
ROW = MID_ROW;
COL = MID_COL;
ROWS = MID_ROWS;
COLS = MID_COLS;
count = MID_COUNT;
game();
input = 0;
system("cls");
break;
}
case 3:
{
ROW = DIF_ROW;
COL = DIF_COL;
ROWS = DIF_ROWS;
COLS = DIF_COLS;
count = DIF_COUNT;
game();
input = 0;
system("cls");
break;
}
case 0:
{
printf("退出\n");
break;
input = 0;
}
default:
{
printf("选择非法,请重新选择\n");
break;
input = 1;
}
}
} while (input);
}
int main()
{
srand((unsigned int)time(NULL));
int input = 0;
do
{
menu();
printf("请输入(0/1)\n");
scanf("%d", &input);
switch (input)
{
case 1:
{
LEVEL();
break;
}
case 0:
{
printf("退出游戏\n");
break;
}
default:
{
printf("输入非法,请重新输入\n");
break;
}
}
} while (input);
}
5.总结与说明:
200余行代码解决扫雷进阶问题。鼠标操作,标记雷的功能,后续实现。
递归部分有很多种写法,这是我自己想的一种,如有更加高效的写法,欢迎交流指正。