扫雷游戏是一款十分经典的单机小游戏。在nn行mm列的雷区中有一些格子含有地雷(称之为地雷格),其他格子不含地雷(称之为非地雷格)。玩家翻开一个非地雷格时,该格将会出现一个数字——提示周围格子中有多少个是地雷格。游戏的目标是在不翻出任何地雷格的条件下,找出所有的非地雷格。
如果Easyx.h函数不明确的话,可以到EasyX Graphics Library for C++这个网址进行查看手册。
此游戏纯属个人练习记录,请大佬飘过~
扫雷游戏是在一堆格子里去排查,格子的话我们可以定义成二维数组的形式,其中这些格子包括雷,和含有多少雷的信息。如下图第一张所示,左上角就是我含有雷的个数,第二张图片上数字部分是它的周围含有几个雷。
int main()
{
// 创建扫雷游戏的数据
int map[ROW][COL] = { 0 };
return 0;
}
我们使用二位数组来设置一个存放 (行)(列)的雷的棋盘,ROW和COL是代表行数和列数,#define 定义出来的。
// 显示地图函数实现。
void ShowMap(const int map[ROW][COL])
{
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
printf("%2d ",map[i][j]);
}
printf("\n");
}
}
要注意的就是打印时需要注意一下打印格式。思路就是两层的For遍历出棋盘的数据。
布置雷,我们可以想象雷的数据设置成-1,当然雷的位置肯定是随机的,所以这里我们需要调用srand()和rand这两个C语言标准库函数进行随机给值,对给定的值进行处理。
// 放置雷的函数实现
void BuryMine(int map[ROW][COL], const int mineCount)
{
int i = 0;
while (i < mineCount)
{
// 产生 随机的 x,y坐标,x限制在ROW内,y限制在COL内。
int x = rand() % ROW;
int y = rand() % COL;
if (map[x][y] == 0)
{
map[x][y] = LABLE_MINE;
i++; //这里只有设置成功一个雷,数量才会减少,保证必须能够设置10个雷。
}
}
}
当然布置完雷还是不行的,我们需要对期周边的数据做提示处理,我们可以看图和看代码,分析其含义,其实就是在雷位置的九宫格周边提示出附近有多少个雷。
在布置雷的时候还需要注意的就是二位数组的越界问题。
// 模拟提示:找到每一个雷周围九宫格加 1 函数实现。
void BuryMineHint(int map[ROW][COL])
{
// 如何做呢?
// 遍历数组
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
// 找到雷的位置
if (map[i][j] == LABLE_MINE)
{
//根据雷周围的坐标:
//遍历范围:x-1 ~ x+1 y-1 ~ y+1
for (int x = i - 1; x <= i + 1; x++)
{
for (int y = j - 1; y <= j + 1; y++)
{
// 排除雷本身,排除越界。
if ((x>=0 && x=0 && y
这是控制台的打印结果:
程序走到这里,我们看一下整体的Main程序中是如何写的?
// 扫雷游戏函数声明。
void MineClearanceGame()
{
// 设置随机数种子 - 利用时间戳。
srand((unsigned)time(NULL));
// 创建扫雷游戏的数据
int map[ROW][COL] = { 0 };
// 放置雷 - 给二维数组中增加 “-1” ,-1就代表是类。
BuryMine(map, MINE_COUNT);
// 模拟提示:找到每一个雷周围九宫格加 1 函数声明。
BuryMineHint(map);
// 打印扫雷地图。
ShowMap(map);
}
上面雷的数据就已经设定好了,可是是运行在控制台中的啊,那么接下来就利用Esayx这个图形库来把数据进行覆盖!
在做贴图之前,我们先把上面的两个函数进行封装一下,因为不管是设置雷,还是让雷周围的九宫格显示出雷的数量,都是属于初始化的范围,所以我们给他封装 -------InitMine() 函数
// 初始化雷函数实现。
void InitMine(int map[ROW][COL], const int mineCount)
{
int count = 0;
while (count < mineCount)
{
// 产生 随机的 x,y坐标,x限制在ROW内,y限制在COL内。
int x = rand() % ROW;
int y = rand() % COL;
if (map[x][y] == 0)
{
map[x][y] = LABLE_MINE;
count++; //这里只有设置成功一个雷,数量才会减少,保证必须能够设置10个雷。
}
}
// 让雷周围的九宫格自己计算出,以自身为中心雷的个数。
// 如何做呢?
// 遍历数组
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
// 找到雷的位置
if (map[i][j] == LABLE_MINE)
{
//根据雷周围的坐标:
//遍历范围:x-1 ~ x+1 y-1 ~ y+1
for (int x = i - 1; x <= i + 1; x++)
{
for (int y = j - 1; y <= j + 1; y++)
{
// 排除雷本身,排除越界。
if ((x >= 0 && x < ROW && y >= 0 && y < COL) && map[x][y] != LABLE_MINE)
{
map[x][y]++;
}
}
}
}
}
}
}
// 因为数据与照片会进行关联,所以我们将数据进行偏移一下。
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
map[i][j] += 20;
}
}
// 数据进行对比:
接下来我们开始编写贴图的函数,提前准备了一些照片。
1、加载照片
IMAGE img[12] = {0};// 主函数定义
// 加载贴图函数实现。
void LoadImg(IMAGE img[])
{
// 加载本地图片
for (int i = 0; i < 12; i++)
{
char imgName[20] = { 0 };
sprintf(imgName, "./images/%d.jpg", i);
loadimage(img + i, imgName, IMG_WIDTH, IMG_HEIGHT);
}
}
以上是加载照片的代码,一共11张照片存入到数组中,这里有一个小技巧,我们利用sprintf将路径存入到imgName这个数组中,这样我们可以直接调用,就不需要一个一个打了。
2、贴图
贴图可以对照画图分析:
// 贴图函数实现。
void DrawMap(int map[ROW][COL], IMAGE img[])
{
// 贴图
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
if (map[i][j] >= 0 && map[i][j] <= 8) // 贴提示数字
{
putimage(j * IMG_WIDTH, i * IMG_HEIGHT, img + map[i][j]);
}
else if (map[i][j] == -1) // 贴雷
{
putimage(j * IMG_WIDTH, i * IMG_HEIGHT, img + 9);
}
else if (map[i][j] > 9 && map[i][j] <= 28) // 贴未打开的图。
{
putimage(j * IMG_WIDTH, i * IMG_HEIGHT, img + 10);
}
else if (map[i][j] > 28)
{
putimage(j * IMG_WIDTH, i * IMG_HEIGHT, img + 11); // 贴标记。
}
}
}
}
// 根据提供的数据进行贴图,我们可以看一下效果。
函数完成说表点击左键,就代表翻牌子,鼠标点击右键就代表标记雷,,值得注意的是,在雷盘上是没有坐标的只有鼠标的位置坐标,我们要找到每一个小方框的位置就需要求行(row),列(col)。求取方式其实和上面的贴图的思路差不多,就是反过来求解:
// 鼠标操作函数实现。
void MouseEvent(int map[ROW][COL])
{
// 判断鼠标是否有动作。
if (MouseHit())
{
// 获取鼠标事件。
MOUSEMSG msg = GetMouseMsg();
int row = msg.y / IMG_WIDTH;
int col = msg.x / IMG_HEIGHT;
// 如果是点击左键。
if (msg.uMsg == WM_LBUTTONDOWN)
{
// 是没有点击过的数据才能进行贴图。
if (map[row][col] >= 19 && map[row][col] <= 28)
{
map[row][col] -= 20;
ShowMap(map);// 控制台查看结果。
}
}
// 如果是雷可以进行标记。
else if (msg.uMsg == WM_RBUTTONDOWN)
{
if (map[row][col] >= 19 && map[row][col] <= 28) // 如果没有点击也没有标记过就进行标记。
{
map[row][col] += 20;
ShowMap(map);// 控制台查看结果。
}
else if (map[row][col] > 28) // 如果已经标记了,就取消标记。
{
map[row][col] -= 20;
ShowMap(map);// 控制台查看结果。
}
}
}
}
// 查看实际效果:
先看一下效果图
当鼠标点击完成狗row和col的坐标是进行保留的,我们可以对这个坐标进行判断,用递归的只是进行扎金花。
// 炸开周围空白的区域函数实现 - 递归。
void OpenNull(int map[ROW][COL], int row, int col)
{
// 判断鼠标点击后的数值是不是好的区域。
if (map[row][col] == 0)
{
// 周围的每一个都要进行越界,并且要排除越界的问题。
for (int i = row-1; i <= row+1; i++)
{
for (int j = col-1; j <= col+1; j++)
{
// 判断如果周围的各自没有被打开,就将他打开。
if ((i >= 0 && i < ROW && j >= 0 && j < COL) && map[i][j] > 19 && map[i][j] <= 28)
{
map[i][j] -= 20;
OpenNull(map,i,j);
}
}
}
}
}
判断游戏,需要每次单机鼠标后,我们就进行判断一下,我们就可以封装一下函数了。
// 判断游戏用过还是失败!
char judge(int map[ROW][COL], int row, int col)
{
// 如果猜到雷了
if (map[row][col] == -1)
{
// 将所有的雷都打开。
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
// 第一种情况的雷:雷没有打开,也没有被标记。
if (map[i][j] == 19)
{
map[i][j] -= 20;
}
else if (map[i][j] == 39)
{
map[i][j] -= 40;
}
}
}
return 'n';
}
// 把所有的区域点开,那就说明游戏胜利了
// 判断数值计算。
int count = 0;
// 判断count的数量
for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
{
if (map[i][j] >= 0 && map[i][j] <= 8)
{
count++;
}
}
}
if (count == ROW * COL - MINE_COUNT)
{
return 'y';
}
return 0;
}
最后我们来给游戏添加的音乐的色彩,其实也很简单。
// 就是在我们想要的地方增加音乐播放的库程序。
mciSendString("close rightClick", NULL, 0, NULL);
mciSendString("open ./images/rightClick.wav alias rightClick", NULL, 0, NULL);
mciSendString("play rightClick", NULL, 0, NULL);
至此这个游戏就已经完成了,当然肯定还有更多优化的地方:比如说来一个菜单?开具的时候设置雷的数量?挑战行和列的数量?
我们演示一下效果吧~