尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

C++麻将源码解析:MFC工程编译、胡牌算法与AI出牌实战

发布时间:2026/9/28 18:08:17

资讯中心
01
ARTICLE

C++麻将源码解析:MFC工程编译、胡牌算法与AI出牌实战

C++麻将源码解析:MFC工程编译、胡牌算法与AI出牌实战
简介这是一份面向C初学者与游戏开发爱好者的麻将游戏项目源码基于Visual C与MFC库开发适合用来学习Windows桌面游戏的整体实现流程。压缩包共135个文件约3.86MB包含9个cpp源文件与10个h头文件承载核心逻辑89个wav音频与5个bmp位图、2个ico图标构成音效与界面素材另有dsw、dsp、opt等工程配置文件和可执行程序方便直接在Visual Studio中打开、编译与运行。项目围绕麻将主题实现了洗牌、出牌、胡牌判断等算法并涉及对话框设计、人机交互与文本说明读取读者可借此理解MFC界面搭建、游戏状态管理与资源打包分发的完整思路。目前已有293人学习下载适合希望从源码层面掌握C小游戏开发的中级学习者参考。1. 从一份 VC6 麻将源码包说起C 桌面游戏到底怎么跑起来如果你手头正好有一份CPP-Mahjong-game.rar解压后看到Mahjong.dsw、MahjongDlg.cpp、MajHu.cpp、CmpThink.cpp这些文件第一反应大概率是这是哪个年代的产物没错.dsw是 Visual C 6.0 的工作区文件不是现在 VS2022 的.sln。这意味着这套麻将源码是典型的 MFC 单文档/对话框工程用 GDI 贴图的方式画牌面逻辑层和界面层耦合在一个工程里。它解决的不是“做一个商业麻将”的问题而是让你能在一份完整可编译的 C 工程里看到洗牌、发牌、吃碰杠胡判断、电脑 AI 出牌这一整套流程是怎么用面向对象的方式串起来的。适合谁适合刚学完 C 语法、想找一个有真实业务逻辑的桌面项目练手的人也适合想研究 MFC 消息映射和 GDI 双缓冲的老派 Windows 开发者。资源包里那几个.bmp就是牌面、骰子、背景和标题图麻将介绍.txt是规则说明Mahjong很可能是编译好的可执行文件。别急着双击 exe先搞清楚工程结构后面编译和改逻辑才不会翻车。2. 拆开 Mahjong.dsw 工程MFC 对话框结构、资源视图与编译链路2.1 为什么是 MFC 对话框工程而不是控制台或 Qt拿到Mahjong.dsw第一件事是判断工程类型。从MahjongDlg.cpp这个文件名就能看出主界面继承自CDialog是一个对话框程序。MFC 对话框工程的好处是资源编辑器直接拖控件、绑变量消息映射用BEGIN_MESSAGE_MAP宏展开对当年写 Windows 程序的人来说是最省事的路径。坏处也很明显界面和逻辑容易糊在一起MahjongDlg.cpp里往往既有OnPaint画牌又有按钮响应调Wanjia.cpp里的出牌函数。如果你用现在 VS2022 打开会提示升级工程升级后.dsw变成.sln但 MFC 库需要单独在安装器里勾选“适用于最新 v143 生成工具的 C MFC”。不勾就会报afxwin.h找不到这是第一个高频坑。2.2 工程文件清单与各自职责把压缩包里的文件按职责分一下后面改代码心里有数文件类型作用Mahjong.dsw工作区VC6 工程入口记录项目配置Mahjong.dsp项目源文件列表、编译链接选项MahjongDlg.cpp界面逻辑对话框初始化、绘图、按钮响应Wanjia.cpp玩家逻辑玩家手牌管理、出牌、吃碰杠MajHu.cpp胡牌判断胡牌牌型检测算法CmpThink.cpp电脑 AI电脑出牌决策Num.bmp / Majpai.bmp资源数字牌面、麻将牌面贴图sezi.bmp / 背景.bmp资源骰子、背景图Title.bmp资源标题画面麻将介绍.txt文档规则说明.ncb和.opt是 IDE 自动生成的浏览数据库和选项文件删掉不影响编译VS 会重建。真正要保留的是.dsp、.dsw和所有.cpp/.h。2.3 用 VS2022 打开并编译的完整步骤常见做法是直接用 VS2022 打开.dsw让它自动升级。但更稳的方式是新建一个空 MFC 对话框工程把源码和资源手动加进去避免升级脚本改坏消息映射。我一般会走手动路线# 1. 解压后先备份原始工程升级失败还能回退 cp -r CPP-Mahjong-game CPP-Mahjong-game-bak # 2. 用 VS2022 新建 MFC 对话框工程命名为 Mahjong # 安装器里确认勾选使用 C 的桌面开发 适用于 v143 的 C MFC # 3. 把原工程的 .cpp/.h 复制进新工程目录 # 把 .bmp 复制进 res 目录并在资源视图里重新导入位图资源 # 资源 ID 要和原 rc 文件里一致否则 LoadBitmap 会返回 NULL代码后说明第一步备份是后悔药VC6 工程升级到 VS2022 时BEGIN_MESSAGE_MAP里的宏有时会因为字符集从 MBCS 变 Unicode 而报错。第二步新建工程是为了拿到正确的 v143 工具集和 Unicode 配置。第三步复制资源时Num.bmp、Majpai.bmp这些位图在LoadBitmap里是按资源 ID 加载的ID 对不上就画不出牌表现为界面一片空白。参数上项目属性里“字符集”建议先设成“使用多字节字符集”因为老代码里大量CString和char*混用直接上 Unicode 会冒出一堆const char*转LPCWSTR的错误。2.4 编译链接常见报错与定位链接阶段最容易出unresolved external symbol通常是 MFC 库没链上。检查项目属性 → 链接器 → 输入 → 附加依赖项确认有mfc140.lib或对应版本。另一个是LNK2005重复定义多半是stdafx.h预编译头没配好把pch.h设为“使用预编译头”即可。运行起来如果弹Debug Assertion Failed看是哪个Afx函数通常是对话框控件指针为空说明DoDataExchange里绑的控件 ID 在资源里被改了。3. 麻将核心逻辑怎么读洗牌、发牌、胡牌判断与电脑 AI 出牌3.1 牌型数据结构与洗牌算法麻将逻辑的起点是牌怎么表示。这份源码里大概率用一个int数组或vectorint存牌每张牌用编号区分万、条、筒、风、箭。常见做法是定义一个enum比如 0-8 是万子一到九9-17 是条子18-26 是筒子27 开始是字牌。洗牌就是对这个数组做随机打乱// 洗牌Fisher-Yates 洗牌保证每种排列等概率 void Shuffle(std::vectorint tiles) { srand((unsigned)time(NULL)); // 用时间做随机种子 for (int i tiles.size() - 1; i 0; --i) { int j rand() % (i 1); // 随机选一个下标 std::swap(tiles[i], tiles[j]); // 交换 } }逻辑说明从后往前遍历每次在当前未处理区间里随机选一张换到当前位置。参数说明tiles是整副牌麻将通常是 136 张万条筒各 36 张加字牌 28 张。rand()的随机性一般如果要做更公平的洗牌可以用std::mt19937配合std::shuffle。注意srand只调用一次放在InitInstance里别每次洗牌都调否则同一秒内开局牌序会重复这是玄学问题的根源之一。3.2 发牌与玩家手牌管理发牌逻辑在Wanjia.cpp里通常是给四个玩家各发 13 张庄家 14 张。手牌一般用std::vectorint存并且排序方便后面判断顺子刻子。排序用std::sort即可。这里有个坑如果手牌不排序胡牌判断里的顺子检测会漏判因为顺子要求连续三张乱序时你按顺序扫是扫不出来的。所以发完牌立刻排序是血泪经验。3.3 胡牌判断算法递归拆解与将牌检测MajHu.cpp是整套源码里最值得读的部分。胡牌判断的标准思路是从手牌里先选一对将牌剩下的牌尝试拆成若干顺子或刻子全部拆完就是胡。用递归实现// 判断剩余牌能否全部拆成顺子或刻子 bool CanFormMelds(std::vectorint tiles) { if (tiles.empty()) return true; // 拆完即成功 int first tiles[0]; // 尝试刻子三张相同 if (count(tiles.begin(), tiles.end(), first) 3) { removeThree(tiles, first); // 移除三张 if (CanFormMelds(tiles)) return true; addThree(tiles, first); // 回溯 } // 尝试顺子first, first1, first2 且同花色 if (isSameSuit(first, first 1) isSameSuit(first, first 2) hasTile(tiles, first 1) hasTile(tiles, first 2)) { removeTile(tiles, first); removeTile(tiles, first 1); removeTile(tiles, first 2); if (CanFormMelds(tiles)) return true; addBack(tiles, first, first 1, first 2); // 回溯 } return false; // 两种都试过失败 } // 胡牌入口枚举将牌 bool IsHu(std::vectorint hand) { for (int i 0; i hand.size(); i) { if (i 0 hand[i] hand[i-1]) continue; // 跳过重复将牌 if (count(hand.begin(), hand.end(), hand[i]) 2) { std::vectorint rest hand; removeTwo(rest, hand[i]); // 移除将牌 if (CanFormMelds(rest)) return true; } } return false; }逻辑说明IsHu先枚举所有可能的将牌对子对每种情况调用CanFormMelds递归拆解。CanFormMelds每次取最小的牌优先试刻子再试顺子失败就回溯。参数说明hand是排序后的手牌isSameSuit要判断两张牌是否属于同一花色且数字连续字牌不能组顺子。这个算法的时间复杂度在 14 张牌规模下完全够用不需要上动态规划。注意递归里的remove和addBack必须成对漏一个就会把手牌改乱导致后续判断全错。3.4 电脑 AI 出牌策略CmpThink.cpp 的取舍逻辑CmpThink.cpp里的电脑 AI 一般不会做太深的搜索常见做法是“打孤张、留搭子”。具体就是给每张牌算一个价值分能组成顺子或刻子的牌加分孤张减分然后打掉分最低的。更简单的是直接打最边上、最不靠张的牌。读这段代码时重点看它怎么评估“搭子”比如手里有 3 万和 5 万那 4 万就是有效进张价值高。如果 AI 总是打错牌先检查它的手牌有没有排序没排序的 AI 等于闭着眼睛打。4. 资源与界面BMP 贴图、GDI 绘图和 MFC 消息映射的避坑清单4.1 位图资源加载与透明贴图牌面用Majpai.bmp一张大图切还是每张牌一个小图取决于源码实现。常见做法是一张大图里按网格排列用BitBlt或StretchBlt截取对应区域画到对话框上。透明背景要用TransparentBlt但它在老 MFC 里需要链接msimg32.lib。如果牌面画出来带白底就是没用透明贴图。资源 ID 在resource.h里定义LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_MAJPAI))加载返回 NULL 就说明 ID 或路径不对。4.2 双缓冲绘图消除闪烁MFC 对话框直接OnPaint里画图刷新时会闪。标准解法是双缓冲先在内存 DC 里画好整帧再一次性BitBlt到屏幕 DC。void CMahjongDlg::OnPaint() { CPaintDC dc(this); CRect rect; GetClientRect(rect); CDC memDC; memDC.CreateCompatibleDC(dc); // 创建内存 DC CBitmap bmp; bmp.CreateCompatibleBitmap(dc, rect.Width(), rect.Height()); memDC.SelectObject(bmp); // 选入内存位图 // 在这里把所有牌、背景画到 memDC DrawAllTiles(memDC); dc.BitBlt(0, 0, rect.Width(), rect.Height(), memDC, 0, 0, SRCCOPY); }逻辑说明所有绘制操作先落在内存 DC最后一步拷贝到屏幕避免逐张绘制时的闪烁。参数说明CreateCompatibleBitmap的宽高用客户区大小BitBlt的SRCCOPY表示直接覆盖。注意内存 DC 和位图要随用随建或者做成成员变量复用频繁创建销毁会拖慢帧率。4.3 消息映射与按钮响应MFC 的消息映射靠BEGIN_MESSAGE_MAP和ON_BN_CLICKED这类宏。如果按钮点了没反应先检查三处资源里按钮 ID 是否和宏里一致、DoDataExchange里有没有绑变量、消息映射宏有没有写在BEGIN_MESSAGE_MAP和END_MESSAGE_MAP之间。漏写END_MESSAGE_MAP会编译报错写错 ID 则静默失效这是最常见的翻车点。4.4 避坑与排查清单现象一编译报afxwin.h找不到。原因VS2022 没装 MFC 组件。解决打开 Visual Studio Installer修改“使用 C 的桌面开发”勾选“适用于最新 v143 生成工具的 C MFC”。现象二程序运行后界面空白牌画不出来。原因位图资源 ID 不匹配或LoadBitmap返回 NULL。解决在LoadBitmap后加ASSERT(bmp ! NULL)断下来看资源视图里的 ID 和代码里是否一致。现象三胡牌判断偶尔漏胡。原因手牌没排序或递归回溯时remove/addBack不成对。解决发牌后立即std::sort并在递归函数里用 RAII 或成对调用保证状态还原。现象四电脑 AI 出牌卡死或死循环。原因AI 选牌时没有排除已打出的牌或者循环条件写错。解决在CmpThink里加打印看它每次选的牌是否在合法手牌范围内。现象五升级到 Unicode 后大量CString报错。原因老代码用char*和sprintf。解决项目属性字符集先设“使用多字节字符集”或者把sprintf换成_stprintf_schar*换成CString。5. 从能跑到能改加一个听牌提示与出牌记录的小技巧把工程跑起来只是第一步真正让这份源码变成自己东西的办法是加功能。我一般会先加一个“听牌提示”因为它的实现路径清晰又能逼你把胡牌判断复用起来。思路是遍历当前手牌里每一张假设打掉它然后对剩下的 13 张枚举所有可能的进张34 种牌用IsHu判断是否胡。如果某张进张能让手牌胡就说明当前处于听牌状态把这张进张记下来显示。// 听牌提示返回所有能胡的进张 std::vectorint GetTingTiles(std::vectorint hand) { std::vectorint result; for (int discard 0; discard hand.size(); discard) { std::vectorint after hand; after.erase(after.begin() discard); // 假设打掉这张 for (int t 0; t 34; t) { // 枚举 34 种牌 if (count(after.begin(), after.end(), t) 4) continue; // 超过四张不可能 std::vectorint test after; test.push_back(t); std::sort(test.begin(), test.end()); if (IsHu(test)) { result.push_back(t); // 记录进张 } } } std::sort(result.begin(), result.end()); result.erase(std::unique(result.begin(), result.end()), result.end()); return result; }逻辑说明对每张可打的牌枚举 34 种可能的进张补进去后调IsHu。参数说明hand是当前 14 张手牌t从 0 到 33 对应万条筒字。注意count判断不能超过四张因为麻将每种牌最多四张。这个函数在 14 张规模下跑一次大概几毫秒放在出牌后调用完全不影响体验。显示的时候把t转成中文牌名比如 0 是“一万”27 是“东”贴到对话框的静态文本上。出牌记录更简单在玩家和电脑每次出牌时往一个CListBox或CListCtrl里插一条字符串格式“玩家 打出 五万”。关键是记录要放在真正执行出牌的那一步别放在按钮响应开头否则玩家取消出牌也会记进去。我习惯在Wanjia.cpp的DiscardTile函数末尾统一记录这样玩家和 AI 走的是同一条路径不会漏。这两个小功能加起来不到一百行但能让你把MajHu.cpp、Wanjia.cpp、CmpThink.cpp之间的调用关系彻底摸清。从那以后我每次拿到一份陌生源码都强制先找一个能复用核心算法的功能加上去跑通了再谈改架构。希望这份拆解帮到你少走点升级工程和资源 ID 对不上的弯路。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。