
简介一份基于QT与C实现的德州扑克游戏完整工程适合C课程设计、期末大作业及QT界面开发新手参考麻雀虽小但功能完善可直接编译运行。压缩包共97个文件包含9个cpp和8个h源码文件、2个ui界面设计、1个pro工程配置以及大量png图片素材、ico图标和doc文档打包后仅9.63MB。项目已吸引514人学习浏览可见其实用价值。代码中为关键模块标注了清晰注释覆盖玩家、庄家、牌堆、AI策略等核心类配合文档可快速掌握QT事件机制、信号槽和布局管理。界面操作直观非常适合用来理解完整桌面应用的开发流程也便于在此基础上二次修改完成作业。整体是一份结构清晰、开箱即用的高分课程设计范例。1. 期末大作业选题“QTC德州扑克”为什么这套代码值得你抄进课设如果这学期要交一份QTC课程设计与其在图书管理、贪吃蛇、计算器里挑来挑去不如看看“基于QTC开发的德州扑克游戏源码详细文档说明期末大作业”这个方向。德州扑克的规则刚好卡在大学C课的知识范围内洗牌要随机算法牌型判定要组合比较下注流程是典型状态机界面还能把QT的信号槽、自绘控件、布局管理全部用上一套代码覆盖五个以上考核点。这里按我自己的交作业路径来写先立工程结构再把规则层跑通然后接界面和自绘最后把编译踩坑和答辩准备放在一起。适合刚拿到源码不知道怎么组织的人也适合想自己重写但怕牌型逻辑翻车的人。你不需要懂QT底层按章节走下去就能得到一份能跑、能改、能讲清楚的设计。2. 先把工程立住三层目录结构、.pro配置和五个核心类2.1 界面层/规则层/数据层目录怎么摆依赖怎么指拿到一个QT项目的源码第一件事不是看main.cpp而是看目录。我习惯把工程拆成core、ui、doc三层源码组织大致是这样HoldemTable/ ├── main.cpp ├── HoldemTable.pro ├── core/ │ ├── card.h / card.cpp │ ├── deck.h / deck.cpp │ ├── hand_evaluator.h / hand_evaluator.cpp │ ├── player.h / player.cpp │ └── game_table.h / game_table.cpp ├── ui/ │ ├── mainwindow.h / mainwindow.cpp │ ├── card_label.h / card_label.cpp │ └── seat_widget.h / seat_widget.cpp └── doc/ └── 期末作业说明.mdcore目录理论上只依赖QtCore不碰任何widgets组件ui目录只负责界面和信号转发。一个很直接的检验方式把core目录下的.cpp单独编译成一个控制台程序如果能通过编译说明界面依赖没有漏进规则层。期末答辩老师问“项目核心逻辑在哪”你就可以直接指GameTable和HandEvaluator这两个类。新手最常见的错误是把牌型判断写在MainWindow里最后越改越乱。界面类要刷新几十个控件还承担着规则判断一旦逻辑出错你连断点都不知道打在哪。分层之后MainWindow只做三件事接收按钮点击、调用GameTable的接口、刷新显示。谁改了底池、谁赢了多少这些状态全部由core层的GameTable回答。2.2 .pro文件里的关键配置c17、widgets模块和/utf-8.pro文件是qmake的入口很多编译问题其实在.pro里就埋下了。期末项目建议直接把.pro写成下面这样QT core gui widgets CONFIG c17 TARGET HoldemTable TEMPLATE app SOURCES \ main.cpp \ core/card.cpp \ core/deck.cpp \ core/hand_evaluator.cpp \ core/player.cpp \ core/game_table.cpp \ ui/mainwindow.cpp \ ui/card_label.cpp \ ui/seat_widget.cpp HEADERS \ core/card.h \ core/deck.h \ core/hand_evaluator.h \ core/player.h \ core/game_table.h \ ui/mainwindow.h \ ui/card_label.h \ ui/seat_widget.h msvc { QMAKE_CXXFLAGS /utf-8 }这里解释三个关键配置。第一widgets模块必须加否则QWidget、QLabel这些类全部找不到。第二c17为的是让代码里可以用结构化绑定和if初始化语句如果你所在实验室编译器比较老改回c11也能编译代价是手动拆pair。第三msvc块里的/utf-8是给MSVC编译器看的防止UTF-8源码里的中文在编译时按GBK解析导致乱码如果你用MinGW这个块不生效应对办法是字符串统一用QStringLiteral包裹。还有一条血泪经验每次增删了.cpp或.h文件一定要先在Qt Creator里执行一次qmake再执行构建。只点“构建”不会自动重新读取.pro后果是“头文件找不到”之类的报错很多人以为是代码问题其实是Makefile没更新。2.3 五个核心类的最小设计谁持有状态谁只发命令一个不夸张的最小设计只需要五个类Card、Deck、HandEvaluator、Player、GameTable。它们的职责边界是这条线的关键。类职责关键接口Card单张牌的数据结构只存rank和suitrank取2~14suit取0~3Deck管理52张牌提供洗牌和发牌shuffle()、drawCard()HandEvaluator纯静态方法判定5张/7张牌型并比较大小evaluateFive()、evaluateSeven()Player玩家状态姓名、筹码、手牌、是否弃牌fold()、addChips()、resetHand()GameTable牌局状态机阶段、底池、盲注、行动轮转advancePhase()、playerAct()、settle()这套设计里GameTable持有Deck和所有Player但本身不继承QObject不包含任何界面控件指针。它的输入是“哪个玩家做了什么动作”输出是“当前状态变了”MainWindow再去拉取状态刷新界面。这样做的好处是规则验证可以在完全脱离界面的事件循环下进行AI模块以后也可以直接复用GameTable。很多网上的源码会把Deck、HandEvaluator全写成MainWindow的私有方法看起来省事但答辩时很难自圆其说。把类拆出来答“为什么这样设计”时你只需要说一句话数据、规则、界面三者隔离规则层可独立测试。3. 从洗牌到摊牌德州扑克规则层的完整C实现3.1 洗牌与发牌用QRandomGenerator替代rand()避免每次启动牌序一样洗牌这件事教科书喜欢讲Fisher-Yates但很多人实际写的时候还是用rand()。rand()有两个问题不设置随机种子会导致每次启动程序牌序完全一样它通常只提供15位随机性洗52张牌时排列空间的覆盖度不够。QT 5.10以后提供了QRandomGenerator底层采用系统随机源线程安全直接和std::shuffle配合。Deck的构造和洗牌实现如下#include QRandomGenerator #include algorithm Deck::Deck() { for (int suit 0; suit 4; suit) for (int rank 2; rank 14; rank) cards_.emplace_back(rank, suit); } void Deck::shuffle() { std::shuffle(cards_.begin(), cards_.end(), *QRandomGenerator::global()); } Card Deck::drawCard() { if (cards_.empty()) return Card(); Card c cards_.back(); cards_.pop_back(); return c; }这里解释两个细节。第一std::shuffle的第三个参数要求传入一个均匀随机位生成器对象QRandomGenerator::global()返回的是指针所以必须解引用再传。第二Deck构造时按“先花色后点数”双重循环生成52张牌保证每局开始时牌序可预期洗牌后才随机。发牌时从vector尾部取牌时间复杂度O(1)。发牌逻辑也有讲究。在德州扑克里每人两张底牌应该是一张一张发的而不是先给1号玩家发两张再给2号玩家发两张。对应代码是两层循环void GameTable::dealHoleCards(int playerCount) { for (int round 0; round 2; round) { for (auto p : players_) { if (!p.folded p.chips 0) p.handCards.push_back(deck_.drawCard()); } } }这个顺序在桌面上就是轮流发牌写完后可以打印每名玩家的handCards.size()来验证必须是每人两张且不会有人多拿。3.2 牌型判定与比大小C(7,5)组合枚举规避贪心判定的边界漏洞德州扑克规则层最核心的部分是牌型判定。常见做法是从7张牌2张手牌5张公共牌里选出最大的5张牌组成牌型。我选择直接用组合枚举C(7,5)等于21把21种5张组合全部算一遍取最大的HandValue。21次判定的耗时在微秒级完全不影响界面流畅度而且不用写各种贪心规则去处理“既是同花又是顺子”的边界。先定义一个HandValue结构体它不只是一个整数等级还带一组用于比较的kickersstruct HandValue { int rank 0; // 0高牌 1一对 2两对 3三条 4顺子 5同花 6葫芦 7四条 8同花顺 std::vectorint kickers; // 从大到小的比较值 bool operator(const HandValue other) const { if (rank ! other.rank) return rank other.rank; return std::lexicographical_compare( other.kickers.begin(), other.kickers.end(), kickers.begin(), kickers.end()); } };rank决定牌型等级kickers存关键牌的点数。比如一对时kickers是{对子点数, 剩余三张踢脚}两对时是{高对点数, 低对点数, 踢脚}。比较时先比rank再逐个比kickers正好对应“先比牌型再比牌面大小”的规则。5张牌的具体判定函数可以这样写#include algorithm #include cassert HandValue evaluateFive(const std::vectorCard cards) { assert(cards.size() 5); std::vectorint ranks; for (const auto c : cards) ranks.push_back(c.rank); std::sort(ranks.rbegin(), ranks.rend()); bool flush true; for (int i 1; i 5; i) if (cards[i].suit ! cards[0].suit) flush false; std::vectorint uniq; for (int r : ranks) if (uniq.empty() || uniq.back() ! r) uniq.push_back(r); bool straight (uniq.size() 5 uniq.front() - uniq.back() 4); if (uniq.size() 5 uniq[0] 14 uniq[1] 5 uniq[2] 4 uniq[3] 3 uniq[4] 2) { straight true; ranks {5, 4, 3, 2, 1}; // A2345 按 5 高顺子比较 } std::vectorstd::pairint, int cnt; for (int r : ranks) { if (cnt.empty() || cnt.back().first ! r) cnt.push_back({r, 1}); else cnt.back().second; } std::sort(cnt.begin(), cnt.end(), [](const auto a, const auto b) { if (a.second ! b.second) return a.second b.second; return a.first b.first; }); HandValue hv; if (flush straight) hv.rank 8; else if (cnt[0].second 4) hv.rank 7; else if (cnt[0].second 3 cnt[1].second 2) hv.rank 6; else if (flush) hv.rank 5; else if (straight) hv.rank 4; else if (cnt[0].second 3) hv.rank 3; else if (cnt[0].second 2 cnt[1].second 2) hv.rank 2; else if (cnt[0].second 2) hv.rank 1; else hv.rank 0; for (const auto p : cnt) hv.kickers.push_back(p.first); return hv; }这段代码有几个容易写错的点。第一cnt排序时先按出现次数降序再按牌值降序这样kickers天然是“先关键牌大小再踢脚”的顺序。第二A2345这种特殊顺子A要降级成1所以判断命中后直接把ranks重置为{5,4,3,2,1}而不是用{14,5,4,3,2}参与比较否则它会比KQJ10 9还大完全反了。7张牌取最大牌型用组合数枚举HandValue evaluateSeven(const std::vectorCard seven) { assert(seven.size() 7); HandValue best; std::vectorint idx {0, 1, 2, 3, 4}; while (true) { std::vectorCard five { seven[idx[0]], seven[idx[1]], seven[idx[2]], seven[idx[3]], seven[idx[4]] }; HandValue hv evaluateFive(five); if (hv best) best hv; int pos 4; while (pos 0 idx[pos] 7 - 5 pos) --pos; if (pos 0) break; idx[pos]; for (int j pos 1; j 5; j) idx[j] idx[j - 1] 1; } return best; }枚举下标的递增是标准组合数生成逻辑这里不做剪枝因为21次调用evaluateFive的开销可以忽略。之所以不用贪心判定是因为“7张里选5张”存在很多交叉情况比如同时有顺子和同花但未必是同花顺贪心容易漏。组合枚举的优点是逻辑简单肉眼可验证答辩时也容易讲。牌型等级表建议直接写进文档方便答辩时展示rank牌型说明8同花顺例如 A♠ K♠ Q♠ J♠ 10♠7四条例如 8♣ 8♦ 8♥ 8♠ A♥6葫芦三条加一对5同花五张同花色但不成顺4顺子五张连续但花色不同3三条三张同点加两张散牌2两对两个对子加一张踢脚1一对一个对子加三张踢脚0高牌什么都不沾比最大单张3.3 下注轮转的状态机翻牌前、翻牌、转牌、河牌的流转条件德州扑克整局的核心状态流转是翻牌前(PreFlop) - 翻牌(Flop) - 转牌(Turn) - 河牌(River) - 摊牌(Showdown)。用枚举加switch实现enum class Phase { PreFlop, Flop, Turn, River, Showdown }; void GameTable::advancePhase() { switch (phase_) { case Phase::PreFlop: burnCard(); dealCommunity(3); phase_ Phase::Flop; break; case Phase::Flop: burnCard(); dealCommunity(1); phase_ Phase::Turn; break; case Phase::Turn: burnCard(); dealCommunity(1); phase_ Phase::River; break; case Phase::River: phase_ Phase::Showdown; break; case Phase::Showdown: settle(); break; } }这里的burnCard()是烧牌从牌堆抽一张直接弃掉不放进公共牌。规则上这是为了防止有人通过上一轮操作顺序推测下一张牌程序里更多是为了模拟真实牌桌仪式感但很多源码会漏掉这一步导致公共牌数量对不上所以要保留。advancePhase()不能由界面按钮直接乱调必须先确认当前下注轮次已经结束。下注轮转的判断逻辑是核心bool GameTable::bettingRoundFinished() const { int baseCall currentCallAmount_; for (auto p : players_) { if (p.folded || p.chips 0) continue; if (p.lastBet ! baseCall) return false; } return true; } void GameTable::nextTurn() { do { currentIdx_ (currentIdx_ 1) % players_.size(); } while (players_[currentIdx_].folded || players_[currentIdx_].chips 0); }下注轮转的边界条件比想象中多。弃牌玩家跳过筹码为0的全押玩家跳过只剩下一个玩家时牌局必须提前结束。另外多数实现会记录lastAggressorIndex也就是最近一次加注的人轮到它时这一轮才结束不然只会陷入无限循环。这个字段在期末文档里建议单独写清楚。3.4 摊牌结算与底池分配先比牌型再算筹码最后处理全押边界摊牌结算时先把未弃牌的玩家挑出来每人用自己的2张手牌加5张公共牌组成7张调用evaluateSeven取最大值然后找出最大值对应的玩家void GameTable::settle() { std::vectorPlayer* showdownPlayers; for (auto p : players_) if (!p.folded) showdownPlayers.push_back(p); if (showdownPlayers.empty()) return; Player* winner showdownPlayers.front(); HandValue bestValue evaluateSeven(combine7(winner-handCards)); for (Player* p : showdownPlayers) { HandValue hv evaluateSeven(combine7(p-handCards)); if (hv bestValue) { bestValue hv; winner p; } } winner-chips pot_; pot_ 0; }combine7是一个辅助函数把手牌和公共牌拼成一个7张vector这里省略展开。需要注意如果出现完全相同的牌型按德州扑克规则应该平分底池。期末项目里可以做成“双赢平分”也可以做成“按座位顺序先到先得”但后者建议在文档里注明是简化处理。全押是另一个边界黑洞。当玩家A投入100而玩家B只投入50时A不能赢走B没投入的部分。完整实现需要边池(side pot)计算对期末作业来说复杂度明显上升。一个诚实的做法是界面端限制同时只有一人可全押或者在文档中写明“当前版本只支持单底池多人全押按总底池平分”。答辩时坦白说明这个取舍远比隐藏bug后被老师试出来要好。4. 把牌桌画出来QT Widgets界面层与自绘扑克牌4.1 主窗口的信号槽链路按钮只发命令规则层不碰界面界面层第一原则按钮点击不直接修改牌局数据而是通过GameTable的接口执行动作然后整体刷新UI。这个链路一旦建立后面的逻辑会非常干净。主窗口里三个动作按钮的连接可以写成connect(btnFold, QPushButton::clicked, this, [] { gameTable_.playerAct(currentPlayerIndex(), PlayerAction::Fold); refreshUi(); }); connect(btnCall, QPushButton::clicked, this, [] { gameTable_.playerAct(currentPlayerIndex(), PlayerAction::Call); refreshUi(); }); connect(btnRaise, QPushButton::clicked, this, [] { gameTable_.playerAct(currentPlayerIndex(), PlayerAction::Raise, raiseSpinBox-value()); refreshUi(); });新式connect语法在这里有两个好处信号和槽参数不匹配会在编译期报错lambda里可以顺手刷新界面。如果你在网上的老代码里看到connect(btn, SIGNAL(clicked()), this, SLOT(onClick()))这种写法字符串一旦拼错运行期才提示“No such slot”期末阶段很容易排查半天。refreshUi()的典型实现是从gameTable_拉取玩家列表、底池、公共牌然后逐个更新控件内容。它不包含任何规则判断只做显示同步。这样即使界面显示有bug也不会污染牌局数据。4.2 CardLabel自绘控件用QPainter画牌面和牌背不依赖图片素材期末大作业如果去找扑克牌图片素材会遇到分辨率不匹配、背景色不统一、素材包里缺牌等各种问题。更省事且答辩更讨喜的做法是自绘控件。继承QWidget重写paintEvent用QPainter画圆角矩形和花色文字class CardLabel : public QWidget { Q_OBJECT public: void setCard(const Card c) { card_ c; hasCard_ true; update(); } void setFaceDown() { hasCard_ false; update(); } protected: void paintEvent(QPaintEvent*) override { QPainter p(this); p.setRenderHint(QPainter::Antialiasing); if (!hasCard_) { p.setBrush(QColor(30, 30, 60)); p.drawRoundedRect(rect(), 8, 8); return; } p.setBrush(Qt::white); p.drawRoundedRect(rect(), 8, 8); QString text rankText() suitText(); QColor color (card_.suit 1 || card_.suit 2) ? Qt::red : Qt::black; p.setPen(color); QFont font; font.setFamily(QStringLiteral(Segoe UI Symbol)); font.setPixelSize(20); p.setFont(font); p.drawText(rect(), Qt::AlignCenter, text); } private: Card card_; bool hasCard_ false; };paintEvent会在调用update()时被触发所以setCard之后调用update()即可重绘。setRenderHint(QPainter::Antialiasing)是抗锯齿开关不设置的话圆角矩形边缘会出现明显锯齿影响观感。花色颜色判断要注意suit默认0黑桃、1红心、2方块、3梅花所以红心方块取红色黑桃梅花取黑色。这里不贴图片素材的另一个原因是可以现场调参。答辩时老师如果问“花色怎么画出来的”直接改一行颜色代码重新编译演示比解释素材来源有说服力得多。4.3 复用玩家座位一个QFrame加两个QLabel完成信息展示六个玩家座位如果每个都手动写一遍布局代码会膨胀到没法看。更合理的方式是做一个SeatWidget内部固定结构头像区、两张手牌、筹码数。SeatWidget::SeatWidget(const QString name, QWidget* parent) : QWidget(parent) { auto* layout new QVBoxLayout(this); nameLabel_ new QLabel(name, this); chipsLabel_ new QLabel(QStringLiteral(筹码1000), this); card1_ new CardLabel(this); card2_ new CardLabel(this); layout-addWidget(nameLabel_); layout-addWidget(card1_); layout-addWidget(card2_); layout-addWidget(chipsLabel_); setFixedSize(120, 180); }界面布局上公共牌区域放在桌面中央六名玩家座位按下标围成一圈。MainWindow里用QGridLayout把SeatWidget摆到对应位置。当前轮到的玩家座位可以用样式表高亮边框seatWidget-setStyleSheet( QStringLiteral(QWidget#activeSeat { border: 2px solid #ffcc00; }));注意样式表设置后要确保SeatWidget的objectName正确否则边框不生效。setFixedSize防止布局在筹码位数变化时跳动这个细节虽然小但演示时筹码从999变成1000会导致整个桌面抖一下很掉价。5. 编译与运行避坑Qt 5.15.2、MSVC与中文路径的5条血泪经验这一章的背景环境是Qt 5.15.2配MSVC2019_64加Qt Creator这也是期末机器上最常见的组合。整套环境如果一路绿灯只需要十几分钟但翻车点集中在路径、编码、信号槽三件事上。下面每条按“现象 - 原因 - 解决”来说。5.1 报错“:-1: error: dependent ... qtwidget”时先查Kit路径而不是重装现象打开.pro后第一次构建编译器直接弹出一行超长路径错误报错信息里出现类似“dependent ....\qt\5.15.2\msvc2019_64\include\qtwidget does not exist”的提示整个项目的头文件全部飘红。原因Qt Creator的构建套件(Kit)里Qt Version路径没有正确指向实际安装位置。常见触发场景是从其他机器拷贝了.pro或整个工程目录而工程依赖的绝对路径还残留着别人的盘符或者手动修过QTDIR环境变量指向了一个不存在的include目录。解决打开Qt Creator的“工具 - 选项 - Kits - Qt Versions”检查当前选中的qmake路径是否真实存在。确认后重新执行qmake再重新构建。不要一上来就重装Qt这个报错九成是路径配置问题不是安装损坏。5.2 QString中文乱码源码编码、MSVC和/utf-8之间的三角关系现象按钮文字、日志输出里的中文变成“锟斤拷”或一堆问号英文和数字正常。原因VS系列编译器默认按本地代码页GBK解释窄字符字面量而大多数情况下源码文件保存为UTF-8。Qt 5中QString从const char*构造时会按本地编码解码两边的解释不一致中文就变成乱码。Qt 5.15本身是支持UTF-8源码的但需要追加编译选项。解决在.pro文件里加一行让MSVC按UTF-8解析源码msvc { QMAKE_CXXFLAGS /utf-8 }如果是MinGW工具链msvc块不生效需要把中文字符串统一写成QStringLiteral或者把源码转成带BOM的UTF-8。期末阶段最保险的做法是两者同时做.pro加/utf-8代码里所有中文字符串用QStringLiteral包裹。这样即使换机器出现乱码的概率也会大幅下降。5.3 按钮点击无反应新式connect与带默认参数信号的选择现象编译通过界面也正常启动但点击“跟注”“加注”按钮后界面毫无变化日志窗口也没有任何输出。原因这通常不是信号没触发而是连接方式有隐患。老式SIGNAL/SLOT宏里信号或槽函数名写错一个字符编译期不报错运行期只在输出窗口打印“No such signal/slot”。很多人根本不看Qt Creator的“输出”面板于是表现为按钮无反应。另一种常见情况是按钮被setEnabled(false)之后忘了恢复界面处于灰化状态。解决统一使用新式语法连接信号和lambda不要在期末代码里混用字符串宏。连接后用qDebug加一行输出确认触发链路是通的。connect(btnCall, QPushButton::clicked, this, [] { qDebug() QStringLiteral(clicked call); gameTable_.playerAct(currentPlayerIndex(), PlayerAction::Call); refreshUi(); });顺带提一个隐蔽点QPushButton::clicked信号带一个bool参数如果你的lambda不接收参数新式connect也能正常连接但如果槽函数是一个无参成员函数就需要注意编译期对参数数量的检查规则。出现不确定时优先用lambda壳包一层最省事。5.4 高分屏界面发虚QApplication创建前打开High-DPI属性现象在高分屏笔记本上运行整个界面字体和控件边缘发虚按钮文字有毛边窗口尺寸偏小。原因Qt 5默认不自动启用高分屏缩放系统DPI超过100%时界面按物理像素绘制最终被系统拉伸自然就糊了。Qt 5.15.2需要手动开启。解决在main函数里创建QApplication之前设置属性#include QApplication int main(int argc, char *argv[]) { QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }注意setAttribute必须发生在QApplication对象构造之前否则不会生效。如果以后把项目切到Qt 6这行可以不写因为Qt 6默认开启高DPI缩放。5.5 Unicode花色显示成方框字体缺失不是程序错误现象界面上的花色字符显示成一个个小方框或问号其他文字正常。原因当前控件所用的字体不包含♠♥♦♣这些Unicode字符的glyph。Windows上默认字体有时会缺字符尤其是精简版系统或远程桌面环境。解决在绘制时显式指定一个包含扑克牌花色的字体比如Windows自带的Segoe UI SymbolQFont font; font.setFamily(QStringLiteral(Segoe UI Symbol)); font.setPixelSize(20);如果机器连这个字体都没有就不要和字体死磕直接把花色映射成S/H/D/C字母或者自己用QPainter画实心圆和尖角图标。功能不受影响答辩时也不会因为字体问题翻车。Linux环境下可以尝试Noto Sans Symbols但期末项目建议优先保证Windows上能跑通。6. 验收、自测与答辩让期末项目经得起追问6.1 手动测试用例表与日志开关交作业之前至少把下面这张手工测试表跑一遍并把结果写进文档里场景操作期望结果弃牌翻牌前点击弃牌该玩家手牌隐藏不再参与下注轮转跟注前一玩家加注100后跟注当前玩家lastBet等于100底池增加100加注点击加注并输入200当前玩家lastBet变为200后续玩家需跟注200全押筹码不足跟注额时点跟注玩家筹码清零后续轮转跳过该玩家摊牌河牌后自动结算底池归入最高牌型玩家界面弹出赢家提示为了排查方便可以在GameTable里加一个日志开关只在调试构建打印#ifdef QT_DEBUG qDebug() QStringLiteral([牌局] %1 行动底池 %2) .arg(playerName).arg(pot_); #endif发布构建时QT_DEBUG未定义这些日志自动剔除不用手动删。6.2 不依赖GTest的回归测试assert 一个main入口跑完核心规则用Google Test搭测试框架对期末项目反而增加负担。直接用assert写一个回归测试函数挂在单独的控制台main入口里#include cassert void testHandEvaluator() { std::vectorCard royal { Card(14, 1), Card(13, 1), Card(12, 1), Card(11, 1), Card(10, 1) }; assert(evaluateFive(royal).rank 8); std::vectorCard lowStraight { Card(14, 0), Card(5, 0), Card(4, 0), Card(3, 0), Card(2, 0) }; assert(evaluateFive(lowStraight).rank 4); std::vectorCard fullHouse { Card(10, 0), Card(10, 1), Card(10, 2), Card(4, 0), Card(4, 1) }; std::vectorCard flush { Card(2, 3), Card(5, 3), Card(8, 3), Card(11, 3), Card(13, 3) }; assert(evaluateFive(fullHouse) evaluateFive(flush)); // A2345是最小顺子应小于6-high顺子 }这个测试的巧妙之处在于它同时验证了三件事皇家同花顺判定为8、A2345被识别为顺子、葫芦大于同花。跑测试时用Debug配置assert才会生效一旦某个断言失败程序会直接中断并报告行号问题范围锁定非常快。6.3 答辩加分点AI跟注决策、Monte Carlo胜率模拟、QML重构方向如果想让项目在同班同学里脱颖而出三个方向可以任选一个做深。第一是给电脑玩家加一个简单AI不写复杂策略只用期望值判断模拟胜率乘以当前底池如果大于跟注所需筹码就跟否则弃牌。第二是在界面显示当前手牌胜率做法是Monte Carlo模拟随机补全对手手牌和剩余公共牌两万次后统计胜率。第三是QML重构方向把Widgets界面替换成QML规则层GameTable用Q_INVOKABLE暴露给前端。答辩时说到“规则层和界面层彻底解耦将来换QML界面不需要动C逻辑”这句话老师通常会追问细节答出来就是亮点。我交期末作业前最怕的不是程序跑不起来而是老师问一个边角规则我说不清。后来养成的习惯是每写一条规则就在测试代码里留一条断言每换一次编译器先跑一遍core层测试再开界面。这个习惯帮我避开了很多隐蔽问题希望也能帮到你。本文还有配套的精品资源点击获取