C++图形化扫雷

    扫雷主体部分的c++代码实现,以及Qt的简单介绍,并且通过Qt来实现扫雷游戏的图形化,鼠标点击时间的判断,信号与槽的使用,以及游戏难度的选择以及自定义和记录时间功能的添加。

一、主体函数的C++实现

1.游戏的初始化

最简单的扫雷游戏是一个9×9的方块,因此,我们可以建立一个9×9的二维数组,先对其进行初始化,然后,在其中存储相关的数据。

1
2
3
4
#define ROW 9
#define COL 9
int gamedata[ROW][COL]; //记录原始数据
memset(gamedata, 0, sizeof(gamedata));

不同状态的记录,扫雷游戏每一个小块的状态可以对应表示成的不同的数字。

1
2
3
//0~8 该方块周围的雷的数量
//88 该方块为雷
//777 该方块处于待点击状态

雷的生成,简单的扫雷游戏中,会在9×9的81方块中随机生成10个雷,转化成计算机的语言就是,从1~81中任意取10个数,但是这样的选取有一定的缺点,在我们取出随机数之后,我们很难将随机数与相应的雷产生联系,带来了不必要的麻烦,解决方案是,我们可以生成十组横纵坐标1~9之间的随机数(避免重复的情况),这样下来,随机数的选取和雷的位置对应能够更加的简单,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define BOOM_NUM 10
int boom_num_now = 0; //已经生成的雷的数量
while (boom_num_now != BOOM_NUM)
{
//这里随机数使用了QRandomGenerator头文件中的函数
int x=QRandomGenerator::global()->bounded(0,ROW);
int y=QRandomGenerator::global()->bounded(0,COL);
if (gamedata[x][y] == 88)
continue;
else
gamedata[x][y] = 88;
boom_num_now++;
}

每一个方块周围雷数量的统计,通过一个循环来计数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int tmp;
for (int i = 0; i < ROW; i++)
for (int j = 0; j < COL; j++)
{
tmp = 0;
if (gamedata[i][j] == 88)
continue;
for (int dx = i - 1; dx <= i + 1; dx++)
for (int dy = j - 1; dy <= j + 1; dy++)
{
if (dx >= 0 && dy >= 0 && dx < ROW && dy < COL && gamedata[dx][dy] == 88)
tmp++;
}
gamedata[i][j] = tmp;
}

生成一个向玩家展示的数组,来记录每一点击产生的结果,如下所示:

1
2
3
4
int view[ROW][COL];
for (int i = 0; i < ROW; i++)
for (int j = 0; j < COL; j++)
view[i][j] = 777;

2.鼠标点击事件的判断

鼠标点击一个方块,即选择了对应左边的数据,如果选中了雷,那么游戏结束,如果选中了其他方块,则需要判断其周围有没有雷,以此来翻开其它的方块,其本质是一个深度搜索(DFS),我们可以通过递归来完成这个过程,具体的思想是:

先通过循环判断点击方块一周的方块是否有雷,如果没有雷的话,就相当于我们点击了周围一圈的方块,可以直接递归调用该函数,递归返回的条件分为两种,一种是该方块已经翻开,一种是该方块周围有雷的存在,具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void dfs(int x, int y)
{
int flag = 0;
if (view[x][y] != 777)
return;
view[x][y] = gamedata[x][y];
for (int dx = x - 1; dx <= x + 1; dx++)
for (int dy = y - 1; dy <= y + 1; dy++)
{
if (dx >= 0 && dx < ROW && dy >= 0 && dy < COL)
if (gamedata[dx][dy] == 88)
{
flag = 1;
break;
}
}
if (flag == 0)
{
for (int dx = x - 1; dx <= x + 1; dx++)
for (int dy = y - 1; dy <= y + 1; dy++)
if (dx >= 0 && dx < ROW && dy >= 0 && dy < COL)
dfs(dx, dy);
}
}

3.游戏胜利的条件判断

扫雷游戏的胜利条件判断较为简单,如果剩余的没有点击的方块数量(没有点击或者已经被标记)和雷数相同,就表示游戏取得了胜利,代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool whetherwin()
{
int temp = 0;
for (int i = 0; i < ROW; i++)
for (int j = 0; j < COL; j++)
{
if (view[i][j] == 777)
temp++;
}
if (temp == BOOM_NUM)
return true;
else
return false;
}

至此,就完成了整个扫雷游戏的主体函数部分,再加上输入输出函数,和一些额外条件的判断,就可以得到一个基本的命令行版本的扫雷游戏。

二、从C++过渡到Qt

1.窗口的搭建

打开Qt creator,新建一个Qt Widgets Application,点击mainwindow.ui进入设计界面,通过拖动左栏的窗口控件,完成图形化窗口的搭建。

2.扫雷界面的绘制

QPixmap头文件中主要用来绘图的函数是drawPixmap(),根据Qt官方文档的描述,drawPixmap(),有多种重载函数,由于为扫雷游戏准备的资源文件已经是确定了像素的,只需要对绘图的位置和不同的图片进行选择,所以选择以下所示的重载函数。

1
inline void QPainter::drawPixmap(int x, int y, const QPixmap &pm)// x,y表示需要绘图位置的坐标,pm是需要绘制的图片{    drawPixmap(QPointF(x, y), pm);}

使用循环,在paitEvent()内调用drawPixmap()函数即可绘制主要的窗口:

1
2
3
4
5
6
7
8
#define BLOCK_HEIGHT 30
#define BLOCK_WIDTH 20
#define MENU_WIDTH 26+40
QPainter painter(this);for (int i = 0; i < ROW; i++)
{
for (int j = 0; j < COL; j++)
painter.drawPixmap(i * BLOCK_HEIGHT, MENU_WIDTH + j * BLOCK_WIDTH, view[i][j]);
}

3.鼠标点击事件的具体实现

根据Qt官方文档,和鼠标点击相关的头文件是QMouseEvent,官方文档内,对获取鼠标点击位置的描述函数是:

1
2
inline int x() const { return qRound(l.x()); }
inline int y() const { return qRound(l.y()); }

这样得到的位置坐标是对应的像素坐标,我们知道每一个方块的边长,通过取余数的方式可以获得具体的在扫雷游戏中的坐标

1
2
int x = event->x() / BLOCK_WIDTH;
int y = (event->y() - MENU_WIDTH) / BLOCK_HEIGHT; //需要剪掉菜单栏的距离

在实际游戏时,鼠标左右键点击的效果不同,QMouseEvent中对鼠标点击事件的描述是

1
inline Qt::MouseButton button() const { return b; }

该函数会返回一个枚举类型,对应着不同的鼠标点击,具体的实现如下:

1
2
if (event->button() == Qt::LeftButton) {}
else if (event->button() == Qt::RightButton) {}

4.点击数字功能实现

Windows平台的扫雷游戏中,用户可以点击已被翻开的数字方块,如果周围所有的雷已被标记,则可以直接翻开未被标记的方块,如果标记雷的数量是正确的,但是标记的具体方块是错误的,则会直接导致游戏结束,如果已经标记的雷的数量和点击数字方块的数字不同,则直接等待下一次用户的操作。

实现该功能的思路是,先判断鼠标左键点击的位置,得到该方块周围雷的数量,然后循环周围已经被标记的雷的数量,以及标记的是否正确,如果标记错误,则可以直接触发游戏结束命令,如果标记正确且和对应的雷数量相同,则调用上文已经叙述过的dfs()函数,翻开方块,代码的具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
else if (event->button() == Qt::LeftButton && mine->view[x][y] != 777 && mine->view[x][y] != -1)
{
int aim = mine->view[x][y];
int temp_flag = 0;
int error = 0;
for (int dx = x - 1; dx <= x + 1; dx++)
for (int dy = y - 1; dy <= y + 1; dy++)
if (dx >= 0 && dx < mine->getrow() && dy >= 0 && dy < mine->getcolumn())
{
if (mine->view[dx][dy] == -1 && mine->gamedata[dx][dy] != 88)
error = 1;
if (mine->view[dx][dy] == -1)
{
temp_flag++;
}
}
if (temp_flag == aim)
{
if (error == 1)
{
gameover = true;
runtime->stop();
gamestart = 1;
alreadyflag = 0;
for (int i = 0; i < mine->getrow(); i++)
for (int j = 0; j < mine->getcolumn(); j++)
{
if (mine->view[i][j] == -1 && mine->gamedata[i][j] != 88)
mine->gamedata[i][j] = -777;
}
repaint();
return;
}
for (int dx = x - 1; dx <= x + 1; dx++)
for (int dy = y - 1; dy <= y + 1; dy++)
if (dx >= 0 && dx < mine->getrow() && dy >= 0 && dy < mine->getcolumn())
{
if (mine->view[dx][dy] == 777)
{
dfs(dx, dy);
repaint();
}
}
}
}

5.程序的执行步骤

  • 用户开始选择开始游戏,程序自动产生两个数组:gamedataview
  • 程序自动给gamedata进行初始化,view的值则全部设置成为UNKNOWN,即尚未打开状态,程序根据数组view,将当前的游戏状态通过drawPixmap()绘制到窗口上
  • 用户点击窗口,触发QMouseEvent,进入函数进行相应的判断,根据鼠标的左右键点击的不同,进入不同的分支
  • 同时每次触发完鼠标点击时间之后,都进行一次是否获胜的判断。同时,定义bool类型的全局变量gameover,如果游戏已经失败或者已经z取胜,则将gameover设置为true,不再进入鼠标点击时间的判断。

三、额外功能的实现

1.不同难度的选择和自定义

  • 传统扫雷游戏可以选择不同的难度,此次实现的扫雷游戏应该也有这个功能。为了实现这一功能,可以定义一个block类,以此来储存不同情况下的横宽以及雷的数量,同时,由于不同的情况下,gamedataview的初始化不同,所以游戏的初始化也应该放在block类中完成。

  • block类中主要定义了:扫雷游戏的长,扫雷游戏的高,扫雷游戏雷的数量,当前已经经过的时间。同时上述的成员均为private成员,所以同时需要定义public的成员函数,来一一对应,获得上述对象的值。

  • 在整个游戏开始时,先建立一个block类的指针,其中第一次进入消息循环时,使用初级难度作为初始化的值、在用户选择不同难度的时侯,可以通过传给指针不同的地址,来进行处理。

  • 首先类需要处理的是游戏的初始化,由于需要根据类的构造函数来创建数组,所以显然需要动态申请内存空间,这里可以在类中定义二维指针,然后通过new来动态开辟内存空间,具体实现如下(这里只展示其中一个数组的初始化方法,另外一个数组的初始化方法完全相同):

    1
    2
    3
    4
    5
    6
    7
    8
    view = new int *[row];for (int i = 0; i < row; i++)    
    view[i] = new int[column];
    for (int i = 0; i < row; i++)
    for (int j = 0; j < column; j++)
    {
    gamedata[i][j] = 0;
    view[i][j] = 777;
    }
  • 实现用户自定义游戏难度即是要接收用户输入的数据,可以通过新建一个窗口来接收数据,但在接收数据的途中,需要注意用户输入数据的合理性。行和列数不能为负或者过大,以及用户输入的雷的数量不能多于用户输入的行列数乘积,否则会造成非法访问的问题,导致程序异常退出。

2.剩余雷数量的计算以及呈现

全局变量alreadyflag来记录已经被鼠标左键标记的雷的数量,同时每次检测到鼠标左键点击的时候,根据情况的不同,即时更改,同时,在每轮游戏结束时,将alreadyflag变量重新初始化。

雷的数量的绘制也应写在paintEvent()内部,获得剩余雷的数量之后,使用分支语句,直接将数字呈现给用户。需要注意的是,为了防止用户标记的雷的数量超过游戏本生雷的数量,所以在绘制前需要判断是否剩余未标记雷的数量为负值

3.游戏时间记录与绘制

Qt中与时间有关的头文件主要有QTimer,新建一个QTimer对象,通过调用start()函数控制Qt计时器的开始以及时间间隔,通过connect()函数,连接timeout()信号与自定义的槽函数,随着时间的变化执行相应的不同操作。

记录时间变化的槽函数可以使用block类的成员函数,在block类中初始化一private类型的时间变量,每次调用槽函数使该变量递增,即可记录当前时间,具体的代码实现如下:

1
2
3
4
5
6
7
//mainwindow.cppblock 
*mine = new block(ROW, COLUMN, BOOM_NUM);runtime = new QTimer(this);connect(runtime, SIGNAL(timeout()), this, SLOT(on_secondadd()));connect(runtime, SIGNAL(timeout()), this, SLOT(update()));
void MainWindow::on_secondadd()
{
mine->addtime();
}
//block.cppvoid block::addtime(){ time_now++;}//时间绘制部分与雷数量呈现部分相同

四、源码及素材来源

        源码
        扫雷图片素材来源