正则表达式一般用来描述字符串,所以我们把麻将和牌的组合先变成字符串。麻将中包含以下牌面:

  • 条:幺鸡、二条、三条、四条、五条、六条、七条、八条、九条
  • 筒:一筒、二筒、三筒、四筒、五筒、六筒、七筒、八筒、九筒
  • 万:幺万、二万、三万、四万、五万、六万、七万、八万、九万
  • 其他:红中

牌的摆放顺序和搭配也很重要,能影响人的判断,决定能否及时上听、避免漏和;同样,也能够简化正则匹配问题。所以在这里假设已经按搭配成「刻、顺子、对子、杠」,顺子中以上面列表顺序从小到大依次排好。

和牌后共计 14 张,再加上明杠或暗杠 0 到 3 个,所以必然符合 正则一——张数

^(?:幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]){14,17}$

牌中必然有至少一个的刻或杠,符合 正则二——杠刻

(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3}

牌中也可能有顺子,符合 正则三——顺子

(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k))

另外还必须有一对,考虑到排序后对子前后的牌可能是顺子中的大牌或小牌,所以不做断言,只需符合 正则四——对子

(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k

局部地区中不允许清一色,应该符合 正则五——两门

(?:\[鸡条\].\*\[万筒\])|(?:.\*\[万条鸡\])|(?:.\*\[筒条鸡\])

最后,有一个非常严肃的问题,这副牌必须符合 正则六——不断幺

\[幺一九中\]

好,我们现在整体回顾一下,和牌应该:

  1. 张数符合;
  2. 整副牌符合「此处可能有对子,顺子或杠刻,此处可能有对子,顺子或杠刻,此处可能有对子,此处可能有对子,顺子或杠刻,此处可能有对子,顺子或杠刻,此处可能有对子」;
  3. 杠刻至少一个;
  4. 杠最多有三个;
  5. 对子只有一个;
  6. 最多缺一门;
  7. 不能断幺;
  8. 牌面相同的牌不得超过4张(有人会出老千出得这么明显么!)。

下面以之前的六个正则为基础,进行一些加强判断。

整副牌由被「对子」隔开的连续的「顺子、杠刻」组成,正则二三四可以归纳为 正则七——结构

^(?:(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k)?(?:(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k)))|(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k)?)+$

杠刻前后可以是首尾,或者杠刻顺子对;正则七可以进化为 正则八——必有杠刻

^(?:(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})|(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k))|(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k))\*(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})(?:(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})|(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k))|(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k))\*$

局部地区要求上听时手里至少要有四张,所以即使在可和对对碰时允许有两个相同牌面的对子,也只可能有最多三杠;前后可能是首尾或「顺子、刻、对子」的杠,允许有 0 到 3 个。3个杠时,张数17张,除去杠之外的5张牌应该由至少一个对子与一套「顺子、刻」组成,无论如何组合,都不会发生出现同样四张牌、却不是杠的情况;验证非4杠时也就不用考虑两对的情况,无需为对子再做负断言,正则有所简化;即符合 正则九——至多三杠

^(?!(?:.\*(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{3}.\*){4,}).\*$

「幺鸡幺鸡幺鸡三条四条五条五条六条七条幺万幺万幺万九万九万」不应认为包含对五条;但「幺鸡幺鸡幺鸡三条四条五条五条五条五条六条七条二万二万二万」则应算做有对五条,所以对子两侧应该是首尾或顺子杠刻,符合 正则十——仅一对

^(?:(?:(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k)))|((?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3}))\*(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k)(?:((?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k)))|((?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3}))\*$

综上,以相互独立的条件中任意一个做正则主体语句,其他条件做断言,即可构造出匹配麻将和牌的正则。例,正则十一——和牌

^(?=^(?:幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]){14,17}$)(?=(?:.\*\[鸡条\].\*\[万筒\].\*)|(?:.\*.\*\[万条鸡\].\*)|(?:.\*.\*\[筒条鸡\].\*))(?=.\*\[幺一九中\].\*)(?=^(?:(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})|(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k))|(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k))\*(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})(?:(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3})|(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k))|(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k))\*$)(?=^(?!(?:.\*(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{3}.\*){4,}).\*$)(?:(?:(?:(?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k)))|((?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3}))\*(?:(?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k)(?:((?:幺鸡二条三条|一筒二筒三筒|幺万二万三万|(?:(?\[条筒万\])\\k四\\k)|(?:(?\[条筒万\])\\k五\\k)|(?:(?\[条筒万\])\\k六\\k)|(?:(?\[条筒万\])\\k七\\k)|(?:(?\[条筒万\])\\k八\\k)|(?:(?\[条筒万\])\\k九\\k)))|((?(幺鸡|一筒|幺万|红中|\[二三四五六七八九\]\[条筒万\]))\\k{2,3}))\*)$

目测该表达式会有压缩精简的空间,但恐使其仅存不多的可读性丧失殆尽。那么有人会问了,这样只匹配了哈尔滨麻将,那么「吉津冀秦晋鄂皖沪浙湘蜀粤」各地的麻将和牌正则能不能都给写了 呢?对此,我的回答只有一个字:

格稳! : (

嗯,严肃一点,其实基于正则表达式的断言,可以像搭积木一样使用不同的组合来适应各种麻将的规则。适当的捕获分组甚至能解决番数的计算,不过这只是未验证过的猜测。