第三次实训工作报告

设计题目

华南理工大学:植此青绿

本周工作小结:

本周我主要进行了以下工作:

  1. 创建地图
    • 生成了足够数量的地图,并成功地存储在指定的文件路径下。
  2. 创建关卡对应按钮(后期可能会改为拖拽)
  • 创建4个点击按钮用于实现游戏对应的点击逻辑,后期考虑到用户的可玩性,会考虑改为拖拽。
  1. 实现核心算法
  • 边界判定:在玩家填充格子时,通过检查所选区域的底部行和右侧列来确保所选区域不超出拼图的边界。
  • 地图破解:参考隐藏地图的信息,我实现了玩家正确填充格子的功能。当玩家填充的地图与隐藏地图完全匹配时,即视为破解成功。

环境与工具

  • 开发环境:Qt Creator 13.0.0
  • 操作系统:Windows 11

前置工作

  1. 地图创建
    • 设计了不同的地图布局,确保游戏难度适中。
    • 地图存储在了指定的文件路径 "C:/cat/qqzl.txt" 下。
  2. 核心算法设计
    • 详细设计了边界判定和地图破解的算法逻辑,为后续的编程工作提供了清晰的方向。

程序功能说明

各模块功能
  • 选择网格大小: 根据玩家选择的网格大小更新游戏状态。
  • 点击拼图格子: 根据当前选择的网格大小修改相应区域的数字。

后期预备开启功能:

  • 开始游戏: 初始化计时器、隐藏地图和游戏区域。
  • 撤销操作: 撤销上一步的修改。
  • 提交游戏: 将游戏结果保存到文件中。
  1. 四个点击按钮的创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for (int i = 0; i < 4; ++i)
{
int gridSize =i+1; // 修改此处,使网格大小为 1x1、2x2、3x3、4x4

QPushButton *button = new QPushButton(QString::number(gridSize) + "x" + QString::number(gridSize), this);
button->setMinimumWidth(150); // 设置最小宽度为150
button->setMaximumWidth(150); // 设置最大宽度为150
button->setMinimumHeight(50); // 设置最小高度为50
button->setMaximumHeight(50); // 设置最大高度为50
connect(button, &QPushButton::clicked, this, [this, gridSize]() { on_GridSizeButton_clicked(gridSize); });
// 设置按钮之间的间距
gridSizeLayout->addSpacing(50); // 添加20像素的间距
gridSizeButtons[i] = button;
gridSizeButtons[i]->setStyleSheet(
"font-family: \"Microsoft YaHei\"; font-size: 28px; ");
gridSizeLayout->addWidget(button);
}
image-20240424223321584
  1. 地图有解判定
  • 边界判定:在填充格子时,需要确保所选区域不超出拼图的边界。这可以通过检查所选区域的底部行和右侧列来实现。
  • 地图破解:根据隐藏地图的信息,玩家需要正确地填充格子。当玩家的填充与隐藏地图完全匹配时,即为破解成功。
  • 点击拼图格子: 遍历点击的区域内的所有格子,将其数字增加1。

主要实现如下:

两个主要方法:一个用于判断点击以实现叠加,一个用于判断游戏是否胜利

on_PuzzleCell_clicked 方法

  1. 检查按钮点击状态:如果 isButtonClicked 标志变量为 false,则直接返回,不处理格子点击事件。
  2. 检查点击区域:计算点击区域的底部行和右侧列,并检查它们是否超出拼图边界。
  3. 更新单个格子或多个格子
    • 如果 currentGridSize1,则更新单个点击的格子的文本。
    • 否则,更新以点击格子为起点,大小为 currentGridSize 的区域内的所有格子的文本。
    • 重置按钮点击状态:将 isButtonClicked 重置为 false
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
void LevelOnepa::on_PuzzleCell_clicked(QLabel *cell, int clickedRow, int clickedCol)
{
if (!isButtonClicked)
{ // 检查标志变量状态
return; // 若未点击按钮,则不处理格子点击事件,直接返回
}
int bottomRow = clickedRow + currentGridSize - 1;
int rightCol = clickedCol + currentGridSize - 1;

if (bottomRow >= puzzleSize || rightCol >= puzzleSize)
{
// 点击区域超出了拼图边界,此次操作无效
QMessageBox::warning(this, tr("警告"), tr("您选择的区域超过了拼图边界!"));
return;
}
if (currentGridSize == 1) // 修正:当使用1x1按钮时,直接更新单个格子的文本
{
cell->setText(QString::number(cell->text().toInt() + 1));
}
else
{
for (int row = clickedRow; row < std::min(clickedRow + currentGridSize, 6); ++row) {
for (int col = clickedCol; col < std::min(clickedCol + currentGridSize, 6); ++col) {
QLabel *targetCell = puzzleCells[row * puzzleSize + col];
targetCell->setText(QString::number(targetCell->text().toInt() + 1));
}
}
}
isButtonClicked=false;//需要重置一下
}

on_GridSizeButton_clicked 方法

这个方法处理改变拼图大小按钮点击的事件。它执行以下操作:

  1. 检查所有按钮是否已用完:检查所有按钮是否已达到其最大使用次数。
  2. 比较拼图和隐藏地图:调用 comparePuzzleWithHiddenMap 方法比较玩家拼图与隐藏地图。其中隐藏地图为一开始我存储的地图,用于与玩家操作后的地图进行比较。
  3. 检查游戏状态
    • 如果所有按钮都已用完并且拼图与隐藏地图匹配,游戏成功。
    • 如果所有按钮都已用完但拼图与隐藏地图不匹配,游戏失败。
  4. 检查当前按钮是否已达最大使用次数:如果当前按钮已达到其最大使用次数,则直接返回。
  5. 设置当前网格大小:将 currentGridSize 设置为点击按钮的大小。
  6. 重置按钮点击状态:将 isButtonClicked 设置为 true
  7. 递增按钮使用计数:增加当前按钮的使用计数。
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
void LevelOnepa::on_GridSizeButton_clicked(int size)
{
bool allButtonsUsed = true;
for (int i = 1; i <= 4; ++i)
{
if (useCounts[i] < maxUseCounts[i])
{
allButtonsUsed = false;
break;
}
}
bool puzzleMatchesHidden = comparePuzzleWithHiddenMap();
if(allButtonsUsed && puzzleMatchesHidden)
{
isGameFinished = true;
showSuccessDialog(); // 显示成功对话框
timer->stop();
}
else if(allButtonsUsed && !puzzleMatchesHidden)
{
isGameFinished = true;
showFailureDialog(); // 显示失败对话框
timer->stop();
}

if (useCounts[size] >= maxUseCounts[size ])
{ // 检查当前按钮的计数器是否已达最大使用次数
return; // 若已达最大使用次数,则忽略此次点击事件
}

currentGridSize = size;
isButtonClicked = true; // 设置标志变量为 true,表示已点击按钮

useCounts[size]++; // 递增当前按钮的计数器

}

两个辅助方法:

mousePressEvent 方法

这个方法处理鼠标点击事件,特别是左键点击事件。当玩家在 puzzleGrid 区域内点击鼠标左键时,该方法会执行以下操作:

  1. 获取鼠标点击的位置:获取鼠标点击在 puzzleGrid 中的位置坐标。
  2. 检查点击位置:检查点击位置是否在 puzzleGrid 区域内,如果不在,则直接返回。
  3. 计算点击的单元格索引:根据 puzzleSize(拼图大小)和 puzzleGrid 的尺寸计算出实际点击的单元格索引。
  4. 获取点击的单元格:找到对应索引的 cellWrapper
  5. 检查子标签:遍历 cellWrapper 中的所有子对象,查找被点击的 QLabel
  6. 处理点击事件:如果找到被点击的 QLabel,则调用 on_PuzzleCell_clicked 方法处理点击事件。
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
void LevelOnepa::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton) {
return; // 仅处理左键点击事件
}

// 获取鼠标点击在 puzzleGrid 中的位置
QPoint gridPos = event->pos();

// 检查点击位置是否在 puzzleGrid 区域内
if (!puzzleGrid->parentWidget()->rect().contains(gridPos)) {
return; // 点击位置不在 puzzleGrid 内,直接返回
}

// 转换为相对于 puzzleGrid 左上角的坐标
gridPos -= puzzleGrid->geometry().topLeft();

// 根据 puzzleSize 计算实际点击的单元格索引
int cellWidth = puzzleGrid->geometry().width() / puzzleSize;
int cellHeight = puzzleGrid->geometry().height() / puzzleSize;

int clickedRow = gridPos.y() / cellHeight;
int clickedCol = gridPos.x() / cellWidth;

// 通过索引获取对应的单元格(cellWrapper)
QWidget *cellWrapper = puzzleGrid->itemAt(clickedRow * puzzleSize + clickedCol)->widget();

if (cellWrapper) {
QLabel *label = nullptr;

// 遍历点击的 QWidget(cellWrapper)中的所有 QLabel,找到被点击的那个
for (QObject *child : cellWrapper->children()) {
label = qobject_cast<QLabel *>(child);

if (label && label->underMouse()) {
break;
}
}

if (label)
{
on_PuzzleCell_clicked(label, clickedRow, clickedCol);
}
}
}

comparePuzzleWithHiddenMap 方法

这个方法用于比较玩家地图与隐藏地图,确保它们是相同的。

  1. 遍历每个单元格:对于每一个单元格,它获取玩家地图和隐藏地图的值。
  2. 比较值
    • 当隐藏地图的值为 2 时,玩家地图的值必须是 02
    • 当隐藏地图的值为 0 时,玩家地图的值必须是 0
    • 当隐藏地图的值为 4 时,玩家地图的值必须是 4
    • 当隐藏地图的值为 1 时,玩家地图的值必须是 13

如果有任何不匹配的情况,该方法将返回 false,表示玩家地图与隐藏地图不匹配。如果所有的比较都通过了,该方法将返回 true,表示玩家地图与隐藏地图匹配。

隐藏地图和用户地图的比较主要参照于游戏规则中对于各种块的定义和玩法,因游戏规则而异。

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
bool LevelOnepa::comparePuzzleWithHiddenMap()
{
for (int row = 0; row < puzzleSize; ++row)
{
for (int col = 0; col < puzzleSize; ++col)
{
QLabel *cell = puzzleCells[row * puzzleSize + col];
int playerValue = cell->text().toInt();
int hiddenValue = hiddenMap[row * puzzleSize + col];
if (hiddenValue == 2 && playerValue != 0 && playerValue != 2)
{
return false; // 当隐藏地图格子为2时,玩家地图格子不是0或2,则返回 false
}
else if (hiddenValue == 0 && playerValue != 0)
{
return false; // 当隐藏地图格子为0时,玩家地图格子不是0,则返回 false
}
else if (hiddenValue == 4 && playerValue != 4)
{
return false; // 当隐藏地图格子为4时,玩家地图格子不是4,则返回 false
}
else if (hiddenValue == 1 && playerValue != 1 && playerValue != 3) {
return false; // 当隐藏地图格子为1时,玩家地图格子不是1或3,则返回 false
}
}
}
return true; // 所有条件均满足,返回 true
}

初步效果如下:

image-20240424222900813

一些细枝末节但是也不可缺少的实现:

对图片和数字两个QLable的结合,也是在程序设计时候出现的第一个难关,起初我是将图片和数字都放在一个QLable里面,但数字总是离奇消失,无论怎么修改都没用,百般无奈下才得知QLable只能够设计一种属性,迫不得已通过布局加两个QLabel实现了地图布局的可视化

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
for (int row = 0; row < puzzleSize; ++row)
{
for (int col = 0; col < puzzleSize; ++col)
{
QLabel *textCell = new QLabel(this);
textCell->setFixedSize(70, 70); // 示例尺寸,根据实际需求调整
textCell->setStyleSheet("color: black; font-size: 20pt; text-align: center; vertical-align: middle");
textCell->setText(QString::number(0));

// 创建特殊符号图片标签
QLabel *symbolCell = new QLabel(this);
symbolCell->setFixedSize(60, 60); // 2:1 // 示例尺寸,根据实际需求调整
symbolCell->setAlignment(Qt::AlignCenter); // 图片居中显示

// 设置初始特殊符号
setSpecialSymbolForCell(symbolCell, hiddenMap[row * puzzleSize + col]);

// 用一个布局将文本标签和特殊符号图片标签组合在一起
QVBoxLayout *cellLayout = new QVBoxLayout();
cellLayout->setContentsMargins(0, 0, 0, 0);
symbolCell->setAttribute(Qt::WA_TransparentForMouseEvents, true);
cellLayout->insertWidget(0,symbolCell);
cellLayout->insertWidget(1,textCell);
// 将组合后的布局添加到拼图网格中
QWidget *cellWrapper = new QWidget(this);
cellWrapper->setLayout(cellLayout);
puzzleCells[row * puzzleSize + col] = textCell;
puzzleGrid->addWidget(cellWrapper, row, col);
}
}

还有一些页面美化工作

如调用的 setStyleSheet函数等,后期还会继续进行页面美化与添加有趣的功能。

1
2
3
returnButton->setStyleSheet(
"font-family: \"Microsoft YaHei\"; font-size: 30px; font-weight: bold; color: black; "
"border-radius: 8px; padding: 15px; min-width: 60px; min-height: 30px;");

寄语

希望在接下来的几周能够追求更高的代码质量和用户体验。