Pythonライブラリの「麻雀(mahjong)」って？？

#「mahjong」
みなさん、Pythonに「**mahjong**」というライブラリがあるのをご存知でしょうか？

↓Pythonライブラリ「**mahjong**」
https://pypi.org/project/mahjong/


名の通り、このライブラリは**麻雀**のライブラリです！
(麻雀の英訳は"mahjong")


今回は、上記URLの内容をまとめて、実際に使ってみました！

#ライブラリ詳細
mahjongができる事を一言で表すと

「**Mahjong hands calculation**」
つまり、麻雀の手計算を行えます。

URLの記事に記載されているProject description部分を読んでいきましょう！

>Python2.7 and 3.5+ are supported.
We support the Japanese version of mahjong only (riichi mahjong).


Pythonのバージョンは、最新のものを使っておけば、まず問題はないでしょう。
そして、日本のリーチ麻雀のみ対応とも書かれています。

中国麻雀は、役の数が日本のリーチ麻雀より多かったり、
フリテンが大丈夫だったりと複雑ですからね...


##Riichi mahjong hands calculation

>This library can calculate hand cost (han, fu with details, yaku, and scores) for riichi mahjong(Japanese version).

このライブラリでは、リーチ麻雀の「**翻数**」、「**符数(詳細含む）**」、「**役**」、「**点数**」を計算する事ができるそうです！

素晴らしい...普通に実装するとなると恐ろしく大変そうです...



さらに、オプションとして以下のようなルール変更にも対応しているそうです。
(記載されているテーブルの内容を簡略化しました。)


1. 喰いタン (あり or なし)
2. 赤ドラ (あり or なし)
3. ダブルヤクマン (あり or なし)
4. 数えヤクマン (翻数13以上で役満 or 13以上でも三倍満 or 13以上で役満,26以上でダブル役満)
5. 切り上げマンガン (あり or なし)
6. ピンフ (1翻20符 or 1翻30符)
7. ピンフツモ (ツモの+2符あり or なし)
8. レンホー (5翻 or 役満)
9. ダイシャリン, ダイチクリン, ダイスウリン (あり or なし)


かなりのオプションに対応していて驚きです。

数えヤクマンや、ピンフがややこしいですが、
触れなければ基本のルールには対応できます。


>The code was validated on tenhou.net phoenix replays in total on 11,120,125 hands.
So, we can say that our hand calculator works the same way that tenhou.net hand calculation.

そして、このライブラリはあの有名な麻雀ゲーム「**天鳳**」で出た**11,120,125**個のアガリ手を、
確認できているそうです！

天鳳同様の計算を実現できているライブラリを無償で使えるなんて...まさに神


#mahjongを使ってみた！

では、試しに色々やってみましょう！

まずは、準備！
Pythonは最新であればまず問題なし！
他のライブラリと同様に

`pip install mahjong`

を実行すれば準備完了！

では、早速計算できるか試してみましょう！


```python
#計算
from mahjong.hand_calculating.hand import HandCalculator
#麻雀牌
from mahjong.tile import TilesConverter
#役, オプションルール
from mahjong.hand_calculating.hand_config import HandConfig, OptionalRules
#鳴き
from mahjong.meld import Meld
#風(場&自)
from mahjong.constants import EAST, SOUTH, WEST, NORTH

#HandCalculator(計算用クラス)のインスタンスを生成
calculator = HandCalculator()

#結果出力用
def print_hand_result(hand_result):
     #翻数, 符数
     print(hand_result.han, hand_result.fu)
     #点数(ツモアガリの場合[左：親失点, 右:子失点], ロンアガリの場合[左:放銃者失点, 右:0])
     print(hand_result.cost['main'], result.cost['additional'])
     #役
     print(hand_result.yaku)
     #符数の詳細
     for fu_item in hand_result.fu_details:
          print(fu_item)
     print('')

```

あとは、アガリ形などの情報を
`caluculator.estimate_hand_value()`
に引数として与えれば計算することができます！

`caluculator.estimate_hand_value()`の引数は、
・tiles(**麻雀牌のアガリ形**)
・win_tile(**アガリ牌**)
・melds(**鳴き**)
・dora_indicators(**ドラ**)
・config(**オプション**)があります。
https://github.com/MahjongRepository/mahjong/blob/master/mahjong/hand_calculating/hand.py


##例1(ロン or ツモ)
![IMG_3852.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217886/71e2067b-2d7b-6768-74d5-dde3fa142c63.jpeg)
**ロンでアガった場合**
プレイヤー：子
3翻40符　放銃者:5200点
役：タンヤオ, サンショクドウコウ

```python:example01_ron.py

#アガリ形(man=マンズ, pin=ピンズ, sou=ソーズ, honors=字牌)
tiles = TilesConverter.string_to_136_array(man='234555', pin='555', sou='22555')

#アガリ牌(ソーズの5)
win_tile = TilesConverter.string_to_136_array(sou='5')[0]

#鳴き(なし)
melds = None

#ドラ(なし)
dora_indicators = None

#オプション(なし)
config = None

#計算
result = calculator.estimate_hand_value(tiles, win_tile, melds, dora_indicators, config)
print_hand_result(result)

結果出力
>3 40
5200 0
[Tanyao, Sanshoku Doukou]
{'fu': 30, 'reason': 'base'} #メンゼンロン
{'fu': 4, 'reason': 'closed_pon'} #暗刻
{'fu': 4, 'reason': 'closed_pon'} #暗刻
{'fu': 2, 'reason': 'open_pon'} #明刻
```
**ツモでアガった場合**
プレイヤー：子
6翻40符　親:6000点, 子3000点
役:メンゼンツモ, タンヤオ, サンアンコウ, サンショクドウコウ

```python:example01_tsumo.py
#アガリ形(上と同じ)
tiles = TilesConverter.string_to_136_array(man='234555', pin='555', sou='22555')

#アガリ牌(上と同じ)
win_tile = TilesConverter.string_to_136_array(sou='5')[0]

#鳴き(なし)
melds = None

#ドラ(なし)
dora_indicators = None

#オプション(ツモを追加,Falseだとロン)
config = HandConfig(is_tsumo=True)

#計算
result = calculator.estimate_hand_value(tiles, win_tile, melds, dora_indicators, config)
print_hand_result(result)

出力結果
>6 40
6000 3000
[Menzen Tsumo, Tanyao, San Ankou, Sanshoku Doukou]
{'fu': 20, 'reason': 'base'} 
{'fu': 4, 'reason': 'closed_pon'} #暗刻
{'fu': 4, 'reason': 'closed_pon'} #暗刻
{'fu': 4, 'reason': 'closed_pon'} #暗刻
{'fu': 2, 'reason': 'tsumo'} #ツモ
```

ロンとツモの差は、
`caluculator.estimate_hand_value()`の引数である
`config`(オプション)に`config=HandConfig(is_tsumo=True)`があるかないかです。

ツモ以外にも、
・リーチ　　　　　　　→　`is_riichi`
・イッパツ　　　　　　→　`is_ippatsu`
・リンシャンカイホウ　→　`is_rinshan`
・チャンカン　　　　　→　`is_chankan`
・ハイテイ　　　　　　→　`is_haitei`
・ホウテイ　　　　　　→　`is_houtei`
・ダブルリーチ　　　　→　`is_daburu_riichi`
・流しマンガン　　　　→　`is_nagashi_mangan`
・テンホー　　　　　　→　`is_tenhou`
・レンホー　　　　　　→　`is_renhou`
・チーホー　　　　　　→　`is_chiihou`

が存在するので、`is_tsumo=True`と同様にすれば設定できます。


##例2(ドラ, 風)
![IMG_3853.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217886/7e2991f2-098d-e1e8-c66b-f608475bcfab.jpeg)

**場風が東, 自風が南でアガった場合**
プレイヤー：子, 自風:南
4翻40符　放銃者:8000点
役:リーチ, ヤクハイ(自風), ドラ2

```python:example02_south.py
#アガリ形(honors=1:東, 2:南, 3:西, 4:北, 5:白, 6:發, 7:中)
tiles = TilesConverter.string_to_136_array(man='677889', pin='88', sou='456', honors='222')

#アガリ牌(マンズの8)
win_tile = TilesConverter.string_to_136_array(man='8')[0]

#鳴き(なし)
melds = None

#ドラ(表示牌,裏ドラ)
dora_indicators = [
    TilesConverter.string_to_136_array(pin='7')[0],
    TilesConverter.string_to_136_array(sou='9')[0],
]

#オプション(リーチ, 自風, 場風)
config = HandConfig(is_riichi=True, player_wind=SOUTH, round_wind=EAST)

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>4 40
8000 0
[Riichi, Yakuhai (wind of place), Dora 2]
{'fu': 30, 'reason': 'base'} #メンゼンロン
{'fu': 8, 'reason': 'closed_terminal_pon'} #ヤオチュウ牌の暗刻
```
**場風が東, 自風が東でアガった場合**
プレイヤー：親, 自風:東
3翻40符　放銃者:7700点
役:リーチ, ドラ2

```python:example02_east.py
#アガリ形(honors=1:東, 2:南, 3:西, 4:北, 5:白, 6:發, 7:中)
tiles = TilesConverter.string_to_136_array(man='677889', pin='88', sou='456', honors='222')

#アガリ牌(マンズの8)
win_tile = TilesConverter.string_to_136_array(man='8')[0]

#鳴き(なし)
melds = None

#ドラ(表示牌,裏ドラ)
dora_indicators = [
    TilesConverter.string_to_136_array(pin='7')[0],
    TilesConverter.string_to_136_array(sou='9')[0],
]

#オプション(リーチ, 自風, 場風)
config = HandConfig(is_riichi=True, player_wind=EAST, round_wind=EAST)

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>3 40
7700 0
[Riichi, Dora 2]
{'fu': 30, 'reason': 'base'} #メンゼンロン
{'fu': 8, 'reason': 'closed_terminal_pon'} #ヤオチュウ牌の暗刻
```
ドラは、`dora_indicators`に表示牌を記述する事で設定できます。

自風は`config`の`player_wind`、場風は`config`の`round_wind`に
`EAST`(東), `SOUTH`(南), `WEST`(西), `NORTH`(北)を指定する事で設定できます。
つまり、`player_wind=EAST`にする事で、プレイヤーを親に設定することができます。


##例3(鳴き, オプションルール, 赤ドラ)
![IMG_3856.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217886/c32870fc-1472-fe50-72c2-252ac06d14d5.jpeg)
**リンシャンカイホウの場合**
プレイヤー：子
3翻40符　親:2600点, 子:1300点
役:リンシャンカイホウ, タンヤオ, 赤ドラ1

```python:example03_rinshan.py
#アガリ形(赤ドラは0,またはrを用いる(並び順はなんでもOK), has_aka_dora=Trueに変更)
tiles = TilesConverter.string_to_136_array(man='022246', pin='333', sou='33567', has_aka_dora=True)

#アガリ牌(マンズの6)
win_tile = TilesConverter.string_to_136_array(man='6')[0]

#鳴き(チー:CHI, ポン:PON, カン:KAN(True:ミンカン,False:アンカン), カカン:CHANKAN, ヌキドラ:NUKI)
melds = [
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='2222'), False),
    Meld(Meld.PON, TilesConverter.string_to_136_array(pin='333')),
    Meld(Meld.CHI, TilesConverter.string_to_136_array(sou='567'))
]

#ドラ(なし)
dora_indicators = None

#オプション(ツモ, リンシャンカイホウ, 喰いタン・赤ドラルールを追加)
config = HandConfig(is_tsumo=True,is_rinshan=True, options=OptionalRules(has_open_tanyao=True, has_aka_dora=True))

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>3 40
2600 1300
[Rinshan Kaihou, Tanyao, Aka Dora 1]
{'fu': 20, 'reason': 'base'}
{'fu': 16, 'reason': 'closed_kan'} #カン符(アンカン)
{'fu': 2, 'reason': 'open_pon'}
{'fu': 2, 'reason': 'tsumo'}
```

**ロンアガリの場合**
プレイヤー：子
2翻40符　放銃者:2600点
役:タンヤオ, 赤ドラ1

```python:example03_ron.py
#アガリ形(上に同じ)
tiles = TilesConverter.string_to_136_array(man='022246', pin='333', sou='33567', has_aka_dora=True)

#アガリ牌(上に同じ)
win_tile = TilesConverter.string_to_136_array(man='6')[0]

#鳴き(上に同じ)
melds = [
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='2222'), False),
    Meld(Meld.PON, TilesConverter.string_to_136_array(pin='333')),
    Meld(Meld.CHI, TilesConverter.string_to_136_array(sou='567'))
]

#ドラ(なし)
dora_indicators = None

#オプション(喰いタン・赤ドラルールを追加)
config = HandConfig(options=OptionalRules(has_open_tanyao=True, has_aka_dora=True))

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>2 40
2600 0
[Tanyao, Aka Dora 1]
{'fu': 20, 'reason': 'base'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 2, 'reason': 'open_pon'}
```
喰いタンのため、`is_tsumo=True`が追加されていても、
役にツモはありませんし、きちんとタンヤオがありますね。

赤ドラが含まれている場合は、
アガリ形とオプションにそれぞれ設定することを忘れないようにしましょう！


喰いタンや赤ドラ以外の他のオプションルールも、
`config`の`HandConfig(options)`で設定可能です。

・ダブルヤクマン　　　　　　　→　`has_double_yakuman(T or F)`
・数えヤクマン　　　　　　　　→　`kazoe_limit(kazoe_limit = HandConfig.KAZOE_LIMITED,HandConfig.KAZOE_SANBAIMAN,HandConfig.KAZOE_NO_LIMIT)`
・切り上げマンガン　　　　　　→　`kiriage(T or F)`
・ピンフ　　　　　　　　　　　→　`fu_for_open_pinfu(T or F)`
・ピンフツモ　　　　　　　　　→　`fu_for_pinfu_tsumo(T or F)`
・レンホー　　　　　　　　　　→　`renhou_as_yakuman(T or F)`
・ダイシャリン　　　　　　　　→　`has_daisharin(T or F)`
・ダイチクリン&ダイスウリン　 →　`has_daisharin_other_suits(T or F)`

##例4(チンイツ, 数えヤクマン, 咲さん)
![IMG_3900.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/217886/ff79b010-70f7-a972-42b5-129c703b7479.jpeg)

**13翻以上をヤクマンとする場合(通常の数えヤクマン)**
プレイヤー：子
29翻80符　親:16000点, 子:8000点
役:リンシャンカイホウ, トイトイ, サンアンコウ, サンカンツ, チンイツ, 赤1, ドラ16

```python:example04_limited.py
#アガリ形(牌が一種類しかない場合は、has_aka_doraは必要なし)
tiles = TilesConverter.string_to_136_array(man='22244455777999')

#アガリ牌(マンズの赤5, ですが普通のマンズの5と意味は一緒)
win_tile = TilesConverter.string_to_136_array(man='5')[0]

#鳴き(ダイミンカン:true, アンカン:False)
melds = [
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='7777'), False),
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='2222'), False),
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='9999'), True)
]

#ドラ(表示牌を枚数分だけ)
dora_indicators = [
    TilesConverter.string_to_136_array(man='1')[0],
    TilesConverter.string_to_136_array(man='1')[0],
    TilesConverter.string_to_136_array(man='6')[0],
    TilesConverter.string_to_136_array(man='8')[0],
]

#オプション(OptionalRulesのkazoe_limitをKAZOE_NO_LIMITへ, 赤がある場合はここで設定)
config = HandConfig(is_tsumo=True,is_rinshan=True,options=OptionalRules(kazoe_limit=HandConfig.KAZOE_LIMITED, has_aka_dora=True))

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>29 80
16000 8000
[Rinshan Kaihou, Toitoi, San Ankou, San Kantsu, Chinitsu, Dora 16, Aka Dora 1]
{'fu': 20, 'reason': 'base'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 16, 'reason': 'open_terminal_kan'}
{'fu': 4, 'reason': 'closed_pon'}
{'fu': 2, 'reason': 'pair_wait'}
{'fu': 2, 'reason': 'tsumo'}
```

**13翻以上をサンバイマンとする場合**
プレイヤー：子
29翻80符　親:12000点, 子:6000点
役:リンシャンカイホウ, トイトイ, サンアンコウ, サンカンツ, チンイツ, 赤1, ドラ16


```python:example04_sanbaiman.py
#アガリ形(上に同じ)
tiles = TilesConverter.string_to_136_array(man='22244455777999')

#アガリ牌(上に同じ)
win_tile = TilesConverter.string_to_136_array(man='5')[0]

#鳴き(上に同じ)
melds = [
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='7777'), False),
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='2222'), False),
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='9999'), True)
]

#ドラ(上に同じ)
dora_indicators = [
    TilesConverter.string_to_136_array(man='1')[0],
    TilesConverter.string_to_136_array(man='1')[0],
    TilesConverter.string_to_136_array(man='6')[0],
    TilesConverter.string_to_136_array(man='8')[0],
]

#オプション(OptionalRulesのkazoe_limitをKAZOE_SANBAIMANへ)
config = HandConfig(is_tsumo=True,is_rinshan=True,options=OptionalRules(kazoe_limit=HandConfig.KAZOE_SANBAIMAN, has_aka_dora=True))

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>29 80
12000 6000
[Rinshan Kaihou, Toitoi, San Ankou, San Kantsu, Chinitsu, Dora 16, Aka Dora 1]
{'fu': 20, 'reason': 'base'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 16, 'reason': 'open_terminal_kan'}
{'fu': 4, 'reason': 'closed_pon'}
{'fu': 2, 'reason': 'pair_wait'}
{'fu': 2, 'reason': 'tsumo'}
```

**13翻以上をヤクマン,26翻以上をダブルヤクマンとする場合**
プレイヤー：子
29翻80符　親:32000点, 子:16000点
役:リンシャンカイホウ, トイトイ, サンアンコウ, サンカンツ, チンイツ, 赤1, ドラ16

```python:example04_no_limit.py
#アガリ形(上に同じ)
tiles = TilesConverter.string_to_136_array(man='22244455777999')

#アガリ牌(上に同じ)
win_tile = TilesConverter.string_to_136_array(man='5')[0]

#鳴き(上に同じ)
melds = [
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='7777'), False),
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='2222'), False),
    Meld(Meld.KAN, TilesConverter.string_to_136_array(man='9999'), True)
]

#ドラ(上に同じ)
dora_indicators = [
    TilesConverter.string_to_136_array(man='1')[0],
    TilesConverter.string_to_136_array(man='1')[0],
    TilesConverter.string_to_136_array(man='6')[0],
    TilesConverter.string_to_136_array(man='8')[0],
]

#オプション(OptionalRulesのkazoe_limitをKAZOE_NO_LIMITへ)
config = HandConfig(is_tsumo=True,is_rinshan=True,options=OptionalRules(kazoe_limit=HandConfig.KAZOE_NO_LIMIT, has_aka_dora=True))

#計算
result = calculator.estimate_hand_value(tiles, win_tile,melds,dora_indicators, config)
print_hand_result(result)

>29 80
32000 16000
[Rinshan Kaihou, Toitoi, San Ankou, San Kantsu, Chinitsu, Dora 16, Aka Dora 1]
{'fu': 20, 'reason': 'base'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 16, 'reason': 'closed_kan'}
{'fu': 16, 'reason': 'open_terminal_kan'}
{'fu': 4, 'reason': 'closed_pon'}
{'fu': 2, 'reason': 'pair_wait'}
{'fu': 2, 'reason': 'tsumo'}
```

おまけで、咲さんのようなアガリ形です。
チンイツに赤ドラが含まれている場合は、
`Tiles`(アガリ形)に`has_aka_dora=True`は必要なく
`config`(オプション)のみで大丈夫です。
(`Tiles`に`has_aka_dora=True`があるとエラーを返されます。)

`dora_indicators`(ドラ)には、表示されている牌を表示されている枚数分だけ追加しましょう。



##シャンテン数
mahjongは手計算だけではなく、
シャンテン数も計算することができます！

```python:example04_shanten.py
#シャンテン数
from mahjong.shanten import Shanten

#Shanten(シャンテン数計算用クラス)のインスタンスを生成
shanten = Shanten()

#手牌14枚
tiles = TilesConverter.string_to_34_array(man='13569', pin='123459', sou='443')

#計算
result = shanten.calculate_shanten(tiles)
print(result)

#結果
>2
```

#まとめ
今回、自分用の備忘録としてまとめてみました。
説明が不足している部分はほとんど補えたのではないでしょうか。
(ヌキドラや特殊なオプションルールは試していませんがw)

このライブラリは、麻雀大好き人間としてはたまりませんね！(笑)
このライブラリを用いて、また新たな麻雀アプリの開発ができたらと思います。