
简介基于QTC开发的德州扑克游戏完整源码是面向计算机专业毕业设计、课程设计以及项目开发实践的优质参考项目。项目实现了完整的对局流程包含发牌、下注、牌型比较、AI对手决策等核心功能并配有可视化游戏界面代码模块划分清晰经过严格测试可直接运行也便于在此基础上二次开发。压缩包内共98个文件主要包含9个cpp源文件、8个头文件、2个ui界面文件、64张png图片及多张jpg素材同时附带规则说明doc文档、资源管理qrc和工程配置文件整体体积约9.63MB目前已有343人学习下载。通过研读源码可以掌握QT界面搭建、C面向对象设计、多文件工程组织以及简单AI算法设计等关键技能对完成同类游戏项目或理解桌面应用开发流程大有帮助。1. 从零做一个能运行的德州扑克这个项目到底在解决什么问题“基于QTC开发的德州扑克游戏”这类题目是毕业设计和课程设计里出现频率最高的那类界面要像样、规则要完整、源码要能讲。它看起来就是一个扑克牌桌但拆开之后实际是三条线用Qt把窗口、牌面、按钮和筹码画出来用C把洗牌、发牌、牌型判断和下注轮次算清楚再用信号槽把这两层接起来。做这个题的人通常不是要做一个商用平台而是要在有限时间内拿出一套能演示、能答辩、能扩展的工程。这篇笔记就按这个目标来写先从环境搭建讲起再到核心规则建模最后把常见的坑逐个填掉。2. 搭建QtC开发环境版本怎么选第一版窗口怎么跑起来2.1 选择Qt版本、编译器与构建工具做课程设计或毕业设计我一般建议直接用Qt 5.15.2 LTS搭配MinGW 64位这套组合不要追新。原因很实际5.15 LTS的教程和现成代码最多网上搜到的问题几乎都能找到对应答案MinGW版本不需要额外装Visual Studio运行库答辩时把程序拷到别的机器上缺失运行库的概率比MSVC版本低很多。热词里也常出现“qt安装”“qt creator”“qt 5.15.2下载安装”可见大部分人在第一步就被版本折腾过。如果本机已经装了多个Qt版本务必在Qt Creator里给项目指定同一个构建套件Kit并且保证编译和运行用的是同一套Qt库。后面避坑章节会专门讲版本混用造成的编译错误这里先记住一个原则整个项目从qmake到运行都锁死在一个Qt版本上。下面这张表是搭建阶段需要用到的组件和它们各自的作用组件用途说明Qt 5.15.2 (MinGW 64-bit)核心库与工具链安装时勾选Qt Widgets模块即可QML暂时不需要Qt CreatorIDE 设计器入口内置Qt Designer可视化编辑界面qmake构建工具课程设计阶段比CMake简单.pro文件两三行就能编出可执行文件Qt Designer界面设计器在Qt Creator里双击.ui文件就能打开有不少人图省事用VS Code写C再手动调Qt我不是很推荐这条路。VS Code配C环境本身就要花不少时间还要额外配置Qt的include路径和构建任务对赶进度的项目来说完全是多余成本。直接在Qt Creator里建工程qmake会帮你把路径全部处理好把精力留给游戏逻辑更划算。2.2 用Qt Creator建立第一个QWidget项目并跑通信号槽建项目的步骤很标准打开Qt Creator选择“Application Qt Widgets Application”取项目名如TexasHoldem基类选QMainWindow。这里要注意生成的工程默认会带一个MainWindow类后面所有界面代码都写在这个类里规则逻辑单独放到另一个目录不要混在一起。生成之后先别急着写游戏先把这个最小骨架跑通// main.cpp #include QApplication #include QWidget #include QVBoxLayout #include QPushButton #include QTextEdit int main(int argc, char *argv[]) { QApplication app(argc, argv); QWidget win; win.setWindowTitle(QStringLiteral(QTC 德州扑克骨架)); auto *layout new QVBoxLayout(win); auto *log new QTextEdit(win); auto *btn new QPushButton(QStringLiteral(发牌), win); log-setReadOnly(true); layout-addWidget(log); layout-addWidget(btn); // 用lambda表达式连接按钮点击信号C11标准写法 QObject::connect(btn, QPushButton::clicked, log, [log]() { log-append(QStringLiteral(收到点击信号)); }); win.show(); return app.exec(); }对应的.pro文件内容QT widgets CONFIG c11 TARGET TexasHoldem TEMPLATE app SOURCES main.cpp这段代码里有两个关键点。第一QVBoxLayout负责自动排列控件窗口缩放时按钮和日志框会跟着变化不用手动算坐标这是Qt里最基础的布局方式后面牌桌的所有控件都会套在这种布局里。第二QObject::connect是信号槽的核心这里用lambda表达式接收按钮的点击信号比传统的SLOT()宏写法更直观也是现代Qt项目的主流写法。如果connect成功点击“发牌”按钮后日志区会出现一行文字。一个常见的小问题信号槽里的lambda捕获了log指针如果log对象先于按钮销毁点击会出问题。在这个骨架程序里两个控件都是win的子对象生命周期一致所以安全。后面的项目里凡是捕获this或界面指针的lambda都要保证对象生命周期覆盖连接的有效期我一般把连接写在窗口构造函数而不是在局部函数里就是为了避免这种悬垂问题。2.3 工程文件与源码目录组织能跑通骨架之后先别急着堆功能把源码目录规划好。毕业设计答辩时老师一定会问代码结构一个清晰的目录比十万行堆在一起好看得多。我习惯这样组织TexasHoldem/ ├── TexasHoldem.pro ├── main.cpp ├── game/ │ ├── card.h // 扑克牌与牌桌的数据结构 │ ├── deck.h/cpp // 洗牌、发牌 │ ├── hand_evaluator.h/cpp // 牌型判断与比较 │ └── game_table.h/cpp // 下注轮次与状态管理 ├── ui/ │ ├── mainwindow.h/cpp │ └── mainwindow.ui // Qt Designer生成的界面文件这样分层的逻辑很直接game目录只依赖标准库完全不碰任何Qt界面类ui目录负责显示和交互。将来想换界面框架或者把逻辑提取成控制台版本做自动化测试game目录里的代码可以原封不动带走。热词里“qt mvvm框架”被频繁搜到说明很多人也在思考界面和逻辑怎么解耦这个目录结构就是最朴素的MVP分离思路。从下一章开始进入规则建模这部分是纯C不需要窗口也能编译运行这也是刻意为之先把规则算对再画界面。3. 德州扑克核心规则建模洗牌、发牌与牌型判断怎么写才不返工3.1 用结构体表达扑克牌与手牌牌面、花色与叫牌写扑克游戏第一步是定义基础数据结构。一张牌最少需要两个属性点数rank和花色suit。点数我建议直接用整数 2 到 14 表示2 到 10 就是数字本身11 是 J12 是 Q13 是 K14 是 A。不要用字符串或字符来表示点数后面做顺子判断和比牌排序时会非常痛苦。// game/card.h #ifndef CARD_H #define CARD_H enum Suit : int { Spade 0, // 黑桃 Heart 1, // 红心 Club 2, // 梅花 Diamond 3 // 方块 }; struct Card { int rank; // 2..1414 代表 A int suit; // 0..3对应上面枚举 }; #endif // CARD_H之所以把A映射成14而不是1是因为绝大多数牌型比较里A是最大的。唯一的例外是A-2-3-4-5这条特殊顺子它在顺子判定时要被特殊处理这点后面专门讲。Card这里用简单的struct而不是class因为牌就是一组公开数据不需要封装数据成员直接暴露能大幅减少样板代码。玩家和手牌的数据结构可以这样定义struct PlayerState { int id; // 座位号从0开始 QString name; // 界面显示名称 int chips; // 当前筹码 std::vectorCard hole; // 两张底牌最多2张 bool folded; // 是否弃牌 int currentBet; // 本轮已下注筹码 };严格来说这个结构体里出现了QString会让game目录依赖Qt。如果想保持纯C把name换成std::string即可。我写到这段时通常直接给game目录里的类型全部用标准库界面层再做一次转换这样后续编译测试更快。3.2 洗牌与发牌随机数种子是个隐形炸弹很多人在洗牌这里第一次翻车。最常见的错误是用srand(time(NULL))配合rand() % n来洗牌然后发现每局开始的手牌都差不多或者两次运行程序结果完全一样。问题不在随机函数本身而在种子初始化的位置和方式。正确的做法是用C11的random库和std::shuffle// game/deck.cpp #include deck.h #include algorithm #include random std::vectorCard buildDeck() { std::vectorCard deck; deck.reserve(52); for (int suit 0; suit 4; suit) { for (int rank 2; rank 14; rank) { deck.push_back({rank, suit}); } } return deck; } void shuffleDeck(std::vectorCard deck) { // static局部变量只在第一次调用时初始化 // 之后程序运行期间一直复用这个随机数引擎 static std::mt19937 rng(std::random_device{}()); std::shuffle(deck.begin(), deck.end(), rng); }这里的关键是static std::mt19937 rng(...)。把随机数引擎声明成函数内的局部静态变量它的初始化只发生一次不会出现重新播种导致牌序重复的问题。std::random_device{}()从系统熵池取种子通常比time(NULL)更不可预测虽然在某些平台上它可能退化为伪随机但对课设和毕设来说完全够用。std::shuffle要求传入一个均匀随机数生成器作为第三个参数它内部会做元素交换避免手写Fisher-Yates算法时下标越界的风险。如果你确实想手写洗牌算法记住关键点从后往前遍历对于第 i 个位置在[0, i]闭区间内随机选一个下标交换而不是[i, n)开区间后者会让某些排列出现的概率偏高。发牌逻辑就简单了每次从牌堆顶部取一张Card dealOneCard(std::vectorCard deck) { Card c deck.back(); deck.pop_back(); return c; }这里有个后续会用到的设计考量一手德州扑克只需要52张牌的一部分但发到最后如果还要记录每个玩家实际拿到了哪些牌最好在发牌的同时把发出的牌也从deck里移除这样能避免同一张牌发给两个人的越界问题。常见做法是发牌函数直接返回std::optionalCard牌堆空了返回空值。C17以上可以直接用std::optional如果编译器标准是C11就用空Card加一个bool hasCard的返回值来标记。3.3 牌型判断与比牌把比较器写到能被机器人和玩家共用牌型判断是整个项目里最容易写错、也最容易被答辩老师追问的部分。德州扑克最终的牌型大小顺序是皇家同花顺、同花顺、四条、葫芦、同花、顺子、三条、两对、一对、高牌。但注意从程序实现角度皇家同花顺只是同花顺的特例通常我们不单独定义一种牌型而是在同花顺里判断最大牌是否为A用来显示特殊名字。评估一手牌7张选5张的思路是这样的统计各点数出现的次数再判断是否为同花和顺子最后组合出牌型。下面给出点数和顺子判断的核心代码这是后面evaluateHand的基础// game/hand_evaluator.cpp 片段 #include vector #include array // 统计 2..14 每个点数出现了几次 std::arrayint, 15 countByRank(const std::vectorCard cards) { std::arrayint, 15 cnt{}; for (const Card c : cards) { cnt[c.rank]; } return cnt; } // 判断是否顺子若是则返回顺子最大点数到 high bool isStraight(const std::arrayint, 15 cnt, int high) { // 从大到小找连续5张 for (int h 14; h 5; --h) { bool ok true; for (int r h; r h - 4; --r) { if (cnt[r] 0) { ok false; break; } } if (ok) { high h; return true; } } // 特殊顺子A-2-3-4-5 if (cnt[14] 0 cnt[2] 0 cnt[3] 0 cnt[4] 0 cnt[5] 0) { high 5; return true; } return false; }countByRank构建了一张点数统计表下标就是点数值就是出现次数。为什么用arrayint,15而不是map因为点数范围固定是2到14数组访问是O(1)且不会引入红黑树开销15个int总共60字节对性能毫无压力。isStraight从14往下扫找到任何一组连续5个点数齐全的序列就判定为顺子并以这组序列的最大值作为比较依据。A-2-3-4-5这条特殊顺子单独处理它的high设定为5这在后续比牌时能自然放入tieRanks数组参与大小比较。整个evaluateHand函数返回一个HandEval结构包含牌型枚举和一组用于比牌的点数序列enum HandType : int { HighCard 0, OnePair, TwoPair, ThreeOfAKind, Straight, Flush, FullHouse, FourOfAKind, StraightFlush }; struct HandEval { HandType type; std::vectorint tieRanks; // 从大到小排列用于同牌型时比较 };tieRanks的填充规则是比牌的关键。举个例子一对的tieRanks应该是{对子的点数, 剩余三张高牌从大到小}比如一对K加A、Q、9那就是{13, 14, 12, 9}两对则是{大对点数, 小对点数, 剩余单张}。这样两个玩家都是两对时可以先比大对再比小对最后比单张完全不依赖手牌顺序。下面是比较器的实现// 返回 true 表示 a 赢过 b bool betterHand(const HandEval a, const HandEval b) { if (a.type ! b.type) return a.type b.type; size_t n std::min(a.tieRanks.size(), b.tieRanks.size()); for (size_t i 0; i n; i) { if (a.tieRanks[i] ! b.tieRanks[i]) { return a.tieRanks[i] b.tieRanks[i]; } } return false; // 完全平局由池底平分 }先比牌型牌型相同再逐位比较tieRanks这套逻辑是所有牌型判断的核心。我见过不少初学者把比牌和牌型判断写在一起在一个函数里做几十个if-else判断谁大谁小最后改一个规则就牵一发动全身。把“一手牌是什么”和“两手牌谁大”分开成两个方法机器人玩家、胜负判定、界面高亮都能复用同一套逻辑这也是这个项目最值得写的设计点。4. 用Qt Designer把牌桌界面画出来布局、牌面与筹码显示4.1 设计器里的三栏布局左侧控件、中间窗体、右侧属性进入界面阶段打开mainwindow.uiQt Designer会自动启动。整体是经典的三栏结构左侧是控件面板中间是正在编辑的窗体右侧是属性编辑器。先把窗体尺寸拉到大约 1000x700这样能容下6个座位和公共牌区。设计牌桌时不要一上来就想到处贴控件。德州扑克的界面核心区域只有三个顶部是公共牌区5张牌横向排列中间是6个玩家座位每人显示头像/名称、筹码、两张底牌、下注条底部是操作区弃牌、过牌/跟注、加注/全下四个按钮。在Designer里先拖上三个QGroupBox当作容器分别命名为groupPublicCards、groupPlayers、groupActions再用垂直布局把它们从上到下排列。座位先不用摆6组控件可以先做两个座位逻辑层面用代码动态生成否则ui文件里堆满控件后面调整会很痛苦。这里要特别说明一个Designer的使用习惯不要用鼠标手动拖控件坐标全部用水平/垂直布局来排列。Qt的布局系统会负责控件的自动伸缩和窗口缩放适配。答辩演示时如果窗口被拉大拉小手动布局的控件会挤成一团用布局的就不会。4.2 把牌面与筹码接到数据层setText、repaint与QPixmap路径在设计器里摆好框架之后就要把界面和第三章的逻辑层连起来。牌面显示有两种策略一种是用QLabel直接显示文字比如“A♠”开发速度快、不依赖图片资源另一种是用QPushButton的icon显示扑克牌图片视觉效果更好。课程设计一般推荐第二种因为视觉冲击力在答辩时很加分。先看文字牌面的实现这段代码可以直接挂在逻辑层返回数据之后#include QString QString cardText(const Card c) { static const char* RANKS[] {, , 2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, A}; static const QString SUITS[] { QString::fromUtf8(♠), QString::fromUtf8(♥), QString::fromUtf8(♣), QString::fromUtf8(♦) }; return QString(%1%2) .arg(QString::fromUtf8(RANKS[c.rank])) .arg(SUITS[c.suit]); }这里有个隐藏坑♠这类符号在Windows源码文件里如果不注意编码MSVC编译出来可能变成乱码。用QString::fromUtf8包装一下可以规避大半问题更保险的做法是把它们放进.qrc资源文件单独加载。热词里的“qt designer界面设计”经常伴随乱码问题出现我在项目里就会用这套fromUtf8统一处理。如果要用图片牌面路径问题必须在编码阶段就处理。常见踩坑是只写相对路径cards/1.png结果程序从构建目录启动时找不到图片。我习惯直接把图片加入资源文件QPixmap pix(QStringLiteral(:/cards/14_0.png)); if (pix.isNull()) { // 资源加载失败时兜底显示文字避免界面空掉 btn-setText(cardText(c)); } else { btn-setIcon(QIcon(pix)); btn-setIconSize(QSize(72, 100)); }:开头的路径是Qt资源系统内部路径不依赖当前工作目录程序在哪个目录启动都能找到。isNull判断必须有一旦资源名拼错、图片缺失至少还有一个文字兜底界面不会出现一个空白按钮。筹码显示建议直接用QLabel更新文本比如QStringLiteral(筹码: %1).arg(player.chips)。不要在每次筹码变化时新建QLabel控件创建和销毁是有开销的频繁操作会导致界面闪烁。正确姿势是创建一次之后只调setText更新内容。如果发现某块区域更新不及时检查是否调用了update()或repaint()虽然事件循环通常会自动触发重绘但高频率更新时显式请求重绘更稳妥。4.3 别让界面连逻辑动作按钮的触发与信号槽绑定操作区按钮是最直接体现信号槽用法的位置。在Designer里把四个按钮拖好之后回到MainWindow构造函数里做连接不要在ui文件里直接写链接到槽那样代码分散不好查。// ui/mainwindow.cpp 构造函数片段 connect(ui-btnFold, QPushButton::clicked, this, []() { table-playerAction(GameAction::Fold); }); connect(ui-btnCall, QPushButton::clicked, this, []() { table-playerAction(GameAction::Call); }); connect(ui-btnRaise, QPushButton::clicked, this, []() { table-playerAction(GameAction::Raise, ui-spinRaise-value()); }); connect(ui-btnAllIn, QPushButton::clicked, this, []() { table-playerAction(GameAction::AllIn); });每个按钮只做一件事把一个动作转成逻辑层的调用。加注按钮额外从QSpinBox读取加注金额这样界面控件的变化不会污染逻辑层。把界面按钮和逻辑层动作解耦之后以后想加快捷键、命令行触发、AI自动操作都是往table-playerAction这个入口加调用方不需要动界面代码。按钮的可用状态也要跟着游戏状态走。比如当前玩家已经全下弃牌和加注按钮应该禁用。这个同步逻辑放在哪里我的习惯是收到状态变更信号后统一刷新按钮状态而不是在各个按钮的响应函数里各自处理。在MainWindow里定义一个updateActionButtons()方法由game_table的状态变化信号触发保持界面状态永远由逻辑层单向驱动。5. 避坑与常见问题编译失败、卡死、随机数不变这些坑怎么填5.1 fatal: cannot mix incompatible Qt library——Qt库文件版本混用现象编译时报出fatal: cannot mix incompatible Qt library (version ex50601) with this library这类错误程序根本无法链接。热词里这个错误被反复搜索说明不是个例。原因ex50601对应的Qt库版本和当前编译环境使用的Qt版本不一致通常是机器上装了多个Qt版本或者CMake里指定的Qt路径和编译器实际找到的路径不是同一个。常见触发场景是之前用Qt 5.6写过程序后来又装了Qt 5.15系统PATH里旧的Qt bin目录排在前面。解决先确认当前环境实际指向哪个版本。Windows下在Qt Creator的“工具 选项 Kits”里查看当前套件使用的Qt版本然后在命令行执行where qmake和qmake -v看输出路径。如果是CMake项目查看CMAKE_PREFIX_PATH变量是否指向预期的Qt目录。终极处理办法是卸载不用的Qt版本或者把需要的版本路径手动加到项目构建环境的最前面。在这个项目里我干脆就只用Qt Creator的Kit管理功能不在系统PATH里写死任何Qt路径从根上减少混用可能。5.2 UI线程卡死为什么一加特效程序就无响应现象点击“发牌”按钮后窗口立刻变白并显示“未响应”更奇怪的是运行几分钟后又能恢复或者跟注动作明显卡顿半秒。原因把耗时操作直接写在了信号槽处理函数里。虽然洗牌很快但如果把动画、音效播放、大量QPixmap加载都塞进同一个槽事件循环被长时间占用界面无法响应点击和重绘。现代Qt里所有界面操作都跑在GUI线程任何阻塞都会表现为程序假死。解决把重型计算放进QThread或QtConcurrent::run只把结果跨线程传回界面线程更新。如果只是洗牌和发牌这种毫秒级操作用QTimer::singleShot延迟执行也能避免一次槽函数里做太多事情。需要记住的原则是界面线程只做UI更新逻辑层能异步就异步。如果担心多线程的锁问题可以把逻辑层的GameTable设计成不加锁的单线程对象用信号槽把玩家动作请求投递到它所在的线程执行这是Qt里典型的“moveToThread”用法复杂度可控答辩还能多讲一个知识点。5.3 QPixmap加载不了牌面图片总是路径错误现象程序编译运行正常但界面上牌面全是空白没有任何报错。把图片路径改成绝对路径后能显示换到答辩机器上又空白了。原因用了相对路径cards/xxx.png而这个路径是相对于程序当前工作目录解析的。在Qt Creator里按F5运行时工作目录通常是构建目录但直接双击exe运行时工作目录又是exe所在目录两次结果不一致。答辩的时候机器环境和你本机不一样问题直接暴露。解决全部改用Qt资源系统。右键项目选择“添加新文件 Qt Qt Resource File”新建resources.qrc把图片文件拖进资源系统然后代码里统一用:/cards/xxx.png加载。这个路径只跟资源文件内容相关与工作目录无关。如果资源加载按理说没问题还是空白优先检查qrc文件里路径是否写错大小写Qt资源路径是大小写敏感的。5.4 随机数每局一样种子初始化在哪里决定了你会不会翻车现象每次运行程序第一手发的牌一模一样或者加了srand(time(NULL))之后循环里每次洗牌结果仍然重复。原因srand被错误地放进了洗牌函数内部每次洗牌都用当前时间重新播种。如果两次洗牌发生在同一秒内种子相同后续随机序列也相同。另一种情况是srand放在了循环里重复播种导致序列重置。解决按前面3.2节的做法用static std::mt19937引擎在整个程序生命周期内只初始化一次。如果你沿用旧式rand()写法至少把srand(time(NULL))放到main()开头并且整个项目只调用一次。std::shufflemt19937是现代C的推荐方案不仅解决了种子问题rand() % n产生的模偏差也不存在了。5.5 信号槽连了却没反应moc与connect的隐形规则现象代码里明明写了connect点击按钮后槽函数就是不执行也不报任何错误。原因最常见的有三种。一是忘了把类声明Q_OBJECT导致moc没有为这个类生成元对象代码信号槽机制直接失效。二是connect函数参数写错比如信号或槽写成了同名不存在的函数编译期不报错运行时静默失败。三是连接时接收方对象已经被销毁信号发出去没有接收者。解决检查顺序固定为三步。第一步类头文件确认有Q_OBJECT宏且文件被包含在SOURCES或HEADERS里让qmake扫描到修改头文件后重新qmake编译一次。第二步用新式语法connect(sender, Sender::signal, receiver, Receiver::slot)编译器会在参数不匹配时报错而不是运行时静默失败。第三步检查接收对象生命周期比如在构造函数里连接子对象的信号到this的槽如果子对象析构了而连接没断开Qt新版本会输出警告但老版本不会。如果连接的是lambda建议把connect的返回值用Q_ASSERT检查一下逻辑上不应该失败的地方早期暴露比后期灵异事件好得多。热词里“qt模拟鼠标点击事件”也常被搜到实际使用QTest::mouseClick配合信号槽能模拟按钮点击做自动测试后面一章会用到。6. 上线前的验证与进阶把逻辑层跑一个自动测试再加动画德州扑克作为课程设计项目光能点按钮是不够的最好在界面之前先证明规则判断是正确的。多年来的经验告诉我界面做得再漂亮比牌逻辑错了在演示时也会被一眼看穿。我习惯为hand_evaluator写一个独立的控制台验证程序用最朴素的assert验证牌型// test_hand_eval.cpp // 不依赖Qt直接编译运行Visual C/MinGW均可 #include cassert #include vector #include game/hand_evaluator.h int main() { // 测试同花顺黑桃9、10、J、Q、K HandEval h1 evaluateHand(std::vectorCard{ {9, Spade}, {10, Spade}, {11, Spade}, {12, Spade}, {13, Spade} }); assert(h1.type StraightFlush); assert(h1.tieRanks.front() 13); // 最大点数为K // 测试四条2四条 单张8 HandEval h2 evaluateHand(std::vectorCard{ {2, Spade}, {2, Heart}, {2, Club}, {2, Diamond}, {8, Spade} }); assert(h2.type FourOfAKind); assert(h2.tieRanks[0] 2 h2.tieRanks[1] 8); // 测试特殊顺子 A-2-3-4-5 int high 0; std::arrayint, 15 cnt{}; cnt[14] 1; cnt[2] 1; cnt[3] 1; cnt[4] 1; cnt[5] 1; assert(isStraight(cnt, high) high 5); assert(betterHand(h1, h2)); // 同花顺应大于四条 return 0; }这个测试文件跑通之前不要进界面阶段。它能在10秒内验证牌型判断和比较器是否可靠也方便在答辩时现场演示“先有逻辑后有界面”的开发流程。进阶层面如果时间还有富余可以加一个简单的AI玩家让机器人用第三章的evaluateHand评估自己手牌强度做最粗浅的决策。公共牌发出后根据牌型概率决定跟注或弃牌。这不需要多复杂但能让界面看起来像个真正的牌局而不是自己跟自己打。另一个值得做的功能是把每局的关键动作写入JSON文件答辩时复盘那局牌的过程很有说服力。我这么多年做这类项目的习惯始终没变逻辑层永远先于界面层跑通界面随时可以推倒重来规则算对了牌局才立得住。希望帮到你。本文还有配套的精品资源点击获取