简介:基于Qt 5.12开发的可视化21点纸牌游戏,是一份面向C++初学者的GUI课程设计源码包。项目以按钮交互模拟玩家与电脑对弈,完整实现要牌、停牌、加倍、分牌等规则逻辑,并处理好点数累计与超过21点判负的机制。资源共86个文件,其中58张扑克牌PNG素材覆盖完整牌面,10个C++源文件与8个头文件构成核心程序,另含2个UI界面设计文件、项目配置文件和图标资源,压缩包仅2.2MB,目录按场景、模型等模块划分,便于逐段阅读。目前已有1469人下载学习。通过阅读源码,可以系统掌握Qt窗口与布局管理(网格布局、盒式布局)、信号与槽事件处理、QResource资源加载、随机发牌算法和异常兼容处理等实践技能。对于课设答辩或入门Qt开发,这套代码提供了可直接运行的完整示例,也可在其上扩展动画效果或联网对战,适合作为二次开发的起点。
1. Qt可视化界面版21点游戏:课设真正的难点不在画窗口
拿到“Qt可视化界面版21点游戏(课设)”这个题,大多数人的第一反应是去找扑克牌图片素材,或者研究怎么把窗口做得花哨。做过一遍之后你会发现,界面只占三成工作量,真正让人熬夜的是牌局规则怎么用代码表达清楚:A到底是1还是11,庄家到17停牌这个边界条件藏在哪里,黑杰克什么时候该按1.5倍赔付。这些规则如果直接写死在按钮点击函数里,界面一调整逻辑就跟着散架,这是Qt课设最常见的翻车点。这篇文章面向正在做Qt课程设计的学生,也面向想拿游戏项目练手信号槽、布局和状态机的Qt初学者,目标是让你用最少的资源做出一个能跑、能答辩、能讲清楚内部结构的21点游戏。
2. 从牌堆到牌力:用C++把21点规则先写成能测的逻辑层
2.1 用结构体还是QString表示一张牌:数据结构的取舍
课程设计里最常见的做法有两种:一种是用QString直接存“红桃A”“黑桃10”,另一种是定义一个Card结构体,把花色和点数分开存。我强烈建议用结构体,因为21点计算牌力时只关心点数,显示时又需要花色和名称,这两个需求在数据结构上应该分开,而不是存一个拼好的字符串再去反复解析。
// card.h #pragma once #include <QString> enum Suit { Spade, Heart, Club, Diamond }; struct Card { Suit suit; int rank; // 1=A, 2~10按面值, 11=J, 12=Q, 13=K int value() const { if (rank >= 10) return 10; // J/Q/K 都按10点 return rank; } QString text() const { static const char* rankStr[] = {"", "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"}; static const char* suitStr[] = {"♠", "♥", "♣", "♦"}; return QString("%1%2").arg(suitStr[suit]).arg(rankStr[rank]); } };代码里的rank字段是牌力计算的核心依据,1代表A,11到13分别代表JQK。value()函数把JQK统一折算成10点,这个函数在后续所有牌力计算里都会复用,避免在界面层到处写if判断。text()函数只在显示时调用,它把花色符号和点数拼成一个短字符串,可以直接放到QLabel上。两个函数职责分离,后面界面做不做图片升级都不影响逻辑。
这里有一个容易忽略的参数细节:花色枚举没有给具体数值,默认从0到3,这正好对应suitStr数组的下标。如果你在别处把花色存成QString,后续排序、判同花这类操作都会更啰嗦,虽然21点不需要同花判断,但这种设计习惯能帮你少走弯路。
2.2 牌堆类:洗牌、发牌和越界保护
牌堆用标准库容器存储52张牌,洗牌用std::shuffle而不是已经废弃的std::random_shuffle。Qt 5.15对应的编译器大多支持C++17,random_shuffle在C++17里已经被移除,继续用它你会得到编译错误。
// deck.h #pragma once #include <vector> #include <algorithm> #include <random> #include "card.h" class Deck { public: Deck() { rebuild(); } void rebuild() { cards.clear(); for (int s = 0; s < 4; ++s) for (int r = 1; r <= 13; ++r) cards.push_back({static_cast<Suit>(s), r}); } void shuffle() { std::mt19937 rng(std::random_device{}()); std::shuffle(cards.begin(), cards.end(), rng); } Card deal() { if (cards.empty()) rebuild(); // 牌发完自动重开一副 Card c = cards.back(); cards.pop_back(); return c; } private: std::vector<Card> cards; };deal函数里加了一个保护:如果牌堆空了就自动rebuild。21点一局最多用掉十几张牌,52张通常绰绰有余,但课设演示时如果连续玩很多局,或者你加了多副牌功能,这个保护能让程序不因越界崩溃。std::random_device用来给mt19937提供随机种子,每次启动游戏洗牌结果都不同,这一点比用固定种子更能通过老师的演示检查。
2.3 手牌点数计算:A的1/11换算用减差值而不是重新遍历
21点规则里最绕的就是A既可以当1点也可以当11点,计算时要先把所有A按1点累加,再判断有没有空间把某个A升级成11点。很多课设代码在拿到一手牌后遍历两次,第一次数A的个数,第二次算总值,结果状态一变就出bug。我的做法是一次遍历同时算总值和A的数量,最后统一调整。
// handvalue.h #pragma once #include <vector> #include "card.h" // 返回手牌最优点数,soft表示当前手牌存在“A算11点不爆”的软牌状态 int handValue(const std::vector<Card>& hand, bool& soft) { int total = 0; int aces = 0; soft = false; for (const Card& c : hand) { total += c.value(); if (c.rank == 1) ++aces; } while (total <= 11 && aces > 0) { total += 10; // 把一张A从1点升级为11点 --aces; soft = true; if (total > 21) break; // 升级后超过21就退回,进入while条件判断 } if (aces > 0 && total <= 11) soft = true; return total; }这里要注意循环条件里的细节:total <= 11时才考虑升级,因为只有总值不超过11,加10才不爆。如果手牌是A+9,total先是10,升级成20,soft为true,这就是软20,因为如果后续再要一张超过1点的牌,还可以把A降回1点不爆。如果手牌是A+5+7,total是13,不具备升级条件,A只能算1点。很多翻车案例就是没考虑这个“软牌降级”的场景,只算了一次就返回。
软牌状态在界面层要不要展示,取决于你的课设要求,但逻辑层必须把这个信息返回。事实上庄家17点停牌的判断也依赖soft:庄家拿到软17时有的规则要求继续要牌,有的规则要求停牌,逻辑层统一处理会比界面层写if容易测试得多。
2.4 庄家自动打法:规则与界面分离的边界
庄家要牌到17点这个规则,应该放在一个独立的Game类或者至少是独立函数里。课程设计常见的演示路径是“玩家点完停牌后,庄家自动要牌”,如果你把庄家的while循环直接写在按钮槽函数里,界面会在循环期间完全卡死,因为UI线程被阻塞了。正确的做法是把庄家每一步要牌封装成一个函数,由界面层的定时器一步步触发。
// gamelogic.h #pragma once #include <vector> #include "card.h" enum GameResult { PlayerBust, DealerBust, PlayerWin, DealerWin, Push }; // 根据双方点数判定结果,不关心牌型,只关心数值 GameResult judgeRound(int playerTotal, int dealerTotal, bool playerBust, bool dealerBust);judgeRound的判定顺序是:先看双方是否爆牌,爆牌优先级最高;然后比大小,平局返回Push。这个函数写完后可以直接跑单元测试,不需要打开界面。逻辑层和界面层的边界就划在这里:逻辑层只处理数值和规则,界面层只负责显示和收集操作。
实际开发中我习惯把Deck、handValue、judgeRound这些代码放在一个logic文件夹里,界面文件放在ui文件夹里,这样即使后面想从QWidget换成QML,逻辑层一行都不用动。这正是Qt课程设计答辩时最值得讲的亮点:你的代码分层是清晰的。
3. Qt界面搭建:用Qt Designer辅助布局与卡片显示方案
3.1 主窗口选QWidget还是QMainWindow:课设推荐的窗口骨架
如果你在Qt Creator里新建项目,向导会让你选QMainWindow、QWidget还是QDialog。很多课设模板默认选QMainWindow,因为它自带菜单栏、状态栏和工具栏。但一个单局21点根本用不到菜单栏,QMainWindow额外的布局层级反而让新手犯迷糊。我一般选QWidget做根窗口,所有控件放在垂直布局里,结构一目了然。
// mainwindow.h #pragma once #include <QWidget> class QLabel; class QPushButton; class MainWindow : public QWidget { Q_OBJECT public: explicit MainWindow(QWidget* parent = nullptr); private slots: void onStartClicked(); void onHitClicked(); void onStandClicked(); private: void setupUi(); QLabel* dealerCardsLabel; QLabel* playerCardsLabel; QLabel* resultLabel; QPushButton* startButton; QPushButton* hitButton; QPushButton* standButton; };QWidget默认没有菜单栏,你能放的东西就是中心控件本身,布局代码更好写。setupUi函数负责创建所有子控件并放到布局里,构造函数里调用它。这种写法把界面构造和逻辑处理分开,后期把一个按钮从布局里拿出来调整位置,不会误删逻辑代码。
3.2 展示手牌的三种方案对比:QLabel换图、样式表、自绘
展示一张牌有三种常见方案:准备54张扑克牌图片用QLabel+QPixmap显示,用样式表把QLabel画成卡片样式,或者继承QWidget用paintEvent自绘。表格对比如下:
| 方案 | 视觉上限 | 开发量 | 资源依赖 | 推荐场景 |
|---|---|---|---|---|
| QLabel+图片 | 高 | 小 | 需要图片文件 | 有现成素材时 |
| QLabel+QSS样式表 | 中 | 小 | 无 | 课设最稳妥 |
| paintEvent自绘 | 高 | 大 | 无 | 想展示绘图能力时 |
课设时间紧张的时候,我推荐第二种。不用找图片素材,不用担心图片失真,一套样式表就能把牌面画出来。每张牌的牌背就用深色背景加边框来实现。
// mainwindow.cpp 片段 void MainWindow::setupUi() { dealerCardsLabel = new QLabel("庄家牌:", this); playerCardsLabel = new QLabel("玩家牌:", this); resultLabel = new QLabel("点击“开始”发牌", this); dealerCardsLabel->setMinimumHeight(120); playerCardsLabel->setMinimumHeight(120); dealerCardsLabel->setStyleSheet( "QLabel { background-color: #2d5f3e; color: white;" " border-radius: 8px; padding: 8px; font-size: 24px; }"); playerCardsLabel->setStyleSheet( "QLabel { background-color: #f5f5dc; color: #222;" " border-radius: 8px; padding: 8px; font-size: 24px; }"); startButton = new QPushButton("开始", this); hitButton = new QPushButton("要牌", this); standButton = new QPushButton("停牌", this); hitButton->setEnabled(false); standButton->setEnabled(false); }样式表里的background-color模拟牌桌底色,绿色给庄家区域、米色给玩家区域,这样即使没有图片,课程设计截图看起来也像回事。font-size设到24px是为了让“A♠”这类文本在120像素高的区域里足够清晰。setMinimumHeight保证窗口拉伸时标签高度不会塌掉,这是界面布局里容易被忽略的细节。
3.3 布局结构:不用setGeometry,用QHBoxLayout和QVBoxLayout
新手写Qt界面最顺手的就是setGeometry硬坐标,代码写起来快,但窗口一拉伸控件就乱飞。课程设计展示时老师很容易拖拽窗口大小,一旦乱布局分数就不好看了。正确做法是把所有控件交给布局管理器。
void MainWindow::setupUi() { // 上半区:庄家 QVBoxLayout* dealerLayout = new QVBoxLayout; dealerLayout->addWidget(dealerCardsLabel); // 中间区:结果提示 QVBoxLayout* centerLayout = new QVBoxLayout; centerLayout->addWidget(resultLabel); // 下半区:玩家 QVBoxLayout* playerLayout = new QVBoxLayout; playerLayout->addWidget(playerCardsLabel); // 按钮区 QHBoxLayout* buttonLayout = new QHBoxLayout; buttonLayout->addWidget(startButton); buttonLayout->addWidget(hitButton); buttonLayout->addWidget(standButton); buttonLayout->addStretch(); // 总布局 QVBoxLayout* mainLayout = new QVBoxLayout(this); mainLayout->addLayout(dealerLayout, 1); mainLayout->addLayout(centerLayout, 0); mainLayout->addLayout(playerLayout, 1); mainLayout->addLayout(buttonLayout, 0); }addLayout的第二个参数是拉伸因子:庄家区域和玩家区域各占1份,中间结果提示不参与拉伸,按钮区也不拉伸。这样窗口变大时,多出来的空间会分配给两个牌区,提示文字和按钮始终居中在合适位置。这里没有用QSpacerItem,因为拉伸因子已经够了,多用反而让代码难读。
另外有一点值得说:布局的嵌套顺序决定了垂直方向的结构,先建三个子布局再合并成mainLayout,比在一个布局里塞五个控件更容易调整。如果想在两侧加留白,可以在mainLayout两侧再加QHBoxLayout包一层,而不是到处setContentsMargins。这个习惯从课设带出去,工作后写复杂表单也够用。
3.4 桌面布局和Qt Designer的关系
热词里经常看到qt designer界面设计,Qt Creator自带的Qt Designer可以把.ui文件拖拽设计,再通过uic生成界面代码。课设里用.ui文件和纯代码布局都可行,但我个人更推荐纯代码:代码里的注释、变量名、布局结构都能直接展示,答辩被问到“这个按钮在哪个文件里”时,你一句“在mainwindow.cpp的setupUi里”比“在designer里拖的”更有说服力。
如果你非要用Qt Designer,注意一个坑:.ui文件里的对象名和代码里手工创建的变量名冲突时,编译阶段很难察觉,运行时可能出现改了Designer界面但代码不生效的诡异问题。解决方案是二选一:要么全走.ui+ui_xxx.h,要么全走代码,别混着来。
4. 用信号槽把按钮和牌局状态机接起来
4.1 四个游戏状态:Idle、PlayerTurn、DealerTurn、RoundEnd
21点一局牌的流程是固定的:发牌后进入玩家回合,玩家停止要牌后进入庄家回合,双方结算后回到空闲等待下一局。这个流程用枚举状态机来管理,比在按钮槽函数里写一堆if else清晰得多,也更接近正式项目的写法。
// gamestate.h #pragma once enum class GameState { Idle, // 空闲,等待开始 PlayerTurn, // 玩家回合,可要牌或停牌 DealerTurn, // 庄家回合,自动要牌 RoundEnd // 结算完成,等待下一局 };状态机的核心原则是:任何一个按钮响应函数,先检查当前状态是否允许这个操作,不允许就直接return。比如要牌按钮只在PlayerTurn状态下有效,如果玩家已经爆牌或者已经停牌,点了就不该有任何反应。这个检查放在界面层还是逻辑层都可以,我习惯放在按钮槽函数最开头,因为这样代码读起来最直接:先看状态,再走流程。
4.2 发牌、要牌、停牌三个按钮的槽函数实现
信号槽是Qt课设的必考知识点,把clicked信号和自定义槽函数用new语法连接,已经是Qt 5之后的标准写法。下面这段代码连接了三个按钮,每个槽函数内部都按状态机模型处理。
// mainwindow.cpp void MainWindow::setupUi() { // ... 前面的控件创建代码 ... connect(startButton, &QPushButton::clicked, this, &MainWindow::onStartClicked); connect(hitButton, &QPushButton::clicked, this, &MainWindow::onHitClicked); connect(standButton, &QPushButton::clicked, this, &MainWindow::onStandClicked); } void MainWindow::onStartClicked() { if (state != GameState::Idle) return; deck.shuffle(); playerHand.clear(); dealerHand.clear(); playerHand.push_back(deck.deal()); dealerHand.push_back(deck.deal()); playerHand.push_back(deck.deal()); dealerHand.push_back(deck.deal()); state = GameState::PlayerTurn; startButton->setEnabled(false); hitButton->setEnabled(true); standButton->setEnabled(true); refreshDisplay(); }onStartClicked里先重置手牌数组,再交替各发两张牌,然后切换状态并更新按钮可用性。注意发牌顺序:玩家第一张、庄家第一张、玩家第二张、庄家第二张,和真实赌桌规则一致。逻辑层的dealerHand使用std::vector ,这里不需要卡片控件对象,因为显示用的QLabel文本是从这个数组生成的。
refreshDisplay函数做的事情是遍历两个手牌数组,把所有卡的text()拼成一个字符串,同时计算双方点数显示到结果标签上。这里不再单独列代码,因为重复逻辑简单,但你要记住:刷新显示必须在每次发牌、要牌、停牌之后调用,忘记调用就会发生界面显示和实际状态不一致的诡异问题,这是课设里最常见的“黑匣子”现象。
4.3 要牌与停牌的边界条件:爆牌、21点、庄家回合
要牌不止是“往数组里塞一张卡再刷新”,还伴随着爆牌检查。玩家要牌后点数超过21就直接进入RoundEnd,不能继续操作;恰好21点时应该在界面提示“21点”,并自动进入庄家回合。这两个边界条件漏掉任何一个,演示时都会被老师一眼看穿。
void MainWindow::onHitClicked() { if (state != GameState::PlayerTurn) return; playerHand.push_back(deck.deal()); bool soft = false; int total = handValue(playerHand, soft); refreshDisplay(); if (total > 21) { resultLabel->setText("爆牌!你输了"); state = GameState::RoundEnd; hitButton->setEnabled(false); standButton->setEnabled(false); startButton->setEnabled(true); } else if (total == 21) { onStandClicked(); // 21点自动停牌 } } void MainWindow::onStandClicked() { if (state != GameState::PlayerTurn) return; state = GameState::DealerTurn; hitButton->setEnabled(false); standButton->setEnabled(false); dealerTurn(); // 进入庄家流程 }这里有个容易踩坑的点:onStandClicked里直接调用另一个槽函数,这在Qt里是合法的、也鼓励的,因为槽函数就是普通成员函数。但要注意别在onStandClicked里又connect同一个信号,否则会出现重复信号连接的bug。巧妙复用槽函数可以简化状态流转:玩家21点时不需要额外写一段庄家回合代码,直接复用停牌逻辑即可。
onHitClicked里的total==21判断放在total>21之后,如果爆了就不执行停牌。这个顺序看起来是小事,但如果你把两个判断写反,手牌21点但已经爆的时候就会误判成自动停牌。这种边界问题很难靠跑一次发现,最好的办法是在代码里直接按“先判断爆牌、再判断精确21点”的顺序写清楚。
4.4 庄家回合的异步处理:QTimer::singleShot避免界面卡死
庄家要牌到17点这个逻辑,如果写成while循环,程序会一直等循环结束才刷新界面,视觉效果就是点完“停牌”后界面卡住一阵子。正确做法是用QTimer::singleShot让庄家每一步延迟几百毫秒执行,形成逐张翻牌的效果。
void MainWindow::dealerTurn() { if (state != GameState::DealerTurn) return; bool soft = false; int dealerTotal = handValue(dealerHand, soft); if (dealerTotal < 17) { dealerHand.push_back(deck.deal()); refreshDisplay(); QTimer::singleShot(600, this, &MainWindow::dealerTurn); } else { finishRound(); } }QTimer::singleShot的第一个参数是毫秒延迟,第二个参数是接收对象,第三个是槽函数。dealerTurn里判断庄家点数不足17就再要一张牌,600毫秒后再次进入dealerTurn;达到17或以上就调用finishRound结算。这种递归调用不会阻塞UI线程,玩家能看到庄家一张张翻牌的过程,演示效果也好。
finishRound里要做的事包括:计算双方点数并调用judgeRound判定胜负,更新结果标签,把所有按钮恢复成可用状态,同时把state置回Idle。注意状态切换必须在按钮可用性更新之前或之后保持一致,不要出现“按钮已经可点但state还是DealerTurn”的窗口期,否则玩家在结算瞬间点“开始”会触发异常行为。
4.5 按钮可用性驱动的用户引导
状态机的另一个作用是控制按钮的可用性,让玩家不被告知也明白当前能做什么。开局只能点“开始”,发完牌后只有“要牌”和“停牌”可用,“开始”灰掉;结算后“开始”重新亮起,其他两个灰掉。这些setEnabled调用集中在状态切换处,而不是散落在各处。
这种做法有一个好处:演示时状态条理清楚,老师问“这个按钮什么时候变灰”你直接答“状态机从PlayerTurn切到DealerTurn时”。如果需求变更,比如加了“加倍”按钮,只需要在状态机里加一个分支,不需要改动其他按钮的逻辑。
5. Qt课设常见坑:从编译失败到运行期边界问题
5.1 现象:点击“要牌”按钮后程序闪退,报“0000005”访问冲突
原因:手牌容器未初始化。常见于直接定义std::vector playerHand成员变量,但在某个槽函数里没有clear就push_back,或者deck为空时调用了deal。Qt的QVector和std::vector在debug模式下会提供越界检查,但release模式下直接踩内存,闪退点位千奇百怪。
解决:在每个回合开始前显式调用playerHand.clear()和dealerHand.clear(),deck.deal()内部做空牌堆保护。如果你用了QVector,建议统一用at()而不是[]访问元素,at()带越界检查,[]不带。这一点在Qt 5.15里依然成立,属于C++容器通用注意事项。
5.2 现象:A的点数总是算错,手牌A+10显示21但结果却是黑杰克判到了庄家头上
原因:黑杰克的定义是“开局两张牌合计21点”,而“要牌要出来的21点”只能算普通21点,不算黑杰克,赔付率不同。很多课设只写了judgeRound,没有在开局发完牌后单独判断黑杰克,结果就是把普通21点当黑杰克处理了。
解决:在onStartClicked发完牌之后加一个判断,如果玩家两张牌点数之和等于21,直接标记blackJack=true,结算时按照1.5倍赔付计算。这里必须用handValue函数计算而不是直接比较两张牌的点数和,因为A+J这种组合只有通过value()计算才能正确得到21点。另外注意牌数判断:玩家手牌size必须等于2才算黑杰克,要牌要到6张牌的21点不算。
5.3 现象:Qt Creator编译报“cannot mix incompatible Qt library (version ex50601) with this library”或“unknown module(s) in qt: webenginewidgets”
原因:前者是编译时链接的Qt库和运行时加载的Qt库版本不一致,常见于电脑里装了多个Qt版本,或者环境变量PATH指向了旧版本。后者是.pro文件里加了QT += webenginewidgets,但当前安装的Qt套件里没有这个模块。热词里的qt安装问题、qt 5.15.2下载大多属于这一类。
解决:qmake项目打开.pro文件,检查QT变量,21点游戏只需要QT += core gui和针对高版本Qt的widgets。然后确认Tools选项里选中的套装和编译器匹配,MinGW版就配MinGW编译器,MSVC版就配MSVC编译器。清理构建目录、重新qmake、再构建,多数版本冲突都能解决。如果你根本不用WebEngine,千万不要为了贪图齐全乱加模块,这个模块体积大且编译慢,课设完全用不到。
5.4 现象:窗口最大化后卡片或者文字控件挤成一团,或者出现大片空白
原因:使用了绝对坐标setGeometry定位控件,没有放进布局管理器;或者布局里设置了固定大小setFixedSize,窗口拉伸时控件尺寸不变。
解决:把所有控件加入布局管理器,并去掉不必要的setFixedSize。QLabel显示牌文本时,用setMinimumHeight保证标签在窗口缩小时也能展示内容。如果确实需要固定尺寸的卡片图片,把固定大小的QLabel放进QHBoxLayout并设置居中对齐,让布局管理器决定卡片在整个窗口中的位置而不是自己死磕坐标。
5.5 现象:界面完全正常,但发布到别的机器上运行时提示缺少Qt5Core.dll等动态库
原因:开发机上Qt库的路径没有随exe一起打包。Qt Creator的debug模式运行会自动找开发目录里的库,但生成的exe拷到其他机器就找不到动态库。这是课设演示时的经典翻车现场。
解决:发布时用Qt自带的windeployqt工具,命令行进入exe所在目录,执行windeployqt 你的程序名.exe,它会自动拷贝所需的Qt模块和DLL。如果用了MinGW套装,还需要拷贝libgcc_s_seh-1.dll等运行库,这时用windeployqt --compiler-runtime参数一并处理。发布之前,把exe放到一台没有装Qt的虚拟机里跑一遍再提交,这是最可靠的验证方式。
6. 答辩前的最后升级:加倍、验证和演示顺序
6.1 加一个“加倍”按钮:状态机扩展的实战练习
如果课设要求功能更丰富,最值得加的是加倍(Double Down):玩家在拿到前两张牌后可以选择加倍,同时筹码翻倍、且只能再要一张牌。实现方式是在GameState里加一个DoubleDown状态,或者在PlayerTurn状态里加一个bool doubleDown标志。按钮槽函数里先检查手牌数量等于2,然后设置加倍标志并立刻发一张牌,发完后跳过玩家操作直接进入庄家回合。这个改动正好能向老师展示你对状态机模型的理解深度。
6.2 用几组固定牌型验证逻辑:不要只靠随机牌局
答辩前用构造好的手牌去验证核心规则,比玩十局全靠运气可靠得多。我常用的验证方式是在一个debug槽函数里直接给playerHand和dealerHand赋值固定牌型,而不是走洗牌发牌流程。测试用例包括:A+9=20的软牌、A+5+7=13的A降级、10+J+K=爆牌、两张牌黑杰克、庄家16要牌17停牌边界。把这些用例的结果打印出来,确认逻辑无误后再删掉调试入口。
6.3 演示顺序建议
演示时先启动程序点“开始”,展示初始两张牌;然后点“要牌”两次,说明点数累加逻辑;再点“停牌”,展示庄家异步要牌;最后出结果。中间最好有一局故意要牌爆掉,展示失败状态和按钮禁用逻辑。整个流程控制在三分钟内。另外,把第2章的handValue单测结果截图放进课设报告,比贴十张界面截图更能证明工作量,也更能回答老师“你这个规则靠谱吗”的提问。
Qt这个方向在找工作面试里也常被问到,能讲清楚状态机、布局、信号槽这三大件,比做几个炫酷动画更扎实。我自己当初做课设时把逻辑层和界面层完全分离,后面改需求时省了大量时间;希望这篇文章能帮你的课设少踩几个坑,一次通过。
本文还有配套的精品资源,点击获取