news 2026/10/26 16:42:50

VC6麻将源码迁移实战:胡牌算法与AI出牌逻辑解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC6麻将源码迁移实战:胡牌算法与AI出牌逻辑解析

简介:这是一份基于C++与Visual C++开发的麻将游戏项目源码,面向具备一定C++基础、希望学习Windows桌面游戏开发的开发者与在校学生。项目采用MFC类库构建图形界面,完整实现了洗牌、出牌、胡牌判断等核心逻辑,并包含人机对战交互设计,适合作为课程设计、毕业设计或游戏编程入门的参考案例。压缩包共135个文件,约3.86MB,其中89个wav音频与5个bmp位图承担音效和界面素材,9个cpp源文件与10个h头文件构成游戏主体逻辑,另含dsw、opt等Visual Studio工程配置及可执行程序,便于直接运行与二次开发。目前已有295人学习下载。通过研读源码,读者可掌握MFC界面搭建、麻将算法实现、文本说明读取及项目组织管理等实用技能,快速理解C++游戏开发的完整流程。

1. 从一份 2000 年代的 VC 麻将工程说起:它到底能跑出什么

如果你手里正好有一份CPP-Mahjong-game.rar,解压后看到Mahjong.dsw、MahjongDlg.cpp、MajHu.cpp、CmpThink.cpp这些文件,第一反应大概率是:这是哪个年代的老古董?没错,这是一套典型的 Visual C++ 6.0 + MFC 时代的麻将游戏工程,用.dsw工作区文件组织项目,用MahjongDlg.cpp承载主对话框逻辑,用MajHu.cpp处理胡牌判断,用CmpThink.cpp做电脑玩家的出牌决策。它不是一个能直接双击就玩的现代游戏,而是一份可以编译、可以调试、可以拆开看内部逻辑的完整源码包。

它适合谁?适合想搞懂麻将胡牌算法怎么落地的人,适合想拿一个真实 MFC 工程练手的人,也适合想从老代码里翻出牌型判断、洗牌、AI 出牌这些模块,移植到自己项目里的人。不适合谁?不适合只想找个成品麻将游戏打发时间的人,因为它的界面和交互放在今天看确实粗糙。但如果你愿意花一个下午把它编译起来,你会得到一份比很多教程都实在的参考实现。

2. 把工程跑起来:VC6 到 VS2022 的迁移与编译参数

2.1 先看清工程结构再动手

解压之后不要急着双击Mahjong.dsw,先花两分钟把文件分个类。资源文件里Background.bmp、Majpai.bmp、sezi.bmp、Num.bmp、Title.bmp分别是背景、麻将牌面、骰子、数字和标题图,这些是 MFC 对话框绘制时直接加载的位图资源。代码文件里MahjongDlg.cpp是主对话框实现,Wanjia.cpp大概率是玩家类,MajHu.cpp是胡牌判断,CmpThink.cpp是电脑思考逻辑。.clw是 ClassWizard 的类信息文件,.ncb是浏览数据库,.opt是工程选项,这三个文件在迁移时可以直接删掉,让 IDE 重新生成。

常见做法是先把整个目录复制一份,保留原始包不动,在副本上操作。因为 VC6 工程一旦被新版 VS 转换,.dsw和.dsp会被替换成.sln和.vcxproj,原始工程文件就回不去了。这一步算是给自己留个后悔药。

2.2 用 VS2022 打开并处理迁移报告

VS2022 可以直接打开.dsw,它会弹出迁移向导,把 VC6 工程转成现代工程。转换完成后重点看两处:一是字符集,VC6 默认多字节字符集,新版默认 Unicode,如果不改,CString和char*混用处会报一堆错;二是 MFC 版本,工程属性里要把“使用 MFC”设为“在共享 DLL 中使用 MFC”或“在静态库中使用 MFC”。

# 如果不想用 IDE 迁移,也可以用命令行先看一眼工程里引用了哪些库 find . -name "*.dsp" -o -name "*.dsw" | xargs grep -i "LIBS\|CFLAGS" 2>/dev/null

这段命令的作用是快速提取旧工程里链接的库和编译选项,方便你在迁移后对照检查有没有漏掉依赖。参数上-i是忽略大小写,2>/dev/null是把权限或路径错误吞掉,避免刷屏。

迁移后如果编译报error C2440: “static_cast”: 无法从“const char [X]”转换为“LPCWSTR”,说明字符集没统一。最省事的做法是在工程属性里把“字符集”改成“使用多字节字符集”,而不是去改几百处字符串。这是老工程迁移的血泪经验:能改工程设置就不要改代码。

2.3 资源位图的加载路径与调试断点

MFC 对话框程序加载位图通常走LoadBitmap或CBitmap::LoadBitmap,资源 ID 在resource.h里定义。如果编译通过但运行时报“找不到资源”,先检查.rc文件里位图有没有被正确包含。VC6 时代的.rc文件有时用绝对路径引用位图,迁移后路径失效,需要手动改成相对路径或把位图重新导入资源视图。

// MahjongDlg.cpp 中常见的位图加载片段(示意) CBitmap bmp; if (!bmp.LoadBitmap(IDB_BITMAP_MAJPAI)) { TRACE(_T("加载麻将牌面位图失败,检查资源 ID 与 .rc 文件\n")); }

逻辑说明:LoadBitmap返回布尔值,失败时不会抛异常,只会让后续绘制变成空白。参数IDB_BITMAP_MAJPAI是资源 ID,必须和resource.h里的定义一致。调试时在LoadBitmap后面下断点,看返回值是 TRUE 还是 FALSE,比盲目改代码快得多。

3. 胡牌判断与电脑出牌:MajHu.cpp 和 CmpThink.cpp 的算法拆解

3.1 胡牌判断的递归思路与边界条件

MajHu.cpp是整套代码里最值得细看的部分。麻将胡牌判断的经典做法是递归拆解:把手牌按花色和点数排序,先找将牌(对子),剩下的牌尝试拆成顺子或刻子,全部拆完则胡牌。常见实现里会用一个长度为 34 的计数数组表示每种牌的数量,然后写一个CheckHu递归函数。

// MajHu.cpp 中胡牌判断的简化逻辑(示意,非原文件逐行) bool CheckHu(int count[34], int needPair) { // 找第一张还有剩余的牌 int i = 0; while (i < 34 && count[i] == 0) i++; if (i == 34) return needPair == 0; // 牌拆完且将牌已处理 // 尝试拆刻子 if (count[i] >= 3) { count[i] -= 3; if (CheckHu(count, needPair)) { count[i] += 3; return true; } count[i] += 3; } // 尝试拆顺子(仅限序数牌,且 i 不能跨花色边界) if (i < 27 && i % 9 <= 6 && count[i+1] > 0 && count[i+2] > 0) { count[i]--; count[i+1]--; count[i+2]--; if (CheckHu(count, needPair)) { count[i]++; count[i+1]++; count[i+2]++; return true; } count[i]++; count[i+1]++; count[i+2]++; } // 尝试拆将牌 if (needPair && count[i] >= 2) { count[i] -= 2; if (CheckHu(count, 0)) { count[i] += 2; return true; } count[i] += 2; } return false; }

逻辑说明:count[34]里 0 到 8 是万子,9 到 17 是条子,18 到 26 是筒子,27 到 33 是字牌。needPair为 1 表示还没选将牌,为 0 表示将牌已选。递归的每一步都尝试三种拆法,任意一种能走通就返回 true。参数上最容易翻车的是顺子判断里的i % 9 <= 6,如果漏掉这个条件,字牌和跨花色的牌会被错误地当成顺子,导致诈胡。

3.2 电脑出牌决策的权重设计

CmpThink.cpp负责电脑玩家出什么牌。老代码里常见做法是给每张牌算一个“危险度”或“有用度”,然后打掉最没用的那张。有用度通常考虑:这张牌能不能和手里的牌组成顺子或刻子、离听牌还有多远、牌池里已经出现了几张。一个简单可用的权重表如下:

因素权重方向说明
能与手牌组成顺子加分相邻牌越多,越不该打
能与手牌组成刻子加分手里已有两张,第三张价值高
牌池中出现次数减分出现越多,剩余越少,越难成搭
花色集中度加分同一花色牌多,做清一色潜力大
孤张字牌减分单张字牌成搭概率低,优先打

实际代码里通常把这些因素加权求和,选分数最低的牌打出。参数怎么调?如果你想让电脑更激进,就降低“牌池中出现次数”的减分权重;想让它更保守,就提高“孤张字牌”的减分幅度。没有标准答案,取决于你想让游戏难度落在哪个区间。

3.3 洗牌与发牌的随机性处理

洗牌逻辑一般放在Wanjia.cpp或主对话框初始化里。常见做法是用一个数组存 136 张牌(万条筒各 36 张加字牌 28 张),然后用rand()做 Fisher-Yates 洗牌。注意 VC6 时代的rand()质量一般,如果要做可复现的测试,可以用固定种子srand(42),这样每次运行发牌顺序一致,方便调试胡牌逻辑。

// 洗牌示意:Fisher-Yates for (int i = 135; i > 0; --i) { int j = rand() % (i + 1); std::swap(tile[i], tile[j]); }

逻辑说明:从后往前遍历,每次把当前位置的牌和前面随机一张交换。参数i从 135 开始是因为数组下标 0 到 135 共 136 张。rand() % (i + 1)保证j落在[0, i]范围内。如果写成rand() % 136,会出现自己和自己交换,虽然不影响正确性,但洗牌均匀性会打折扣。

4. 避坑与排查:老 MFC 工程迁移中最容易翻车的五件事

4.1 现象:编译报一堆LPCWSTR类型错误

原因:VC6 工程默认多字节字符集,VS2022 新建工程默认 Unicode,迁移时字符集设置没有跟着改。解决:工程属性 → 配置属性 → 高级 → 字符集,改成“使用多字节字符集”。如果不想改工程,也可以在代码里统一用_T()宏包裹字符串,但工作量更大。

4.2 现象:程序启动后对话框一片空白,位图不显示

原因:.rc文件里位图路径是绝对路径,迁移后路径失效,或者位图资源 ID 冲突。解决:在资源视图里重新导入位图,确认resource.h里的 ID 没有重复。用TRACE在LoadBitmap后打印返回值,快速定位是哪张图加载失败。

4.3 现象:胡牌判断偶尔误判,明明没胡却提示胡了

原因:顺子判断没有限制花色边界,字牌或跨花色的牌被错误组合。解决:在顺子判断里加i < 27 && i % 9 <= 6条件,确保只有序数牌且不跨花色才能组成顺子。这是MajHu.cpp里最经典的坑,没有之一。

4.4 现象:电脑出牌逻辑死循环,游戏卡住

原因:CmpThink.cpp里选牌时没有排除已经打出的牌,或者权重计算出现全部为零的情况,导致选不出牌。解决:在选牌循环里加一个兜底逻辑,如果所有候选牌权重相同,直接打第一张。同时检查牌池数组有没有越界访问。

4.5 现象:迁移后.dsw文件消失,想回退到 VC6 打不开

原因:VS2022 迁移时直接替换了工程文件,没有保留原始.dsw和.dsp。解决:操作前复制整个目录,在副本上迁移。如果已经覆盖了,从原始 RAR 包里重新解压一份即可。这个坑的代价最小,但最让人后悔,因为原始包可能已经被你删了。

5. 从这份源码里能带走的三个进阶技巧

5.1 把胡牌判断抽成独立模块

MajHu.cpp里的递归逻辑不依赖 MFC,你可以把它单独抽出来,改成一个纯 C++ 的MahjongHu类,输入一个std::vector<int>表示手牌,输出是否胡牌。这样就能在命令行程序、单元测试甚至其他语言的项目里复用。我一般会先写一组测试用例:标准胡牌、七对、十三幺、诈胡,跑通了再往游戏里接。

// 抽离后的调用示意 std::vector<int> hand = {1,1,1,2,3,4,5,6,7,8,9,9,9,9}; MahjongHu hu; bool result = hu.Check(hand); // result 为 true 表示胡牌

逻辑说明:hand里每个数字代表一张牌的点数,这里简化了花色。实际使用时需要把花色编码进数字,比如万子 1 到 9、条子 11 到 19、筒子 21 到 29、字牌 31 到 37。参数设计上,输入用vector比数组灵活,输出用bool比返回错误码直观。

5.2 用固定种子复现 bug

调试胡牌逻辑时最头疼的是“偶尔出错”,因为洗牌随机。把srand的种子固定成某个值,比如srand(20240501),然后记录每次发牌顺序和出牌序列。一旦复现出误判,就能精确知道是哪手牌触发的。这个习惯我从第一次调MajHu.cpp就养成了,后来每次改递归条件都强制走一遍固定种子的回归测试。

5.3 把位图资源换成 PNG 或直接绘制

老工程的位图是.bmp,体积大且不支持透明。如果你打算把这个项目现代化,可以把Majpai.bmp换成 PNG,用 GDI+ 或 Direct2D 加载。更彻底的做法是不用位图,直接用Rectangle和DrawText画牌面,这样缩放不失真,也方便做高 DPI 适配。这一步不算难,但需要把MahjongDlg.cpp里的OnPaint重写一遍。

迁移方向改动点收益
位图换 PNG资源加载 + Alpha 混合体积小,支持透明
直接绘制重写 OnPaint高 DPI 友好,无资源依赖
逻辑抽离新建纯 C++ 类可测试,可复用
种子固定srand 参数化可复现,方便回归

从那以后我每次拿到老 MFC 工程,都先复制一份、固定随机种子、把核心算法抽出来跑测试,再动界面。这套流程帮我省下了大量“改一处崩三处”的时间。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/26 16:41:43

Codex + Obsidian:用知识库给AI编程助手装上长期记忆

1. 为什么要把 Codex 和 Obsidian 拼在一起用1.1 一个真实痛点&#xff1a;AI 每次都在“从零开始”我用了大半年 AI 编程助手&#xff0c;最大的感受不是它不够聪明&#xff0c;而是它太“健忘”。每次开一个新会话&#xff0c;它就像刚入职的实习生&#xff0c;你得重新交代项…

作者头像 李华
网站建设 2026/10/26 16:41:16

消费级GPU跑MoE被PCIe卡脖子?ThunderEP专家并行优化实战

1. 为什么消费级 GPU 跑 MoE 会被 PCIe 卡住脖子 1.1 从一个反直觉的现象说起 如果你最近在折腾本地大模型推理&#xff0c;大概率动过跑 MoE&#xff08;Mixture of Experts&#xff0c;混合专家&#xff09;架构模型的念头。DeepSeek-V2、Mixtral 8x7B 这类模型&#xff0c;…

作者头像 李华
网站建设 2026/10/26 16:39:49

从屏幕到硬件:AI Agent控制真实设备的关键技术与SDK开源实践

最近 AI 圈让我最兴奋的消息&#xff0c;不是又一个参数规模更大的模型发布&#xff0c;而是 Muse 这款 AI Agent 应用登顶 App Store&#xff0c;并且把它的 SDK 开源了。作为长期关注 AI Agent 从云端走向终端、从对话走向行动的人&#xff0c;这件事比表面看起来要严重得多…

作者头像 李华
网站建设 2026/10/26 16:38:57

VC++手搓CA系统:数字证书签发与签名验证实战拆解

简介&#xff1a;这份资源是面向高校计算机与信息安全专业学生的毕业设计参考包&#xff0c;围绕CA系统&#xff08;证书授权中心&#xff09;展开&#xff0c;帮助读者理解数字证书与数字签名的原理并完成可运行的小型CA实现。内容涉及公钥基础设施、RSA公钥加密、SHA-1或MD5哈…

作者头像 李华
网站建设 2026/10/26 16:38:56

基于遗传算法的LQR控制器优化设计:MATLAB实现与调参避坑指南

简介&#xff1a;这份资源面向控制工程、自动化及车辆工程方向的学习者与研究者&#xff0c;聚焦LQR控制器设计中加权矩阵难以确定这一痛点&#xff0c;提供一套基于遗传算法进行全局寻优的完整案例。资源以汽车主动悬架为被控对象&#xff0c;将性能指标作为目标函数&#xff…

作者头像 李华
网站建设 2026/10/26 16:38:18

机器人模仿学习实战:ACT与Diffusion Policy算法解析

1. 项目概述LeRobot是一个专注于机器人学习与控制的开放框架&#xff0c;第二章教程将深入探讨模仿学习算法及其训练Pipeline的实现。模仿学习作为机器人技能获取的重要途径&#xff0c;通过观察专家演示来学习策略&#xff0c;避免了传统强化学习中的探索成本问题。在实际机器…

作者头像 李华