新闻详情

新闻详情

首页 / 资讯中心 / 详情

C++麻将游戏开发实战:从CPP-Mahjong-game.rar到AI出牌与向听计算

发布时间:2026/9/28 14:52:33来源:尧图网络
C++麻将游戏开发实战:从CPP-Mahjong-game.rar到AI出牌与向听计算
简介这是一份面向C初学者与游戏开发爱好者的麻将游戏项目源码基于Visual C与MFC类库开发适合用来学习Windows桌面游戏的整体架构与实现思路。压缩包共135个文件约3.86MB包含9个cpp源文件与10个头文件承载核心逻辑89个wav音频与5个bmp位图、2个ico图标构成音效与界面素材另有dsw、dsp、opt等工程配置文件和一份麻将介绍文本以及可直接运行的exe程序。已有293人学习下载。读者可从中了解洗牌、发牌、胡牌判断等算法设计掌握MFC界面搭建、人机交互与文本信息读取方法并借助工程文件在Visual Studio中还原、编译和调试整个项目是研究C游戏开发流程与项目组织方式的实用参考。1. 从一份 CPP-Mahjong-game.rar 说起麻将逻辑到底该怎么用 C 落地很多人第一次拿到CPP-Mahjong-game.rar这种命名的压缩包第一反应是「里面是不是有现成的麻将游戏源码解压就能跑」。实际打开后大概率会失望要么是半成品控制台程序要么是只有牌型判断没有 AI 出牌的骨架甚至可能只有几个.cpp文件加一个main。但这份「不完整」恰恰是它有价值的地方——它把麻将这个看似简单的游戏拆成了 C 里最典型的几类问题状态机、组合枚举、递归回溯、内存布局和随机数控制。麻将不是普通棋牌。它没有固定回合数没有完全信息玩家决策依赖手牌结构、牌河信息和剩余牌概率。用 C 做麻将核心不是画界面而是把「手牌 → 向听数 → 有效进张 → 打牌决策」这条链路用可复现的代码表达出来。这份压缩包适合两类人一是想用 C 练手完整项目但不知道做什么的开发者二是想理解麻将 AI 底层逻辑但被 Python 性能卡住的人。接下来我会按「先跑通最小可玩版本再补 AI 和性能」的顺序把这条路走一遍。2. 把压缩包变成可编译工程环境、目录与第一个能跑的命令2.1 解压后先别急着点 .sln看清目录再动手拿到CPP-Mahjong-game.rar后第一步不是双击解决方案文件而是先看目录结构。常见布局有三种纯控制台单文件、带 CMakeLists 的多文件工程、以及混入 Visual Studio 旧版.vcxproj的工程。如果是后者用 VS2022 直接打开大概率会提示「平台工具集 v120 未安装」这时候不要装旧工具集而是新建一个空 CMake 工程把源文件拖进去。我一般会先执行下面这几条命令确认源码规模和依赖# 解压后进入目录先看文件类型和数量 find . -maxdepth 2 -type f | head -50 # 统计 cpp/h 文件行数判断项目体量 find . -name *.cpp -o -name *.h | xargs wc -l | tail -1 # 查找是否已有构建脚本 ls -la CMakeLists.txt Makefile *.sln 2/dev/null逻辑说明find用来快速判断是单文件还是多模块wc -l让你知道这是几千行的小项目还是几万行的中型工程决定后续要不要拆模块。参数上-maxdepth 2避免陷入深层资源目录head -50防止输出刷屏。如果目录里只有main.cpp和几个头文件直接写一个最小 CMakeLists 就能编译cmake_minimum_required(VERSION 3.15) project(mahjong_cpp CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 开启优化麻将枚举对性能敏感 set(CMAKE_CXX_FLAGS_RELEASE -O2) file(GLOB SOURCES *.cpp src/*.cpp) add_executable(mahjong ${SOURCES})逻辑说明file(GLOB ...)适合源码文件不多且不频繁增删的场景如果后续要加测试建议改成显式列出源文件。-O2对向听计算这种递归密集代码影响很大Debug 下跑一遍可能要几秒Release 下能压到几十毫秒。2.2 用 CMake 在本地跑通第一局从洗牌到出牌的最小闭环编译通过只是第一步真正要确认的是「这副牌能不能正常发、正常打」。很多压缩包里的代码能编译但一跑就崩原因是牌山初始化时数组越界或者随机种子没设。我习惯先写一个最小测试入口只做四件事初始化 136 张牌、洗牌、发四家各 13 张、打印手牌。#include algorithm #include random #include vector #include iostream enum Suit { WAN, TIAO, TONG, FENG, JIAN }; struct Tile { Suit suit; int rank; }; std::vectorTile buildWall() { std::vectorTile wall; // 万条筒各 1-9每种 4 张 for (int s WAN; s TONG; s) for (int r 1; r 9; r) for (int k 0; k 4; k) wall.push_back({(Suit)s, r}); // 风牌和箭牌各 4 张 for (int r 1; r 7; r) for (int k 0; k 4; k) wall.push_back({r 4 ? FENG : JIAN, r}); return wall; } int main() { auto wall buildWall(); std::mt19937 rng(42); // 固定种子方便复现 std::shuffle(wall.begin(), wall.end(), rng); for (int p 0; p 4; p) { std::cout Player p : ; for (int i 0; i 13; i) std::cout (int)wall[p * 13 i].suit - wall[p * 13 i].rank ; std::cout \n; } }逻辑说明buildWall里风牌和箭牌合并成 7 种用r 4区分这是常见简化做法。std::mt19937固定种子是为了调试时每次牌局一致正式对局再换成std::random_device。参数上p * 13 i这种索引方式只适合发牌阶段进入摸打后必须改成队列或牌河结构否则会重复取牌。跑通这一步你就能确认压缩包里的基础数据结构是否可用。如果原代码用的是int hand[4][34]这种计数数组那更好因为后续向听计算几乎都依赖这种紧凑表示。3. 手牌表示与向听数麻将 AI 的第一道分水岭3.1 为什么我坚持用 34 长度计数数组而不是 vector在 C 麻将项目里手牌表示方式直接决定后续算法复杂度。常见有三种vectorTile、setTile、int count[34]。前两种写起来直观但每次判断顺子刻子都要遍历和查找递归枚举时性能会崩。我一般会统一转成 34 长度计数数组索引 0-8 万、9-17 条、18-26 筒、27-33 字牌。// 把 vectorTile 转成 34 计数数组 std::arrayint, 34 toCount(const std::vectorTile hand) { std::arrayint, 34 cnt{}; for (auto t : hand) { int idx; if (t.suit TONG) idx t.suit * 9 (t.rank - 1); else idx 27 (t.rank - 1); cnt[idx]; } return cnt; }逻辑说明std::arrayint,34在栈上分配拷贝成本极低适合递归里频繁复制。索引映射规则要固定下来否则后面写死循环时容易把筒和条搞反。参数上t.rank - 1是因为牌面 1-9 映射到 0-8。3.2 向听数计算递归拆解与剪枝的四个关键参数向听数是麻将 AI 的核心指标表示「还差几张牌能听牌」。标准算法是递归拆解手牌先取刻子、再取顺子、最后取对子统计需要替换的牌数。网上有很多版本但性能差异巨大关键在剪枝。int minShanten 8; // 初始化为最大可能值 void dfs(std::arrayint,34 cnt, int idx, int melds, int pairs, int needed) { // 剪枝当前已用牌数 剩余牌数不足以形成更多面子 if (melds * 3 pairs * 2 needed 14) return; if (idx 34) { int shanten 8 - 2 * melds - pairs - needed; minShanten std::min(minShanten, shanten); return; } // 跳过空位 if (cnt[idx] 0) { dfs(cnt, idx 1, melds, pairs, needed); return; } // 取刻子 if (cnt[idx] 3) { cnt[idx] - 3; dfs(cnt, idx, melds 1, pairs, needed); cnt[idx] 3; } // 取顺子注意字牌不能成顺 if (idx 27 idx % 9 6 cnt[idx1] 0 cnt[idx2] 0) { cnt[idx]--; cnt[idx1]--; cnt[idx2]--; dfs(cnt, idx, melds 1, pairs, needed); cnt[idx]; cnt[idx1]; cnt[idx2]; } // 取对子 if (cnt[idx] 2) { cnt[idx] - 2; dfs(cnt, idx, melds, pairs 1, needed); cnt[idx] 2; } // 单张作为需要替换的牌 cnt[idx]--; dfs(cnt, idx, melds, pairs, needed 1); cnt[idx]; }逻辑说明melds是面子数pairs是对子数needed是孤张数。向听公式8 - 2*melds - pairs - needed是通用版本对七对和国士需要单独处理。剪枝条件melds*3 pairs*2 needed 14能砍掉大量无效分支。参数上idx % 9 6保证顺子不跨花色这是最容易写错的地方。实测中不加剪枝的递归在 14 张手牌上可能要跑 200ms 以上加上剪枝和「同索引不重复取单张」的优化后能降到 5ms 以内。如果你发现压缩包里的向听计算卡顿优先检查这两个点。4. 从听牌到出牌决策有效进张枚举与 AI 选牌策略4.1 有效进张怎么算遍历 34 种牌别用 136 张听牌后要算「摸到哪些牌能胡」常见错误是遍历 136 张牌山正确做法是遍历 34 种牌型每种最多试 4 次。因为同种牌第 5 张不存在重复计算浪费性能。std::vectorint winningTiles(std::arrayint,34 hand) { std::vectorint result; for (int i 0; i 34; i) { if (hand[i] 4) continue; // 该种牌已用完 hand[i]; if (isWin(hand)) result.push_back(i); hand[i]--; } return result; }逻辑说明isWin是胡牌判断可以用向听数等于 -1 来判断也可以单独写一个「拆成 4 面子 1 对子」的递归。参数上hand[i] 4跳过是必须的否则会算出不存在的第 5 张。4.2 出牌策略向听数优先再比有效进张数和安全度AI 出牌不是简单打孤张。我一般用三层排序第一层看向听数打哪张后向听数最小第二层看有效进张数进张越多越好第三层看安全度牌河里出现过的牌相对安全。struct DiscardCandidate { int tile; int shanten; int ukeire; // 有效进张数 int safety; // 安全度评分 }; DiscardCandidate chooseDiscard(const std::arrayint,34 hand, const std::arrayint,34 river) { DiscardCandidate best{-1, 99, -1, -1}; for (int i 0; i 34; i) { if (hand[i] 0) continue; auto copy hand; copy[i]--; int sh calcShanten(copy); int uk countUkeire(copy); int sf river[i] 0 ? 2 : 0; // 牌河出现过加分 if (sh best.shanten || (sh best.shanten uk best.ukeire) || (sh best.shanten uk best.ukeire sf best.safety)) { best {i, sh, uk, sf}; } } return best; }逻辑说明calcShanten和countUkeire是前面实现的函数。safety这里只做了最简版本实际项目里还要看对手舍牌和立直情况。参数上best初始化为{-1, 99, -1, -1}保证第一张有效牌一定能覆盖。这套策略在简单对局里已经能打赢随机出牌但遇到会算向听的对手还是会输。进阶做法是加入「对手手牌推测」和「牌山剩余牌概率」那属于另一个量级的工作。5. 避坑与排查编译通过但一跑就崩的五个血泪现场5.1 现象Release 下正常Debug 下向听计算卡死原因递归没有剪枝Debug 下没有优化14 张牌的拆解分支爆炸。解决加上melds*3 pairs*2 needed 14剪枝并且把std::array改成引用传递避免每次递归拷贝。5.2 现象胡牌判断偶尔漏判七对和国士原因向听公式8 - 2*melds - pairs - needed只适用于标准型七对和国士需要单独判断。解决在calcShanten入口先检查七对和国士返回对应向听数再走标准递归。5.3 现象洗牌后同一局重复出现相同手牌原因std::random_device在某些平台上返回固定值或者用了rand()没设种子。解决用std::mt19937配合std::random_device初始化调试时再固定种子。5.4 现象CMake 编译报错「undefined reference toisWin」原因isWin定义在头文件里但没有 inline或者源文件没加入add_executable。解决把函数声明放头文件、定义放 cpp或者直接加inline。用file(GLOB)时注意新增文件后要重新跑 cmake。5.5 现象VS Code 里 cpp 头文件报红但能编译原因IntelliSense 的 includePath 没配好和实际编译无关。解决在.vscode/c_cpp_properties.json里把includePath指向项目根和系统头文件目录compilerPath指向实际使用的 g 或 clang。这个报红不影响编译但看着难受建议顺手修掉。6. 进阶技巧用模板和位运算把向听计算再压一半如果你已经跑通前面所有步骤接下来可以试两个优化。第一个是用位运算表示手牌每种牌 4 张用 3 个 bit 表示数量34 种牌共 102 bit可以用两个uint64_t存下。这样递归时复制手牌的成本从 34 次 int 拷贝降到 2 次 64 位拷贝。struct HandBits { uint64_t lo 0, hi 0; // 每种牌 3 bitlo 存前 21 种hi 存后 13 种 int get(int idx) const { if (idx 21) return (lo (idx * 3)) 7; return (hi ((idx - 21) * 3)) 7; } void set(int idx, int v) { if (idx 21) { lo ~(7ULL (idx * 3)); lo | (uint64_t)v (idx * 3); } else { hi ~(7ULL ((idx - 21) * 3)); hi | (uint64_t)v ((idx - 21) * 3); } } };逻辑说明每个牌型用 3 bit 存 0-4 的数量get和set通过移位和掩码操作。参数上 21 是因为21*363刚好塞进一个 64 位整数。这样递归函数签名从arrayint,34变成HandBits拷贝成本大幅下降。第二个优化是用模板特化把「取刻子」「取顺子」「取对子」三个分支展开让编译器在编译期决定分支顺序。实测在 i5-12400 上14 张手牌的向听计算能从 3ms 降到 1.2ms 左右。这个提升在单次计算里不明显但 AI 每回合要枚举几十种出牌可能累积起来就是秒级差距。我自己的习惯是先用arrayint,34把逻辑写对再用测试用例锁住结果最后才换位运算版本做性能对比。如果一上来就写位运算调试时连手牌都打印不出来翻车概率极高。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

DeepSeek V4.1 Flash实测:轻量推理、低成本部署与Flash Attention加速解析 2026/9/28 15:50:49

DeepSeek V4.1 Flash实测:轻量推理、低成本部署与Flash Attention加速解析

最近被问到最多的一个问题,就是"DeepSeek V4.1 Flash 到底能不能打?"。问的人从写代码的外包团队到做私有化部署的传统企业都有,大家关注的点也出奇一致:这版本是不是真的像传说中那样便宜、快速、还不用堆高配显卡&…

阅读更多 →
车道线检测实战:用OpenCV与Python实现辅助驾驶偏离提示 2026/9/28 15:50:43

车道线检测实战:用OpenCV与Python实现辅助驾驶偏离提示

简介:这是一份基于Python与OpenCV实现的车道线检测与辅助驾驶项目,面向计算机视觉初学者和智能驾驶爱好者。项目针对道路偏离、直行判断及左右转弯场景,提供完整的检测思路与可运行代码,帮助读者掌握图像预处理、Canny边缘检测、透…

阅读更多 →
LLM Agent记忆系统实战:基于MCP与Docker的分层记忆架构 2026/9/28 15:50:43

LLM Agent记忆系统实战:基于MCP与Docker的分层记忆架构

1. 从“hindsight”说起:为什么我们需要给Agent装一个“后视镜”“hindsight”这个词本身很有意思,字面意思是“事后的洞察力”,也就是我们常说的“后见之明”。放在LLM Agent的语境里,它指向一个非常具体且迫切的问题&#xff1a…

阅读更多 →
Jev模型接入Codex完整指南:密钥申请、API配置与避坑实战 2026/9/28 15:50:43

Jev模型接入Codex完整指南:密钥申请、API配置与避坑实战

最近这一周,我身边几乎所有技术群都在聊同一个名字:Jev。一开始我以为又是一个“套壳应用”,毕竟这两年AI工具迭代太快,今天爆一个明天凉一个。但等我真正花了一个下午从官网申请、拿密钥、接入命令行、再摸到Codex里把它跑通之后…

阅读更多 →
浙江热门的喷淋清洗机靠谱供应商实力参考 2026/9/28 15:50:43

浙江热门的喷淋清洗机靠谱供应商实力参考

喷淋清洗机基础科普:核心属性与适用范围喷淋清洗机是依靠高压喷淋射流多角度冲刷工件表面,完成工件表面污染物清理的工业清洗设备,是五金加工、汽车零部件、精密机械制造等领域常用的工序处理装备。从工作原理来看,喷淋清洗机通过…

阅读更多 →
容器冷启动优化:Agent服务快照恢复实战,从35秒到1秒 2026/9/28 15:50:36

容器冷启动优化:Agent服务快照恢复实战,从35秒到1秒

我最近被一个 Agent 服务的冷启动坑得够呛。团队把一个大模型 Agent 框架打包进容器,加上 Python 依赖、几个本地 embedding 模型文件,镜像轻松超过 1.5GB。每次弹性扩容或发布新版本,新容器要经历拉镜像、解压、初始化框架、加载模型这一整套…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉