麻雀ルールの簡単な確認
麻雀は「牌」を14枚手札(手牌)に揃え、その牌の組み合わせによって決まる点数を積み上げていくゲームである。
牌は大きく分けて数牌と字牌の2種類が存在し、それぞれ下記の種類がある。
数牌 (マンズ)
数牌 (ソーズ)
数牌 (ピンズ)
字牌
牌は1種類あたり4枚ずつ用意されており、 ゲーム全体を見ると 34種類の牌が 136枚 用意されていることになる。
冒頭で、牌の組み合わせによって点数が決まると紹介したが、点数が決まるためには手牌を4面子・1雀頭の形にする必要がある。
**「面子」**とは、3つの牌の組を差し、3つとも同じ牌の 刻子、3つの牌が同じ種類の数牌かつ数字が連続している 順子 がある。
刻子
順子
2つの牌の組で2つとも同じ牌の場合、その組のことを**「雀頭」**と呼ぶ
雀頭
すなわち、4面子・1雀頭とはこのような形の手牌のことを示している。
麻雀の点数の決まり方
手牌を4面子・1雀頭の形にした時、その形によって**「役」、「飜」、「符」**が決まり、これらによって点数が計算される。
例えば上図の手牌には、同じ牌の順子が2つ存在する。これによって決まる役が「一盃口」である。
また、この手牌では2〜8までの数牌だけが使われている。このように、1、9の数牌、字牌が無い状態で成立するのが「断么」という役である。
一盃口と断么はそれぞれ1飜の役であるため、この手牌は全体で 2飜となる。
次に「符」を確認する。この手牌ではが刻子で揃っている(4符)。
そして、をロン上がりしたとすると、これはとのノベタン待ちになるため、2符が付く。
基本符の20符 + メンゼンロン上がりの10符 + 刻子の4符 + ノベタン待ちの2符 = 36符 => 40符 (1の位は繰り上げする)
この手牌は2飜・40符、 2600点の手となる。
(国士無双・七対子は例外)
全体像とゴール
1const point: MahjongKun<["o2", "o3", "o4", "o2", "o3", "o4", "C9", "C9", "C9", "I5", "I6", "I7", "o4", "o4"], "o2", [], "ron", "child"> = "1300";23// compile error4// const point: MahjongKun<["o2", "o3", "o4", "o2", "o3", "o4", "C9", "C9", "C9", "I5", "I6", "I7", "o4", "o4"], "o2", [], "ron", "child"> = "1000";
このように、
ここで、これからの手順を整理する。
1牌の定義2手牌、面子、雀頭の定義3面子の刻子、順子の判定4役・飜・符が決まった時の点数計算5断么の役判定6面子・待ちの形の符計算
1. 牌の定義
typescriptでは Literal Type、Template Literal Typeの2つの型が使える。 Literal Typeはリテラル自身を型として扱えるというものである。文字列を例に説明すると、
1type AnyString = string;2type OnlyHoge = "hoge";34const s1: AnyString = "any_string"5const s2: OnlyHoge = "hoge"6// compile error7// const s3: OnlyHoge = "fuga"
Template Literal Typeはテンプレートリテラルを型として扱えるというものである。
1type OnlyHogePrefixString = `hoge_${string}`;23const s1: OnlyHogePrefixString = "hoge_hoge"4const s2: OnlyHogePrefixString = "hoge_fuga"5// compile error6// const s3: OnlyHogePrefixString = "fuga_hoge"
このような
1type Fruit = "apple" | "orange" | "banana"2type Animal = "dog" | "cat"3// dog_apple, dog_orange, dog_banana, cat_apple, cat_orange, cat_banana4type AnimalPlusFruitType = `${Animal}_${Fruit}`;
この性質を使うことで、牌の型を簡単に定義出来る。それぞれの字牌の形から連想して、
1type Num = "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9";2type TileMark = "I" | "o" | "C";3type Tile = `${TileMark}${Num}`;
これだけで、字牌の定義が完了である。
1type I1 = "I1";2type I2 = "I2";3type I3 = "I3";4...
というような作業を行う必要が無い。
2. 手牌、面子、雀頭の定義
typescriptでは配列の型を扱う場合、array型とtuple型の2種類が存在する。tuple型はコンパイル時に各インデックスに、どの型の要素が入っているかが決まるという点でarray型と異なる。
1type ArrayString = ("a" | "b" | string)[];2type TupleString = ["a", "c", string];
ここで、
必然的に配列の要素数も決定されるので、
1type NumberOfElements = ["a", "c", string]["length"]2const num: NumberOfElements = 33// compile error4// const num: NumberOfElements = 4
それでは、手牌の定義を行う。4面子・1雀頭の計14枚の手牌が定義できれば十分である。(ここではカンは考慮しない)
1type Hand = [2 Tile, Tile, Tile,3 Tile, Tile, Tile,4 Tile, Tile, Tile,5 Tile, Tile, Tile,6 Tile, Tile,7]
このように、手牌の定義にtuple型を使うことで、手牌の3番目の牌の型を
次に、面子と雀頭の定義を行う。ここでは手配の並びは人間によって並び替えられていると仮定する。
1type Chunk1<HAND extends Hand> = [HAND[0], HAND[1], HAND[2]]2type Chunk2<HAND extends Hand> = [HAND[3], HAND[4], HAND[5]]3type Chunk3<HAND extends Hand> = [HAND[6], HAND[7], HAND[8]]4type Chunk4<HAND extends Hand> = [HAND[9], HAND[10], HAND[11]]5type Pair<HAND extends Hand> = [HAND[12], HAND[13]]6// type Pair2<HAND> = [HAND[12], HAND[13]]
すると、このようにインデックスアクセスによって、14牌を4面子・1雀頭に分けて扱えるようになる。
3. 面子の刻子、順子の判定
- Template Literal Typeでinferして、数字の順番を判定
- Conditional extends で、刻子判定
4. 役・飜・符が決まった時の点数計算
- ConcatAllとlengthで麻雀計算限定の足し算ができる
- 符は繰り上げ (これも麻雀特化)
5. 断么の役判定
- 便利型 AndAll
- 3.を使って、4面子・1雀頭の形になっていること
- 全てに1, 9が含まれていないこと