本次作业的目标是使用 TypeScript 编写一个简化的立直麻将和牌判断 & 符数计算函数。
立直麻将一共使用万、饼、索(条)三种花色的数字牌和七种字牌,每一个花色的数字牌有一到九共计九种。数字牌的称呼为数字加上相应花色,如三饼、九万等,七种字牌则分别为东南西北白发中。
作为简记,万、饼、索一般分别写为 m、p、s,所以三饼可以简记为 3p,九万可以简记为 9m。七种字牌按照东南西北白发中的顺序简记为 1z 到 7z。此外,记录多张牌时,往往按照万、饼、索、字牌的顺序从左向右记录,同花色的数字牌内部按照数字增序记录,花色的标记(m、p、s、z)仅需写一次。例如手中持有下述牌的时候:
- 一万、二万、五万、五万、九万、六饼、六饼、七饼、一索、东、东、白、白、中
手牌可以写为:
- 12559m667p1s11557z
在不考虑赤牌的条件下,立直麻将一共有
本次作业中你只需要考虑判定最后的手牌是否和牌,不需要考虑游戏流程(摸风、开牌、鸣牌、立直、流局、荒牌等)。在不考虑任何鸣牌(即吃碰杠)的情况下,麻将中宣告和牌时,手中需要有 14 张牌。在不考虑七对子与国士无双的条件下,合法的和牌必须能够将 14 张牌按照至少一种方式拆解为 4 个面子和 1 个雀头。
面子是刻子和顺子的统称。刻字指的是 3 张一样的牌,如 333p、777z。顺子则指的是同花色的、数字相连的数字牌(字牌无法构成顺子),如 123p、789s。面子都是三张牌构成的组合。
雀头指的是 2 张一样的牌,如 11p、66z。雀头都是两张牌构成的组合。
4 个面子和 1 个雀头恰好合计为
【例 1】手牌 122334m12344p333z 可以拆解为四个面子 123m、234m、123p、333z 和一个雀头 44p,所以是合法的和牌
【例 2】手牌 122334m123344p33z 无法拆解为四个面子和一个雀头,不是合法的和牌
【例 3】手牌 111222333m123p33z 可以拆解为四个面子 111m、222m、333m、123p 和一个雀头 33z,是合法的和牌。但这一副手牌具有另外的拆解方式,即 123m、123m、123m、123p 和 33z
当和牌后,需要计算和牌得分,而计算得分的一步是计算符数。这里忽略和牌方式、杠的差别,统一在门前清自摸、非坎边吊和牌、东场南家的基础上介绍本次作业中使用的符数计算规则。
定义 1m、9m、1p、9p、1s、9s 和所有字牌共计 13 种牌称为幺九牌,将 1z、2z、5z、6z、7z 共计 5 种牌称为役牌。符数计算规则为:
- 底符 20 符
- 每当手牌中有一个幺九牌构成的刻子,加计 8 符
- 每当手牌中有一个非幺九牌构成的刻子,加计 4 符
- 若雀头是役牌,加计 2 符
- 若手牌有多种拆解方式,取最后计算出的符数较高的拆解方式(注意,本规则与标准规则不一致)
【例】手牌 111222333m123p33z 按照 111m、222m、333m、123p、33z 拆解,符数为
该文件中为你定义了可以使用的一些类型。
Tile
枚举类型定义了所有种类的麻将牌并赋值为一个整数,其中一到九万为 1 到 9,一到九饼为 11 到 19,一到九索为 21 到 29,七种字牌分别为 31、33、35、37、39、41、43。
这样设计的原因为,构成顺子的三种牌必然是在数字上相连的。可以注意到九万和一饼分别为 9、11,九饼和一索分别为 19、21 并且字牌内部的间隔都是 2,保证了不同花色牌不相连和字牌不相连。
此外数组 YAOCYU, YAKUHAI
已经帮你写好了幺九牌和役牌的种类。
表示手牌的类型 Hand
是牌组 Block
的数组,牌组 Block
定义为若干种类相同的牌构成的组。牌组 Block
使用 base
字段记录这一组牌的种类,使用 count
字段记录这一组牌的数量。
这样,手牌 111222333m123p33z 应当表示为:
[
{ base: Tile.M1, count: 3 },
{ base: Tile.M2, count: 3 },
{ base: Tile.M3, count: 3 },
{ base: Tile.P1, count: 1 },
{ base: Tile.P2, count: 1 },
{ base: Tile.P3, count: 1 },
{ base: Tile.Z3, count: 2 },
]
之后定义了表示面子的接口 Mentsu
。其中 base
字段表示该面子的代表牌,type
字段表示该面子的种类,type="Shuntsu"
表示为顺子,type="Kotsu"
表示为刻子。刻子的代表牌的定义是显然的,顺子的代表牌定义为其中数字最小的牌。
这样,刻子 333p 应当表示为:
{ base: Tile.P3, type: "Kotsu" }
顺子 234m 应当表示为:
{ base: Tile.M2, type: "Shuntsu" }
另外有表示雀头的类型 Atama
,其只需要写入代表牌即可。
最后,表示一手合法和牌的拆解方式的 Agari
则由四个面子和一个雀头组成。
该文件主要导出一个函数 getFuCount
,该函数接收一个手牌的表示,若该手牌未和牌,则返回 -1
,若该手牌和牌,则返回该手牌符数。
该函数的工作原理为:
- 通过
parseHand
函数将字符串格式的手牌表示解析为Hand
类型- 这里
parseHand
写得极其脆弱,随便构造点非法输入就 hack 了,请大家下手轻点 >_<
- 这里
- 通过
splitHand
函数将Hand
格式的手牌尝试分解为所有可能的和牌拆解方式Agari[]
- 通过
getAgariFu
函数计算所有拆解方式的符数,并最后取最高的输出
这里 splitHand
函数需要大家实现。接口语义定义为:
- 如果手牌未和牌,返回空数组
[]
- 否则返回所有的、去重的和牌拆解方式构成的数组
事实上由于后面还要算符取最大值,不去重也没事情
- 保证传入的
Hand
数组中每一项count
均非负且不大于 4,合计为 14,并且按照base
升序排列
该函数可以考虑通过 DFS 等方式完成,本次作业对算法效率毫无要求,仅为了让大家熟悉 TS 语言。
事实上的立直麻将算分规则相当繁复,本次作业忽略了相当大量的细节。如果大家对天凤等成熟的在线立直麻将对战平台的算分算法感兴趣,可以在了解霍夫曼编码等知识的基础上,阅读天凤平台在 2008 年开始使用的“暴力打表”算法(日语文本警告):
请在不修改其他函数的基础上完成作业,完成后通过 yarn build && yarn start
即可运行测试,如果最终输出 PASSED!
即代表通过测试。测例很简单的,不要怕 >_<
本作业不设置 DDL,不需要提交批改,请大家随便玩耍这个 toy example。
如果有问题可以在微信群 @Ashitemaru 交流。
或许可以和 @Ashitemaru 一起打麻将?雀魂 ID:52353425
或许可以和 @Ashitemaru 打 maimai 拼机?已经和这次作业没一点关系了吧!