Skip to content

sast-summer-training-2023/sast-ts

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Very simplified Mahjong calculator

本次作业的目标是使用 TypeScript 编写一个简化的立直麻将和牌判断 & 符数计算函数。

立直麻将规则简化版

立直麻将一共使用万、饼、索(条)三种花色的数字牌和七种字牌,每一个花色的数字牌有一到九共计九种。数字牌的称呼为数字加上相应花色,如三饼九万等,七种字牌则分别为东南西北白发中

作为简记,万、饼、索一般分别写为 mps,所以三饼可以简记为 3p,九万可以简记为 9m。七种字牌按照东南西北白发中的顺序简记为 1z7z。此外,记录多张牌时,往往按照万、饼、索、字牌的顺序从左向右记录,同花色的数字牌内部按照数字增序记录,花色的标记(m、p、s、z)仅需写一次。例如手中持有下述牌的时候:

  • 一万、二万、五万、五万、九万、六饼、六饼、七饼、一索、东、东、白、白、中

手牌可以写为:

  • 12559m667p1s11557z

在不考虑赤牌的条件下,立直麻将一共有 $3 \times 9 + 7 = 34$ 种牌。其中,每种牌均有四张,故立直麻将共使用 $34 \times 4 = 136$ 张牌。

本次作业中你只需要考虑判定最后的手牌是否和牌,不需要考虑游戏流程(摸风、开牌、鸣牌、立直、流局、荒牌等)。在不考虑任何鸣牌(即吃碰杠)的情况下,麻将中宣告和牌时,手中需要有 14 张牌。在不考虑七对子与国士无双的条件下,合法的和牌必须能够将 14 张牌按照至少一种方式拆解为 4 个面子和 1 个雀头

面子是刻子顺子的统称。刻字指的是 3 张一样的牌,如 333p、777z。顺子则指的是同花色的、数字相连的数字牌(字牌无法构成顺子),如 123p、789s。面子都是三张牌构成的组合。

雀头指的是 2 张一样的牌,如 11p、66z。雀头都是两张牌构成的组合。

4 个面子和 1 个雀头恰好合计为 $3 \times 4 + 2 = 14$ 张牌,为宣告和牌时手中的牌的总数。

【例 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 拆解,符数为 $20 + 8 + 4 + 4 = 36$ 符,按照 123m、123m、123m、123p、33z 拆解,符数为 $20$ 符。故取前者,这一副手牌符数为 $36$

作业要求与代码说明

类型定义 types.ts

该文件中为你定义了可以使用的一些类型。

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 则由四个面子和一个雀头组成。

核心函数 lib.ts

该文件主要导出一个函数 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 拼机?已经和这次作业没一点关系了吧!

About

TypeScript HW of SAST summer training 2023.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published