正在整理这一页的内容…
Project Detail
从截图识别到抓包解析:我的麻将学习助手
事情的起因其实挺简单:同事喊我打麻将,打着打着发现还挺有意思。我们玩的也不大,五毛、一块的,没有搞什么夸张翻倍,运气差一点四个小时也就输几十块。钱倒不是重点,主要是每次输也有点不服气,哈哈。既然要玩,那就不如认真学一下:手里这些牌为什么该留,为什么该打,场上出了哪些牌以后风险怎么变,这些东西光靠感觉很容易乱。 所以我一

事情的起因其实挺简单:同事喊我打麻将,打着打着发现还挺有意思。我们玩的也不大,五毛、一块的,没有搞什么夸张翻倍,运气差一点四个小时也就输几十块。钱倒不是重点,主要是每次输也有点不服气,哈哈。既然要玩,那就不如认真学一下:手里这些牌为什么该留,为什么该打,场上出了哪些牌以后风险怎么变,这些东西光靠感觉很容易乱。
所以我一开始的想法是做一个“麻将学习助手”:它能拿到当前手牌、场上已经打过的牌,然后把这些信息交给 AI,让 AI 分析现在更适合打哪张牌、理由是什么。这样我一边玩一边看建议,慢慢也能理解牌效、听牌、进张、安全牌这些概念,提升一下自己的麻将水平。
第一版:截图 + 识图
最开始我想得比较直接:既然人在屏幕上能看到牌,那程序应该也能看。于是第一版走的是截图识别路线,也就是现在 mjstudy-py 这套。
这套里面主要用到这些东西:
pywin32调 Windows API 截屏,支持全屏、窗口、手动框选区域。OpenCV做图像处理,切出手牌区域和桌面弃牌区域。Pillow处理调试图、模板图、截图标注。numpy做图像矩阵计算。PyQt6做桌面 GUI,显示当前手牌、剩余牌、AI 建议、调试信息。- 模板匹配和 KNN 模型识别麻将牌。
- 本地牌理逻辑计算向听数、待牌、有效进张。
- 配置远程大模型后,把识别到的牌面交给模型分析。
大概流程是:程序先截取游戏窗口画面,然后定位底部手牌区域,把每张牌切出来,再通过模板/KNN 判断它是哪一张牌。桌面上已经打出来的牌也用类似方式识别。最后把手牌、弃牌、剩余牌数量整理成结构化数据,交给 AI 或本地规则去分析。
这个方案优点是通用,理论上不需要理解游戏内部协议,只要能看到画面就能做。但是实际做下来,识图这条路没有想象中稳。
识别不稳定主要体现在几个地方:
- 游戏 UI 的牌不是标准正面图,角度、缩放、遮挡都会影响识别。
- 不同位置的牌方向不一样,自己手牌、上家、下家、对家的弃牌都要分别处理。
- 光靠模板匹配很容易把相近的牌识别错,比如条子、筒子里一些图案差别很细。
- 截图区域、窗口大小、分辨率一变,切牌逻辑就容易偏。
- 一旦识别错一张牌,后面的 AI 分析就全歪了。
所以第一版虽然能跑起来,也能看到“当前手牌”和“建议出牌”,但我越来越觉得:麻将这种东西对数据准确性要求太高,识图只要偶尔错一张,用户就会失去信任。学习助手最关键的是“牌面数据得准”,否则 AI 分析得再认真也没意义。
第二版:逆向小程序 + 抓包拿数据
后来我换了思路:既然画面是游戏渲染出来的,那游戏客户端内部肯定早就拿到了准确的牌局数据。与其让程序从图片里猜,不如想办法从游戏数据源头拿。
于是我找到了微信小程序的包,开始看里面的代码和协议逻辑。这个目录就是 C:\Users\Administrator\Desktop\mjgame,里面有微信小游戏相关的 JS 资源,也有后面整理出来的 ws_mahjong_ai 工具。
这版的核心思路变成了:
- 用
mitmproxy/mitmdump抓微信小程序的 HTTP 和 WebSocket 流量。 - 把 WebSocket 二进制帧保存成 JSONL 日志。
- 解析游戏自定义二进制协议。
- 根据消息 ID 还原当前牌局状态。
- 得到准确的手牌、弃牌、碰杠、摸牌、出牌、听牌提示等信息。
- 再把结构化数据交给 AI 分析。
这一版里比较关键的技术点是 ws_mahjong_ai:
capture_addon.py是 mitmproxy 插件,用来捕获 HTTP 请求、WebSocket 握手和 WebSocket 消息。capture.py负责启动mitmdump,支持local、mixed、regular模式。protocol.py解析包头、消息长度、消息 ID、扩展头、压缩标记,并处理 zlib 解压。mahjong_codec.py把协议里的牌字节转换成1m、5p、7s、5z这种标准麻将牌编码。state.py根据服务端和客户端消息重建牌桌状态。pipeline.py把抓包事件串起来,生成当前局面的快照,然后触发 AI 分析。gui.py用 Tkinter 做了一个实时界面,可以看 WebSocket 帧、协议详情、当前牌局状态和 AI 建议。
这条路线比截图识别更“硬核”,但效果也明显更好。因为它不是猜图片,而是直接从游戏通信数据里拿牌。
项目里已经支持了一些关键消息,比如:
30当前牌局状态同步34开局广播36摸牌广播37出牌请求38出牌广播40动作结果广播57胡牌提示58听牌提示107打哪张听什么
这些消息一旦解析出来,程序就能知道:我是谁、庄家是谁、我的手牌是什么、其他玩家打过什么、场上出现过哪些牌、当前是否听牌、打哪张能听哪些牌。这样 AI 拿到的数据就非常干净,不再依赖图像识别的运气。
AI 分析部分
不管是截图版还是抓包版,最终目标都是一样的:把牌局数据整理成 AI 能理解的上下文。
一个比较理想的输入大概包括:
- 当前手牌
- 自己已经打出的牌
- 对手打出的牌
- 场上已经可见的牌
- 每张牌理论剩余数量
- 当前是否听牌
- 可胡哪些牌
- 打哪张能听什么
- 最近一次动作,比如摸牌、出牌、碰杠、开局同步
AI 的任务不是玄学地说“打这张好”,而是要给出理由:比如这张牌价值低、打出后向听数更好、有效进张更多、保留某个搭子更合理,或者这张牌已经见过很多张,风险相对低。
本地规则 AI 可以做基础牌效判断,比如向听数、有效进张、剩余牌统计。远程大模型则更适合把这些信息组织成更像人的解释,告诉用户为什么这么打。对我来说,这个项目真正有意思的地方就在这里:它不是只给一个答案,而是能把打牌思路讲出来。
两条路线的对比
截图识别路线像是“人眼视角”:看到什么就识别什么。它比较直观,也比较通用,但是准确率受画面影响很大。
抓包解析路线像是“游戏数据视角”:直接读取游戏客户端和服务器之间传输的数据。它准确、稳定、信息更完整,但前提是要能理解协议,还要处理抓包、证书、WebSocket、压缩、二进制结构这些问题。
做完之后我的感受是:如果只是做个演示,截图识别已经挺酷了;但如果真想做一个可靠的麻将学习助手,抓包解析才是更靠谱的方向。因为麻将分析非常依赖准确数据,牌错一张,结论就可能完全不一样。
现在这个项目的意义
这个项目对我来说不只是“写了个麻将外挂”这么简单,我更愿意把它看成一个学习工具。起因是想少输点钱,哈哈,但做着做着,它变成了一个很完整的小系统:
- 从 Windows 截图到 OpenCV 图像识别。
- 从微信小程序包到协议逆向。
- 从 mitmproxy 抓包到 WebSocket 二进制解析。
- 从牌局状态重建到 AI 出牌建议。
- 从本地牌理规则到大模型解释。
它把很多技术点串在了一起,而且每一步都有明确目的:不是为了炫技,而是为了解决“怎么准确知道当前牌局”和“怎么把打牌思路讲明白”这两个问题。
后面如果继续做,我觉得重点应该放在三件事上:
- 把协议解析继续补全,让更多玩法、更多消息都能稳定还原。
- 把 AI 分析从“推荐一张牌”升级成“解释一套思路”,让用户真的能学到东西。
- 加一个复盘模式,把每一局的关键选择记录下来,打完之后再看哪里打得好、哪里打得亏。
最开始只是同事喊打麻将,结果一路从截图识别做到抓包逆向,也算是挺有意思的一段折腾。至少下次再输几十块的时候,我可以安慰自己:这钱也算交给技术调研了。