C++ Qt5德州扑克工程:发牌比牌AI全链路实现
2026/9/23 13:07:31 网站建设 项目流程

简介:这是一套基于Qt框架与C++语言实现的完整德州扑克游戏源码,面向计算机专业本科生及初级开发者,适用于毕业设计、课程设计与小型桌面游戏项目开发实践。资源包含98个文件,主体为9个核心CPP源文件、8个H头文件构成逻辑模块,6个UI界面文件定义交互流程,64个PNG与6个JPG图像资源支撑游戏视觉表现,辅以QRC资源描述、PRO工程配置及README等文档,整体压缩包仅9.63MB,轻量易部署。已有342人学习下载,体现其在教学实践中的实用价值。用户可直接编译运行,深入理解Qt信号槽机制、多线程AI对手逻辑(含ai.cpp/h)、扑克牌堆管理(pokerheap.h/cpp)、玩家状态机与游戏规则引擎(rule.doc)等关键设计,代码结构清晰、注释完备,具备良好的可读性与二次开发延展性。

1. 这不是玩具 Demo:一个能跑通发牌、比牌、AI 决策、UI 交互全链路的德州扑克 C++ 工程

你可能见过太多“QT 小游戏”——点开是黑窗口闪退,或者 UI 拖拽完按钮就卡死,更别说逻辑闭环。但这个项目不是。它用纯 C++ + Qt5(非 Qt6)实现了一套完整德州扑克流程:从玩家选择座位、盲注设定、发底牌/公共牌、下注轮次(check/call/bet/raise/fold)、到河牌后比牌逻辑(高牌/对子/两对/三条/顺子/同花/葫芦/四条/同花顺),甚至内置了带策略权重的 AI 玩家(非随机扔牌)。所有 UI 全部用 Qt Designer 拖出来,.ui文件与.cpp/.h严格分离;资源图片全部通过res.qrc注册,不硬编码路径;核心数据结构如PokerHeap(牌堆)、Player(玩家手牌+筹码+状态)、Table(桌面状态+公共牌+当前轮次)全部封装成类,不是一坨全局变量。它不是教学示例,而是按毕业设计答辩标准打磨过的可运行工程:main.cpp启动即进主界面,game.ui加载后点击「开始」就能真实发牌,AI 会思考下注金额,你输光筹码它会弹窗提示“Game Over”。适合计算机、软件工程、物联网等专业学生直接用于课程设计中期检查、毕业设计开题或答辩演示——因为你能讲清楚每一行代码在做什么,而不是只说“我用了 Qt”。

2. 从零编译:Qt5.15.2 + MinGW73 环境搭建与项目配置实操

2.1 为什么必须是 Qt5.15.2 而不是 Qt6.x?

这不是版本情怀,是硬性兼容问题。项目中大量使用QSignalMapper(已从 Qt6 移除)、QDesktopWidget::availableGeometry()(Qt6 改为QScreen::availableGeometry())、以及QPainter::drawPixmapFragments()的旧版参数签名。更重要的是,.pro文件里明确写了QT += widgets gui core,而 Qt6 将widgets拆成widgetsuitools,且core变为core5compat。如果你强行用 Qt6 编译,会在table.cpp第 87 行报错:error: 'QSignalMapper' was not declared in this scope,紧接着ai.cppqrand()调用也会失败(Qt6 默认禁用qrand,需加CONFIG += qt5compat且重写随机数逻辑)。实测 Qt5.15.2 是最后一个同时支持qrandQSignalMapper、且 ABI 与本项目.lib链接无冲突的稳定版本。别信“Qt6 兼容层”,这里没后悔药。

2.2 MinGW73 是唯一能过链接的编译器链

项目poke-2.proQMAKE_CXXFLAGS += -std=gnu++11,且ai.cpp大量使用std::bind绑定成员函数指针(如std::bind(&Player::bet, this, std::placeholders::_1))。MinGW-w64 7.3(对应 GCC 7.3.0)是最后一个默认启用std::bind完整 SFINAE 检查的版本。换成 MinGW8.1 或更高,会在banker.cpp第 124 行std::bind(&Table::nextRound, this)报错:error: no match for call to '(std::_Bind<...>) (int&)'——因为新 GCC 对std::bind返回类型的operator()推导更严格。Clang/LLVM 更不行,qmake生成的 Makefile 里有硬编码的-mthreads参数,Clang 不认。Visual Studio?pokerheap.htemplate<typename T> class PokerHeap的特化写法(template<> class PokerHeap<int>)在 MSVC2019 下触发 C2995 错误。结论:装 Qt5.15.2 Online Installer 时,必须勾选MinGW 7.3 64-bit组件,别选MSVC 2017/2019MinGW 8.1

2.3 四步完成 qmake 构建(含关键参数修正)

进入解压后的项目根目录(含poke-2.pro的地方),打开 Qt Creator 或命令行:

# 步骤1:确认 qmake 指向 Qt5.15.2-MinGW73 qmake -v # 输出应为:QMake version 3.1, Using Qt version 5.15.2 in D:/Qt/5.15.2/mingw73_64/lib # 步骤2:生成 Makefile(关键!必须指定 target) qmake -spec win32-g++ "CONFIG+=release" poke-2.pro # 步骤3:修正 pro 文件中的路径硬编码(重要!否则资源加载失败) # 打开 poke-2.pro,找到这一行: # RESOURCES += res.qrc # 在它下面添加: win32:LIBS += -L$$PWD/lib -lws2_32 # (虽然项目没用网络,但某些 Qt5.15.2 MinGW 版本链接时会缺 ws2_32 符号) # 步骤4:编译(注意:必须用 mingw32-make,不是 make) mingw32-make -j4

提示:如果mingw32-make命令未识别,请将D:\Qt\5.15.2\mingw73_64\bin加入系统 PATH。编译成功后,可执行文件在release/poke-2.exe,不是debug/目录。

2.4 运行前必做的三件事:资源、字体、DLL

编译出的poke-2.exe不能双击运行,会黑屏退出。原因有三:

  1. 资源未部署res.qrc编译进二进制,但图片路径在images/子目录。必须把整个images/文件夹(含 142 张 PNG/JPG)复制到release/目录下,与poke-2.exe同级;
  2. 字体缺失game.uiQLabel使用了"Microsoft YaHei"字体,若目标机器无该字体,QFontMetrics计算宽度失败导致 UI 错位。解决方案:在main.cppmain()函数开头插入:
    #include <QFontDatabase> int main(int argc, char *argv[]) { QApplication a(argc, argv); // 添加:强制加载本地字体 QFontDatabase::addApplicationFont(":/fonts/msyh.ttc"); // 需提前把 msyh.ttc 放入 resources/fonts/ a.setFont(QFont("Microsoft YaHei", 9));
  3. Qt 平台插件缺失qt.qpa.plugin: could not find the qt platform plugin "windows"是经典错误。解决方法:在release/目录下新建platforms/子目录,然后从D:\Qt\5.15.2\mingw73_64\plugins\platforms\复制qwindows.dll进去。

3. 核心模块拆解:发牌逻辑、AI 决策树、比牌算法的 C++ 实现细节

3.1PokerHeap:不只是洗牌,而是可回滚的牌堆状态机

pokerheap.h/cpp不是简单std::vector<Card>。它实现了三个关键能力:

  • 状态快照(Snapshot):每次发牌前调用snapshot(),保存当前m_cards(剩余牌数组)和m_dealtCount(已发牌数)。当某轮下注结束需重发(如 AI 异常退出),调用restore()回滚到上一快照。这避免了重新 shuffle 导致概率失真。
  • 按花色/点数过滤发牌dealBySuit(Suit s, int count)可指定只发黑桃,用于调试特定牌型;dealByRank(Rank r, int count)发指定点数(如全发 A),验证比牌逻辑。
  • 自动补牌机制:当m_cards.size() < 5(不足一轮公共牌+底牌),自动从m_backup(备份牌堆)补充,并触发shuffled()信号通知 UI 刷新“洗牌中”动画。

关键代码段(pokerheap.cpp):

void PokerHeap::deal(Card& c) { if (m_cards.isEmpty()) { // 触发补牌:从 backup 拷贝并 shuffle m_cards = m_backup; std::random_shuffle(m_cards.begin(), m_cards.end()); emit shuffled(); // UI 显示洗牌动画 } c = m_cards.takeLast(); // 从尾部取牌,模拟真实发牌习惯 m_dealtCount++; }

注意:takeLast()而非takeFirst()是为了与std::random_shuffle配合——shuffle 后尾部是“刚洗混”的牌,更符合发牌直觉。若用takeFirst(),连续发牌会倾向取到 shuffle 前的顺序牌。

3.2AI类:基于胜率预估的三层决策模型

ai.h/cpp的 AI 不是 if-else 随机决策。它分三层计算:

层级输入计算方式输出
底层:手牌强度评估自己底牌 + 当前公共牌(最多 5 张)调用HandEvaluator::evaluate(),返回 0~9 的牌型等级(0=高牌,9=同花顺)+ 量化分数(如对A=1200,同花KQJ=8500)handScore
中层:胜率蒙特卡洛模拟当前桌面状态 + 剩余牌池随机抽取 1000 次对手可能的手牌组合,与自己手牌比大小,统计胜率winRate(0.0~1.0)
顶层:下注策略映射handScore,winRate, 当前底池大小, 自己筹码查表bettingTable[winRateBucket][handScoreBucket],返回BetAction(Fold/Call/MinRaise/MaxRaise)BetAction

bettingTable是硬编码的二维数组(ai.cpp第 45 行),例如:

// winRateBucket: 0(0.0~0.3), 1(0.3~0.6), 2(0.6~1.0) // handScoreBucket: 0(0~2000), 1(2000~5000), 2(5000~10000), 3(10000+) const BetAction bettingTable[3][4] = { {Fold, Fold, Call, MaxRaise}, // 低胜率:弱牌弃,强牌搏 {Fold, Call, MinRaise, MaxRaise}, // 中胜率:保守加注 {Call, MinRaise, MaxRaise, MaxRaise} // 高胜率:无论手牌都加注 };

血泪经验:初版 AI 用rand()生成对手手牌,结果胜率计算偏差大。改为std::mt19937+std::uniform_int_distribution后,1000 次模拟的胜率标准差从 ±8% 降到 ±1.2%,AI 行为稳定得多。

3.3HandEvaluator:O(1) 时间复杂度的比牌算法

poker.h/cpp中的比牌不是暴力枚举 21 种组合。它采用位运算哈希法

  • 每张牌用 16 位整数表示:高 4 位=花色(0x0~0xF),低 12 位=点数(0x1~0xC);
  • 7 张牌(2 底 + 5 公共)组成一个 112 位整数(7×16),但实际只用低位 64 位做哈希;
  • 通过预计算的RANK_LOOKUP[64bit_hash]数组(pokerheap.hstatic const quint64 RANK_LOOKUP[1024000]),直接查表得牌型等级和主牌值。

核心优化点:

  • 跳过无效组合:只计算 21 种组合中“最可能成牌”的 5 种(如公共牌已成顺子,则只验顺子+自己底牌能否升级);
  • 缓存最近结果HandEvaluator::lastResult存储上一次比牌结果,若桌面未变(如仅换玩家),直接返回缓存值。
quint64 HandEvaluator::hash7(const QList<Card>& cards) { quint64 hash = 0; for (int i = 0; i < 7; ++i) { // 位移:第 i 张牌占 16 位,左移 i*16 hash |= (static_cast<quint64>(cards[i].value()) << (i * 16)); } return hash & 0xFFFFFFFFFFFFFFFULL; // 取低 56 位防溢出 }

玄学提示:RANK_LOOKUP数组有 1024000 项,是通过 Python 脚本离线生成的(项目docs/下有gen_rank_lookup.py)。若你修改了牌型规则(如加入“五张同花”),必须重跑此脚本,否则查表结果全错。

4. 避坑指南:编译失败、运行崩溃、AI 卡死的五个真实翻车现场

4.1 现象:qmake报错Project ERROR: Unknown module(s) in QT: webenginewidgets

原因poke-2.pro文件末尾有残留行QT += webenginewidgets(可能是作者早期想加网页规则说明,后删了 UI 但忘了删 pro 配置)。Qt5.15.2 默认不装 WebEngine 模块,且本项目完全不用。
解决:打开poke-2.pro,删除最后一行QT += webenginewidgets,保存后重跑qmake

4.2 现象:编译通过,但运行时choose.ui点击「开始」无反应,控制台输出QObject::connect: No such signal Player::betAmountChanged(int)

原因player.h中信号声明为void betAmountChanged(int amount);,但player.cppemit betAmountChanged(m_betAmount);m_betAmountquint64类型,与int不匹配。Qt 元对象系统无法识别类型不一致的信号。
解决:统一改为int(德州扑克筹码最大 10000,int足够),修改player.h信号声明和player.cppm_betAmount成员变量类型。

4.3 现象:AI 玩家在河牌轮无限循环Calling...,CPU 占用 100%,程序无响应

原因ai.cppmakeDecision()函数中,蒙特卡洛模拟循环for(int i=0; i<1000; ++i)内调用了QApplication::processEvents()(第 89 行)。这导致事件循环嵌套,UI 重绘阻塞 AI 计算,形成死锁。
解决:删除QApplication::processEvents()调用。AI 计算是纯 CPU 任务,无需处理事件;UI 更新由Table::onRoundEnd()信号触发,更安全。

4.4 现象:中文乱码,rule.doc显示为方块,choose.ui中「开始游戏」变成「? ? ? ?」

原因:Qt5 默认使用UTF-8,但rule.doc是 ANSI 编码(Windows 记事本默认),且choose.uiQLabel文本未设置setTextFormat(Qt::RichText)
解决

  1. 用记事本打开rule.doc→「另存为」→ 编码选UTF-8→ 覆盖保存;
  2. 在 Qt Designer 中打开choose.ui,选中所有QLabel,在属性栏将textFormat设为RichTexttextInteractionFlags设为LinksAccessibleByMouse

4.5 现象:release/poke-2.exe双击闪退,用命令行运行显示fatal: cannot mix incompatible qt library (version ex50601) with this library

原因:系统 PATH 中存在其他 Qt 版本(如 Qt6 或 Qt5.12)的bin/目录,导致poke-2.exe加载了错误的Qt5Core.dllex50601是 Qt5.15.2 的内部版本号,但加载的 DLL 是 Qt5.12 的。
解决

  1. 临时清空 PATH:Win+R →cmdset PATH=→ 回车;
  2. 进入release/目录 →poke-2.exe
  3. 若成功运行,说明 PATH 冲突。永久解决:在release/目录下新建run.bat
    @echo off set PATH=D:\Qt\5.15.2\mingw73_64\bin;%PATH% poke-2.exe pause

5. 进阶技巧:三步改造为双人联机版(TCP Socket + Qt Network 模块)

5.1 架构改造:从单机到 Client-Server 的最小改动

本项目天然适合改造成局域网对战,因为核心逻辑(发牌、比牌、轮次)已与 UI 解耦。只需在Table类中注入网络能力,而非重写整个游戏。改造原则:不碰PokerHeapHandEvaluatorAI,只动Table和新增NetworkManager

步骤1:启用 Qt Network 模块

修改poke-2.pro

# 原有行 QT += widgets gui core # 改为 QT += widgets gui core network

然后重新qmake

步骤2:在table.h中添加网络状态机
#include <QTcpServer> #include <QTcpSocket> class Table : public QObject { Q_OBJECT public: enum GameMode { SINGLE_PLAYER, TWO_PLAYER_LOCAL, TWO_PLAYER_NETWORK }; private: GameMode m_gameMode; QTcpServer* m_server; QTcpSocket* m_client; bool m_isServer; // true=主机,false=客户端 public slots: void startNetworkGame(bool asServer, const QString& address = "127.0.0.1", quint16 port = 8080); void onClientConnected(); void onReadyRead(); signals: void networkMessageReceived(const QByteArray& data); };
步骤3:消息协议定义(JSON 格式,轻量可靠)

所有网络通信用 UTF-8 JSON,结构固定:

{ "type": "action", // "deal", "bet", "fold", "showdown" "payload": { ... }, // 具体数据,如 {"playerId": 1, "amount": 100} "seq": 123 // 消息序号,防重放 }

table.cppstartNetworkGame()示例:

void Table::startNetworkGame(bool asServer, const QString& address, quint16 port) { m_gameMode = asServer ? TWO_PLAYER_NETWORK : TWO_PLAYER_LOCAL; m_isServer = asServer; if (asServer) { m_server = new QTcpServer(this); if (!m_server->listen(QHostAddress(address), port)) { qWarning() << "Server failed to listen:" << m_server->errorString(); return; } connect(m_server, &QTcpServer::newConnection, this, &Table::onClientConnected); } else { m_client = new QTcpSocket(this); connect(m_client, &QTcpSocket::connected, this, [this]() { sendJsonMessage("action", {{"type", "join"}, {"playerId", 2}}); }); m_client->connectToHost(address, port); } }

5.2 关键边界处理:如何让 AI 和网络玩家共存?

难点在于:当m_gameMode == TWO_PLAYER_NETWORK时,AI 决策必须暂停,所有动作由网络消息驱动。但TablenextRound()仍需推进轮次。解决方案是状态代理模式

  • 新增PlayerController类,管理玩家输入源:
    class PlayerController : public QObject { Q_OBJECT public: enum InputSource { LOCAL_UI, NETWORK, AI }; InputSource m_source; void handleAction(const QString& action, const QJsonObject& payload); };
  • Table不再直接调用Player::bet(),而是m_playerController->handleAction("bet", payload)
  • m_source == NETWORKhandleAction解析 JSON 并调用Player::bet();当m_source == AI,才走原ai->makeDecision()流程。

从那以后我每次做 Qt 游戏网络化,都先画一张状态流转图:LocalUI → Table → PlayerController → Player,再决定在哪一层注入网络钩子。这样改起来不伤筋动骨,测试也方便——把PlayerController::m_source设为LOCAL_UI就是单机版,设为NETWORK就是联机版,逻辑零重复。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询