寒假的时候作为一个接触C语言一个学期的咸鱼,笔者尝试挑战了贪吃蛇的编写
不过当时的笔者在编写过程中仍然大量借助了其他各路大佬的思路与算法
时至暑假,笔者决定编写扫雷小游戏,并且全程尝试按照自己的设计思路来编写
虽然过程经常遇阻,最终设计也存有大量优化空间
历时三天笔者终于肝出了扫雷小游戏
在独立的编程中笔者也还是有了很多收获
以下尝试从头解析自己的程序,希望也能对大家有帮助(滑稽)
先上一两张效果图吧
困难游戏难度:
结束界面:
这里的背景字体颜色可以用 system(“color XX”); 调控
我的将自己程序整理为两个头文件:
Algorithm.h
Mineweeper.h
前者是扫雷随机生成类,检索雷区,光标移动等相应一系列算法;
后者则是开始,结束等界面的界面设计;
三种难度的棋盘均是正方形,
只是尺寸差异;
偷懒之下就没有考虑动态开辟空间,
以最大棋盘为准定义了数组 arr[25][25];
具体如下:
typedef struct information
{
string str = "□"; //对应显示在界面上的点,初始化为棋盘的默认格子"□"
string num = "null";
//对应每个点所隐含的信息,没有读取J K键的情况不会加以打印
//"■"代表该点为中心的九宫格中没有地雷,笔者成为空点
//"①"——"⑧"代表该点为中心的九宫格中有1——8个地雷,笔者成为数字点
int judge = 0; //确定区域时是否被遍历的标记
int x = 0; //是否含有地雷的标记,0对应false,1对应true
}Inf;
在笔者的扫雷程序中
以 WASD 进行上下左右的移动
用 J 进行插旗操作
用 K 进行区域确认
在这过程中首先要用
"*"
代替原来的光标
这涉及光标消失的函数
void infogone()
{
HANDLE hwind; //定义窗口句柄
hwind = GetStdHandle(STD_OUTPUT_HANDLE); //获取窗口信息
CONSOLE_CURSOR_INFO info; //定义光标结构体
GetConsoleCursorInfo(hwind, &info); //获取当前窗口的光标信息
info.bVisible = 0; //将光标的是否可视修改为0
SetConsoleCursorInfo(hwind, &info); //将窗口中的光标信息按照结构体重新设置
}
同理若要调回可视即将 info.bVisible 重新修改为 1 即可;
而光标又需要通过读取WASD键在地图上自由移动;
这就需要移动光标位置的函数
void gotoxy(int x, int y)
{
COORD pos = { (short)x ,(short)y }; //涵盖光标位置信息的结构体(x,y)
HANDLE hOutput; //获取窗口句柄
hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //读取窗口信息
SetConsoleCursorPosition(hOutput, pos); //根据结构体中的参数直接移动光标
}
综合这两个函数,
以及加以判断条件的循环 (触雷时跳出) 和对:
_getch();
_kbhit();
的应用,加上边界的判断条件,
就可以实现光标在棋盘上的自由移动和切换
完整的代码可以通过文章尾的链接下载
扫雷最最核心的模块就是以上两个算法,
相比之下界面无非只是起锦上添花的作用;
生成地雷决定了在整个棋盘上能随机的生成地雷,确保了每次游戏的随机性;
而检索区域则对应每次点击之后判断是否触雷是否安全,
并生成一整块提示区域以供玩家的继续游戏;
一下将这两个算法逐一分析:
首先是随机生成地雷:
笔者的第一想法是在二维数组中随机生成地雷,并且每生成一个就以遍历的方式和之前的地雷比较,若重复则重新生成,若不重复则继续,直到生成指定数量的地雷;
这个想法是笔者常规性地思维,但随即发现棋盘一个二维数组,加上遍历的循环,总共是有四个循环的嵌套,显然这在算法时间复杂度上是和毁灭性的(即使针对10X10,10个地雷的小棋盘也是如此);
笔者最终通过尝试,发现了一种伪随机生成地雷的方法,虽然不能真正意义上做到棋盘上每个点生成地雷的概率是完全一致的,但是能基本上是地雷较为均匀较为随机地分布于整个棋盘,
而最重要的是其仅仅是线性的时间复杂度;
void creatMine(int k) //k是棋盘边长-1
{
for (int i = 0; i < k * k / 8; i++)
arr[i / (k + 1)][i % (k + 1)].x = 1; //现在数组0——(k*k/8-1)的位置生成指定数量的地雷,k*k/8是棋盘地雷的总数;
int swapTime = 800; //设置交换次数
srand((unsigned)time(NULL)); //初始化随机种子
for (int i = 0; i < swapTime; i++)
{
int row1 = rand() % (k + 1);
int col1 = rand() % (k + 1);
int row2 = rand() % (k + 1);
int col2 = rand() % (k + 1); //随机生成两组位置
swap(arr[row1][col1].x, arr[row2][col2].x); //将这两处的.x信息交换,实现更新数组,致使地雷逐渐分布开来
}
}
从数学上分析,当变量 swaptime 足够大(趋于正无穷)时,能实现真正的随机分布
但笔者在调试后发现,当 swaptime 赋值为 800 时,已经基本能实现较为均匀随机的分布了
对一个循环重复800次,其时间开销是完全可以接受的;
不过各位大佬如果有更完美的算法,欢迎指正交流
其次是检索区域:
关于检索区域,笔者倒是没有太多的犹豫,显然要以递归的模式不断检索,
当选中某一点时,遍历以该点为中心的九宫格,以此类推;
直到最后递归区域被数字点包围即可:
(数字点是笔者自己起的名字,详见上文)
void printAndWarnMine(int x, int y, int a, int b, int k)//x,y是该点在数组中的位置;a,b是该点在棋盘中的坐标,k是棋盘边长-1
{
if (arr[x][y].num != "■") //如果该点自身为数字点
{
arr[x][y].str = arr[x][y].num;
gotoxy(a, b);
cout << arr[x][y].str; //打印当前点对应的数字,即该九宫格中的地雷数
return;
}
for (int i = x - 1; i <= x + 1; i++)
{
for (int j = y - 1; j <= y + 1; j++)
{
if (j<0 || j>k || i<0 || i>k) continue; //若超出边界则跳过此次循环
if (arr[i][j].num == "√") continue; //若该点为地雷也跳过此次循环
arr[i][j].str = arr[i][j].num;
gotoxy(a + (j - y) * 2, b + i - x);
cout << arr[i][j].str;
if (i == x && j == y) continue; //若遇到自身点,跳过此次循环
if (arr[i][j].num != "■" && arr[i][j].judge == 0)
arr[i][j].judge = 1; //若为数字点,停止递归,同时将.judge刷新为1,代表该点已被遍历过
else if (arr[i][j].num == "■" && arr[i][j].judge == 0)
{
arr[i][j].judge = 1; //同理于上述操作
printAndWarnMine(i, j, a + (j - y) * 2, b + i - x, k);
//若为空点,则继续递归
}
}
}
}
这里笔者认为结构体中 .judge 是一个重要的变量,
在遍历后将其更新为 1 能够确保递归算法不会在九宫格大量的重叠区域上浪费过多的时间
使该算法在递归时检索的每一点都是上一次递归未检索的,
确保了在递归过程中 不会进入死循环 ;
以上两个算法写出后,再加上对数组中各结构体的参数的初始化,
就能成功地在地图上实现地雷的随机生成和区域的检索提示了;
完整的代码可以通过文章尾的链接下载
有了以上的铺垫,加上必要的开始界面,结束界面和计时系统
就可以将各个部分连贯为一个完整的扫雷程序了!
笔者还在程序中添加了一个较为乞丐的 记忆功能
能储存场上 有待插旗的个数 和 上一项操作的坐标和具体内容
当然各位如果有时间有精力有兴趣,不妨把输入的昵称写入文件,
为每一个玩家都提供一个最高纪录,
这个也是不错的功能,不过笔者算是肝不动了。。。
最后提供源代码和exe文件,源代码是多文件的,博客中实在不方便写出
这里提供一个百度云链接吧(不知道为什么VS 2019写出的文件压缩文件夹会那么大。。。)
链接:点击这里
提取码:8mts