麻将实战中,最常见的牌型问题不是不认识牌,而是“看一眼就懂,一上场就错”。很多玩家在复盘时看到高手拆掉一个看似不错的搭子,或者听一张冷门牌,会觉得自己也会这么打;真正轮到自己出牌,却常常在几个选项之间犹豫,最后凭感觉打出错误的牌。“麻将实战牌型一看就会,一打就费”说的就是这种落差。
问题不在于记忆力,而在于判断标准。看牌型只需要回答“现在听起来是哪几张”,打牌却要回答“下一步怎么做才更接近和牌”。这两个问题看着是一回事,实际上需要完全不同的工具:一个是牌型识别,一个是决策计算。全文会用牌效率的概念说明为什么看会了还会打错,并用一个只依赖 Python 标准库的听牌判断器,把听牌范围、剩余枚数这些关键指标算出来,再用结果复盘常见错误,最后给出一张出牌前可以逐项对照的检查清单。
1. 为什么“一看就会,一打就费”?
1.1 看懂牌型不等于会做取舍
牌型识别只回答“现在是什么状态”,决策要回答“现在该做什么”。比如看到 123456789m 3455p,你可以一眼看出这手牌听 2p 和 5p;但出牌前真正要回答的问题是:上一手应该打 3p 还是 6p?这个选择需要比较进张、剩余枚数、安全度和对手行为。
“一看就会”通常停留在第一层:能认出牌长什么样,能说出听哪些牌。“一打就费”则发生在第二层:面对两个以上选项时,没有稳定的标准来决定选哪个。而第二层能力并不是靠多看牌型就能练出来的,它需要一个可重复的计算框架。
用一句通俗的话说:知道这手牌听什么,是“会读”;知道为什么这么打,是“会打”。大多数玩家缺的是后者。
1.2 牌型判断的四个可计算指标
要减少“凭感觉”,先把牌型判断拆成可计算的指标。下面四个指标在实战中经常被混为一谈,但它们解决的是不同问题。
| 指标 | 含义 | 计算方式 | 新手误区 |
|---|---|---|---|
| 向听数 | 距离和牌还差几次有效进张 | 当前手牌距离“4 组面子加 1 对将牌”的差距 | 只看有没有听,不看差几手 |
| 有效牌 | 摸到后能让向听数减少的牌 | 枚举下一巡可能摸到的牌,计算对向听的影响 | 把任何可能的牌都算成进张 |
| 听牌种类 | 当前听哪几张牌 | 枚举 34 种牌,加入后能和牌的就是听牌 | 只会数种类,不数剩余张数 |
| 剩余枚数 | 某张牌在牌山中还可能存在的数量 | 从 4 张总数中减去手牌、河牌、副露和可见牌 | 只看自己手里有没有,不看场上 |
这四个指标的意义在于,把“感觉这手牌不错”换成“这手牌有 6 个进张,其中 2 个已经出现在河牌”。用数字代替直觉,才能避免每次都用同一个错误理由说服自己。
1.3 为什么知道指标还会打错
知道指标只完成了第一步,第二步是在真实牌局里快速获取可见信息,并给候选项排序。信息不全、时间压力、情绪波动都会让判断退化。新手最常犯的错,是在信息不足时过早做决定,比如序盘就固定单钓一张,或者明明还能拆搭子,却先打了安全牌。
“一看就会,一打就费”的另一个原因,是复盘时只核对结果,不核对过程。胡了一手牌就认为打法对,没胡就认为运气差,这样的复盘不会积累经验。正确做法是把每一手牌都看作一个决策样本:当时的可见牌有哪些,可选方案有哪些,最终选了什么,为什么这么选。
2. 先准备一个可以运行的牌型判断环境
2.1 教学规则和简化约定
为了把牌型判断变成可运行代码,先把规则做简化:只讨论普通和牌型,也就是 4 组面子加 1 对将牌。牌种只保留万、筒、条三类,去掉字牌;不处理七对、国士、役种判定。这种简化不影响本篇讨论的核心:进张和听口判断。
如果你是打日麻、川麻或国标麻,规则细节会有差异,但听牌判断和牌效率的基础逻辑一致。在这个环境下跑通的代码,后续可以根据你的规则扩展。
2.2 Python 环境要求
只需要 Python 3.6 以上版本,不用安装任何第三方库。新建一个mahjong_helper.py文件,把下面的代码放进去,在命令行运行即可。
python --version如果能正常输出 Python 版本号,环境就准备好了。使用 Jupyter Notebook、VS Code 或 PyCharm 都可以,代码不依赖特定编辑器。
2.3 手牌怎么表示
用类似123m456p789s的字符串表示手牌。字母m、p、s分别表示万、筒、条,数字表示点数。例如123m表示一万、二万、三万,45p表示四筒、五筒。
parse_tiles函数会把这样的字符串展开成m1、m2、m3这样的列表,方便后续统计和排序。
hand = parse_tiles("1112345678999m") print(hand)输出会是一串m1到m9的列表。用字符串表示的好处是直观,方便复盘时把牌谱上的手牌直接复制进来。
3. 最小听牌判断器:用代码验证牌型
3.1 判断思路
听牌判断的本质是枚举:当前手牌有 13 张,把牌山中可能存在的 34 种牌依次补进来,如果补进来后的 14 张牌能拆成 4 组面子加 1 对将牌,那么这张补进来的牌就是听牌。
实现分三层:
is_win判断 14 张牌能不能拆成标准型。can_form_all_sets判断去掉雀头后,剩余牌能不能全部组成刻子或顺子。listen枚举 34 种补牌,得到听牌列表。
注意这里说的“可能存在的 34 种牌”不包括字牌。代码只处理万、筒、条。
3.2 完整代码
# 麻将牌型判断工具(教学版) # 只处理万、筒、条,不考虑字牌、七对、国士等特殊牌型 TILE_STRINGS = [s + str(n) for s in "mps" for n in range(1, 10)] def tile_key(tile): # tile 格式:"m1"、"p3"、"s9" return (tile[0], int(tile[1:])) def parse_tiles(s): tiles = [] i = 0 while i < len(s): suit = s[i] i += 1 nums = "" while i < len(s) and s[i].isdigit(): nums += s[i] i += 1 for ch in nums: tiles.append(suit + ch) return tiles def count_tiles(tiles): d = {} for t in tiles: d[t] = d.get(t, 0) + 1 return d def can_form_all_sets(counts): counts = {k: v for k, v in counts.items() if v > 0} if not counts: return True tile = min(counts.keys(), key=tile_key) cnt = counts[tile] # 尝试刻子 if cnt >= 3: c = counts.copy() c[tile] -= 3 if can_form_all_sets(c): return True # 尝试顺子 if tile[0] in "mps" and int(tile[1:]) <= 7: num = int(tile[1:]) t1 = tile[0] + str(num + 1) t2 = tile[0] + str(num + 2) if counts.get(t1, 0) > 0 and counts.get(t2, 0) > 0: c = counts.copy() c[tile] -= 1 c[t1] -= 1 c[t2] -= 1 if can_form_all_sets(c): return True return False def is_win(tiles): if len(tiles) % 3 != 2: return False counts = count_tiles(tiles) for head in list(counts.keys()): if counts[head] < 2: continue c = counts.copy() c[head] -= 2 if can_form_all_sets(c): return True return False def listen(hand): waits = [] for tile in TILE_STRINGS: if hand.count(tile) >= 4: continue if is_win(hand + [tile]): waits.append(tile) return waits if __name__ == "__main__": hand1 = parse_tiles("1112345678999m") waits1 = listen(hand1) print("手牌1:", sorted(hand1, key=tile_key)) print("听牌:", waits1) print("各听牌剩余枚数:", {t: 4 - hand1.count(t) for t in waits1}) print("听口总数:", len(waits1)) hand2 = parse_tiles("123456789m3455p") waits2 = listen(hand2) print("手牌2:", sorted(hand2, key=tile_key)) print("听牌:", waits2) print("各听牌剩余枚数:", {t: 4 - hand2.count(t) for t in waits2}) print("听口总数:", len(waits2))这段代码的关键点在can_form_all_sets:它每次选择当前最小的一张牌,先试刻子,再试顺子。如果两条路都走不通,就返回假。is_win则先尝试每一张可能有雀头的牌,把雀头拆掉后,再检查剩余牌能否全部组成面子。
3.3 运行结果
在mahjong_helper.py所在目录运行:
python mahjong_helper.py预期输出如下:
手牌1: ['m1', 'm1', 'm1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9', 'm9', 'm9'] 听牌: ['m1', 'm2', 'm3', 'm4', 'm5', 'm6', 'm7', 'm8', 'm9'] 各听牌剩余枚数: {'m1': 1, 'm2': 3, 'm3': 3, 'm4': 3, 'm5': 3, 'm6': 3