麻雀のアガリ形の判別

# 麻雀のアガリ形の判別

天和の確率が33万回に1回らしいので、
試行してみたくなったのでpythonで書いてみたけど、
これがなんというか思ったよりめんどくさかった。

アルゴリズムはまんまこちらのを。
http://www.onionsoft.net/hsp/mahjong.txt

役とか待ちとかはまぁ…元気があったら~~今度書く、かも~~。

書いてみた＠[2014/9/25] 
http://qiita.com/arc279/items/1a7853ad8e2dc35961d1

_python2.7.6_で動作確認。

## 基本になるとこ

愚直に書いてみた。
めんどくさいから_builtin_の_list_継承しちゃってるけど、アレなら委譲にするとよいと思う。

洗牌は_random_に丸投げ。

```python:mj.py
#!/usr/bin/env python
# -*- coding: utf8 -*-

import itertools
import random
from collections import OrderedDict

class Yama(list):
    u'''牌山'''
    WANPAI_NUM = 14

    class TsumoDoesNotRemain(Exception):
        u'''王牌しか残ってない'''
        pass

    def __init__(self):
        pais = [ Pai.from_index(i) 
                for i in range(Pai.TOTAL_KIND_NUM * Pai.NUM_OF_EACH_KIND) ]
        # 洗牌
        random.shuffle(pais)
        super(Yama, self).__init__(pais)

    def tsumo(self):
        u'''自摸'''
        if len(self) <= self.WANPAI_NUM:
            raise self.TsumoDoesNotRemain

        return self.pop(0)

    def wanpai(self):
        return self[-self.WANPAI_NUM:]

    def haipai(self):
        u'''配牌'''
        tehais = [ Tehai(), Tehai(), Tehai(), Tehai() ] # 東(親) 南 西 北

        # 4*3巡
        for j in range(0, 3):
            for tehai in tehais:
                for i in range(0, 4):
                    pai = self.tsumo()
                    tehai.append(pai)

        # ちょんちょん
        for tehai in tehais:
            pai = self.tsumo()
            tehai.append(pai)

        pai = self.tsumo()
        tehais[0].append(pai)

        return tehais

class Pai(object):
    u'''牌'''

    TOTAL_KIND_NUM = 34          # M/P/S + 字牌合わせた全ての種類
    NUM_OF_EACH_KIND = 4         # 1種類につき4枚
    NUM_OF_EACH_NUMBER_PAIS = 9  # M/P/S の数字牌は1..9まで

    class Suit:
        M = 0   # 萬
        P = 1   # 筒
        S = 2   # 策
        J = 3   # 字

        NAMES = {
            M: u"萬",
            P: u"筒",
            S: u"策",
            J: u"　",
        }

    class Num:
        NAMES = {
            1: u"一",
            2: u"二",
            3: u"三",
            4: u"四",
            5: u"五",
            6: u"六",
            7: u"七",
            8: u"八",
            9: u"九",
        }

    class Jihai:
        E   = 1
        S   = 2
        W   = 3
        N   = 4
        HAK = 5
        HAT = 6
        CHU = 7

        NAMES = {
            E:   u"東",
            S:   u"南",
            W:   u"西",
            N:   u"北",
            HAK: u"白",
            HAT: u"撥",
            CHU: u"中",
        }

    @classmethod
    def yaochupai(cls):
        u'''么九牌'''
        return [
            cls(cls.Suit.M, 1),
            cls(cls.Suit.M, 9),
            cls(cls.Suit.P, 1),
            cls(cls.Suit.P, 9),
            cls(cls.Suit.S, 1),
            cls(cls.Suit.S, 9),
            cls(cls.Suit.J, cls.Jihai.E),
            cls(cls.Suit.J, cls.Jihai.S),
            cls(cls.Suit.J, cls.Jihai.W),
            cls(cls.Suit.J, cls.Jihai.N),
            cls(cls.Suit.J, cls.Jihai.HAK),
            cls(cls.Suit.J, cls.Jihai.HAT),
            cls(cls.Suit.J, cls.Jihai.CHU),
        ]

    def __init__(self, suit, num):
        self.suit = suit
        self.num  = num

    @property
    def index(self):
        return self.suit * self.NUM_OF_EACH_NUMBER_PAIS + self.num

    def __repr__(self):
        #return str((self.suit, self.num))    # タプル表示
        if self.suit == Pai.Suit.J:
            return Pai.Jihai.NAMES[self.num].encode('utf-8')
        else:
            return (Pai.Num.NAMES[self.num] + Pai.Suit.NAMES[self.suit]).encode('utf-8')

    def __eq__(self, other):
        return self.suit == other.suit and self.num == other.num

    @classmethod
    def from_index(cls, index):
        kind = index % cls.TOTAL_KIND_NUM

        if True:
            suit = kind / cls.NUM_OF_EACH_NUMBER_PAIS
            num  = kind % cls.NUM_OF_EACH_NUMBER_PAIS + 1
        else:
            if 0 <= kind < 9:
                suit = cls.Suit.M
                num  = kind - 0 + 1
            elif 9 <= kind < 18:
                suit = cls.Suit.P
                num  = kind - 9 + 1
            elif 18 <= kind < 27:
                suit = cls.Suit.S
                num  = kind - 18 + 1
            elif 27 <= kind < 34:
                suit = cls.Suit.J
                num  = kind - 27 + 1

        assert(cls.Suit.M <= suit <= cls.Suit.J)
        assert(1 <= num <= cls.NUM_OF_EACH_NUMBER_PAIS)

        return cls(suit, num)

class Tehai(list):
    u'''手牌'''

    @staticmethod
    def sorter(a, b):
        u'''理牌の方法'''
        return a.suit - b.suit if a.suit != b.suit else a.num - b.num

    def rihai(self):
        u'''理牌'''
        self.sort(cmp=self.sorter)
        return self

    def aggregate(self):
        u'''{牌種 : 枚数} の形に集計'''
        hash = { x[0]: len(list(x[1])) for x in itertools.groupby(self.rihai()) }
        ret = OrderedDict()
        # キーの牌はソートされた状態を保つように
        for x in sorted(hash.keys(), cmp=self.sorter):
            ret[x] = hash[x]
        return ret

    def show(self):
        u'''見やすい形に表示'''
        line1 = u"|"
        line2 = u"|"
        for pai in self.rihai():
            if pai.suit != Pai.Suit.J:
                line1 += Pai.Num.NAMES[pai.num] + u"|"
                line2 += Pai.Suit.NAMES[pai.suit] + u"|"
            else:
                line1 += Pai.Jihai.NAMES[pai.num] + u"|"
                line2 += u"　|"

        print line1.encode("utf-8")
        print line2.encode("utf-8")
```

## アガリ型のチェック

七対子と国士無双って雀頭あるの？
この辺は解釈によってアレなのでうまいこと適当に。
ここでは七対子は雀頭なし、国士無双は2枚のやつが雀頭になるように書いてみた。

判定部分はクラスに切り出すのめんどくさかったのでクロージャで。

```python:mj.py
def check_hohra(tehai):
    u'''和了の形をチェック'''
    assert(len(tehai) == 14)
    pais = tehai.aggregate()
    keys = pais.keys()
    length = len(keys)
    #print pais, keys, length

    def check_chitoitsu(pais):
        u'''七対子チェック'''
        if all([ num == 2 for pai, num in pais.items()]):
            return (), [ (pai, pai) for pai, num in pais.items() ]
        return None

    def check_kokushimusou(pais):
        u'''国士無双チェック'''
        if length != 13:
            return None

        yaochupai = Pai.yaochupai()
        mentsu = []
        for pai, num in pais.items():
            if pai not in yaochupai:
                return None

            # TODO: ここ2枚あるのが雀頭ってことでいいのか？
            if num == 2:
                atama = (pai, pai)
            else:
                assert(num == 1)
                mentsu.append(pai)

        return atama, mentsu


    def search_syuntu(pais):
        u'''順子を探す'''
        for i in range(length):
            if pais[keys[i]] >= 1:
                first = keys[i]
                try:
                    second = keys[i+1]
                    third  = keys[i+2]
                except IndexError as e:
                    # 残り2種無い
                    continue

                if first.suit == Pai.Suit.J:
                    # 字牌は順子できない
                    continue

                if not (first.suit == second.suit and first.suit == third.suit):
                    # 牌種違う
                    continue

                if not ((second.num == first.num+1) and (third.num == first.num+2)):
                    # 連番じゃない
                    continue

                if pais[second] >= 1 and pais[third] >= 1:
                    pais[first]  -= 1
                    pais[second] -= 1
                    pais[third]  -= 1
                    return (first, second, third)

        return None

    def search_kohtu(pais):
        u'''刻子を探す'''
        for j in range(length):
            if pais[keys[j]] >= 3:
                pais[keys[j]] -= 3
                return (keys[j], keys[j], keys[j])

        return None

    # 七対子
    tmp = pais.copy()
    ret = check_chitoitsu(tmp)
    if ret:
        return [ ret ]

    # 国士無双
    tmp = pais.copy()
    ret = check_kokushimusou(tmp)
    if ret:
        return [ ret ]

    # 基本形
    candidate = []
    for i in range(length):
        # 頭を探す
        if not (pais[keys[i]] >= 2):
            continue

        tmp = pais.copy()
        atama = (keys[i], keys[i])
        tmp[keys[i]] -= 2

        mentsu = []
        while True:
            #print tmp
            ret = search_syuntu(tmp) or search_kohtu(tmp)
            if ret is None:
                ret = search_kohtu(tmp) or search_syuntu(tmp)
                if ret is None:
                    # 順子も刻子もできない
                    break
            mentsu.append(ret)

        #print atama, mentsu, tmp
        if len(mentsu) == 4:
            # 4面子1雀頭の形
            candidate.append( (atama, mentsu) )

    return candidate
```

## 天和のチェック

親の配牌でアガリの形になってればいいので、まぁこうなるよね。

```python:mj.py

def check_tenho():
    for cnt in (x for x in itertools.count()):
        yama = Yama()
        oya, _, _, _ = yama.haipai()
        ret = check_hohra(oya)
        if ret:
            print cnt
            oya.show()
            for atama, mentsu in ret:
                print atama, mentsu
            break

if __name__ == '__main__':
    # 100回くらい試す
    for x in range(100):
        check_tenho()

```

## で、結果は

60万回試行しても出ないときは出ないし、出るときは2万回くらいで出る。
＃それでも2万回かよ…とか__思わなかったら__たぶんなんかが麻痺してるかもしれない。

まぁ、試行回数が多くなってくると大数の法則で33万回に落ち着くんだと思う。たぶん。
もうなんかやたら時間かかるしめんどくさいから10回くらいしか試行してない。
書いたら満足した。

役とか待ちとかの判別まで入れるならたぶん書き直さないと駄目だなコレ…


## おまけ

デバッグ用のアレ。

```python:mj.py
    class Debug:
        u'''for debug'''

        TEST_TEHAIS = [
            [2, 3, 3, 4, 4, 5, 5, 5, 5, 6, 6, 6, 6, 7],
            [0, 0, 8, 8, 13, 13, 20, 20, 25, 25, 29, 29, 31, 31],      # ちーといつ
            [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33, 9],      # こくしむそう
            [33, 33, 33, 32, 32, 32, 31, 31, 31, 0, 0, 0, 2, 2],    # だいさんげん
            [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 1],      # ちゅうれんぽうとう
            [19, 19, 20, 20, 21, 21, 23, 23, 23, 25, 25, 32, 32, 32],      # りゅういーそう
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 5, 5, 0, 1, 2],      # ちんいつ いっつー いーぺーこう
        ]

        @classmethod
        def tehai_from_indexes(cls, indexes):
            assert(len(indexes) == 13 or len(indexes) == 14)
            return Tehai([ Pai.from_index(x) for x in indexes ])

        @classmethod
        def test_tehai(cls, idx = None):
            if not idx:
                # 14牌自摸る
                yama = Yama()
                return Tehai([ yama.tsumo() for x in range(14) ])
            else:
                return cls.tehai_from_indexes(cls.TEST_TEHAIS[idx])
```

## 気になる人は

自分の手で確かみてみろ！