简介这是基于C编写的麻将游戏项目开发环境为Visual C采用MFC构建图形界面代码与素材一起封装在RAR包中适合C初学者、游戏开发爱好者以及想研究经典棋牌逻辑的开发者。压缩包共135个文件主体源代码为9个cpp与10个头文件涵盖玩家操作、胡牌判断、电脑AI等核心模块89个wav音频和bmp、ico、cur等图片资源提供背景、牌面、骰子、窗口图标与音效支持dsw、dsp等工程文件便于在Visual Studio中恢复项目配置另有麻将介绍.txt辅助说明玩法同时包含rc/rc2资源脚本和plg日志等辅助文件结构清晰。包体仅3.86MB轻巧完整。通过学习这份代码可以掌握MFC窗口程序的搭建方法、麻将洗牌与胡牌算法、多模块协作开发以及将资源文件与程序一同打包发布的思路对于学习C桌面应用与Visual Studio项目管理也有参考价值。目前已有293人浏览学习运行exe即可直观体验适合对照源码逐模块拆解。1. 这个 C 麻将游戏包是什么解压前先看清门道在源码站搜“mahjong”或“麻将”大概率会看到一个叫 CPP-Mahjong-game.rar 的包文件名后面还跟着mahjong和 _麻将 两个标签。这类包流传很广解压出来通常是十几个 .cpp 和 .h 文件在命令行里就能跑起一局完整的麻将发牌、理牌、出牌、吃碰杠、判胡都有。对多数人来说这个包最值钱的是牌型数据结构和判胡算法那几十行代码可以拆出来用到自己的项目里。它适合三类人学过 C 但没独立做过项目的新手、需要课程设计源码底子的人以及想研究判胡和听牌逻辑的工程师。动手之前先确认一件事这个 rar 能不能正常解开、代码能不能编译。这篇笔记就按这个顺序往下走。2. rar 解压与文件校验拿到源码包先做的 3 个动作源码站上被反复转载的 rar 包质量参差不齐。命名里带_mahjong_、_麻将这种标签说明它是被某个目录收录后重新打包的——经过二手转存压缩包本身损坏、被塞进无关文件甚至加密都是家常便饭。所以拿到手的第一件事不是解压而是先做三件看起来有点多余、实际上省时间的事看元信息、找密码、管编码。2.1 解压前先看元信息文件清单、加密标记与顶层目录我一般先用unrar或7z列一下压缩包内容不解压只读清单。这个动作能暴露大多数问题里面到底是源码还是编译好的 exe、是不是分卷包、有没有加密。# 列出压缩包内文件清单不解压 unrar l CPP-Mahjong-game.rar # 用 7z 同样能做到输出格式更规整 7z l CPP-Mahjong-game.rarl参数是 list 的意思只读取压缩包的中央目录不会往磁盘写任何文件速度很快。看输出时重点盯三点。第一文件名后缀有没有.cpp、.h、.c、.hpp如果列表里全是.jpg和.exe那这个包大概率不是源码包是别人打包好的现成程序跟你的学习目标对不上。第二有没有顶层目录正常源码包会有一个根文件夹比如CPP-Mahjong-game/所有文件在它下面如果一堆.cpp直接散落在压缩包根目录后面编译时文件管理会麻烦一点。第三加密标记unrar l输出里文件名右侧会出现星号或[encrypted]字样7z l会在类型列显示加密属性这个信息决定你要不要接着找密码。看完清单再决定是否解压。列表里出现分卷编号.r00、.r01说明这只是一个分卷的其中一段单独解压必然报错得把同目录下所有分卷下载齐全放在一起再解。2.2 “rar密码移除”工具别碰先找来源与注释不少资源站为了引流打包时会设置密码。下载页面通常会写“解压密码xxx”有时候也会把密码写进压缩包注释。如果你第一反应是去搜“rar密码移除”这里先泼盆冷水rar 加密用的是 AES-128 或 AES-256正经密码移除工具是不存在的市面上打着这个旗号的东西大部分是广告捆绑和假破解装完不仅解不开包还会给电脑塞进一堆推广件属于典型的翻车路径。正确的顺序是先回下载页找说明文字再解压工具里看注释。Windows 下用 WinRAR 打开压缩包右侧信息面板有“注释”区域很多站会把密码放在那里。命令行下可以这样试# 详细模式列出压缩包信息部分版本会输出注释块 unrar v CPP-Mahjong-game.rar如果注释里明确写了密码就直接用。还有一种常见做法某些包的密码被伪装成一个网址解压时直接输入那个网址这种也算约定俗成。实在找不到密码最稳的办法是换一个下载源而不是和压缩包死磕。一个需要密码的源码包即使你破解出来了里面代码也未必值回时间成本。2.3 编码与换行乱码要从解压阶段就开始管中国开发者发布的 C 源码包源文件编码大部分是 GBK因为 Windows 简体中文版默认就是这个。但在 Linux 或 macOS 上解压后用文本编辑器打开全是乱码。这不是文件坏了是编码不匹配。另一个隐藏问题是换行符Windows 用的是\r\nLinux 用的是\n老版本编译器在混用换行符时会给警告虽然通常不影响编译但配合编码问题会让人误判故障点。解压后用file先看文件类型和编码这个命令比任何编辑器都直观# 查看文件编码与换行符格式 file main.cpp # 如果显示 Non-ISO extended-ASCII 或 ISO-8859基本可断定是 GBK # 转成 UTF-8-f 指原编码-t 指目标编码 iconv -f GBK -t UTF-8 main.cpp -o main_utf8.cpp mv main_utf8.cpp main.cpp # 顺手把 CRLF 换成 LF避免编译器报警告 dos2unix main.cppiconv的-f和-t参数指定了转换路径这里是从 GBK 转到 UTF-8。做这个转换之前先file确认一下原始编码有些包可能已经是 UTF-8再转一次反而会把中文注释转坏。dos2unix只处理换行符不改编码两个命令独立使用。Windows 用户如果不想装这些工具用记事本打开另存为 UTF-8 也能达到同样效果但记得一次处理全部.cpp和.h否则混着编译反而更乱。编码问题建议在解压阶段统一解决不要拖到编译阶段。原因很简单编译器报的错如果是英文的你还能顺着行号找如果错误信息里飘着一堆乱码字符你根本分不清是语法问题还是编码问题排查成本直接翻倍。3. 编译运行 CPP 麻将项目从 dev cpp 到命令行 g 的最小路径源码包解压出来了接下来是编译。这一步卡住最多人的不是语法错误而是不知道这个项目该用什么方式构建。麻将项目不像那种“简单的单例类”练手代码只有一个 main.cpp它往往拆成好几个源文件还可能有自己的头文件依赖关系。先看清楚工程结构再决定编译方式能省掉一半的报错。3.1 先读工程结构Makefile、CMakeLists 还是 dev 文件解压完先看根目录有没有这几个文件它们决定了项目的主构建方式有Makefile说明作者用 make 构建进目录直接make通常就行没装 make 的 Windows 用户可以用 mingw32-make。有CMakeLists.txt这是用 CMake 构建的项目需要先cmake ..生成工程再make或cmake --build .。有.dev后缀的文件Dev-C 的工程文件用 Dev-C 打开后点编译运行即可。什么都没有很常见尤其是某些网课作业流传出来的版本。这种就没有“标准”构建流程需要自己组编译命令。我见过最多的情况是第三种作者用 Dev-C 写完了整个麻将游戏打包源码时只把.cpp和.h打了进去工程文件.dev反而被遗漏了。这种包编译时往往要用回老的 Dev-C或者它的现代替代品因为代码里可能用了旧版编译器才支持的写法。如果你机器上装了新版的 Visual Studio 或 clang直接编译老项目经常报一堆 C17 标准之前的兼容错误这时候不是代码错了是编译器标准太新。3.2 最小编译命令从单文件到全量 *.cpp大多数这种麻将项目的源文件数量在 3 到 10 个之间。编译的最小命令其实很短核心是把所有.cpp都交给编译器而不是只编译 main.cpp。# 单文件版本整个项目只有一个 main.cpp 时用 g -stdc11 -Wall main.cpp -o mahjong # 多文件版本把当前目录所有 cpp 一起编译通配符搞定 g -stdc17 -O2 -Wall *.cpp -o mahjong # 如果源码在 src/ 子目录里加上路径 g -stdc17 -O2 -Wall src/*.cpp -o mahjong参数说明-stdc17指定 C 标准老项目建议先降到c11试一次如果报错再根据提示上调-O2开启优化判胡是递归算法不优化的话出牌会有肉眼可见的卡顿-Wall把警告打开虽然警告不等于错误但 C 的警告往往暗示着隐性问题比如未初始化的变量、类型转换丢精度这些在处理牌型数字时很容易踩雷。*.cpp通配符是这条命令的关键。很多新手只编译 main.cpp结果链接阶段报undefined reference to xxx原因就是函数定义在另一个.cpp里而编译器压根没见过那个文件。用通配符把目录下所有.cpp一次性交给 g能规避绝大多数链接错误。如果项目文件多或者你打算长期改它建议写一个简单的 Makefile而不是每次敲这一长串命令。3.3 运行控制台麻将输入、输出与代码页编译成功后得到一个可执行文件。这种游戏包基本是控制台程序没有图形界面运行方式很简单但有两个坑很容易让新手以为程序坏了。# Linux / macOS ./mahjong # Windows先切到 exe 所在目录再运行 cd D:\mahjong mahjong.exe第一个坑是中文乱码。程序里的提示语如果是 UTF-8 编码在 Windows 默认的 GBK 控制台下显示就是乱码反过来源码是 GBK在 Linux 终端下运行也会乱。解决办法是让“源文件编码”和“运行终端编码”保持一致Windows 的 cmd 里可以用chcp 65001切到 UTF-8 代码页或者chcp 936切回 GBK。Dev-C 自带的控制台一般是 GBK所以源码保持 GBK 反而省事。第二个坑是双击运行一闪而过。在 Windows 上双击 exe如果程序跑完就退出黑框会直接关闭什么都看不到。这在开发阶段很容易造成“我编译失败了吧”的误判。处理办法是在 cmd 里手动运行这样窗口会保留输出信息如果源程序确实没有暂停点可以用管道把输出重定向到文件再查看mahjong.exe output.txt 2121把标准错误也并入标准输出这样编译警告和运行时错误都能落盘。4. 麻将判胡逻辑拆解牌型编码与回溯算法的 C 实现编译跑通只是开始。这类项目里真正值得反复读的是判胡模块——它决定了游戏能不能正确识别“胡了”。新手往往觉得判胡很难因为要考虑的牌型太多但实际上主流玩法只有两套逻辑普通牌型3n2 结构和特殊牌型七对、十三幺。这一章把它们的实现逻辑拆开讲代码可以直接抄走。4.1 用 34 个整数表示整副手牌麻将牌一句话概括万、条、筒各 9 种风牌 4 种箭牌 3 种总计 34 种。最常见的数据结构就是int hand[34]数组下标表示牌型值表示剩余张数。编号区间牌面类型0-8一万到九万万子9-17一条到九条条子18-26一筒到九筒筒子27-30东南西北风牌31-33中发白箭牌用整数而非字符串的好处是判断顺子非常直观顺子就是编号连续的三张牌比如hand[i]、hand[i1]、hand[i2]都大于 0。判断花色边界也很简单i % 9就是当前牌在所属花色里的位置只要i % 9 6就说明后面还够两张组成顺子。const int kHandSize 34; // hand[i] 表示第 i 种牌还剩几张初始化为 0 int hand[kHandSize] {0};这段代码定义了核心数据结构。注意kHandSize是常量后面所有遍历手牌的循环都用它不要硬编码 34改起来容易漏。这样的结构足够支撑吃碰杠、听牌、出牌 AI 的所有操作工程上这是最主流的方案。4.2 判胡的核心先找将再递归拆 3 张组普通牌型的规则是手牌总数满足3n 2其中 2 张是一对将牌其余每 3 张组成一个刻子或顺子。判胡的思路是先假设某一种牌做将牌剩下 12 张递归判断能不能全部拆成刻子或顺子。// 递归判断剩余 rest 张牌能否全部拆成刻子或顺子 bool canSplit(int cnt[], int rest) { if (rest 0) return true; // 找到编号最小的非空牌 int mi -1; for (int i 0; i kHandSize; i) { if (cnt[i] 0) { mi i; break; } } // 优先尝试刻子三张相同牌 if (cnt[mi] 3) { cnt[mi] - 3; if (canSplit(cnt, rest - 3)) { cnt[mi] 3; return true; } cnt[mi] 3; } // 再尝试顺子必须是同花色且后两张都存在 if (mi 27 mi % 9 6 cnt[mi 1] 0 cnt[mi 2] 0) { cnt[mi]--; cnt[mi 1]--; cnt[mi 2]--; if (canSplit(cnt, rest - 3)) { cnt[mi]; cnt[mi 1]; cnt[mi 2]; return true; } cnt[mi]; cnt[mi 1]; cnt[mi 2]; } return false; } // 外层枚举哪一张牌做将牌 bool isStandardHu(int cnt[]) { for (int j 0; j kHandSize; j) { if (cnt[j] 2) { cnt[j] - 2; // 14 张去掉 2 张将牌剩 12 张 if (canSplit(cnt, 12)) { cnt[j] 2; return true; } cnt[j] 2; } } return false; }逻辑说明canSplit每次从最小编号的牌开始拆是因为最小的那张牌如果不能组成刻子或顺子这个局面就是死局直接返回 false。这比随机挑牌拆要快得多也避免了重复递归。mi 27限定只有万、条、筒才能组成顺子风牌和箭牌没有顺子的概念如果这里不加边界9 万后面的 1 条会被误判成连续牌。isStandardHu里枚举每张牌做将牌减去 2 张后把剩余的 12 张交给递归处理。这段代码的匹配逻辑有一个隐含前提数组cnt会被修改所以调用方要在递归前后保证数组状态恢复。每次尝试失败后都把减掉的牌加回来这是回溯算法的标准操作漏掉任何一行恢复代码都会让后面的判断全乱。特殊牌型是两个独立分支很多流传的判胡代码压根没处理它们这直接导致七对胡不了。把它们单独实现并在总入口优先判断// 七对七个对子每种恰好 2 张 bool isSevenPairs(int cnt[]) { int pairs 0; for (int i 0; i kHandSize; i) { if (cnt[i] 2) pairs; } return pairs 7; } // 十三幺十三种幺九牌各 1 张另加任意 1 张作将 bool isThirteenOrphans(int cnt[]) { int yao[kHandSize] {0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33}; for (int i 0; i 13; i) { if (cnt[yao[i]] ! 1) return false; } int pairs 0; for (int i 0; i kHandSize; i) { if (cnt[i] 2) pairs; } return pairs 1; } // 总入口三种胡法按顺序判断 bool isHu(int cnt[]) { return isSevenPairs(cnt) || isThirteenOrphans(cnt) || isStandardHu(cnt); }这里要特别说明isSevenPairs判断的是“恰好七个对子”如果手牌同时满足普通牌型和七对比如碰碰胡的某些形态在多数麻将规则里按玩家选择的胡法算代码里用||连接意味着任意一种成立即胡这是最宽容的判断。isThirteenOrphans中的yao数组罗列了所有幺九牌和字牌的编号必须是每种 1 张再加上任意 1 张做将注意这种情况下额外那张将牌也必须是幺九牌之一所以最后查的是“某一张有 2 张”而不是“有一张任意牌”。4.3 出牌与听牌检测拿判胡当引擎判胡函数最实用的衍生功能是听牌检测——判断当前手牌打出哪张后摸到哪些牌能胡。实现思路很暴力但有效遍历每一张手牌假设打掉它再遍历 34 种牌假设摸进来调用isHu验证。// 判断打出手牌 i 后是否至少存在一张牌让手牌组成胡牌 bool isNearWin(int cnt[], int i) { cnt[i]--; // 模拟打出第 i 种牌 for (int j 0; j kHandSize; j) { cnt[j]; // 模拟摸入第 j 种牌 if (isHu(cnt)) { cnt[j]--; // 恢复摸入的牌 cnt[i]; // 恢复打出的牌 return true; } cnt[j]--; // 撤销这次摸入尝试 } cnt[i]; // 撤销打出操作 return false; }复杂度看着高但实际每轮判断只有 34 次isHu调用而判胡递归的剪枝非常充分在普通桌面上耗时可忽略不计。这是很多“简单 AI”的地基AI 出牌前先对每张手牌调用isNearWin如果打掉某张后能听牌就优先打它如果所有选择都听不了再退回随机出牌。cnt数组的恢复逻辑是这段代码最容易出错的地方我在注释里标了每一步对应哪个操作的逆操作抄代码时务必保留这几行。5. 避坑与排查编译和运行 C 麻将时的 5 个高频问题无论你是自己写判胡逻辑还是从网上下载这种项目编译运行下面的问题几乎一定会遇到至少一两个。每一条都按“现象、原因、解决”的顺序写可以直接对照排查。5.1 链接时报 undefined reference源码看着全都在现象编译命令执行后报错提示undefined reference to xxx但打开头文件明明声明了这个函数。原因最常见的情况是只编译了 main.cpp函数定义在 game.cpp 里编译器链接时找不到实现。另一种情况是某个.cpp文件压根没被加入工程Dev-C 里表现为“添加了文件但没勾选编译”。解决用通配符一次编译所有.cpp如果项目在子目录就带上路径g -stdc17 -O2 -Wall *.cpp -o mahjong这招能解决 90% 以上的链接报错。剩下 10% 是声明了但没写实现或者实现函数名拼写和声明不一致——查一下.cpp里的函数签名重点看参数列表是否带了const或引用符号。5.2 中文提示全是乱码现象程序启动后控制台里中文变成娌℃湁杩欎釜鐗或????????英文字符正常。原因源文件是 GBK 编码在 UTF-8 终端下运行或者反过来。命令行程序里所有中文字符串在编译时就被固化成二进制字节序列运行时直接输出不会自动做编码转换。解决两个思路。改文件编码让源码和终端一致或者告诉编译器源文件的编码。第二招更省事# Linux 下源文件是 GBK但想让程序输出 UTF-8 g -finput-charsetGBK -fexec-charsetUTF-8 *.cpp -o mahjong-finput-charset告诉编译器源文件用什么编码存储-fexec-charset指定编译后的字符串常量用什么编码。Windows 下如果终端是 GBK就把-fexec-charset设成GBK或者更简单chcp 65001把终端切到 UTF-8。5.3 判胡结果不对七对和十三幺永远胡不了现象手里是七个对子运行游戏提示“不能胡”手里是十三幺同样不能胡。但普通的顺子刻子牌型判断正常。原因判胡函数只实现了isStandardHu没有处理特殊牌型。网上流传的很多麻将源码都这样作者只写了最常见的 3n2 判法。解决把第 4 章的isSevenPairs和isThirteenOrphans补进你的判胡入口。注意判断顺序先判断特殊牌型再判断普通牌型因为有些牌型同时满足多个条件优先按番数高的算。5.4 解压到一半报错包不完整现象unrar解压过程中提示Unexpected end of archive或CRC failed程序中断文件不全。原因rar 包在下载或转存过程中损坏也可能是分卷包只下载了其中一段。资源站经过多次转载后这种问题很常见。解决解压前先用unrar t测试包完整性这个命令不写入文件只校验每文件块的 CRCunrar t CPP-Mahjong-game.rar如果提示有分卷缺失去下载页把.part1.rar、.part2.rar等全部下载放到同一目录再解压。包损坏的话没有修的必要重新换一个下载源更实际。5.5 双击运行一闪而过看不到任何输出现象Windows 下双击 exe黑框闪一下就消失无法判断程序是否正常、有没有报错。原因程序主流程执行完毕就退出了或者异常崩溃。这种麻将游戏如果是单局制打完一局直接 return控制台自然会关闭。解决先用 cmd 手动运行保留输出。把窗口留下再排查// 在 main 函数 return 0 之前加一段仅用于调试 std::cout 按回车键退出... std::endl; std::cin.get();std::cin.get()会阻塞等待用户按回车。注意这只能是调试手段正式发布不要留——游戏本身的循环如果设计合理玩家主动退出前窗口本就应该保持打开加这行说明主循环结构有问题。6. 验证与改造判胡测试与玩法扩展6.1 最小用例验证判胡在动手改代码之前先给判胡函数喂几个已知结果的手牌。这套最小用例能保证你的算法不是黑匣子改完不翻车。void testHu() { int hand[34] {0}; // 用例 1标准胡 111万 234万 55筒将 hand[0] 3; // 一万 x3 hand[1] 1; // 二万 hand[2] 1; // 三万 hand[3] 1; // 四万 hand[21] 2; // 五筒 x2 std::cout 标准胡: isHu(hand) std::endl; // 期望 1 // 用例 2七对 memset(hand, 0, sizeof(hand)); int seven[7] {0, 1, 2, 3, 4, 5, 6}; for (int i 0; i 7; i) hand[seven[i]] 2; std::cout 七对: isHu(hand) std::endl; // 期望 1 // 用例 313 张不完整牌不应胡 memset(hand, 0, sizeof(hand)); hand[0] 1; hand[1] 1; hand[2] 1; hand[3] 1; std::cout 不完整手牌: isHu(hand) std::endl; // 期望 0 }这个测试函数写进 main 里跑一遍确认三个用例的输出分别是 1、1、0再进入玩法开发。以后每次改动判胡逻辑先跑这个函数能挡住八成回归问题。6.2 改造方向从能跑到好玩跑通整个项目后值得做的改造有三个方向。第一把 AI 出牌从随机改成听牌优先用第 4 章的isNearWin替换原有的随机逻辑这一步让游戏从“能玩”变成“能打”。第二加计番功能番型判断同样基于 34 编码的手牌数组比如清一色检查所有非零牌是否落在同一个花色区间碰碰胡检查是否全是刻子加将牌。第三把控制台输入换成图形界面常见做法是 Qt核心逻辑不变只替换输入输出层。无论选哪个方向都记得保留原始的 rar 包不动改坏了随时能退回最初状态——这是我从各种源码包练习里攒下的习惯。希望帮到你。本文还有配套的精品资源点击获取
