520
384

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-18

#「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

を実行すれば準備完了!

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

#計算
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
ロンでアガった場合
プレイヤー:子
3翻40符 放銃者:5200点
役:タンヤオ, サンショクドウコウ

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点
役:メンゼンツモ, タンヤオ, サンアンコウ, サンショクドウコウ

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

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

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

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に表示牌を記述する事で設定できます。

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

##例3(鳴き, オプションルール, 赤ドラ)
IMG_3856.jpg
リンシャンカイホウの場合
プレイヤー:子
3翻40符 親:2600点, 子:1300点
役:リンシャンカイホウ, タンヤオ, 赤ドラ1

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

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が追加されていても、
役にツモはありませんし、きちんとタンヤオがありますね。

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

喰いタンや赤ドラ以外の他のオプションルールも、
configHandConfig(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

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

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

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

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(オプション)のみで大丈夫です。
(Tileshas_aka_dora=Trueがあるとエラーを返されます。)

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

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

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)

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

520
384
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
520
384

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?