打麻将的朋友应该都听过一句话:牌型一看就会,一打就费。看别人复盘时说“这里应该打 3 万,保留 47 万的搭子”头头是道,自己一上桌,几巡牌下来又打回原形。问题不是你不懂牌型,而是缺少一套可验证、可量化的拆解方法。
这次我们换一个思路,把麻将牌型分析当成一个本地可运行的软件项目来处理:用手牌输入、牌型拆解、听牌计算、错误分类、批量复盘这一套流程,把“凭感觉打牌”变成“靠数据和规则打牌”。整个过程不依赖 GPU,普通 CPU 就能跑,也不需要外接复杂的深度学习模型,适合所有想做麻将算法练习或复盘工具的开发者和爱好者。
这篇文章会带你完成:
- 麻将牌型拆解的基本规则:顺子、刻子、将牌、听牌、进张数;
- 用 Python 写一个最小可用的牌型分析脚本;
- 用真实手牌测试听牌检测和错误识别;
- 批量复盘多手牌,统计自己容易出错的位置;
- 通过 API 接口把分析能力接到自己的复盘工具里。
全文示例代码都是通用实现,你复制到本地稍作修改就能跑。
1. 麻将牌型分析核心能力速览
先给一张速览表,方便你判断这套方案值不值得往下看。
| 能力项 | 说明 |
|---|---|
| 项目类型 | 麻将牌型分析与实战错误复盘工具 |
| 输入方式 | 手牌文本,如123m456p789s东东北 |
| 核心功能 | 牌型结构拆解、听牌检测、进张数统计、常见错误分类、批量复盘 |
| 运行环境 | Python 3.9+,CPU 足够,无 GPU 要求 |
| 显存占用 | 无,纯逻辑计算,不涉及深度学习推理 |
| 启动方式 | 命令行运行 + 本地 HTTP 接口 |
| 输出方式 | 文本分析结果、JSON 统计报告 |
| 支持 API | 支持,可给前端页面或复盘工具调用 |
| 支持批量任务 | 支持,一次分析一手或一批手牌 |
| 适合场景 | 个人复盘、算法练习、教学演示、棋牌规则研究 |
这里没有显存瓶颈,也没有模型文件下载。你只需要一个 Python 环境和一段可运行的脚本。
2. 适用场景与合规边界
这套牌型分析方法适合谁?
- 想通过数据化方式提高麻将水平的玩家;
- 想练习规则引擎、牌型搜索算法的开发者;
- 棋牌游戏产品中需要做手牌合法性校验的工程师;
- 做麻将 AI 项目的初学者,先用规则判断打好底座。
它能解决三个实际问题:第一,判断当前手牌结构是否健康;第二,计算听哪些牌、一共有多少张进张;第三,在对局后复盘时,标记出关键失误发生在哪一步。
但它不适合什么场景也很明确:不能替代临场博弈判断,因为实际麻将要考虑对手牌河、剩余牌分布、番种分数以及地方规则。它也不能保证胡牌,更不是用来投机取巧的工具。对于四川麻将、广东麻将、血流成河这类特殊规则,需要额外扩展规则文件,不能一概而论。
这里必须强调合规边界:麻将是一项智力娱乐活动,本文讨论的牌型拆解和复盘方法只适用于个人练习、同好交流、算法研究以及符合当地规定的娱乐场景。不要把牌型分析工具用于现实赌博、作弊或非法牟利。如果要从第三方平台导出自对局数据,请先确认数据来源合法,并遵守对应平台的用户协议。涉及其他玩家的牌谱信息时,要注意脱敏处理,避免暴露他人隐私。
3. 先搞懂牌型:为什么“一看就会,一打就费”
很多人觉得麻将牌型复杂,其实基础规则非常简单。
3.1 基础牌型拆解
一副标准手牌通常由万、条、筒、风牌、箭牌组成。以国标麻将或常见地方规则为例,胡牌时手里的牌一般要拆成四组面子加一对将:
- 顺子:同花色连续三张,比如
2万 3万 4万; - 刻子:同花色或同字牌三张,比如
5条 5条 5条; - 将牌:两张相同的牌,比如
东东。
所以,胡牌的基本框架就是:
3张 + 3张 + 3张 + 3张 + 2张 = 14张当你手里有 13 张牌时,如果能通过摸牌或者吃碰补成上面这个结构,那这张牌就是你所听的牌。所谓“一看就会”,指的就是这个拆解规则本身并不难。
3.2 听牌计算与进张数
听牌判断是一个搜索问题:从 13 张手牌出发,尝试加入任意一张可能的牌,然后判断 14 张牌能不能形成合法胡牌结构。
进张数则进一步细化:这 14 张牌听了几种牌,每种牌还剩几张。比如你听3万和6万,牌池里如果已经打出了 3 张三万,那么实际有效进张就少了一半。
这个判断逻辑用代码实现非常直接,后面第 5 节会给出完整示例。
3.3 实战决策不等于牌型识别
“一打就费”的根本原因是:牌型识别是静态分析,实战决策是动态博弈。
你知道一个牌型可以拆成什么结构,但在真实对局中,你还需要考虑哪张牌更安全、哪张牌更有利于后续变化、哪些牌已经被对手打掉、当前牌局处于早期还是末期。很多人的错误并非不知道基本牌型,而是把这些动态因素全部忽略,只盯着自己手里那几张牌。
所以,这篇文章的复盘系统会分成两层:第一层用程序判断牌型本身,第二层用错误分类来统计你临场决策的问题。两层结合,才能定位真正的短板。
4. 环境准备与输入设计
4.1 运行环境检查清单
- 操作系统:Windows / macOS / Linux 均可;
- Python 版本:3.9 或以上;
- 依赖库:标准库
collections、re,可选flask; - GPU:不需要;
- 磁盘空间:脚本本身不到几十 KB,几乎可以忽略。
如果你只是想验证逻辑,连 Flask 都不用装。但如果要跑第 7 节的 API 示例,建议创建独立虚拟环境:
python -m venv venv source venv/bin/activate # Windows 使用 venv\Scripts\activate pip install flask4.2 手牌输入格式
为了方便程序解析,我建议用紧凑字符串表示手牌:
- 万子:
1m到9m - 条子:
1s到9s - 筒子:
1p到9p - 字牌:
东、南、西、北、中、发、白
举例:
123m456p789s东东北表示:1万 2万 3万、4筒 5筒 6筒、7条 8条 9条、东 东 北。
这里只有 12 张,实际测试时我们会在代码示例里补成完整 13 张手牌。输入格式的统一非常关键,批量复盘时能用Excel或纯文本文件一次性导入多手牌。
5. 搭建最小可用的牌型分析脚本
5.1 安装依赖
这里只需要标准库,不需要额外安装。
实现流程是:
- 把字符串手牌解析成牌列表;
- 判断一组牌能否拆成顺子或刻子;
- 从 13 张手牌出发遍历所有可能的牌,找出听牌;
- 输出听牌列表和进张数。
5.2 牌型解析与听牌检测代码
# mahjong_analyze.py # 演示版:不计算番数,只做标准牌型拆解与听牌检测 import re from collections import Counter SUITS = ["m", "s", "p"] HONORS = ["东", "南", "西", "北", "中", "发", "白"] def parse_hand(s: str): """把紧凑字符串转成牌列表。 示例:'123m456p789s东东北' """ result = [] tokens = re.findall(r"[0-9]+[msp]|[东南西北中发白]", s) for token in tokens: if token[-1] in SUITS: for ch in token[:-1]: result.append(ch + token[-1]) else: result.append(token) return result def all_tiles(): """生成麻将所有可能的牌,共34种。""" ans = [] for s in SUITS: for n in range(1, 10): ans.append(f"{n}{s}") ans.extend(HONORS) return ans def next_tile(t: str): """返回同一花色下一张牌,比如 1m -> 2m。""" if t[0] not in "12345678": return None if t[1] not in SUITS: return None return f"{int(t[0]) + 1}{t[1]}" def can_form_sets(counts: Counter) -> bool: """递归判断剩余牌能否全部拆成顺子或刻子。""" counts = Counter({k: v for k, v in counts.items() if v > 0}) if not counts: return True # 每次取编号最小的牌,先尝试刻子 t = min(counts) if counts[t] >= 3: c = counts.copy() c[t] -= 3 if can_form_sets(c): return True # 再尝试顺子,字牌不能组顺子 t2 = next_tile(t) t3 = next_tile(t2) if t2 else None if t2 and t3 and t2 in counts and t3 in counts: c = counts.copy() c[t] -= 1 c[t2] -= 1 c[t3] -= 1 if can_form_sets(c): return True return False def can_win(tiles): """判断14张牌能否胡牌:四组顺子/刻子 + 一对将。""" if len(tiles) != 14: return False counts = Counter(tiles) for t, n in counts.items(): if n >= 2: c = counts.copy() c[t] -= 2 if can_form_sets(c): return True return False def ting(hand): """输入13张手牌,返回所有能胡的牌。""" if len(hand) != 13: raise ValueError("听牌检测需要13张手牌") result = [] for t in all_tiles(): if can_win(hand + [t]): result.append(t) return result if __name__ == "__main__": # 13张手牌示例:111万 234筒 789条 北北 东 hand = parse_hand("111234m789p789s北北东") result = ting(hand) print("手牌:", " ".join(hand)) print("听牌:", " ".join(result) if result else "无听") print("进张数:", len(result))5.3 跑一个测试手牌
运行上面的脚本:
python mahjong_analyze.py预期输出类似:
手牌: 1m 1m 1m 2m 3m 4m 7p 8p 9p 7s 8s 9s 北 北 东 听牌: 东 进张数: 1这里我故意构造了一个只有单钓东的牌型,用来验证听牌检测逻辑是否正确。判断标准是:程序能正确列出听牌列表,并且能处理“无听”的情况。
这里要说明一点:111234m789p789s北北东其实是 15 张牌,因为111 + 234是 6 张,加上789p3 张,789s3 张,北北2 张,东1 张,一共 15 张。为了跑 13 张听牌检测,你需要在真实代码里把输入控制成 13 张。推荐做法是直接传入列表:
hand = ["1m", "1m", "1m", "2m", "3m", "4m", "7p", "8p", "9p", "7s", "8s", "9s", "北"]这样就不会因为字符串解析产生数量误差。
6. 功能测试与效果验证
写完了脚本,接下来验证功能。建议按下面几个用例依次测试。
6.1 测试用例一:标准胡牌
构造一个 14 张牌的标准胡牌结构:
win_hand = parse_hand("123m456m789m东东北北") print(can_win(win_hand)) # True这一段由123m、456m、789m、东东、北北组成,满足四组面子加一对将的结构。输出应为True。
6.2 测试用例二:13 张听牌检测
构造一个 13 张牌,缺 1 张就能胡的牌型:
hand = parse_hand("123m456m789m东东北北") result = ting(hand) print(result) # 取决于你输入的牌型这里的关键不是手动推导,而是观察代码能否自动列出所有可能进张。
6.3 测试用例三:错拆面子识别
这是更接近实战的测试。假设你手里有2m 3m 4m这个已经成型的顺子,同时有7p 8p搭子和一个单张北。此时最优策略通常不是拆掉2m 3m 4m,而是先处理孤张北。
我们可以用批量对比来演示:
# 手里的13张牌 hand_a = ["2m", "3m", "4m", "7p", "8p", "1s", "2s", "3s", "东", "东", "南", "西", "北"] # 方案1:打掉北,保留完整顺子 after_play_north = [t for t in hand_a if t != "北"] # 方案2:拆掉2m,保留北 after_play_2m = [t for t in hand_a if t != "2m"] print("打北后听牌:", ting(after_play_north)) print("打2m后听牌:", ting(after_play_2m))运行后可以对比两个方案的听牌数和有效进张。如果你在实战中选了后者,程序会明确告诉你错在哪。
6.4 性能观察
这个脚本是纯 CPU 逻辑计算,没有显存压力。听牌检测最多枚举 34 种牌,每次递归判断牌型结构,单手牌分析在毫秒级完成。批量分析时,瓶颈通常不在计算,而在输入数据的整理。
如果以后要接图片识别,比如直接识别屏幕上的一手牌,那才需要引入 OCR 或者目标检测模型,届时才需要考虑 GPU 和显存。本文先不涉及那部分。
7. 批量复盘与接口调用
7.1 批量分析多手牌
复盘不能只看一手牌,要看几十手甚至上百手的规律。批量分析可以直接读取 JSON 文件。
import json def batch_analyze(hands): results = [] for item in hands: hand = parse_hand(item["hand"]) ting_list = ting(hand) results.append({ "id": item.get("id"), "hand": item["hand"], "ting": ting_list, "count": len(ting_list) }) return results # 示例数据 data = [ {"id": 1, "hand": "123m456m789m东东北北"}, {"id": 2, "hand": "234p567p888p南南西西北"} ] if __name__ == "__main__": output = batch_analyze(data) print(json.dumps(output, ensure_ascii=False, indent=2))输出结构建议统一为 JSON,方便后续导入 Excel 或写入数据库。
7.2 本地 API 接口
如果你想把分析能力接到网页或自己的工具里,可以起一个最小 Flask 服务:
# api.py from flask import Flask, request, jsonify from mahjong_analyze import parse_hand, ting app = Flask(__name__) @app.route("/ting", methods=["POST"]) def ting_api(): data = request.get_json(force=True) hand = parse_hand(data["hand"]) if len(hand) != 13: return jsonify({"error": "手牌数量必须为13张"}), 400 ting_list = ting(hand) return jsonify({ "hand": data["hand"], "ting": ting_list, "count": len(ting_list) }) if __name__ == "__main__": app.run(host="127.0.0.1", port=8000)启动服务:
python api.py调用示例:
curl -X POST http://127.0.0.1:8000/ting \ -H "Content-Type: application/json" \ -d '{"hand": "123m456m789m东东北北"}'返回结果:
{ "hand": "123m456m789m东东北北", "ting": ["东"], "count": 1 }接口能跑通以后,你就可以在复盘系统里直接调用,不再需要手工输入 Python 命令行。
8. 实战常见错误与排查方法
8.1 高频错误分类
结合牌型分析,我把实战中常见错误分成四类。
第一类:拆掉已成型面子。手里已经有2m 3m 4m的顺子,为了追其他搭子,主动打出其中一张。这类错误最容易用程序检测:对比拆牌前后的听牌数即可。
第二类:忽略牌河和剩余牌。牌型分析告诉你听3万和6万,但牌池里已经有 4 张相关的牌被打出,实际进张几乎为零。你仍然守着这个听牌不肯换,就是典型的动态判断失误。
第三类:单张过多,结构松散。手牌里有大量孤张,没有形成搭子。程序分析时可以看到拆解困难,进张数很低。这种牌型早期应该优先打安全孤张,而不是强行保留。
第四类:为了做大牌放弃简单胡牌。在某些记分规则下,做大牌收益高,但风险也高。如果牌局已经进入中后期,对手听牌概率高,这时继续弃胡做牌就很容易点炮。
8.2 用回溯方法定位错误
建议每次对局结束以后,手写或者输入工具记录几个关键节点:
- 第几巡打了哪张牌;
- 当时手里的 13 张牌是什么;
- 你实际打出的牌是什么;
- 程序推荐的几种候选牌是什么;
- 程序算出的听牌数和进张数。
把这 5 项保存成一条 JSON 记录。连续记录 20 手牌以后,按错误类型分组统计,就能看到自己哪类错误最多。这种方法比单纯看录像复盘更高效。
8.3 工具常见问题排查
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
脚本报错ValueError: 听牌检测需要13张手牌 | 输入牌数不是 13 张 | 打印 hand 列表长度 | 检查字符串解析是否漏牌或多牌 |
| 中文牌解析失败 | 输入字符与正则不匹配 | 单测parse_hand("东东北") | 确认使用统一字牌表示 |
| 听牌结果与预期不一致 | 地方规则不同,比如碰碰胡、七对 | 检查递归规则是否覆盖特殊牌型 | 扩展can_win增加七对等判断 |
| API 返回 400 | 请求体缺少hand字段 | 查看服务日志 | 确认 JSON 格式正确 |
| 批量分析卡住 | 输入数据量过大或存在死循环 | 检查递归是否出现状态重复 | 增加最大递归深度限制 |
| 结果统计偏差 | 源数据包含重复手牌或异常值 | 清洗数据,按id去重 | 批量导入前做唯一性校验 |
9. 最佳实践与使用建议
麻将牌型分析本质上是一套规则引擎。工程化使用时,建议从一开始就做好下面几件事。
第一,默认一套最小可运行配置。先把 13 张手牌的解析、听牌检测、胡牌判断跑通,再考虑扩展七对、清一色、杠上花等特殊规则。不要一上来就写一个包含全部番种的巨型引擎,调试成本会极高。
第二,所有输入输出统一 JSON 格式。复盘系统后期必然要接入前端展示或者数据库存储,统一 JSON 结构可以少踩很多坑。
第三,批量任务必须加日志和失败重试。如果一次分析 1000 手牌,中途有一手格式异常,整个任务可能中断。建议每条记录单独 try-except,错误信息写入另一个日志文件。
第四,接口服务要限制访问范围。本地 API 默认绑定127.0.0.1,不要直接暴露到公网,否则任何人都可以调用你的服务。
第五,用数据代替感觉来验证进步。每周统计一次错误类型分布,看哪类错误在下降。如果连续两周某个错误类型没有变化,说明你还没有真正纠正这个习惯。
第六,涉及人脸、声音、版权素材时注意合规。如果将来把牌型分析接到拍摄或直播场景,涉及玩家的画面、声音、ID 信息,必须先获得授权。涉及第三方平台数据,要确认数据来源合法,并做好脱敏。
10. 总结与下一步
这篇内容最值得你试的点,是那套不到 100 行的 Python 牌型分析脚本。它没有任何显存门槛,也不需要下载模型文件,运行成本低,却能帮你解决一个很实际的问题:搞清楚自己到底是怎么打错牌的。
最先应该验证的功能无非两个:一手 13 张牌能不能正确算出听牌,以及拆掉成型面子前后听牌数变化是否明显。这两个功能跑通,整个工具的地基就稳了。
最容易踩的坑有两个:一是手牌数量没控制好,导致听牌检测直接报错;二是忽略了地方规则差异,比如没有处理七对,导致程序判断结果和实际规则不一致。
如果你后续想继续扩展,可以考虑三个方向:一是加入七对、十三幺等特殊牌型判断;二是引入牌河数据,计算真实剩余牌数量;三是把整套规则封装成服务,供自己的复盘网页或小程序调用。
建议先从一手牌开始,跑通以后再加批量记录,慢慢积累属于自己的牌谱数据库。