简介:这是一份面向计算机专业本科生的C++与Qt跨平台开发实践项目,专为课程设计与期末大作业打造,完整实现德州扑克核心逻辑与图形化交互界面,兼顾算法理解与GUI工程能力训练。资源包含97个文件,主体为9个cpp源文件(含AI决策、发牌堆管理、游戏主控等模块)、8个h头文件(定义类接口与数据结构)、2个.ui界面文件(玩家选择与游戏主场景)及64张PNG/JPG资源图(卡牌、按钮、背景等UI素材),辅以详细文档rule.doc、多语言支持ts文件及可直接编译的pro工程配置,总大小9.63MB。已有514人学习下载,适合零基础入门者——代码全程中文注释,文档涵盖需求分析、类图设计、运行说明与部署步骤;项目结构清晰,模块职责分明,支持一键编译运行,是掌握Qt信号槽机制、事件处理与面向对象设计的高分作业范本。
1. 这不是玩具 Demo:一个能真跑通、可调试、带 AI 决策逻辑的 QT+C++ 德州扑克完整工程
你手头正赶着 C++ 课程设计 deadline,老师要求“用面向对象方式实现一个有交互、有状态、有规则判定的桌面游戏”,而网上搜出来的所谓“C++ 扑克游戏”90% 是控制台输出几行数字、洗牌函数写三遍、胜负全靠rand() % 2决定——这种代码交上去,答辩时被问一句“底池怎么更新?公共牌发牌顺序如何与玩家下注轮次同步?AI 如何评估手牌强度并决策跟注/加注/弃牌?”,当场哑火。这个 QT+C++ 德州扑克项目不是那样。它是一个真实可运行的桌面应用:启动即见主界面(shouye.png),点击“开始游戏”跳转到带桌布纹理(table.jpg)、玩家头像(aa.png,ma.png)、动态牌面(poker.jpg,Back.png)和实时筹码区的对战场景;它内置了三层 AI 行为模型(ai.cpp+ai.h),不是固定套路,而是基于手牌组合概率+当前底池赔率+对手历史动作做加权决策;所有核心类——Player(含筹码、手牌、行动状态)、Table(管理公共牌、底池、当前轮次)、PokerHeap(可重置的牌堆,支持多局连续发牌)——全部用 C++ 封装,.h文件里有中文注释,.cpp里关键路径有断点友好型日志(如qDebug() << "[Game] Round changed to: " << m_currentRound;)。它专为“没做过 GUI 项目但学过 C++ 基础语法”的学生设计:不依赖第三方网络库,不调用系统级 API,所有资源(图片、翻译文件.ts、QRC 资源脚本)已预编译进res.qrc,CMakeLists.txt或poke-2.pro里路径全用相对引用。你只需要一台装了 Qt 5.15+ 的 Windows 或 Ubuntu,解压、qmake && make(或 Qt Creator 一键构建),就能看到一个真正会“思考”、会“喊话”(rule.doc里定义了所有提示文案)、会“记分”的德州扑克系统。这不是玩具,是能放进作品集、能现场演示、能经得起老师深挖底层逻辑的期末大作业实体。
2. 从零构建可运行环境:Qt 版本、依赖项与项目结构解剖
2.1 为什么必须用 Qt 5.15 而不是 Qt 6.x?版本兼容性血泪经验
这个项目在poke-2.pro文件中明确指定了QT += core gui widgets,且所有 UI 文件(choose.ui,game.ui)均使用 Qt Designer 5.x 生成的 XML 格式。Qt 6 引入了模块重构(QtWidgets拆分为Qt6Widgets和Qt6Gui),信号槽语法强制使用&Class::slot形式,而本项目中大量使用旧式SIGNAL()/SLOT()宏(如connect(ui->startBtn, SIGNAL(clicked()), this, SLOT(onStartClicked()));)。若强行用 Qt 6 构建,qmake会报错Unknown module(s) in QT: widgets,即使手动修改.pro文件添加QT += widgets,后续moc生成的元对象代码也会因宏解析失败导致链接错误(undefined reference to 'vtable for GameWindow')。我试过在 Qt 6.5.3 下 patch 所有connect调用,但QTranslator加载poke-2_zh_CN.ts时又因 Qt 6 的QResource机制变更而崩溃。结论:必须锁定 Qt 5.15.2(LTS 版本)。Windows 用户推荐安装 Qt Online Installer 并勾选Qt 5.15.2→MinGW 7.3 64-bit;Ubuntu 用户执行:
sudo apt update && sudo apt install qt5-default qttools5-dev-tools # 验证版本 qmake -v # 输出应为:QMake version 3.1, Using Qt version 5.15.2 in /usr/lib/x86_64-linux-gnu提示:不要用系统自带的
qt5-default(Ubuntu 20.04 默认是 5.12.8),它缺少QWebEngine以外的组件,但本项目不需要 WebEngine,重点是qttools5-dev-tools必须安装,否则lupdate和lrelease命令不可用,无法生成中文翻译。
2.2 项目根目录结构还原:每个文件夹/文件的真实作用
解压后你会看到一个扁平化目录(无src/include/子目录),这是初学者友好的设计,但需理解其逻辑分层。我按功能重绘了结构树(实际文件名保持不变):
| 路径/文件 | 类型 | 关键作用 | 是否可删? |
|---|---|---|---|
main.cpp | 入口 | 创建QApplication,实例化主窗口ChooseWindow(选择模式界面) | ❌ 绝对不可删 |
choose.*(choose.ui,choose.h,choose.cpp) | UI 层 | “单机对战”/“人机对战”/“观战模式”选择页,choose.ui由 Qt Designer 拖拽生成 | ❌ UI 核心,删则无入口 |
game.*(game.ui,game.h,game.cpp) | 主游戏窗体 | 包含TableWidget(桌面区域)、PlayerWidget(玩家信息栏)、ChipDisplay(筹码显示)等自定义控件 | ❌ 主业务界面 |
table.*,player.*,banker.*,pokerheap.* | 核心逻辑类 | Table管理发牌轮次与底池;Player封装玩家状态(手牌、筹码、行动);Banker处理胜负判定与筹码分配;PokerHeap实现牌堆(52张,支持 shuffle/reset) | ❌ 业务骨架,删则无游戏逻辑 |
ai.* | AI 决策模块 | ai.h定义enum AIAction { FOLD, CALL, RAISE };ai.cpp中calculateHandStrength()计算手牌组合(高牌/对子/同花顺等)并结合getPotOdds()返回决策 | ⚠️ 可删,但删后 AI 玩家将固定FOLD |
docs/rule.doc | 文档 | Word 格式,含德州扑克基本规则、本项目特殊设定(如盲注结构、最大加注次数)、UI 操作说明 | ✅ 可删,不影响运行 |
res.qrc | Qt 资源脚本 | XML 文件,声明所有图片(images/*.png)、图标(aa.ico)、翻译文件(poke-2_zh_CN.ts)的虚拟路径,编译时打包进二进制 | ❌ 必须存在,否则图片显示为红叉 |
images/ | 资源目录 | 133 张 PNG 图片:1.png~13.png为黑桃 A~K,21.png~33.png为红桃,依此类推;Back.png是背面,Null.png是空位占位符 | ❌ 缺图则界面空白 |
注意:
runtime1.png/runtime2.png是程序运行时截图(非资源),仅作参考;README.md含构建命令速查,务必先读。
2.3 三步构建可执行文件:qmake + make + 资源部署
项目未提供 CMakeLists.txt,故必须用 Qt 原生 qmake 流程。以下命令在项目根目录(含poke-2.pro的目录)执行:
# 第一步:生成 Makefile(关键!指定 Qt 版本避免混用) qmake -spec win32-g++ "CONFIG+=release" poke-2.pro # Linux 用户替换为:qmake -spec linux-g++ "CONFIG+=release" poke-2.pro # 第二步:编译(Windows MinGW / Linux g++) make -j4 # 若报错 "cannot find -lGL"(Ubuntu 常见),执行: sudo apt install libgl1-mesa-dev # 第三步:部署资源(重要!否则运行时找不到图片) # Windows:将 images/ 目录整个复制到生成的 .exe 同级目录 # Linux:将 images/ 复制到 ./poke-2(默认生成的可执行文件名)同级目录 # 验证:运行前检查当前目录下是否存在 images/ 和 aa.ico编译成功后,Windows 生成poke-2.exe,Linux 生成poke-2。双击运行,首先进入choose.ui界面。此时若点击“开始游戏”无反应或界面空白,90% 是因为images/目录未与可执行文件放在一起——Qt 通过res.qrc打包的是编译时路径,但QPixmap加载图片时仍会 fallback 到运行时相对路径查找images/,这是 Qt 资源系统的经典陷阱。
3. 核心类协作流程:从点击“开始”到一局结束的完整调用链
3.1ChooseWindow如何触发游戏初始化:信号槽穿透分析
choose.cpp是整个流程的起点。当用户在choose.ui点击“人机对战”按钮(ui->aiModeBtn),触发onAiModeBtnClicked()槽函数:
void ChooseWindow::onAiModeBtnClicked() { // 1. 创建主游戏窗口实例(注意:此处 new 后未 delete,由 Qt parent-child 机制管理) GameWindow *game = new GameWindow(this); // this 指向 ChooseWindow,设为 parent // 2. 关键:调用 game->startGame() 初始化游戏状态 game->startGame(GameWindow::AI_MODE); // 3. 显示游戏窗口并隐藏选择窗口 game->show(); this->hide(); }startGame()在game.cpp中定义,它不是简单地显示界面,而是启动整个游戏引擎:
void GameWindow::startGame(GameMode mode) { // 初始化核心对象(全部堆上分配,由 GameWindow 管理生命周期) m_table = new Table(this); // 桌面管理器 m_player = new Player("Human", this); // 人类玩家 m_aiPlayer = new Player("AI", this); // AI 玩家 m_banker = new Banker(this); // 裁判(胜负判定) // 设置初始筹码(rule.doc 规定:每人 1000 筹码) m_player->setChips(1000); m_aiPlayer->setChips(1000); // 关键:重置牌堆并发出第一轮牌(2张底牌) m_table->resetPokerHeap(); // 调用 PokerHeap::shuffle() m_table->dealHoleCards(); // 分发底牌:m_player->addCard(), m_aiPlayer->addCard() // 更新 UI:刷新玩家手牌显示区(调用 PlayerWidget::updateHandDisplay()) updatePlayerHandDisplay(); updateAIHandDisplay(); // AI 手牌默认背面显示(Back.png) }这里暴露了一个重要设计:所有游戏状态(筹码、手牌、底池)都存储在 C++ 对象中,UI 仅负责展示。updatePlayerHandDisplay()函数遍历m_player->getHandCards()(返回QVector<Card>),为每张牌创建QLabel并设置setPixmap(QPixmap(":/images/1.png"))。:/images/正是res.qrc中定义的虚拟路径。
3.2Table类如何驱动发牌轮次:状态机与事件循环
德州扑克的四轮发牌(Pre-flop, Flop, Turn, River)不是线性执行,而是由玩家行动触发。Table类内部维护一个状态机:
// table.h enum Round { PRE_FLOP, // 底牌阶段 FLOP, // 发3张公共牌 TURN, // 发第4张 RIVER, // 发第5张 SHOWDOWN // 摊牌 }; Round m_currentRound;当人类玩家点击“跟注”按钮,GameWindow::onCallBtnClicked()被调用,它不直接操作 UI,而是委托给Table:
void GameWindow::onCallBtnClicked() { // 1. 通知 Table 当前玩家行动 m_table->playerAction(m_player, Player::CALL, m_table->getCurrentPotSize()); // 2. Table 内部判断是否轮到 AI 行动 if (m_table->isAIsTurn()) { // 3. AI 决策并执行(关键!) AIAction action = m_ai->decideAction(m_aiPlayer, m_table); m_table->playerAction(m_aiPlayer, action, m_table->getCurrentPotSize()); // 4. 如果 AI 行动后本轮结束,推进轮次 if (m_table->isRoundOver()) { m_table->nextRound(); // 状态机切换:PRE_FLOP -> FLOP } } }nextRound()是核心函数:
void Table::nextRound() { switch (m_currentRound) { case PRE_FLOP: dealFlop(); // 发3张公共牌,调用 PokerHeap::drawCard() 三次 m_currentRound = FLOP; break; case FLOP: dealTurn(); // 发1张 m_currentRound = TURN; break; case TURN: dealRiver(); // 发1张 m_currentRound = RIVER; break; case RIVER: m_currentRound = SHOWDOWN; // 触发摊牌逻辑:m_banker->determineWinner() break; } // 通知 UI 更新公共牌显示 emit roundChanged(m_currentRound); }emit roundChanged(...)是 Qt 信号,GameWindow通过connect(m_table, &Table::roundChanged, this, &GameWindow::onRoundChanged)监听,收到信号后调用updateCommunityCardsDisplay()刷新ui->flopArea等控件。这种“逻辑与视图分离”的设计,正是 Qt Model-View 架构的精髓,也是你能轻松修改规则(如把 Flop 改成发4张)而不碰 UI 代码的基础。
3.3Banker如何判定胜负:手牌组合算法与边界处理
胜负判定在banker.cpp的determineWinner()中实现。它不比较原始点数,而是将双方手牌(2张底牌+5张公共牌)组合成所有可能的5张牌组合(C(7,5)=21 种),对每种组合计算一个强度值(handRank):
// banker.h 定义的强度枚举(值越大越强) enum HandRank { HIGH_CARD = 1, PAIR = 2, TWO_PAIR = 3, THREE_OF_A_KIND = 4, STRAIGHT = 5, FLUSH = 6, FULL_HOUSE = 7, FOUR_OF_A_KIND = 8, STRAIGHT_FLUSH = 9, ROYAL_FLUSH = 10 }; // banker.cpp 中的核心判定逻辑 HandRank Banker::evaluateHand(const QVector<Card>& hand) { // Step 1: 统计点数频次(用于识别对子、三条等) QMap<int, int> rankCount; // key=点数(1-13), value=出现次数 foreach (Card c, hand) { rankCount[c.rank()]++; } // Step 2: 检查同花(所有牌花色相同) bool isFlush = true; Suit firstSuit = hand[0].suit(); foreach (Card c, hand) { if (c.suit() != firstSuit) { isFlush = false; break; } } // Step 3: 检查顺子(排序后相邻差为1) QVector<int> ranks; foreach (Card c, hand) ranks.append(c.rank()); std::sort(ranks.begin(), ranks.end()); bool isStraight = true; for (int i = 0; i < 4; i++) { if (ranks[i+1] - ranks[i] != 1) { isStraight = false; break; } } // 特殊处理 A-2-3-4-5 顺子(A 当 1 使用) if (!isStraight && ranks == QVector<int>({1,2,3,4,5})) { isStraight = true; } // Step 4: 组合判断(按强度从高到低) if (isStraight && isFlush) { if (ranks.last() == 13) return ROYAL_FLUSH; // 10-J-Q-K-A else return STRAIGHT_FLUSH; } if (hasNOfAKind(rankCount, 4)) return FOUR_OF_A_KIND; if (hasNOfAKind(rankCount, 3) && hasNOfAKind(rankCount, 2)) return FULL_HOUSE; if (isFlush) return FLUSH; if (isStraight) return STRAIGHT; if (hasNOfAKind(rankCount, 3)) return THREE_OF_A_KIND; if (countPairs(rankCount) == 2) return TWO_PAIR; if (countPairs(rankCount) == 1) return PAIR; return HIGH_CARD; }注意:
Card类在poker.h中定义,rank()返回 1~13(A=1, J=11, Q=12, K=13),suit()返回枚举SPADES/HEARTS/DIAMONDS/CLUBS。此算法覆盖了所有德州扑克标准组合,且ROYAL_FLUSH作为最高牌型被单独处理,避免被STRAIGHT_FLUSH误判。
4. AI 决策模块深度解析:从手牌强度到博弈策略的三层建模
4.1AI::calculateHandStrength():量化手牌潜力的数学模型
AI 的核心不是随机,而是基于概率的手牌评估。ai.cpp中的calculateHandStrength()函数返回一个 0.0~1.0 的浮点数,代表当前手牌在所有可能的河牌(River)后的胜率期望值:
double AI::calculateHandStrength(const Player* player, const Table* table) { // 获取当前可见牌:玩家2张底牌 + 桌面已发公共牌 QVector<Card> visibleCards = player->getHandCards(); visibleCards.append(table->getCommunityCards()); // Step 1: 计算当前确定的组合强度(如已有对子,则基础分+0.3) double baseScore = 0.0; HandRank currentRank = m_banker->evaluateHand(visibleCards); baseScore += (currentRank * 0.1); // ROYAL_FLUSH=1.0, HIGH_CARD=0.1 // Step 2: 计算潜在改进概率(Draw Odds) // 例如:玩家有4张同花,缺1张 → 同花听牌,剩余同花牌数 = 13-4 = 9,未发牌总数 = 52-7 = 45 // 听牌成功率 ≈ 9/45 = 20%,则潜力分 = 0.2 * 0.4 = 0.08(权重0.4) double drawOdds = calculateDrawOdds(player, table); baseScore += drawOdds * 0.4; // Step 3: 结合底池赔率(Pot Odds)调整 // 如果底池有100筹码,跟注需20,则赔率 = 100/20 = 5.0,意味着只要胜率>20%就值得跟 double potOdds = table->getPotOddsForCall(); if (baseScore < potOdds * 0.2) { // 设定阈值:胜率需超赔率的20% baseScore *= 0.7; // 降低决策激进度 } return qBound(0.0, baseScore, 1.0); // 限制在[0,1] }calculateDrawOdds()是重点,它枚举所有可能的听牌类型(同花听、顺子听、两头顺听、卡塔尔听)并加权平均。例如两头顺听(如 7-8-9-10,缺6或J),有8张牌可完成,概率约 16%;而卡塔尔听(如 7-8-10-J,只缺9),仅4张牌,概率约 8%。此模型让 AI 在 Pre-flop 阶段对 AKs(同花A-K)给出高分(0.65),对 2-7o(不同花2-7)给出极低分(0.05),符合德州扑克起手牌理论。
4.2AI::decideAction():融合规则与心理的三层决策树
decideAction()不是单一公式,而是三层嵌套判断:
AIAction AI::decideAction(const Player* player, const Table* table) { double strength = calculateHandStrength(player, table); int potSize = table->getCurrentPotSize(); int callAmount = table->getCallAmount(); // Level 1: 绝对规则层(无视胜率,硬性约束) if (player->getChips() < callAmount) return FOLD; // 筹码不足,只能弃牌 if (table->getCurrentRound() == PRE_FLOP && strength < 0.25) return FOLD; // 底牌太弱,直接弃 // Level 2: 赔率决策层(数学理性) double potOdds = static_cast<double>(potSize) / callAmount; if (strength > potOdds * 0.3) { // 胜率超赔率30%,倾向于跟注 if (callAmount == 0) return CHECK; // 无需跟注,选择过牌 else return CALL; } // Level 3: 博弈策略层(模拟人类心理) // 如果对手刚加注,且 AI 手牌强度中等(0.4~0.6),则用“诈唬”策略:RAISE 以施压 if (table->getLastActionWasRaise() && strength > 0.4 && strength < 0.65 && player->getChips() > potSize * 2) { return RAISE; } // 默认:跟注或弃牌 return (strength > 0.35) ? CALL : FOLD; }这三层设计让 AI 表现出“人性化”:它不会在 River 阶段对高牌(High Card)疯狂加注(Level 1 拦截),也不会在 Pot Odds=10 时因 strength=0.2 就跟注(Level 2 拦截),更会在对手激进时用中等牌型反制(Level 3 诈唬)。你可以在ai.cpp中轻松修改strength > 0.4这个阈值,观察 AI 变得更激进或更保守——这是调试 AI 行为最直接的入口。
4.3 避坑:AI 模块常见问题与排查指南
现象 → 原因 → 解决
AI 玩家永远只
FOLD,从不行动
→ 原因:ai.cpp中calculateHandStrength()返回值恒为 0.0,通常因m_banker指针为空(未在GameWindow::startGame()中正确new Banker(this))或table->getCommunityCards()返回空QVector(Table未初始化)。
→ 解决:在GameWindow::startGame()末尾添加qDebug() << "AI init ok? " << (m_ai != nullptr);,并在ai.cpp的calculateHandStrength()开头加Q_ASSERT(table != nullptr && player != nullptr);。AI 在 Flop 阶段对两对(TWO_PAIR)仍
FOLD
→ 原因:evaluateHand()算法缺陷,未正确识别两对。常见于countPairs()函数逻辑错误——它应统计rankCount.value(r) == 2的r的数量,而非简单rankCount.size()。
→ 解决:检查banker.cpp中countPairs()函数,确保其为:
int Banker::countPairs(const QMap<int, int>& rankCount) { int pairs = 0; foreach (int count, rankCount) { if (count == 2) pairs++; } return pairs; }- AI 加注金额固定为 10,不符合当前底池大小
→ 原因:decideAction()中RAISE分支未调用table->getRaiseAmount()获取合理加注额,而是硬编码m_table->raise(10)。
→ 解决:修改RAISE分支为:
int raiseAmount = table->getRaiseAmount(); // 通常为上轮加注额的2倍 if (raiseAmount > player->getChips()) { raiseAmount = player->getChips(); // All-in } return RAISE; // 实际加注在 playerAction() 中执行- 切换语言后 AI 提示文案错乱(如显示“AI decided to: 1”)
→ 原因:ai.cpp中qDebug()日志未使用tr()函数,而rule.doc中的中文文案未被lupdate扫描进poke-2_zh_CN.ts。
→ 解决:在ai.cpp中所有日志改为qDebug() << tr("AI decided to: %1").arg(actionToString(action));,并在poke-2.pro中添加TRANSLATIONS += poke-2_zh_CN.ts,然后运行lupdate poke-2.pro重新提取。
5. 调试与验证:用 Qt Creator 断点实测一局游戏的完整生命周期
5.1 设置关键断点:追踪从启动到摊牌的每一帧
要在 Qt Creator 中真正看懂游戏如何运行,不能只看代码,必须实测。以下是我在GameWindow类中设置的 5 个黄金断点(Breakpoint),覆盖完整生命周期:
| 断点位置 | 触发时机 | 查看什么 | 为什么关键 |
|---|---|---|---|
game.cpp行 88:m_table->resetPokerHeap(); | 点击“开始游戏”后,startGame()第一行 | m_table->m_pokerHeap->m_cards.size()应为 52 | 验证牌堆是否正确初始化,若为 0 则PokerHeap::shuffle()未执行 |
table.cpp行 124:dealHoleCards();内部循环for(int i=0; i<2; i++) | Pre-flop 发牌时 | m_player->getHandCards().size()应为 2,m_aiPlayer->getHandCards().size()应为 2 | 确认底牌分发逻辑,若 AI 手牌为 0 则player->addCard()调用失败 |
game.cpp行 215:onCallBtnClicked()中m_table->playerAction(...)调用前 | 人类玩家点击“跟注”瞬间 | m_table->m_currentRound应为PRE_FLOP,m_table->m_potSize应为 20(小盲+大盲) | 验证轮次与底池状态是否同步,若m_potSize=0则盲注未扣除 |
table.cpp行 189:nextRound()中case PRE_FLOP:分支 | 人类与 AI 都行动完毕,进入 Flop 阶段 | m_table->getCommunityCards().size()应为 3,m_table->m_currentRound应变为FLOP | 确保发牌轮次推进正确,若社区牌数≠3 则dealFlop()逻辑错误 |
banker.cpp行 67:determineWinner()中winner = ...赋值后 | Showdown 阶段,胜负判定完成 | winner->getName()应为"Human"或"AI",m_player->getChips()应更新 | 验证筹码分配是否正确,若人类筹码未变则m_banker->distributePot()未执行 |
设置方法:在 Qt Creator 左侧行号栏单击即可。启动调试(Ctrl+R),程序会在第一个断点暂停,按 F5 继续,F10 单步(step over),F11 进入函数(step into)。重点关注变量窗口(Variables pane)中m_player->m_chips、m_table->m_potSize、m_table->m_communityCards的实时值变化。
5.2 验证 AI 决策合理性:手动修改手牌并观察行为
想快速验证 AI 是否真的“会思考”,不必打完整一局。在game.cpp的startGame()中,发完底牌后插入强制手牌:
// 在 m_table->dealHoleCards(); 之后添加 // 强制给人类玩家发皇家同花顺(10-J-Q-K-A 同花) m_player->clearHand(); m_player->addCard(Card(10, SPADES)); m_player->addCard(Card(11, SPADES)); // 强制给 AI 玩家发一对 2(最弱对子) m_aiPlayer->clearHand(); m_aiPlayer->addCard(Card(2, HEARTS)); m_aiPlayer->addCard(Card(2, DIAMONDS));然后启动调试,观察 AI 行为:在 Pre-flop,AI 应立即FOLD(因 strength≈0.05);若人类在 Flop 发出 3-4-5 同花,AI 手牌变成两头顺听+同花听,calculateHandStrength()应返回 ≈0.35,此时 AI 会CALL。这种“注入测试数据”的方式,比盲目点击快 10 倍,是定位 AI 逻辑 bug 的首选。
5.3 性能与内存验证:用 Qt 自带工具检测资源泄漏
大型 Qt 项目易出现QPixmap内存泄漏(图片加载后未释放)。本项目含 133 张 PNG,若每张加载 10 次,内存飙升。验证方法:
- 在 Qt Creator 中,点击Analyze → Qt Creator → Start QML/JS Profiler(虽为 QML 工具,但对 QWidget 也有效)
- 运行程序,反复进入/退出游戏(点击“返回选择”再“开始”)
- 观察Memory标签页:
QPixmap对象数是否持续增长?正常应波动后回落。
若发现泄漏,在PlayerWidget::updateHandDisplay()中检查:
// 错误:每次创建新 QLabel,旧的未 delete QLabel* cardLabel = new QLabel(this); // 正确:复用已有 QLabel,只更新 pixmap if (!m_cardLabels.isEmpty()) { m_cardLabels[i]->setPixmap(pixmap); } else { QLabel* label = new QLabel(this); m_cardLabels.append(label); }从那以后我每次写 Qt 图片显示代码,都强制走一遍
delete检查:先qDebug() << "Creating label";,再在析构函数中qDebug() << "Deleting label";,最后用 Profiler 验证。没有这一步,交付前夜必翻车。希望帮到你。
本文还有配套的精品资源,点击获取