目录
目录
一、class="superseo">扫雷游戏规则说明
二、关键数据结构说明
三、游戏实现
1. 初始化游戏
2.游戏绘制
2.1 绘制背景框
2.2 绘制方格状态
2.3 绘制提示方框
3. 玩家排雷
3.1 鼠标左点击
3.2 鼠标右点击
3.3 鼠标双击
3.4 鼠标移动事件
3.5 点击方格函数说明
4. 定时任务
5.游戏获胜
四、源码地址
扫雷游戏是在windows系统中自带的游戏,他的游戏玩法其实很简单,20世纪初期电脑上没有啥游戏可以玩,所以扫雷游戏也算是当时是最常火爆的一个电脑游戏。
以下文章详细介绍扫雷游戏的实现。
实现语言为C++。
一、扫雷游戏规则说明
- 游戏的难度有三类:简单、中等、困难。
- 简单:8*8的格子一共10颗雷
- 中等:16*16的格子一共40颗雷
- 困难:30*16的格子一共99颗雷
- 初始化时所有的信息都隐藏,为空白格子(蓝色)
- 左键点击格子,如果格子非雷,那么直接显示数字,数字表示周围8个方位的雷的总数,如果该数字为0那么自动点开起周围格子;如果点击的空格是雷,那么游戏结束,显示所有的雷的位置。
- 右键点击空白格子,格子标上小旗,再次右键小旗,格子修改为问号,再次右键,回复为空白格子。
- 左键点击在小旗上无反应,左键点击在问号上同点击在空白格子行为一致。
- 左键双击在数字上,如果在该数字上的小旗数量与数字相同,自动帮助点击所有非标记的空白格子。
(注,如果判断失误,空白格子中含雷,游戏结束)
- 右键在数字上,将该数字对应所有空白格的颜色变浅。
(用于提示)
二、关键数据结构说明
我们可以认为扫雷游戏中的每一个方块都是一个cell。
他保存了方块的所有信息。
我们通过这些信息来绘制这些方格cell。
这里的x、y坐标并不是鼠标的坐标。
他是一个转换值。
下图可以看到他的标识,针对与屏幕坐标转化到方块坐标可以通过公式得到。
数据结构如下:
struct Block {
int x; // x 坐标
int y; // y 坐标
};
enum LeiStatus { isNum = 0, isLei };
enum ShowStatus {
hideNum = 0, // 最原始状态
showNum, // 点击展示其数字,如果点击的为雷那么游戏就直接结束了。
isOk, // 确认这个是雷,即给这个格子标上小旗
isAsk // 不确定这个是什么,给这个格子标上问号
};
/**
* @brief The Cell 每一个格子的结构体
*/
struct Cell : Block {
// 雷的状态,所有的格子有只有两种状态,雷或者是数字。
LeiStatus leiStatus;
// 显示状态,显示、隐藏、标记为小旗、标记为问号
ShowStatus showStatus;
int num; // 表示每个位置的上下左右八个位置的雷的数据,如果雷的状态是
Cell()
{
leiStatus = isNum;
num = 0;
showStatus = hideNum;
}
};
ShowStatus显示状态
hideNum状态:为最初始状态,如下图:
showNum状态:为点开后展示数字的状态。
一下四个格子都是showNum状态。
isOK状态:标识用户确定他是雷,也就是给他标注了小旗。
isAsk状态: 标识模棱两可的状态,也就是问号状态。
LeiStatus状态
记录了方格的性质,他只有两种性质、雷或者数字。
三、游戏实现 1. 初始化游戏
棋盘的初始化可以通过二维数组进行存储,但是我使用一位数组代替二维数组的方式进行。
他们的转化公式如下:
int QGameWidget::getIndex(int block_x, int block_y)
{
if (block_x < 0 || block_y < 0) return -1;
if (block_x >= m_max_block_x || block_y >= m_max_block_y) return -2;
return block_x * m_max_block_x + block_y;
}
先判断横纵坐标是否已经超出了规定范围,然后再使用x*最大x+y的方式转化为二维数组。
那么初始化的时候就方便很多。
初始化代码如下:
void QGameWidget::initLei()
{
// 清空所有的格子
m_cells.clear();
// 初始化小旗的数量。
m_flags = 0;
for (int i = 0; i < m_max_block_y; i++) {
for (int j = 0; j < m_max_block_x; j++) {
Cell lei;
lei.x = i;
lei.y = j;
m_cells.push_back(lei);
}
}
// 初始化雷的位置
int sum = 0;
int size = m_cells.size();
do {
int ran = getRandom(size);
if (m_cells.at(ran).leiStatus != isLei) {
sum++;
m_cells[ran].leiStatus = isLei;
}
} while (sum < m_sum_lei);
// 初始化方格数据
for (int i = 0; i < m_cells.size(); i++) {
// 如果这个位置是雷就不需要统计了
if (m_cells[i].leiStatus == isLei) continue;
int sum = sumLeiStatus(m_cells[i].x, m_cells[i].y, isLei);
m_cells[i].num = sum;
}
}
初始化整体流程如下:
统计当前坐标的雷数量
当前坐标的周围坐标其实就是,遍历(x-1,x,x+1)/(y-1,y,y+1) 需要剔除在当前坐标的情况。
int QGameWidget::sumLeiStatus(int block_x, int block_y, LeiStatus leiStatus)
{
int sum = 0;
int b_x = block_x;
int b_y = block_y;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) {
continue;
}
int index = getIndex(b_x - i, b_y - j);
if (index < 0) {
continue;
}
if (m_cells.at(index).leiStatus == leiStatus) {
sum++;
}
}
}
return sum;
}
《扫雷》游戏初始化数据算法分析_啊渊的博客-CSDN博客
2.游戏绘制游戏绘制其实是将表格中的数据根据方格的状态进行绘制。
根据关键数据结构我们应该很方便的知道应该如何绘制我们的内容。
QT的绘制在paintEvent中进行绘制,我们在更新数据以后可以使用update函数触发页面刷新。
paintEvent由三部分组成,绘制背景框,绘制雷,绘制提示内容。
void QGameWidget::paintEvent(QPaintEvent *event)
{
paintBackground(event);
paintLei(event);
paintTip();
}
2.1 绘制背景框
背景框的绘制最简单其实就是绘制最外一层的框。
void QGameWidget::paintBackground(QPaintEvent *)
{
QPainter painter(this);
painter.drawRect(0, 0, this->width() - 2, this->height() - 2);
}
2.2 绘制方格状态
m_max_block_x:最大的横坐标数量,这里使用变量,因为用户可以自定义数量
m_max_block_y:最大的纵坐标数量。
根据状态绘制。
状态showNum: 如果该状态已经被点开那么显示其数字。
painter.setBrush(Qt::white);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
if (lei->leiStatus == isNum) {
painter.drawText(
i * BLOCK + 2 + BLOCK / 2 - fm.width("1") / 2,
j * BLOCK + 2 + BLOCK / 2 - fm.height() / 2 - 1,
fm.width("1"), fm.height(), 1,
QString::number(lei->num));
}
BLOCK 为每一个方格的宽度,drawRect(x,y,width,height);
首先计算坐标(i*BLOCL +2 ,j*BLOCL +2)
状态isOk:状态为标识小旗状态,因此将其绘制小旗
painter.setBrush(Qt::white);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
painter.drawImage(QRectF(i * BLOCK, j * BLOCK, BLOCK, BLOCK),
QImage(":/resoures/flag.png"));
状态isAsk:状态为问号状态,绘制为以下代码
painter.setBrush(Qt::blue);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,BLOCK - 4);
painter.drawText(
i * BLOCK + 2 + BLOCK / 2 - fm.width("?") / 2,
j * BLOCK + 2 + BLOCK / 2 - fm.height() / 2 - 1,
fm.width("?"), fm.height(), 1, "?");
状态 hideNum:为初始化状态,但是如果游戏结束了那么需要将所有的雷绘制出来。
painter.setBrush(Qt::blue);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,BLOCK - 4);
if (m_gameStatus == gameOver && lei->leiStatus == isLei) {
painter.drawImage(QRectF(i * BLOCK, j * BLOCK, BLOCK, BLOCK),
QImage(":/resoures/lei.png"));
}
效果如下图:
整体绘制逻辑如下:
void QGameWidget::paintLei(QPaintEvent *)
{
QPainter painter(this);
QFont font;
QFontMetrics fm(font);
for (int i = 0; i < m_max_block_x; i++) {
for (int j = 0; j < m_max_block_y; j++) {
Cell *lei = getCell(i, j);
if (lei->showStatus == showNum) {
painter.setBrush(Qt::white);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
if (lei->leiStatus == isNum) {
painter.drawText(
i * BLOCK + 2 + BLOCK / 2 - fm.width("1") / 2,
j * BLOCK + 2 + BLOCK / 2 - fm.height() / 2 - 1,
fm.width("1"), fm.height(), 1,
QString::number(lei->num));
}
} else if (lei->showStatus == isOk) {
painter.setBrush(Qt::white);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
painter.drawImage(QRectF(i * BLOCK, j * BLOCK, BLOCK, BLOCK),
QImage(":/resoures/flag.png"));
} else if (lei->showStatus == hideNum) {
painter.setBrush(Qt::blue);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
if (m_gameStatus == gameOver && lei->leiStatus == isLei) {
painter.drawImage(
QRectF(i * BLOCK, j * BLOCK, BLOCK, BLOCK),
QImage(":/resoures/lei.png"));
}
} else if (lei->showStatus == isAsk) {
painter.setBrush(Qt::blue);
painter.drawRect(i * BLOCK + 2, j * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
painter.drawText(
i * BLOCK + 2 + BLOCK / 2 - fm.width("?") / 2,
j * BLOCK + 2 + BLOCK / 2 - fm.height() / 2 - 1,
fm.width("?"), fm.height(), 1, "?");
}
}
}
}
2.3 绘制提示方框
绘制效果如下:
当鼠标按下时将对应坐标点的周围坐标点绘制为浅蓝色。
代码如下:
void QGameWidget::paintTip()
{
QPainter painter(this);
if (m_isClickLeftButton) {
if (m_currentSelectCell != nullptr) {
int b_x = m_currentSelectCell->x;
int b_y = m_currentSelectCell->y;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (0 == i && 0 == j) continue;
int index = getIndex(b_x + i, b_y + j);
if (index < 0) continue;
if (m_cells.at(index).showStatus == hideNum) {
painter.setBrush(QBrush(QColor(193, 210, 240)));
painter.drawRect((b_x + i) * BLOCK + 2,
(b_y + j) * BLOCK + 2, BLOCK - 4,
BLOCK - 4);
}
}
}
}
}
}
m_currentSelectCell为当前点击的坐标3. 玩家排雷 3.1 鼠标左点击
鼠标左点击,在方格的状态为初始化时候会点开放个展示其数字,如果方格为雷那么游戏结束。
1. 通过屏幕坐标转为游戏方格坐标。
2.如果为左键那么记录当前方格的选中状态以及鼠标状态。
3.点开方格。
clickCell函数
void QGameWidget::mousePressEvent(QMouseEvent *event)
{
// 获取鼠标的坐标点。
int mouse_x = event->x();
int mouse_y = event->y();
if (m_gameStatus == gameInit) {
emit sigGameStart();
m_gameStatus = gameStar;
}
if (m_gameStatus == gameOver) {
return;
}
Block block;
// 通过鼠标的坐标点获取方格的坐标点
if (!getBlockPoint(mouse_x, mouse_y, &block)) {
return;
}
qInfo() << "===================" << event->button();
// 获取根据坐标点获取方格的信息
Cell *cell = getCell(block.x, block.y);
if (event->button() == Qt::LeftButton) {
Block block;
getBlockPoint(mouse_x, mouse_y, &block);
// 记录当前选中的方格
m_currentSelectCell = getCell(block.x, block.y);
// 记录鼠标的点击状态
m_isClickLeftButton = true;
if (!(hideNum == cell->showStatus || isAsk == cell->showStatus)) {
update();
return;
}
clickCell(block.x, block.y);
qInfo() << "===================";
} else if (event->button() == Qt::RightButton) {
qInfo() << "==================aa=";
if (cell->showStatus == hideNum) {
if (m_flags < m_sum_lei) {
cell->showStatus = isOk;
m_flags++;
emit sigFlags(m_flags);
}
} else if (cell->showStatus == isOk) {
qInfo() << "ok";
cell->showStatus = isAsk;
m_flags--;
emit sigFlags(m_flags);
} else if (cell->showStatus == isAsk) {
cell->showStatus = hideNum;
}
isWin();
}
update();
}
3.2 鼠标右点击
记录小旗的数量,并且进行统计。
qInfo() << "==================aa=";
if (cell->showStatus == hideNum) {
if (m_flags < m_sum_lei) {
cell->showStatus = isOk;
m_flags++;
emit sigFlags(m_flags);
}
} else if (cell->showStatus == isOk) {
qInfo() << "ok";
cell->showStatus = isAsk;
m_flags--;
emit sigFlags(m_flags);
} else if (cell->showStatus == isAsk) {
cell->showStatus = hideNum;
}
isWin();
3.3 鼠标双击
双击仅当为方格为数字,并且数字与周围的小旗一致时自动点击周围的方格。
中间的3旁边有3个小旗,因此可以通过双击中间的三来自动点开右上角的数字。
void QGameWidget::mouseDoubleClickEvent(QMouseEvent *event)
{
if (m_gameStatus == gameOver) {
return;
}
// 获取当前的内容
int mouse_x = event->x();
int mouse_y = event->y();
Block block;
bool ret = getBlockPoint(mouse_x, mouse_y, &block);
if (ret) {
// 统计当前的标识类的数量
Cell *cell = getCell(block.x, block.y);
if (nullptr == cell) return;
// 统计被标记为小旗的数量。
int sum = sumShowStatus(cell->x, cell->y, isOk);
if (sum != cell->num || cell->showStatus != showNum) {
return;
}
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (0 == i && 0 == j) continue;
if (getCell(cell->x + i, cell->y + j) != nullptr &&
getCell(cell->x + i, cell->y + j)->showStatus != isOk)
clickCell(cell->x + i, cell->y + j); // 右上
}
}
}
}
3.4 鼠标移动事件
记录当前被选中的方格的位置,用于现实提示。
void QGameWidget::mouseMoveEvent(QMouseEvent *event)
{
// 如果当前有见按钮
if (m_isClickLeftButton) {
int mouse_x = event->x();
int mouse_y = event->y();
Block block;
if (!getBlockPoint(mouse_x, mouse_y, &block)) return;
// 获取当前鼠标所在位置的方格。
m_currentSelectCell = getCell(block.x, block.y);
update();
}
}
3.5 点击方格函数说明
因为很双击以及单击都需要用到点开方格的逻辑因此将其抽象为函数。
《扫雷》游戏递归算法分析_啊渊的博客-CSDN博客_扫雷中的递归算法
1. 通过坐标获取雷的状态。
2. 如果方格数字为0那么需要使用递归的方式点开周围的方格。
void QGameWidget::clickCell(int block_x, int block_y)
{
Cell *cell = getCell(block_x, block_y);
if (nullptr == cell) return;
if (cell->leiStatus == isLei) {
// game over;
m_gameStatus = gameOver;
update();
return;
}
if (cell->num == 0) {
updateLei(block_x, block_y);
} else {
cell->showStatus = showNum;
}
isWin();
}
void QGameWidget::updateLei(int block_x, int block_y)
{
if (getIndex(block_x, block_y) < 0 ||
getIndex(block_x, block_y) >= m_max_block_x * m_max_block_y) {
return;
}
if (m_cells[getIndex(block_x, block_y)].showStatus == hideNum ||
m_cells[getIndex(block_x, block_y)].showStatus == isAsk) {
m_cells[getIndex(block_x, block_y)].showStatus = showNum;
if (m_cells[getIndex(block_x, block_y)].num == 0) {
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (!(i == 0 && j == 0))
updateLei(block_x + i, block_y + j);
}
}
}
}
return;
}
4. 定时任务
当游戏开始的时候启动定时任务,这里使用了QT的信号与槽的机制,将信号发送给界面,让其启动定时器。
绑定定时器
connect(m_timer, SIGNAL(timeout()), this, SLOT(onTimer()));
定时任务,时钟自动+1
void MainWindow::onTimer()
{
m_nTime++;
m_lcdNumber->display(m_nTime);
}
5.游戏获胜
《扫雷》游戏获胜算法分析_啊渊的博客-CSDN博客
bool QGameWidget::isWin()
{
int sum = 0;
for (int i = 0; i < m_cells.size(); i++) {
if (m_cells.at(i).showStatus == showNum) {
sum++;
} else if (m_cells.at(i).leiStatus == isLei &&
m_cells.at(i).showStatus == isOk) {
sum++;
}
}
if (sum == m_cells.size()) {
if (m_gameStatus != gameWin) {
emit sigGameWin();
m_gameStatus = gameWin;
}
return true;
} else {
return false;
}
}
四、源码地址
Minesweeper · master · 啊渊 / QT博客案例 · GitCode
git clone git@gitcode.net:arv002/qt.git
cd Minesweeper
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)