ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

C++ Qt5德州扑克工程:发牌比牌AI全链路实现

C++ Qt5德州扑克工程:发牌比牌AI全链路实现 简介这是一套基于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拆成widgets和uitools且core变为core5compat。如果你强行用 Qt6 编译会在table.cpp第 87 行报错error: QSignalMapper was not declared in this scope紧接着ai.cpp的qrand()调用也会失败Qt6 默认禁用qrand需加CONFIG qt5compat且重写随机数逻辑。实测 Qt5.15.2 是最后一个同时支持qrand、QSignalMapper、且 ABI 与本项目.lib链接无冲突的稳定版本。别信“Qt6 兼容层”这里没后悔药。2.2 MinGW73 是唯一能过链接的编译器链项目poke-2.pro中QMAKE_CXXFLAGS -stdgnu11且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 Studiopokerheap.h里templatetypename T class PokerHeap的特化写法template class PokerHeapint在 MSVC2019 下触发 C2995 错误。结论装 Qt5.15.2 Online Installer 时必须勾选MinGW 7.3 64-bit组件别选MSVC 2017/2019或MinGW 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 CONFIGrelease 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不能双击运行会黑屏退出。原因有三资源未部署res.qrc编译进二进制但图片路径在images/子目录。必须把整个images/文件夹含 142 张 PNG/JPG复制到release/目录下与poke-2.exe同级字体缺失game.ui中QLabel使用了Microsoft YaHei字体若目标机器无该字体QFontMetrics计算宽度失败导致 UI 错位。解决方案在main.cpp的main()函数开头插入#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));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::vectorCard。它实现了三个关键能力状态快照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.cppvoid 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同花顺 量化分数如对A1200同花KQJ8500handScore中层胜率蒙特卡洛模拟当前桌面状态 剩余牌池随机抽取 1000 次对手可能的手牌组合与自己手牌比大小统计胜率winRate0.0~1.0顶层下注策略映射handScore,winRate, 当前底池大小, 自己筹码查表bettingTable[winRateBucket][handScoreBucket]返回BetActionFold/Call/MinRaise/MaxRaiseBetActionbettingTable是硬编码的二维数组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::mt19937std::uniform_int_distribution后1000 次模拟的胜率标准差从 ±8% 降到 ±1.2%AI 行为稳定得多。3.3HandEvaluatorO(1) 时间复杂度的比牌算法poker.h/cpp中的比牌不是暴力枚举 21 种组合。它采用位运算哈希法每张牌用 16 位整数表示高 4 位花色0x0~0xF低 12 位点数0x1~0xC7 张牌2 底 5 公共组成一个 112 位整数7×16但实际只用低位 64 位做哈希通过预计算的RANK_LOOKUP[64bit_hash]数组pokerheap.h中static const quint64 RANK_LOOKUP[1024000]直接查表得牌型等级和主牌值。核心优化点跳过无效组合只计算 21 种组合中“最可能成牌”的 5 种如公共牌已成顺子则只验顺子自己底牌能否升级缓存最近结果HandEvaluator::lastResult存储上一次比牌结果若桌面未变如仅换玩家直接返回缓存值。quint64 HandEvaluator::hash7(const QListCard cards) { quint64 hash 0; for (int i 0; i 7; i) { // 位移第 i 张牌占 16 位左移 i*16 hash | (static_castquint64(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.cpp里emit betAmountChanged(m_betAmount);的m_betAmount是quint64类型与int不匹配。Qt 元对象系统无法识别类型不一致的信号。解决统一改为int德州扑克筹码最大 10000int足够修改player.h信号声明和player.cpp中m_betAmount成员变量类型。4.3 现象AI 玩家在河牌轮无限循环Calling...CPU 占用 100%程序无响应原因ai.cpp的makeDecision()函数中蒙特卡洛模拟循环for(int i0; i1000; 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.ui的QLabel文本未设置setTextFormat(Qt::RichText)。解决用记事本打开rule.doc→「另存为」→ 编码选UTF-8→ 覆盖保存在 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.dll。ex50601是 Qt5.15.2 的内部版本号但加载的 DLL 是 Qt5.12 的。解决临时清空 PATHWinR →cmd→set PATH→ 回车进入release/目录 →poke-2.exe若成功运行说明 PATH 冲突。永久解决在release/目录下新建run.batecho off set PATHD:\Qt\5.15.2\mingw73_64\bin;%PATH% poke-2.exe pause5. 进阶技巧三步改造为双人联机版TCP Socket Qt Network 模块5.1 架构改造从单机到 Client-Server 的最小改动本项目天然适合改造成局域网对战因为核心逻辑发牌、比牌、轮次已与 UI 解耦。只需在Table类中注入网络能力而非重写整个游戏。改造原则不碰PokerHeap、HandEvaluator、AI只动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.cpp中startNetworkGame()示例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 决策必须暂停所有动作由网络消息驱动。但Table的nextRound()仍需推进轮次。解决方案是状态代理模式新增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就是联机版逻辑零重复。希望帮到你。本文还有配套的精品资源点击获取
返回列表