一种不使用递归的用于判定麻将和牌的简单算法,同时可将能够和牌的麻将牌型拆分为面子以便于计算番数。也可以实装于人脑进行和牌判定
如无特殊说明,本文所述麻将规则为日本麻将。(用括号注明一些中国麻将中对应的说法)
定义
麻将牌
在日本麻将中共使用 34 种麻将牌,每种 4 张,共 136 张。
本算法中将每一种麻将牌按顺序编号,其中:
0~8
为一~九万;9~17
为一~九饼(筒);18~26
为一~九索;(以上统称为 “数牌”)27~33
分别为 “东南西北白发中”。(统称为 “字牌”)
(注意,中国麻将对三元牌的排序与日本麻将不同,为 “中发白”。不过这对算法并无任何直接影响,因为三元牌之间地位等价,唯一特殊的役种 “绿一色” 所需发牌编号正好相同)
麻将牌在纯文本中以一个数字和一个字母的方式进行缩写。
- 字母代表花色(suit),其中 “万” 为
m
,“饼” 为p
,“索” 为s
,“字” 为z
。 - 数字代表序数(rank),其中字牌的序数以 “东南西北白发中” 的顺序记作
1~7
。
如 “三索” 记为 3s
,“六万” 记为 6m
,“东” 记为 1z
,“中” 记为 7z
。
(Unicode 确实有麻将牌符号,但是鉴于字体的支持有限,此处使用传统且通用的记牌方法)
可以对麻将牌序号 tileId
进行整数除法以计算花色 tileId/9
or tileId//9
(Python),进行取模以计算序数 tileId%9
。注意得到的结果比麻将牌上的数字少 1
。可以建立字符串映射:
MAHJONG_SUIT_CHAR = "mpsz"
MAHJONG_RANK_CHAR = "123456789"
手牌
麻将玩家在对局时持有的牌。
手牌未摸牌时为 13 张,从牌山中摸牌后有 14 张。如此时未和牌,则需打出一张牌至牌河。
对于一些有规律的牌型,作出以下定义:
- 两张相同的牌,称为对子(pair),如
5p5p
。本文中简称为 “5p
对子”。 - 三张相同的牌,称为刻子(triplet),如
1z1z1z
。本文中简称为 “1z
刻子”。 - 三张同花色且序数连续的牌,称为顺子(sequence),如
7m8m9m
。本文中以起首的7m
简称为 “7m
顺子”。 - 四张相同的牌,经杠操作后成为杠子,其作用与刻子相当,本文中不进行讨论。
顺子和刻子(及杠子)统称为面子(meld)。
和牌
在日本麻将中,和牌共有三类:
- 一般型:为 3+3+3+3+2 牌型,即四组面子(顺子或刻子)加一组对子组成。此时的对子称为雀头(将头)。如
4m5m6m4p5p6p6p7p8p1s1s1s2z2z
。 - 七对型:为 2+2+2+2+2+2+2 牌型,即七组对子。对应役种 “七对子”。如
2m2m7m7m4p4p5p5p9p9p2s2s5z5z
。 - 国士型:由所有的幺九牌(
1m9m1p9p1s9s1z2z3z4z5z6z7z
)组成。和牌时手牌中含有全部的幺九牌各一张(共 13 张),再加上幺九牌的任意一张。对应役种 “国士无双”(十三幺)。如1m9m1p9p1s9s1z2z3z4z5z6z7z7z
。
参考资料:星野Poteto的《日本麻将怎么玩》系列 (用颜文字当头像的都不会是坏人)
算法思路
将一副手牌转换为一个长为 34 的哈希表(数组)map[34]
中,统计各牌出现的数量。map
中元素可能的取值为 0~4
。
国士型和七对型的判断只需再对统计表进行一次统计:
- 国士型对所有的幺九牌序号进行统计,即下标为
[0,8,9,17,18,26,27,28,29,30,31,32,33]
的元素。若和牌,则有 12 个1
及 1 个2
,即[0,12,1,0,0]
。 - 七对型对所有牌进行统计。若和牌,则有 7 个
2
,即[27,0,7,0,0]
。(注意日本麻将中无 “龙七对”,即不能包含相同的对子。否则,同时还应考虑[28,0,5,0,1]
、[29,0,3,0,2]
和[30,0,1,0,3]
的情况。
一般型的判断,首先通过遍历的方法,找出可能作为雀头的牌,即数量大于等于 2 的牌。手牌中最多可能含有 7 个数量为 2 牌(考虑不按照七对计算的 “二杯口” 役种),因此下面的操作最多会进行七次。
将手牌撤去选定的雀头进行检验:按照牌序号对 34 种牌的数量进行一次遍历。若手牌能够和牌且撤去了正确的雀头,那么剩下的所有牌均是顺子或刻子的一部分。对于特定某一种牌的数量,如果剩余:
- 0 张:直接跳过;
- 1 张:必为一个顺子的首张。
- 2 张:必为两个相同顺子的首张。(可考虑役种 “一杯口”)
- 3 张:必为刻子。
- 4 张:必为一个刻子加上一个顺子的首张。
其中,“顺子首张” 只适用于 1~7 m/p/s
即 0~6
、9~15
和 18~24
。对于其它牌出现 1 或 2 张的情况,应直接返回未和牌。
另外,对于这些牌出现 3 或 4 张的情况,虽然也有 “三个相同顺子的首张” 的可能,但计算时基于高点法的原则,应拆分成点数更高的三个刻子。(因为此时手牌不可能组成 “三色同顺” 或 “一气通贯” 这两个需要顺子的役种,只有可能组成 “一杯口” 和 “平和”(最多共 2 番);但以刻子计算则必有 “三暗刻”(2 番),且刻子符数高于顺子)
遍历时为简化判断过程,当判定刻子时对 map
数据不作处理,当判定顺子时直接将 map[i+1]
和 map[i+2]
减去顺子的数量(1
或 2
)。当牌型未能和牌时,缺少牌的数量将减为负数,因此遍历到负数时则直接判定为未和牌。
若 map
经过了一次完整的遍历,则可判定为和牌。
Python 实现
咕
看了好几遍才看懂,幸好你没给代码,不然我不会逼自己写出来
初级版本,只判定门清14张,不考虑鸣牌和开杠
def form_agari(maps):
”’人远之代码https://doc.cpk.moe/mahjong-agari/接收34个元素的maps列表[0,1,1,…,3,4,0,0]输出bool”’
”’十三幺九胡牌”’
shisanyaojiu = maps[0] and maps[8] and maps[9] and maps[17] and maps[18] \
and maps[26] and maps[27] and maps[28] and maps[29] and maps[30] \
and maps[31] and maps[32] and maps[33] \
and (maps[0] ==2 or maps[8] == 2 or maps[9] == 2 or maps[17] == 2 \
or maps[18] == 2 or maps[26] == 2 or maps[27] == 2 or maps[28] == 2 \
or maps[29] == 2 or maps[30] == 2 or maps[31] == 2 or maps[32] == 2\
or maps[33] == 2 )
if shisanyaojiu:
return True
”’七对胡牌包括龙七对”’
qidui = 1
for num in maps:
if num % 2 != 0:
qidui = 0
if qidui:
return True
”’普通形胡牌,没有将牌输出false。对将牌进行遍历,去除顺子和刻子,如胡牌最终列表全为0”’
duizi = []
for i in range(34):
if maps[i] >= 2:
duizi.append(i)
if not duizi:
return False
for i in duizi:
maps1 = maps.copy()
maps1[i] += -2
for j in range(34):
if (j <=6) or (9<=j<=15) or (18<=j<=24):
if maps1[j] == 1:
maps1[j] += -1
maps1[j+1] += -1
maps1[j+2] += -1
elif maps1[j] == 2:
maps1[j] += -2
maps1[j+1] += -2
maps1[j+2] += -2
elif maps1[j] == 3:
maps1[j] += -3
elif maps1[j] == 4:
maps1[j] += -4
maps1[j+1] += -1
maps1[j+2] += -1
elif maps1[j] == 3:
maps1[j] += -3
else:
pass
if maps1 ==[0]*34:
return True
return False