麻雀の聴牌判定プログラム

# ※この記事は2020年に作成しました

# 聴牌判定プログラム
#### イメージ図
<img width="645" alt="dfgdf.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/436155/17f1a073-e95d-2a2c-b3b9-6817a65b22f1.png">

##### 鳴きは一切考慮してないので、手牌は必ず13枚ある前提での聴牌判定
刻子をカウントする関数(CntKotsu)、順子をカウントする関数(CntShunt)、ターツを調べる関数(Tartsu)、それぞれのパターンの時で調べる関数(TenpaiCheck)の4つの関数でできている。
TenpaiChek(tmp)のtmpに手牌を入れ、聴牌ならreturn 1、聴牌でないなら0を返すようにしている。
手牌は[1m,...,9m,1p,...,9p,1s,...9s,東,南,西,北,白,撥,中]の順で34個の配列に入っており、1つ牌が入っていたら該当する場所が1、4つ入っていたら4になる。
手牌例:[0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,]

まずは刻子をカウントする関数(CntKotsu)を下記に示す。

```Python:TenpaiCheck.py
def CntKotsu(tmp):#刻子をカウント
        CntK = 0
        for i in range(34):
                if tmp[i] >=3:
                        tmp[i] -=3
                        CntK += 1
        return CntK
```

この関数ではまずCntKに0を入れ、手牌の中に3枚以上のものがあれば刻子とみなしCntKに1を足し、その牌を手牌から抜いている。

次は順子をカウントする関数(CntShunt)を下記に示す。

```Python:TenpaiCheck.py
def CntShunt(tmp):#順子をカウント
        CntS = 0
        for i in range(25):
                if i==7 or i==8 or i==16 or i==17:continue
                while(tmp[i]>0 and tmp[i+1]>0 and tmp[i+2]>0):
                        tmp[i]-=1
                        tmp[i+1]-=1
                        tmp[i+2]-=1
                        CntS += 1
        return CntS
```

まずCntSに0を入れている。for文で25回しかループさせてないのは字牌で順子がないから。if文では萬子と筒子、筒子と索子の間を跨いで順子があると認識されないようにするためにある。その後whileで順子を調べていき、順子があればCntSに1を足し、手牌からその牌を抜いている。

次にターツを調べる関数(Tartsu)を下記に示す。

```Python:TenpaiCheck.py
def Tartsu(tmp):#残り2牌がターツか調べ、ターツなら1を、そうでないなら0を返す
        for i in range(34):
                if tmp[i]==2:return 1#シャボ待ち
        for i in range(8):
                if tmp[i]==1 and tmp[i+1]==1:return 1#マンズのリャンメンorペンチャン待ち
                if tmp[i+9]==1 and tmp[i+10]==1:return 1#ピンズのリャンメンorペンチャン待ち
                if tmp[i+18]==1 and tmp[i+19]==1:return 1#ピンズのリャンメンorペンチャン待ち
        for i in range(7):
                if tmp[i]==1 and tmp[i+2]==1:return 1#マンズのカンチャン待ち  
                if tmp[i+9]==1 and tmp[i+11]==1:return 1#ピンズのカンチャン待ち
                if tmp[i+18]==1 and tmp[i+20]==1:return 1#ソーズのカンチャン待ち
        return 0
```

残り2牌がターツかどうかを調べている。ターツならreturn 1、そうでないなら0を返す。
残りの2牌が同じ牌ならシャボ待ちでの聴牌、隣り合っていたらリャンメン待ちかペンチャン待ち、1つ飛ばしで隣り合っていたらカンチャン待ちになる。

最後にそれぞれのパターンの時で調べる関数(TenpaiCheck)のプログラムを下記に示す。

```Python:TenpaiCheck.py
def TenpaiCheck(tehai):#聴牌なら1を、そうでないなら0を返す
        tmp = tehai.copy()#刻子と順子で4面子が揃ってる場合（単騎待ち) 刻子→順子の順
        CntK = CntKotsu(tmp)#刻子の数を調べてカウント
        CntS = CntShunt(tmp)#順子の数を調べてカウント
        if CntK+CntS == 4:
                return 1
        
        tmp = tehai.copy()#順子と刻子で4面子が揃ってる場合（単騎待ち) 順子→刻子の順
        CntS = CntShunt(tmp)#順子の数を調べてカウント
        CntK = CntKotsu(tmp)#刻子の数を調べてカウント
        if CntK+CntS == 4:
                return 1
        
        tmp = tehai.copy()#2以上の牌全てを雀頭として刻子→順子の順で調べる
        for i in range(34):
                if tmp[i] >= 2:
                        tmp[i] -=2
                        CntK = CntKotsu(tmp)#刻子を調べカウント
                        CntS = CntShunt(tmp)#順子を調べカウント
                        if CntK+CntS==3:#この時点で3面子1雀頭がある
                                if Tartsu(tmp) ==1: return 1
                        else:#残り2牌がターツでなかったら
                                tmp = tehai.copy()#tmpをリセットして別の2以上の牌を雀頭として考える
        
        tmp = tehai.copy()#2以上の牌全てを雀頭として順子→刻子の順で調べる
        for i in range(34):
                if tmp[i] >= 2:
                        tmp[i] -=2                             
                        CntS = CntShunt(tmp)#順子を調べカウント
                        CntK = CntKotsu(tmp)#刻子を調べカウント
                        if CntK+CntS==3:#この時点で3面子1雀頭がある
                                if Tartsu(tmp) ==1: return 1
                        else:#残り2牌がターツでなかったら
                                tmp = tehai.copy()#tmpをリセットして別の2以上の牌を雀頭として考える
                                        
        tmp = tehai.copy()
        CntToitsu = 0
        for i in range(34):#七対子チェック
                if tmp[i]>=2:
                        tmp[i] -= 2
                        CntToitsu += 1
        if CntToitsu == 6:
                #print('七対子聴牌')
                return 1
        
        tmp = tehai.copy()#国士無双13面待ちのチェック　手牌から一九字牌を抜いて、残りが0枚なら聴牌
        CntHai=0
        for i in range(13):
                if tmp[0]==1:
                        tmp[0]-=1
                        CntHai+=1
                elif tmp[8]==1:
                        tmp[8]-=1
                        CntHai+=1
                elif tmp[9]==1:
                        tmp[9]-=1
                        CntHai+=1
                elif tmp[17]==1:
                        tmp[17]-=1
                        CntHai+=1
                elif tmp[18]==1:
                        tmp[18]-=1
                        CntHai+=1
                elif tmp[26]==1:
                        tmp[26]-=1
                        CntHai+=1
                elif tmp[27]==1:
                        tmp[27]-=1
                        CntHai+=1
                elif tmp[28]==1:
                        tmp[28]-=1
                        CntHai+=1
                elif tmp[29]==1:
                        tmp[29]-=1
                        CntHai+=1
                elif tmp[30]==1:
                        tmp[30]-=1
                        CntHai+=1
                elif tmp[31]==1:
                        tmp[31]-=1
                        CntHai+=1
                elif tmp[32]==1:
                        tmp[32]-=1
                        CntHai+=1
                elif tmp[33]==1:
                        tmp[33]-=1
                        CntHai+=1
        if CntHai == 13:
                print('国士無双13面待ち',tehai)
                return 1

        tmp = tehai.copy()#雀頭がある国士無双のチェック　手牌から一九字牌を抜いて、残りが0枚なら聴牌
        CntHai=0
        for i in range(34):
                if tmp[i]==2:
                        if i==0 or i==8 or i==9 or i==17 or\
                           i==18 or i==26 or i==27 or i==28 or\
                           i==29 or i==30 or i==31 or i==32 or i==33:
                           tmp[i] -= 2 #1,9,字牌のどれかが雀頭の場合その牌を抜く
                           for i in range(11):#残りの手牌から1,9,字牌のどれかを抜いていって全部なくなったら聴牌
                                if tmp[0]==1:
                                        tmp[0]-=1
                                        CntHai+=1
                                elif tmp[8]==1:
                                        tmp[8]-=1
                                        CntHai+=1
                                elif tmp[9]==1:
                                        tmp[9]-=1
                                        CntHai+=1
                                elif tmp[17]==1:
                                        tmp[17]-=1
                                        CntHai+=1
                                elif tmp[18]==1:
                                        tmp[18]-=1
                                        CntHai+=1
                                elif tmp[26]==1:
                                        tmp[26]-=1
                                        CntHai+=1
                                elif tmp[27]==1:
                                        tmp[27]-=1
                                        CntHai+=1
                                elif tmp[28]==1:
                                        tmp[28]-=1
                                        CntHai+=1
                                elif tmp[29]==1:
                                        tmp[29]-=1
                                        CntHai+=1
                                elif tmp[30]==1:
                                        tmp[30]-=1
                                        CntHai+=1
                                elif tmp[31]==1:
                                        tmp[31]-=1
                                        CntHai+=1
                                elif tmp[32]==1:
                                        tmp[32]-=1
                                        CntHai+=1
                                elif tmp[33]==1:
                                        tmp[33]-=1
                                        CntHai+=1
                                if CntHai ==13:
                                    print('国士無双聴牌')
                                    return 1

        return 0
```

最初のtmp = tehai.copy()で手牌をtmpに入れている。この後もtmpを初期化するときに使う。
まず最初に単騎待ちの場合を調べるため、手牌の先頭から刻子を調べて、その後順子を調べる。これで面子が4つあれば聴牌である。
同様のことを順子→刻子の順で調べている。

その次に単騎待ち以外の時は、手牌の先頭から2枚以上あるのを仮の雀頭とみなし、刻子を調べてから順子を調べ、面子が3つ以上あり、残りの2牌がターツであれば聴牌とみなしている。
同様のことを順子→刻子の順で調べている。

それでも聴牌になっていなければ、七対子を調べる。
七対子は手牌の2枚以上ある牌が6つあれば七対子聴牌とする。

その次は国士無双13面待ちの聴牌を調べる。
手牌から1m、9m、1p、9p、1s、9s、東、南、西、北、白、撥、中がそれぞれ1枚だけあったらそれを引くようにする。これを13回繰り返し、手牌が空になっていれば国士無双13面待ち聴牌である。

最後に普通の国士無双の聴牌を調べる。
適当に2枚以上ある牌を雀頭とみなし、残りの11牌から1m、9m、1p、9p、1s、9s、東、南、西、北、白、撥、中をそれぞれ1枚だけあったらそれを引くようにする。これを11回繰り返し、手牌が空になっていれば国士無双聴牌である。

これ以外の場合はノーテンになる。
ただ、待ち牌がたくさんあるようなややこしい手牌の場合、聴牌になっていてもノーテンと判断されるかもしれない。

また、聴牌かそうでないかだけを判定しているので、点数が高いほうの役を選んだり和了牌が何になるかは一切考慮していない。

###国士無双13面待ちに不具合（解決済み）
1日の牌譜で国士無双13面待ちって何回出てくるんやろって興味本位で調べたら、1日で5個でてきた。そんなわけないやろ！って思って国士無双13面待ちの時の手牌を見たら、裸単騎待ちの時に国士無双13面待ちって判定されていた。しかもその裸単騎の1牌が一九字牌のときのみ。
鳴きの場合を全く考慮していなかったことが原因。
このためにCntHaiを作って、一九字牌がそれぞれ1枚だけのときその牌を抜いていってCntHaiを+1していき、CntHaiが13の時に国士無双13面待ちになるようにした。
同様に普通の国士無双もCntHaiを使って判定するようにした。
この結果、調べた2015年1月1日の642対局の牌譜の中には1回も国士無双、国士無双13面待ちは出てこなかった。

- 他にも鳴きを考慮していないことによる不具合があるかもしれない。

#ノーテンを作るプログラム
```python
for i in range(100):
    Noten = [0]*34
    num = 0
    while num < 13:
        r = random.randrange(0,33)
        if Noten[r] < 4:
            Noten[r] += 1
            num += 1
    if TenpaiCheck(Noten) == 0:
        print(i,Noten)
```
ランダムに100個のノーテンの手牌を作るプログラム。
ただし、聴牌の手牌があったら個数は100個から減る。
5000個ノーテンを作ろうとしたら、1個が偶然聴牌の形になった。