快速非递归麻将和牌判定算法

一种不使用递归的用于判定麻将和牌的简单算法,同时可将能够和牌的麻将牌型拆分为面子以便于计算番数。也可以实装于人脑进行和牌判定

如无特殊说明,本文所述麻将规则为日本麻将。(用括号注明一些中国麻将中对应的说法)

定义

麻将牌

在日本麻将中共使用 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/s0~69~1518~24。对于其它牌出现 1 或 2 张的情况,应直接返回未和牌。

另外,对于这些牌出现 3 或 4 张的情况,虽然也有 “三个相同顺子的首张” 的可能,但计算时基于高点法的原则,应拆分成点数更高的三个刻子。(因为此时手牌不可能组成 “三色同顺” 或 “一气通贯” 这两个需要顺子的役种,只有可能组成 “一杯口” 和 “平和”(最多共 2 番);但以刻子计算则必有 “三暗刻”(2 番),且刻子符数高于顺子)

遍历时为简化判断过程,当判定刻子时对 map 数据不作处理,当判定顺子时直接将 map[i+1]map[i+2] 减去顺子的数量(12)。当牌型未能和牌时,缺少牌的数量将减为负数,因此遍历到负数时则直接判定为未和牌。

map 经过了一次完整的遍历,则可判定为和牌。

Python 实现

One thought on “快速非递归麻将和牌判定算法

  1. 看了好几遍才看懂,幸好你没给代码,不然我不会逼自己写出来
    初级版本,只判定门清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

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

WC Captcha + twenty = twenty two