news 2026/9/29 16:43:27

Qt可视化21点游戏课设:逻辑分层与状态机设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt可视化21点游戏课设:逻辑分层与状态机设计

简介:基于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这个方向在找工作面试里也常被问到,能讲清楚状态机、布局、信号槽这三大件,比做几个炫酷动画更扎实。我自己当初做课设时把逻辑层和界面层完全分离,后面改需求时省了大量时间;希望这篇文章能帮你的课设少踩几个坑,一次通过。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 16:43:13

Windows 11下用VSCode编译Betaflight固件:WSL2与GCC工具链配置实战

搞穿越机的人迟早会走到这一步&#xff1a;一直用别人编译好的 Betaflight 固件&#xff0c;总有不甘心的时候。想改个 PID 算法、想塞点自定义编译选项、想长期维护自己的飞控固件&#xff0c;本地编译就跑不掉了。我原本以为&#xff0c;在 Windows 11 上装好 VSCode&#xf…

作者头像 李华
网站建设 2026/9/29 16:41:27

Java校园二手交易平台源码拆解:从环境搭建到交易闭环的毕业设计实战

简介&#xff1a;这份资源是基于Java的校园二手交易平台毕业设计完整源码包&#xff0c;面向计算机相关专业需要完成毕业设计的学生&#xff0c;以及想通过真实项目巩固Java Web开发技能的开发者。项目围绕校园闲置物品发布、浏览、交易等核心场景展开&#xff0c;可作为课程设…

作者头像 李华
网站建设 2026/9/29 16:41:25

starnet桌面智能体网络:OpenRouter与MCP工具链实战

1. 从“starnet”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“starnet”这个项目名&#xff0c;加上关键词里那一串AI agents、desktop、OpenRouter、MCP&#xff0c;我脑子里第一反应是&#xff1a;这大概率是一个把桌面端 AI 智能体和外部模型服务、工具协议串…

作者头像 李华
网站建设 2026/9/29 16:41:11

starnet桌面AI代理运行环境:OpenRouter与MCP实战指南

1. 从“starnet”这个名字说起&#xff1a;它到底想解决什么问题第一次看到“starnet”这个项目名&#xff0c;加上旁边挂着的 AI agents、desktop harness、OpenRouter、MCP 这几个关键词&#xff0c;我脑子里第一反应是&#xff1a;这大概率是一个把“桌面端 AI 代理运行环境…

作者头像 李华
网站建设 2026/9/29 16:40:01

SpringBoot+Vue+MySQL旅游管理系统毕设全流程实战指南

每年到了三、四月份&#xff0c;我都会在后台收到一堆类似的问题&#xff1a;学长&#xff0c;SpringBoot&#xff0b;Vue&#xff0b;MySQL做旅游管理系统当毕业设计到底行不行&#xff1f;表怎么建才不会被答辩老师问倒&#xff1f;前端路由老出问题怎么办&#xff1f;部署文…

作者头像 李华
网站建设 2026/9/29 16:39:19

Memcached stats命令全解析:从基础字段到内存分配排查实战

1. 先把话说在前面&#xff1a;为什么你必须学 stats 命令聊到 Memcached 排查&#xff0c;大部分人第一反应是看监控面板、看缓存命中率曲线&#xff0c;真正落到命令行敲stats的人反而少。我在生产环境踩过几次坑之后&#xff0c;越来越觉得stats命令才是 Memcached 运维里最…

作者头像 李华