扫雷 MineSweeper
用过Windows XP的用户一定不会忘记那一款有趣、烧脑(maybe)的扫雷游戏。(反正我是没有怎么玩过)这里将用C++控制台程序简单实现这个扫雷游戏。
Win7下的扫雷就是下面这个画风的:
首先,我们分析以下这个游戏:
- 地图由正方形小方块组成的矩形,每个小方块内要么有雷,要么没有雷
- 随意点击一个方块开始,如果有雷,爆炸了,则游戏结束,没有爆炸则继续
- 没有爆炸的方块会显示出以它为中心的九宫格内的雷的数量,为空则表示没有
- 一次点击后,如果没有爆炸,且周围有雷,则不会显示出多余方块,如果周围没有雷,则会显示出周围所有没有雷的方块,并且显示出数字的方块会作为边界
- 为什么要这样呢?因为为空白的话,可以直接将周围剩余8个方块依次点击出来(都没有雷),所以电脑帮我们做了这一步,让我们进行接下来的分析
可以看到,上面的过程中最重要的就是:点击的块周围没有雷时,显示出整个可以确定没有雷的一块区域和边界。这很显然要用到广度优先搜索,用队列实现,并不复杂。
实现效果图:
因为是纯粹的C++控制台游戏,所以输入坐标进行挖雷。
游戏逻辑就是:每一次接受输入,然后挖雷,刷新状态,清屏,输出,挖到雷就结束。
雷的数量作为输入,自己设置。然后程序随机生成雷的位置。
实现
IDE:VS 2017 Community
文件结构
|
|----block.h //block类以及Map类的声明
|----stdafx.h //VS必须有这个头文件,可以将一些所有源文件都能用到的头文件在其中声明
|----block.cpp //Map类的方法实现
|____game-MineSweeper.cpp //主函数,逻辑主体
-
——block.h
和block
类定义Map
// 2018.4.5 In XDU
// block class and Map class definition
// IDE: VS 2017 Community
#pragma once
#include <cstdlib>
#include <iostream>
#include <queue>
#include <ctime>
using std::cout;
using std::cin;
using std::endl;
using std::queue;
using std::pair;
const int max_X = ; //也可以将block改造为模板类,将这两个参数作为模板参数传入接口
const int max_Y = ; //如果要实现输入控制地图大小,可在堆上分配数组,作为参数传入构造函数,一维数组模拟二维数组即可
const int offset_X = ; //打印地图时相对纵轴偏移量,下为横轴
const int offset_Y = ;
template<typename T>
T max(T a, T b)
{
return a > b ? a : b;
}
template<typename T>
T min(T a, T b)
{
return a < b ? a : b;
}
struct block
{
bool has_mine; //是否有雷
int mine_around_num; //周围的雷的数量
bool digged; //是否已经被挖开,挖开有雷则直接返回,没有则显示周围的雷的数量
};
class Map
{
private:
block Cube[max_X][max_Y];
public:
//传入雷的数量,将Cube初始化
Map(int n);
//应该要采用广度优先搜索,将所有位置显示出来,挖到雷返回false
bool dig(int x, int y);
//打印地图,若碰到有雷炸了的情况,最后由下一个函数清屏打印最终结果
void print();
//雷挖炸了之后,将所有位置情况打印出来
void print_end();
};
-
——block.cpp
的成员函数实现Map
其中广搜很容易实现。
print
函数很多都是处理输出格式的内容。
#include "stdafx.h"
#include "block.h"
传入雷的数量,对其进行初始化
Map::Map(int n)
{
memset(Cube, , sizeof(Cube));
srand(time()); //随机数种子
for (int i = ; i < n; i++) //随机埋上n个雷
{
int x = rand() % max_X;
int y = rand() % max_Y;
Cube[x][y].has_mine = true;
//cout << "(" << x << "," << y << ")" << endl;
}
for (int i = ; i < max_X; i++) //对地图进行初始化
for (int j = ; j < max_Y; j++)
for (int x = max(i - , ); x <= min(i + , max_X - ); x++)
for (int y = max(j - , ); y <= min(j + , max_Y - ); y++)
if (Cube[x][y].has_mine)
Cube[i][j].mine_around_num++;
}
//应该要采用广度优先搜索,将所有位置显示出来,挖到雷返回false
bool Map::dig(int x, int y)
{
Cube[x][y].digged = true;
if (Cube[x][y].has_mine)
return false;
else
{
if (Cube[x][y].mine_around_num != )
return true;
else
{
queue<pair<int, int>> Q; //采用队列进行广度优先搜索
Q.push({ x,y });
while (!Q.empty())
{
int curx = Q.front().first; //最初挖出来的位置入队
int cury = Q.front().second;
Q.pop();
for (int i = ; i <= ; i++) //表示方向,1~4分别为上下左右
{
int tmpx = curx;
int tmpy = cury;
switch (i) //不会出现default的情况
{
case : tmpx = max(curx - , ); break;
case : tmpx = min(curx + , max_X - ); break;
case : tmpy = max(cury - , ); break;
case : tmpy = min(cury + , max_Y - ); break;
}
if (!Cube[tmpx][tmpy].digged && !Cube[tmpx][tmpy].has_mine) //没有被dig且没有雷且周围没有雷则入队
{
if (Cube[tmpx][tmpy].mine_around_num == ) //挖出来的区域的中间位置,即周围没有雷的位置
{
Q.push({ tmpx,tmpy });
Cube[tmpx][tmpy].digged = true;
}
else
Cube[tmpx][tmpy].digged = true; //被挖出来区域的边界,周围有雷,置其状态但不入队
}
}
}
}
cout << endl;
return true;
}
}
//打印地图,就是这样
void Map::print()
{
for (int oy = ; oy < offset_Y; oy++)
cout << endl;
for (int ox = ; ox < offset_X + + max_X / ; ox++)
cout << " ";
cout << "Y" << endl;
cout << " ";
for (int oy = ; oy < offset_X + ; oy++)
cout << " ";
for (int ox = ; ox < max_Y; ox++)
{
cout << ox;
if (ox < )
cout << " ";
}
cout << endl;
//以上均为控制横向偏移
for (int i = ; i < max_X; i++)
{
for (int ox = ; ox < offset_X; ox++)
cout << " ";
if (i == (max_Y) / )
cout << "X";
else
cout << " ";
if (i < )
cout << " ";
cout << i;
//以上为纵向偏移控制
for (int j = ; j < max_Y; j++)
{
if (!Cube[i][j].digged) //没有被挖
cout << "■";
else if (Cube[i][j].has_mine) //被挖了且有雷
cout << "×";
else if (Cube[i][j].mine_around_num != ) //被挖了且没有雷且周围有雷
cout << " " << Cube[i][j].mine_around_num;
else
cout << " ";
}
cout << endl;
}
}
//雷挖炸了之后,将所有位置情况打印出来,也可以选择只用print()打印出踩中的位置
void Map::print_end()
{
for (int oy = ; oy < offset_Y; oy++)
cout << endl;
for (int ox = ; ox < offset_X + + max_X / ; ox++)
cout << " ";
cout << "Y" << endl;
cout << " ";
for (int oy = ; oy < offset_X + ; oy++)
cout << " ";
for (int ox = ; ox < max_Y; ox++)
{
cout << ox;
if (ox < )
cout << " ";
}
cout << endl;
//以上均为控制横向偏移
for (int i = ; i < max_X; i++)
{
for (int ox = ; ox < offset_X; ox++)
cout << " ";
if (i == (max_Y) / )
cout << "X";
else
cout << " ";
if (i < )
cout << " ";
cout << i;
//以上为纵向偏移控制
for (int j = ; j < max_Y; j++)
{
if (Cube[i][j].has_mine) //有雷
cout << "×";
else if (Cube[i][j].mine_around_num != ) //没有雷且周围有雷
cout << " " << Cube[i][j].mine_around_num;
else
cout << " ";
}
cout << endl;
}
}
-
——主函数game-MIneSweeper.cpp
// 2018.4.5 In XDU
// MineSwepper, classic game in Windows XP, a simple implementation with WIN32 console.
// IDE: VS 2017 Community
// author : Tiko
// Github : aojueliuyun
// I'm a student , happy to study annd sahre, welcome to communicate with me !
#include "stdafx.h"
#include "block.h"
int main(void)
{
int n;
cout << "一个简单的扫雷游戏,向 Windows XP 的扫雷致敬!" << endl;
cout << "核心算法:广度优先搜索 " << endl;
cout << "默认地图大小:20 * 20 , 可在头文件block.h中修改。" << endl;
cout << "另外横向纵向的偏移量也可在block.h中修改。" << endl;
cout << "规则:" << endl << "使用■表示未挖开的位置,×表示挖开炸掉了的雷," << endl;
cout << "数字表示以该格点为中心的九宫格内的雷的数量,空白为没有雷!" << endl;
cout << "玩法:" << endl << "因为是纯控制台,所以采用做标输入的方式挖雷。" << endl;
cout << "请输入地雷的数量(20左右为宜):";
cin >> n;
Map mymap(n);
int x, y;
system("cls");
while (true)
{
mymap.print();
cout << "\t 请输入坐标(x,y),用空格隔开! ps: x为纵,y为横" << endl;
cout << "\t\t\t";
cin >> x >> y;
while (cin.fail() || x< || x >= || x >= max_X || y >= max_Y) //读取输入失败或者输入不在指定范围
{
system("cls");
mymap.print();
cout << "\t 请输入坐标(x,y),用空格隔开! ps: x为纵,y为横" << endl;
cout << "\t\t输入有误,请重新输入!" << endl;
cin.clear(); //更改cin状态标示符
cin.ignore(); //清空输入缓冲区
cout << "\t\t\t";
cin >> x >> y;
}
if (!mymap.dig(x, y))
{
system("cls");
mymap.print(); //亦可选择print_end()函数,将所有位置打印出来
cout << "\t\t你踩中了(" << x << "," << y << ")" << endl;
cout << "\t\t恭喜客官中雷,慢走不送了,您吶!" << endl;
break;
}
system("cls"); //清屏,很好用的感觉
}
cout << "\t\t查看所有雷的位置 y/n ? ";
char c;
cin.get(); //读取回车
if (cin.get(c) && c == 'y')
{
system("cls");
mymap.print_end();
}
cout << "\t\t";
return ;
}
另外VS中的
stdafx.h
等文件请自行添加。
结语
本来想写一个控制台的RPG游戏,有等级、经验、地图、道具、材料、装备、NPC等元素的勇士屠龙然后变成龙的RPG游戏的。但是大概很多个小时之后,发现很多地方的设计有问题。所有暂时搁置,等到先把 Bjarne Stroustrup 大佬的 The C++ programming language 看完了之后再来写。所以今天先写了一个扫雷,大概写了两个小时,调Bug又花了两个小时,然后控制格式逻辑什么的又花了大概两个小时。一天就这么就过去了,其实说到底就是一个广度优先搜索而已。以后要提高效率。
源码
Github,直接下载源码。