恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Qt实现数字华容道大作业:从逆序数可解性到界面信号槽全解析
首页
资讯中心
/
Qt实现数字华容道大作业:从逆序数可解性到界面信号槽全解析
Qt实现数字华容道大作业:从逆序数可解性到界面信号槽全解析
发布时间:2026/10/9 1:02:51
简介面向高级语言程序设计课程学生的C/Qt项目范本以图形化数字华容道小游戏为载体完整展示从控制台字符版到Qt界面版的实现路径。项目用Szhrd类承载游戏核心逻辑在Qt Creator中构建不同难度窗口配合绘图函数、槽函数完成按钮跳转与棋盘刷新并加入通关彩蛋与背景音乐/音效便于理解面向对象、事件驱动编程、信号槽机制和基础UI设计。压缩包共126个文件、约6.17MB其中41个cpp和35个h为源码实现31个ui为界面布局pro/user为工程配置pdf课程设计报告可供撰写文档时参考mp4、mp3、wav用于演示视频和游戏音频目录结构清晰可按源码、界面、文档、媒体分类检索。目前已有209人浏览学习内含完整的Qt工程、课程设计报告和媒体素材可直接运行体验也可对照源码学习游戏状态管理、绘图刷新、信号槽连接以及逻辑与界面分离的具体写法。1. 图形化数字华容道大作业算法只占一半Qt界面才是分水岭很多人选“图形化数字华容道小游戏”做C大作业是因为算法那部分看起来不难——一个数组、一遍BFS报告就能写好几页。可真正到演示那天翻车的往往是“基于Qt实现图形界面”这半句话窗口怎么搭、格子怎么画、鼠标点下去怎么变成一次合法移动加上打乱后的局面还必须保证可解。我会按自己实现这个项目的顺序来讲从逆序数可解性判定到Qt Widgets绘制、信号槽交互、编译部署避坑最后是一份答辩自测清单。新手能照着一步步做熟手可以直接跳去看参数和边界。2. 先立住棋盘逻辑逆序数、状态表示与打乱策略2.1 状态表示一维数组加零号空白格图形界面前先想清楚内存里的棋盘长什么样。数字华容道常见两种规格3×3的8-puzzle和4×4的15-puzzle。标题没指定尺寸常见大作业用4×4的15-puzzle格子多、难度够、报告好写如果指导老师要求3×3把几个4改成3就行。下面默认4×4。我用一维vector存16个格子0表示空白格1到15是数字块。为什么不用二维数组因为一维索引和Qt的网格行列换算是一行代码的事row idx / 4, col idx % 4反过来idx row * 4 col。后面鼠标点击换算和移动判断都用这个公式省掉一堆循环。// puzzle_state.h #pragma once #include vector // 0 表示空白格其余为 1..15 // 使用一维索引idx row * 4 col struct PuzzleState { std::vectorint board; // 长度 16下标 0 是左上角 int zeroPos 0; // 空白格当前位置避免每次查找 };zeroPos记录空白格位置避免每次移动都去find(0)。移动数字块其实就是一次swap把数字和0交换再把zeroPos更新成新空白位置。这个设计后面所有功能都围绕它转第4章的move、shuffle、胜利判定都用同一个数据源。2.2 可解性判定逆序数奇偶性答辩高频题随机打乱的棋盘不一定能拼回来。4×4拼图有一个硬约束把棋盘展成一维序列跳过0统计逆序数——也就是每对数字中前大后小的对数。当空白格在右下角时逆序数为偶数才可解空白不在右下角时还要加上“空白行从底部数起的行号”总和为偶数可解。这就是为什么很多教程里生成棋盘后要检查isSolvable不可解就重新洗牌。// puzzle_solver.cpp #include vector int inversionCount(const std::vectorint board) { int cnt 0; for (int i 0; i 16; i) { if (board[i] 0) continue; for (int j i 1; j 16; j) { if (board[j] 0) continue; if (board[j] board[i]) cnt; } } return cnt; } // 返回 true 表示该局面可解 bool isSolvable(const std::vectorint board) { int inv inversionCount(board); int zeroRow 0; for (int i 0; i 16; i) { if (board[i] 0) { zeroRow i / 4; break; } } // 从底部数起最后一行是 0最上面一行是 3 int zeroRowFromBottom 3 - zeroRow; return (inv zeroRowFromBottom) % 2 0; }inversionCount的双重循环对16个格子来说开销可以忽略因为它只在新游戏时跑一次。注意zeroRowFromBottom的计算最后一行算0最上面一行算3。如果改成3×3的8-puzzle把16改成9、3改成2即可。这个判定函数建议留在源码里报告里配上数学推导答辩时老师基本都会问到“你怎么保证可解”。2.3 打乱策略从目标态随机走而不是随机填数我的做法是生成目标态然后让空白格随机执行200到500次合法移动。每一次随机移动都产生一个合法可达的局面所以打乱结果必然可解省去isSolvable重试的循环。这是在“随机填数加校验”和“移动生成”之间我选后者的原因逻辑简单、一次通过代码里不需要while循环反复洗牌。// puzzle_game.cpp #include vector #include random // 从目标态出发随机移动 steps 次board 和 zeroPos 都会被改写 void shuffleBoard(std::vectorint board, int zeroPos, int steps) { static std::mt19937 rng(std::random_device{}()); int cur zeroPos; for (int s 0; s steps; s) { std::vectorint dirs; if (cur / 4 0) dirs.push_back(-4); // 上 if (cur / 4 3) dirs.push_back(4); // 下 if (cur % 4 0) dirs.push_back(-1); // 左 if (cur % 4 3) dirs.push_back(1); // 右 int d dirs[rng() % dirs.size()]; std::swap(board[cur], board[cur d]); cur d; } }dirs里放的是相对位置的偏移量-1和1是左右-4和4是上下。先收集所有合法方向再随机抽一个避免直接rand后越界。rng的静态种子来自std::random_device每次启动打乱顺序不同如果想让某次演示可复现可以把种子值打印出来这也是报告里能写的一句亮点。steps300对4×4已经足够乱再大也不会明显提升随机性反而让新游戏按钮有肉眼可见的卡顿。2.4 自动求解的选型8-puzzle用BFS15-puzzle换IDA*课程设计的加分项通常是一个自动求解按钮。选型上3×3的8-puzzle状态数大约36万BFS内存完全扛得住队列加visited表就能出结果4×4的15-puzzle状态数超过10^13BFS会把内存撑爆要上IDA*配曼哈顿距离启发函数。如果时间紧最稳妥的定位是只做3×3自动求解4×4保留手动模式报告里把选型理由写清楚分数不会低。#include queue #include string #include unordered_set #include utility // 8-puzzle BFS棋盘用 9 个字符表示0 是空白格 // 例如 123406758目标态统一为 123456780 // 返回最少步数不可达返回 -1 int bfsPuzzle(const std::string start) { const std::string goal 123456780; if (start goal) return 0; std::queuestd::pairstd::string, int q; std::unordered_setstd::string visited; q.push({start, 0}); visited.insert(start); while (!q.empty()) { auto [s, steps] q.front(); q.pop(); int z (int)s.find(0); // 4 个方向上(-3)、下(3)、左(-1)、右(1) int moves[] {-3, 3, -1, 1}; for (int d : moves) { // 列边界检查同一行内才允许左右移动 if (d -1 z % 3 0) continue; if (d 1 z % 3 2) continue; int nz z d; if (nz 0 || nz 9) continue; std::string ns s; std::swap(ns[z], ns[nz]); if (visited.count(ns)) continue; if (ns goal) return steps 1; visited.insert(ns); q.push({ns, steps 1}); } } return -1; }BFS第一次展开到goal时步数一定最小这是它作为最短路径算法的保证。visited用字符串做key9个字符的状态对36万规模开销尚可换成4×4就扛不住了要去IDA*。列边界检查是经典坑z % 3 0时不能左移z % 3 2时不能右移否则会从一行的末尾穿到另一行开头生成非法移动。这段代码里的结构化绑定需要C17编译配置在第3章pro文件里对应。逻辑层到这里已经和Qt完全解耦所以我建议先写一个不弹窗的控制台版本用命令行验证可解性和移动逻辑再接界面。好处是后面Qt界面出问题时你能确定问题在界面层还是逻辑层排查范围直接砍一半。3. 用Qt Widgets把棋盘画出来paintEvent、网格计算与鼠标坐标3.1 Widgets还是QML大作业默认选WidgetsQt做界面有两条主流路线Qt Widgets和QML。产品原型快速迭代选QML没问题动画漂亮、声明式语法写完就有效果但大作业时间紧我推荐Widgets。理由很简单Widgets的QPainter绘图API和C对象模型是一套不需要额外学语法课程设计报告里的类图、流程图用Widgets画起来也顺QML的信号槽连接方式反而要多解释一页。环境上常见组合是Qt 5.15.2配MSVC2019 64位Kit或者Qt 6配MinGW。下面这份pro文件QT widgets在Qt 6里已经默认包含但Qt 5需要显式加写上不亏。# puzzle.pro QT core gui widgets TARGET Puzzle15 TEMPLATE app CONFIG c17 SOURCES \ main.cpp \ MainWindow.cpp \ BoardWidget.cpp HEADERS \ MainWindow.h \ BoardWidget.h \ PuzzleGame.hCONFIG c17是必须的前面BFS代码用了结构化绑定MSVC如果不开启这个标准会报C17特性错误。TARGET是exe名字尽量不要用中文部署时省很多事。3.2 BoardWidget继承QWidget重写paintEvent画棋盘有两个常见做法放16个QPushButton或QLabel或者继承QWidget重写paintEvent。按钮方案直观但16个控件要管理信号、样式、焦点视觉上还有立体边框网格线永远对不齐。paintEvent方案一个控件画全部点击统一在一个mousePressEvent里换算坐标代码量少而且好调样式。// BoardWidget.h #pragma once #include QWidget #include QVector class BoardWidget : public QWidget { Q_OBJECT public: explicit BoardWidget(QWidget* parent nullptr); void setBoard(const QVectorint board); // 从逻辑层同步棋盘数据 signals: void cellClicked(int idx); // 点击的格子索引行优先 protected: void paintEvent(QPaintEvent* event) override; void mousePressEvent(QMouseEvent* event) override; private: QVectorint m_board; // 长度160表示空白 int m_rows 4; int m_cols 4; };m_rows和m_cols先写死4后续要支持3×3就改成构造函数参数。setBoard只保存数据并调用update()真正绘制交给paintEvent这是Qt绘图的标准姿势——不要在外部拿painter强行画交给事件循环统一重绘。// BoardWidget.cpp绘制部分 #include BoardWidget.h #include QPainter #include QMouseEvent void BoardWidget::paintEvent(QPaintEvent* /*event*/) { QPainter painter(this); painter.setRenderHint(QPainter::Antialiasing, true); int side qMin(width(), height()); // 保证棋盘始终正方形 int margin 8; // 外边框留白 int cell (side - margin * 2) / m_cols; // 单个格子边长 for (int r 0; r m_rows; r) { for (int c 0; c m_cols; c) { int idx r * m_cols c; QRectF rect(margin c * cell, margin r * cell, cell - 4, cell - 4); // 减4制造格子缝隙 if (m_board[idx] 0) { painter.fillRect(rect, QColor(240, 240, 240)); // 空白格浅灰 } else { painter.fillRect(rect, QColor(51, 122, 183)); // 数字块深蓝 QFont font painter.font(); font.setPixelSize(cell / 3); painter.setFont(font); painter.setPen(Qt::white); painter.drawText(rect, Qt::AlignCenter, QString::number(m_board[idx])); } } } }cell用控件宽高的小值再减去margin计算窗口被拖成扁长方形时棋盘仍然保持正方形数字也不会被拉伸。rect里减去4是格子间的视觉缝数字块之间留2像素左右的间隙看起来比贴在一起清爽。空白格涂浅灰、数字格深蓝白字是最常见的课程设计配色报告里截图干净想加分就把颜色提取成常量甚至做主题切换。3.3 鼠标点击坐标到格子索引的换算// BoardWidget.cpp鼠标事件部分 void BoardWidget::mousePressEvent(QMouseEvent* event) { int side qMin(width(), height()); int margin 8; int cell (side - margin * 2) / m_cols; int x event-position().x(); // Qt 6 写法 int y event-position().y(); int col (x - margin) / cell; int row (y - margin) / cell; if (row 0 || row m_rows || col 0 || col m_cols) return; emit cellClicked(row * m_cols col); QWidget::mousePressEvent(event); }注释里那步“先减margin再除cell”是经典坑漏掉margin的话点击会整体偏左上越靠近边缘偏差越大。这里用Qt 6的position()返回QPointFQt 5.15里没有position()要改成event-pos().x()。坐标换算后直接发cellClicked信号告诉外部“点到了哪个格子”具体怎么移动由MainWindow里的槽函数决定BoardWidget这个控件不关心游戏规则。注意Qt 5 用event-pos()Qt 6 用event-position().toPoint()两套API不通用。照着网上的教程抄代码之前先确认你的Qt版本否则编译报错会浪费半小时。3.4 MainWindow组装棋盘、计步标签、新游戏按钮棋盘控件画好了还得有个主窗口把它装起来。用QVBoxLayout把BoardWidget、计步QLabel、时间QLabel、新游戏按钮放成一列。这是最朴素的布局但结构清楚报告里画界面模块图和类图都好画。// MainWindow.cpp 构造函数核心片段 MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { m_boardWidget new BoardWidget(this); m_stepLabel new QLabel(QStringLiteral(步数: 0), this); m_timeLabel new QLabel(QStringLiteral(用时: 0 秒), this); auto* newGameBtn new QPushButton(QStringLiteral(新游戏), this); auto* central new QWidget(this); auto* layout new QVBoxLayout(central); layout-addWidget(m_boardWidget, 1); // 1 表示占满剩余空间 layout-addWidget(m_stepLabel); layout-addWidget(m_timeLabel); layout-addWidget(newGameBtn); setCentralWidget(central); resize(420, 520); setWindowTitle(QStringLiteral(数字华容道)); connect(newGameBtn, QPushButton::clicked, this, MainWindow::onNewGame); connect(m_boardWidget, BoardWidget::cellClicked, this, MainWindow::onCellClicked); }布局里addWidget的第二个参数1是伸缩因子棋盘占满标签和按钮剩下的区域。resize(420, 520)是给4×4棋盘留的换成3×3可以缩到340×440。窗口标题用QStringLiteral而不是直接传中文是为了配合后面要讲的MSVC编码问题如果你已经在pro文件加了/utf-8编译选项直接传中文字符串也可以但fromUtf8这套写法不挑环境。4. 把点击变成移动PuzzleGame类、信号槽联动与胜利判定4.1 数据层独立PuzzleGame把规则和界面隔开界面控件只负责画和点游戏规则放PuzzleGame类里。这么拆有两个理由第一逻辑层可以脱离Qt单独测试第二报告里的模块划分和类图就是现成的不用额外编。PuzzleGame持有m_board、m_zeroPos、m_steps三个核心成员对外暴露move、shuffle、isSolved界面层不直接改m_board。// PuzzleGame.h #pragma once #include QVector class PuzzleGame { public: explicit PuzzleGame(int size 4); bool move(int row, int col); // 尝试移动该格子相邻才成功 void shuffle(int steps 300); // 从目标态出发随机打乱 bool isSolved() const; // 胜利判定 int steps() const { return m_steps; } QVectorint board() const { return m_board; } private: int m_size; QVectorint m_board; // 长度 m_size * m_size int m_zeroPos 0; int m_steps 0; };我这里用QVector而不是std::vector因为要和Qt的信号槽、界面刷新直接兼容少一次数据拷贝转换。move返回bool移动不合法返回false界面层根据返回值决定要不要刷新和计步。m_zeroPos在shuffle和move里必须同步更新如果漏了会出现“点了空白格旁边的格子没反应”或者数字块瞬移的怪问题。4.2 move的实现曼哈顿距离判别法// PuzzleGame.cpp #include PuzzleGame.h #include cmath #include algorithm bool PuzzleGame::move(int row, int col) { int idx row * m_size col; int zr m_zeroPos / m_size; int zc m_zeroPos % m_size; // 两个格子曼哈顿距离恰好是 1才算相邻 bool adjacent (std::abs(zr - row) std::abs(zc - col)) 1; if (!adjacent) return false; std::swap(m_board[idx], m_board[m_zeroPos]); m_zeroPos idx; m_steps; return true; }判断相邻用曼哈顿距离等于1比分别枚举上下左右四个方向更简洁也不会漏掉斜对角误判。如果你的棋盘是3×3m_size传3这个函数一行都不用改。注意只有相邻才加步数有些实现把点击空白格本身也算一步演示时会被老师看出问题。4.3 计步与计时QLabel加QTimer界面有两个标签一个显示步数一个显示用时。步数在每次移动后读m_game.steps()刷新时间用QTimer每秒触发一次累加秒数。不需要毫秒级精度QTimer的interval设为1000就够。千万别在槽里用Sleep阻塞等待界面会卡死老师一拖窗口就露馅。// MainWindow.cpp 计时相关 void MainWindow::onNewGame() { m_game.shuffle(300); // 从目标态走300步 m_boardWidget-setBoard(m_game.board()); // 触发重绘 m_elapsedSec 0; m_stepLabel-setText(QStringLiteral(步数: 0)); m_timeLabel-setText(QStringLiteral(用时: 0 秒)); m_timer-start(1000); // 每秒一次 } void MainWindow::onTimerTick() { m_elapsedSec; m_timeLabel-setText(QStringLiteral(用时: %1 秒).arg(m_elapsedSec)); }计时开始放在onNewGame里统一处理胜利后stop()。如果忘了stop胜利弹窗还在走秒答辩时会掉印象分。QTimer::start()如果被重复调用会自动重置计时不需要先stop再start。4.4 胜利判定与弹窗// PuzzleGame.cpp bool PuzzleGame::isSolved() const { for (int i 0; i m_size * m_size - 1; i) { if (m_board[i] ! i 1) return false; // 1..15 按位置放好 } return m_board[m_size * m_size - 1] 0; // 空白必须在末尾 }这个判定只看1到15是否归位还要保证最后一个格子是0。如果只检查前15个相等、不管0在不在末尾会出现数字全对、空白卡在中间的非法“胜利”这个边界条件值得专门写进测试用例。胜利后的弹窗逻辑// MainWindow.cpp 槽函数 void MainWindow::onCellClicked(int idx) { int row idx / 4; int col idx % 4; if (m_game.move(row, col)) { m_boardWidget-setBoard(m_game.board()); m_stepLabel-setText(QStringLiteral(步数: %1).arg(m_game.steps())); if (m_game.isSolved()) { m_timer-stop(); QMessageBox::information(this, QStringLiteral(完成), QStringLiteral(恭喜%1 步完成用时 %2 秒) .arg(m_game.steps()).arg(m_elapsedSec)); } } }onCellClicked拿到的是棋盘索引先换算成row和col再交给PuzzleGame::move。move成功才刷新界面和步数标签isSolved为真时停止计时并弹窗。这段代码是信号槽联动的核心报告里把“点击→信号→槽→move→重绘→判定”画成时序图答辩时最好讲。4.5 重开一局重置全部状态// MainWindow.cpp void MainWindow::onNewGame() { m_game.shuffle(300); // 从目标态随机走300步必然可解 m_boardWidget-setBoard(m_game.board()); m_elapsedSec 0; m_timer-start(1000); m_stepLabel-setText(QStringLiteral(步数: 0)); m_timeLabel-setText(QStringLiteral(用时: 0 秒)); }onNewGame里做了四件事重新打乱、同步棋盘、清零步数和计时、重新启动定时器。你可能注意到onNewGame和onTimerTick里都出现了m_elapsedSec清零说明计时状态只由这一个变量掌控避免多个地方各自维护秒数导致界面显示不一致。4.6 课程设计报告怎么组织这四个模块报告骨架建议按这个顺序写问题描述什么是数字华容道为什么选Qt→需求分析功能需求列出新游戏、手动移动、自动求解、计时计步性能需求提一句响应时间小于0.1秒→总体设计模块划分图和核心流程图→详细设计PuzzleGame、BoardWidget、MainWindow三个类的职责和关键函数说明→测试用例表→总结。模块划分图要和源码一致我见过不少报告里画了五个模块、源码里只有俩类答辩被问住的时候很难圆。把你实际写的类都画进去哪怕少画一个没实现的功能也比虚画强。5. 编译与运行避坑MSVC套件、中文路径与运行库的五类典型翻车5.1 Kit选错导致 -1: error 依赖路径报错现象Qt Creator编译时头部日志出现类似:-1: error: dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets does not exist后面跟着一长串找不到头文件的报错编译速度也特别慢。原因绝大多数情况是Kit和Qt版本不匹配。下载的Qt是MSVC 2019 64位版本却在构建套件里选了MinGW 64位Kit编译器用g去找msvc2019_64的include路径路径拼接自然失败。其次常见的是工程路径或Qt安装路径里有中文MSVC预处理相对路径时把反斜杠解析错。解决打开“工具→选项→Kits”逐个检查当前Kit的“Qt version”和“C compiler”。Qt 5.15.2对应MSVC2019 64位时编译器必须是“Microsoft Visual C Compiler 16.0 x64”想用MinGW就得下载对应MinGW版本的Qt包两者不互通。然后把Qt装到D:\Qt、工程放D:\course\puzzle15这类纯英文路径清掉build目录后重新qmake再编译。5.2 中文路径与中文工程名现象编译能通过但exe运行时报找不到资源或者生成的exe文件名在别的电脑上双击没反应还有的在中文用户名下部署时windeployqt复制dll失败。原因Qt的qmake和后处理工具对中文路径处理一直不省心。Windows中文用户目录C:\Users\张三会在部署脚本里造成路径转义问题工程路径里含中文时Makefile里的相对路径也容易错位。解决工程根目录、构建输出目录、Qt安装目录全部用英文exe和报告文件名也别用中文。这不算玄学是历代课程设计踩出来的共识。实在绕不开可以装Qt 6.2以上版本对中文路径的兼容好了很多但没必要拿大作业冒险。5.3 MSVC下的中文乱码现象代码里用QStringLiteral(“数字华容道”)设置窗口标题编译后标题变成一串乱码但在Qt Creator里看源码是正常的换MSVC命令行编译又正常了。原因MSVC默认按系统代码页中文Windows是GBK/936读源文件而Qt Creator默认把文件保存成UTF-8。UTF-8编码的中文被MSVC当作GBK解读每个汉字拆成两个乱码字符显示自然不对。解决在pro文件里加一行MSVC: QMAKE_CXXFLAGS /utf-8强制MSVC按UTF-8解析所有源文件同时把Qt Creator的“编辑→设置编码”统一为UTF-8。代码里再用QString::fromUtf8或QStringLiteral包一层双保险前面所有界面字符串我都这么写。这个问题在Linux/Ubuntu上不常见GCC默认UTF-8但项目如果要在两个平台都编译统一按上面的写法最稳。5.4 QRC资源加载失败现象运行时图片或样式表加载不出来代码写QPixmap(“:/images/tile.png”)QPixmap::isNull()返回true界面一片空白但不报错。原因qrc文件的前缀和代码里的资源路径对不上。Qt资源系统里冒号后面是前缀加文件路径比如qrc里前缀是“/images”、文件放在资源根目录那完整路径就是“:/images/tile.png”前缀少了、大小写写错、文件没加进qrc都会静默失败。解决在Qt Creator里双击.qrc文件检查前缀和条目。前缀统一用“/”图片按目录层级放好代码里就写“:/images/tile.png”。注意Linux以及国产的麒麟、Ubuntu系统区分大小写Windows不区分所以跨平台演示时文件名大小写必须和qrc里完全一致否则在Windows上好好的拷到Linux上就白屏。5.5 上演示机时缺dll现象release编译出的exe拷到没装Qt的机器上双击提示“找不到Qt5Widgets.dll”或者提示缺少vcruntime140.dll要求安装Visual C Redistributable。原因Qt程序依赖Qt运行库和编译器运行库默认不静态打进exe。别人的电脑没装Qtexe就缺依赖跑不起来。解决在Qt命令行环境里进入exe所在目录执行windeployqt puzzle15.exe它会自动复制需要的Qt dll和platforms插件MSVC编译的exe还要带Microsoft Visual C Redistributable运行时最省事是让演示机装一次vcredist或者把vcruntime140.dll一起复制到exe目录。演示务必用release版本debug版会拖带大量调试dll部署包体积翻倍还容易因为依赖不齐当场翻车。6. 答辩前的验收自测功能清单与三个常问细节6.1 五分钟跑完的自测清单用例操作预期结果正常移动点击空白格相邻的数字块该数字块滑入空白步数加1非法移动点击空白格斜对角或隔一个的格子界面不动步数不变新游戏可解连续点“新游戏”10次每次用isSolvable校验所有局面都可解计时从0开始胜利判定从目标态逆向走一步再点回正确位置弹窗“完成”计时停止窗口缩放拉伸窗口到宽窄不同的比例棋盘仍为正方形格子不越界这五条按顺序跑完不超过五分钟。最后一条常被忽略教室里演示机的屏幕分辨率和你开发机不一样窗口拉伸后格子绘制错位是高频事故。6.2 老师常问的三个细节与标准答法第一个为什么你的shuffle一定可解。答从目标态执行合法随机移动生成的局面等价于某个合法解序列的逆过程必然可解不是随机填数后碰运气。第二个步数从哪一步开始计。答新游戏后m_steps置0第一次合法移动才加1打乱过程不计步。有人把shuffle时的内部移动也算进步数演示时新游戏一开局就是几百步一眼穿帮。第三个自动求解会不会卡界面。答求解放后台线程通过信号把步骤序列回传主线程UI只做setBoard和update如果没做自动求解就讲清为什么BFS对15-puzzle内存不够、IDA*的曼哈顿距离怎么算比硬说“我没做”强得多。6.3 顺手加一个隐藏自检入口在大作业里保留一个隐藏自检入口是加分习惯。我在MainWindow构造函数里加一个临时按钮或快捷键触发一段自检代码构造一个差一步到终局的棋盘调用move和isSolved把结果qDebug输出。演示前把按钮注释掉源码里留着报告里写“内置自检用例”答辩时现场跑给老师看。最后说一个自己的习惯无论是多小的Qt大作业我都会先写不带界面的PuzzleGame类用命令行把可解性和移动逻辑全部验证完再接界面。这个习惯让我后面每一行界面代码都有确定的数据层可依赖省掉大量排错时间。希望帮到你。本文还有配套的精品资源点击获取