[PYTHON] Discrimination of mahjong waiting form

Discrimination of mahjong waiting form

http://qiita.com/arc279/items/7894d582a882906b94c7 Continuation of this.

It was a lot more annoying than I expected.

After some trial and error, it turned out to be disastrous. That's interesting, so I'll post it while leaving the trial process.

I'm writing quite roughly, so I'd be happy if you could tell me if it's strange.

Here is the reference. Since it is only Chiniisou judgment, I started from here and rewrote it quite a bit with my super interpretation. http://d.hatena.ne.jp/staebchen/20100403/1270256158

I've used up closures and generators If you try to rewrite it with something other than python, it may be difficult, but that's okay.

The basics

It's the same as the one before the basics, but I've reworked it as it is, so I'll put it all again.

I personally don't like putting code outside # gist.

I hope it's completed in one place.

mj2.py


#!/usr/bin/env python
# -*- coding: utf8 -*-

import itertools
import random

class Yama(list):
    u'''Wall'''
    WANPAI_NUM = 14

    class TsumoDoesNotRemain(Exception):
        u'''Only the king is left'''
        pass

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

    def tsumo(self):
        u'''Self-reliance'''
        if len(self) <= self.WANPAI_NUM:
            raise self.TsumoDoesNotRemain

        return self.pop(0)

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

    def haipai(self):
        u'''Distribution'''
        tehais = [ Tehai(), Tehai(), Tehai(), Tehai() ] #east(parent)South West North

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

        #Choncho
        for tehai in tehais:
            pai = self.tsumo()
            tehai.append(pai)

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

        return tehais

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

    TOTAL_KIND_NUM = 34          # M/P/S +All types of tiles
    NUM_OF_EACH_KIND = 4         #4 sheets per type
    NUM_OF_EACH_NUMBER_PAIS = 9  # M/P/The number tile of S is 1..Up to 9

    class Suit:
        M = 0   #Man
        P = 1   #Tube
        S = 2   #Measure
        J = 3   #Character

        NAMES = {
            M: u"Man",
            P: u"Tube",
            S: u"Measure",
            J: u" ",
        }

    class Num:
        NAMES = {
            1: u"one",
            2: u"two",
            3: u"three",
            4: u"four",
            5: u"Five",
            6: u"Six",
            7: u"Seven",
            8: u"Eight",
            9: u"Nine",
        }

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

        NAMES = {
            E:   u"east",
            S:   u"South",
            W:   u"West",
            N:   u"North",
            HAK: u"White",
            HAT: u"Repellent",
            CHU: u"During ~",
        }

    @classmethod
    def all(cls):
        u'''All tiles'''
        return [cls(suit, num)
                for suit in cls.Suit.NAMES
                for num in range(1, cls.NUM_OF_EACH_NUMBER_PAIS+1)
                if suit != cls.Suit.J
            ] + [ cls(cls.Suit.J, num) for num in cls.Tsuhai.NAMES.keys() ]

    @classmethod
    def yaochupai(cls):
        u'''Tanyao chuu'''
        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.Tsuhai.E),
            cls(cls.Suit.J, cls.Tsuhai.S),
            cls(cls.Suit.J, cls.Tsuhai.W),
            cls(cls.Suit.J, cls.Tsuhai.N),
            cls(cls.Suit.J, cls.Tsuhai.HAK),
            cls(cls.Suit.J, cls.Tsuhai.HAT),
            cls(cls.Suit.J, cls.Tsuhai.CHU),
        ]

    @classmethod
    def chuchanpai(cls):
        u'''Nakahari tile'''
        yaochupai = cls.yaochupai()
        return [ x for x in cls.all() if x not in yaochupai ]

    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 - 1

    def is_next(self, other, index=1):
        u'''Whether it is the next number tile'''
        if self.suit != self.Suit.J: #Not a tile
            if self.suit == other.suit: #The tiles are the same
                if other.num == (self.num + index): #Serial number
                    return True
        return False
        
    def is_prev(self, other, index=1):
        u'''Whether it is the previous number tile'''
        return self.is_next(other, -index)

    @classmethod
    def is_syuntsu(cls, first, second, third):
        u'''Whether it is Junko'''
        #return second.is_prev(first) and second.is_next(third)
        return first.is_next(second) and first.is_next(third, 2)

    def __repr__(self):
        #return str((self.suit, self.num))    #Tuple display
        if self.suit == self.Suit.J:
            return self.Tsuhai.NAMES[self.num].encode('utf-8')
        else:
            return (self.Num.NAMES[self.num] + self.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):
        u'''Get from 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)

    @classmethod
    def from_name(cls, name):
        u'''Get from name'''
        for x in cls.all():
            if name == repr(x):
                return x
        return None

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

    @staticmethod
    def sorter(a, b):
        u'''How to tile'''
        return a.suit - b.suit if a.suit != b.suit else a.num - b.num

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

    @classmethod
    def aggregate(cls, tehai):
        u'''{Tile seed:Number of sheets}Aggregate in the form of'''
        hash = { x[0]: len(list(x[1])) for x in itertools.groupby(tehai.rihai()) }
        #Return the key (sorted tiles) with it
        return hash, sorted(hash.keys(), cmp=cls.sorter)

    def show(self):
        u'''Display in an easy-to-read form'''
        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.Tsuhai.NAMES[pai.num] + u"|"
                line2 += u" |"

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

    @classmethod
    def search_syuntsu(cls, pais, keys):
        u'''Find Junko
The argument is aggregate()Pass in the same form as the return value of.'''
        for i in range( len(keys)-2 ):   #No need to check the last 2 sheets
            tmp = pais.copy()
            first = keys[i]
            if tmp[first] >= 1:
                try:
                    second = keys[i+1]
                    third  = keys[i+2]
                except IndexError as e:
                    #There are no remaining 2 types
                    continue

                if not Pai.is_syuntsu(first, second, third):
                    continue

                if tmp[second] >= 1 and tmp[third] >= 1:
                    tmp[first]  -= 1
                    tmp[second] -= 1
                    tmp[third]  -= 1
                    #Junko found,The remaining tiles
                    yield (first, second, third), tmp

    @classmethod
    def search_kohtu(cls, pais, keys):
        u'''Find the engraving
The argument is aggregate()Pass in the same form as the return value of.'''
        for i, p in enumerate(keys):
            tmp = pais.copy()
            if tmp[p] >= 3:
                tmp[p] -= 3
                #Found engraving,The remaining tiles
                yield (p, p, p), tmp

Waiting check

It was a terrible thing to write in good condition. But no. It's annoying to fix it anymore.

It might be a little suspicious, such as waiting for 9 faces. I can't check it properly because there are too many types of waiting ...

It's just a waiting form and no role judgment is included.

Also, I'm exhausted, so I can't wait for Chiitoitsu and Kokushi Musou. Well, I think it can be done with a little care to distinguish the Agari type.

mj2.py


def check_tenpai(tehai):
    u'''Check the shape of the tenpai'''
    # TODO:Waiting for Chiitoitsu and Kokushi Musou not checked
    assert(len(tehai) == 13)

    # (Atama,face,Wait)Form of
    candidate = set()

    def check_machi(mentsu, tartsu):
        u'''Examine the shape of the wait'''
        assert(len(mentsu) == 3)

        keys = sorted(tartsu.keys(), cmp=Tehai.sorter)
        #print mentsu, tartsu, keys

        def check_tanki():
            u'''Single horse waiting check'''
            for i, p in enumerate(keys):
                tmp = tartsu.copy()
                if tmp[p] == 3:
                    #The remaining face is engraved
                    assert(len(tmp) == 2)
                    tmp[p] -= 3
                    tanki = { pai: num for pai, num in tmp.items() if num > 0 }.keys()
                    #Plunge into the face
                    ins = tuple( sorted(mentsu + [(p, p, p)]) )
                    candidate.add( ((), ins, tuple(tanki)) )
                else:
                    #The remaining face is Junko
                    first  = p
                    try:
                        second = keys[i+1]
                        third  = keys[i+2]
                    except IndexError as e:
                        continue

                    if not Pai.is_syuntsu(first, second, third):
                        continue

                    tmp[first]  -= 1
                    tmp[second] -= 1
                    tmp[third]  -= 1
                    tanki = { pai: num for pai, num in tmp.items() if num > 0 }.keys()
                    #Plunge into the face
                    ins = tuple( sorted(mentsu + [(first, second, third)]) )
                    candidate.add( ((), ins, tuple(tanki)) )

        def check_non_tanki():
            u'''Waiting check other than single horse'''
            for i, p in enumerate(keys):
                tmp = tartsu.copy()

                #Sparrow head check
                if not tmp[p] >= 2:
                    continue
                tmp[p] -= 2
                atama = (p, p)

                for j, q in enumerate(keys):
                    #Double-sided, edge
                    try:
                        next = keys[j+1]
                        if q.is_next(next):
                            ins = tuple( sorted(mentsu) )
                            candidate.add( (atama, ins, (q, next) ) )
                            break
                    except IndexError as e:
                        pass

                    #Fitting
                    try:
                        next = keys[j+1]
                        if q.is_next(next, 2):
                            ins = tuple( sorted(mentsu) )
                            candidate.add( (atama, ins, (q, next) ) )
                            break
                    except IndexError as e:
                        pass

                    #Sogo
                    if tmp[q] >= 2:
                        ins = tuple( sorted(mentsu) )
                        candidate.add( (atama, ins, (q, q) ) )
                        break

        check_tanki()
        check_non_tanki()

    #Search for 3 faces
    pais, keys = Tehai.aggregate(tehai)
    #print pais, keys

    if True:
        #I wonder if it's done recursively
        def search_mentsu(depth, proc):
            searchers = [Tehai.search_syuntsu, Tehai.search_kohtu]
            #Junko/Search for engraving
            def inner(pais, mentsu = [], nest = 0):
                if nest < depth:
                    for search in searchers:
                        for m, a in search(pais, keys):
                            inner(a, mentsu + [m], nest+1)
                else:
                    proc(mentsu, pais)
            inner(pais)

        search_mentsu(3, lambda mentsu, pais:
                check_machi(mentsu, { x[0]:x[1] for x in pais.items() if x[1] > 0 })
            )
    else:
        #If you write solidly like this
        searchers = [Tehai.search_syuntsu, Tehai.search_kohtu]
        for p1 in searchers:
            for p2 in searchers:
                for p3 in searchers:
                    #Application
                    for m1, a1 in p1(pais, keys):
                        for m2, a2 in p2(a1, keys):
                            for m3, a3 in p3(a2, keys):
                                mentsu = [m1, m2, m3]
                                #The remaining tiles
                                tartsu = { x[0]:x[1] for x in a3.items() if x[1] > 0 }
                                check_machi(mentsu, tartsu)

    return candidate

Test

That for debugging.

mj2.py


    class Debug:
        u'''for debug'''

        TEST_HOHRA = [
            [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],      #When
            [0, 8, 9, 17, 18, 26, 27, 28, 29, 30, 31, 32, 33, 9],      #Kokushi Musou
            [33, 33, 33, 32, 32, 32, 31, 31, 31, 0, 0, 0, 2, 2],    #Daisangen
            [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8, 1],      #Churenpoto
            [19, 19, 20, 20, 21, 21, 23, 23, 23, 25, 25, 32, 32, 32],      #Ryu-so
            [0, 1, 2, 3, 4, 5, 6, 7, 8, 5, 5, 0, 1, 2],      #Chinitsu Ittsu Epaco
        ]

        TEST_TENPAI = [
            [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 8],      #Genuine Churenpoto
            [1, 2, 3, 4, 5, 5, 5, 6, 20, 20, 21, 22, 23],  #Kan-chan Shabo(Junko is entwined)
            [13, 14, 15, 18, 19, 19, 20, 21, 24, 24, 24, 31, 31],   #Ryanmen Kan-chan
            [25, 25, 25, 1, 2, 3, 11, 12, 13, 11, 23, 23, 23],  #Ryanmen Tanki
            [25, 25, 25, 1, 2, 3, 11, 12, 13, 11, 12, 23, 24],   #Ryanmen
            [1, 2, 3, 4, 4, 6, 7, 8, 9, 10, 11, 29, 29],    #Shabo
        ]

        @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_hohra(cls, idx = None):
            u'''Achievement test'''
            return cls.tehai_from_indexes(cls.TEST_HOHRA[idx])

        @classmethod
        def test_tenpai(cls, idx = 0):
            u'''Tenpai test'''
            return cls.tehai_from_indexes(cls.TEST_TENPAI[idx])

        @classmethod
        def gen_tehai(cls, num = 14):
            u'''Make an appropriate arrangement'''
            assert(num == 13 or num == 14)
            yama = Yama()
            return Tehai([ yama.tsumo() for x in range(num) ])

        @classmethod
        def gen_hohra(cls):
            u'''Make an appropriate agari shape'''
            tehai = Tehai()

            def gen_syuntsu():
                u'''Make Junko'''
                first = Pai.from_index(random.choice(range(Pai.TOTAL_KIND_NUM)))
                if first.suit == Pai.Suit.J:
                    #Character tiles cannot be Junko
                    return None

                if first.num > 7:
                    # (7 8 9)The above cannot be Junko
                    return None

                second = Pai(first.suit, first.num+1)
                third  = Pai(first.suit, first.num+2)

                if tehai.count(first) == 4 or tehai.count(second) == 4 or tehai.count(third) == 4:
                    #Insufficient remaining number
                    return None

                return [first, second, third]

            def gen_kohtu():
                u'''Make engraving'''
                pai = Pai.from_index(random.choice(range(Pai.TOTAL_KIND_NUM)))
                if tehai.count(pai) >= 2:
                    #Insufficient remaining number
                    return None
                return [pai, pai, pai]

            def gen_atama():
                u'''Make a sparrow head'''
                pai = Pai.from_index(random.choice(range(Pai.TOTAL_KIND_NUM)))
                if tehai.count(pai) >= 3:
                    #Insufficient remaining number
                    return None
                return [pai, pai]

            tehai.extend(gen_atama())   #Sparrow head

            #If Junko and Kokuko have the same probability of appearance, we will weight them.
            weighted_choices = [(gen_syuntsu, 3), (gen_kohtu, 1)]
            population = [val for val, cnt in weighted_choices for i in range(cnt)]
            while len(tehai) < 14:
                ret = random.choice(population)()
                if ret is not None:
                    tehai.extend(ret)
            return tehai

        @classmethod
        def gen_tenpai(cls):
            u'''Make a tenpai shape appropriately'''
            tehai = cls.gen_hohra()
            assert(len(tehai) == 14)
            #Pull out one piece from the Agari shape
            tehai.pop(random.randrange(len(tehai)))
            return tehai


    class Test:
        u'''for test'''

        @classmethod
        def check_tenho(cls):
            u'''Tenwa check'''
            import sys
            for cnt in (x for x in itertools.count()):
                print >>sys.stderr, cnt
                yama = Yama()
                oya, _, _, _ = yama.haipai()
                ret = check_hohra(oya)
                if ret:
                    print "---------------------------------------------"
                    print cnt
                    oya.show()
                    for atama, mentsu in ret:
                        print atama, mentsu
                    break

        @classmethod
        def check_machi(cls, times = 100):
            u'''Check a lot of waiting'''
            for x in range(times):
                tehai = Debug.gen_tenpai()
                ret = check_tenpai(tehai.rihai())
                if not ret:
                    #When I come here, I'm not tempered. The point is a malfunction. Tehai to be modified.
                    print oya
                    print [ Pai.from_name(repr(x)).index for x in oya ]
            print "complete."


if __name__ == '__main__':
    Test.check_machi()

And the result is

For the time being, even if you check a lot of tenpai shapes, you probably won't miss them ... The question is whether I can list all the waits properly ...

I think there is room for improvement because I'm doing a lot of wasteful things.

Please let me know if there are any bugs.

For your reference

If you check the waiting of genuine Kuren Baotou, it looks like this.

The first line is Tehai.

2nd and subsequent lines Waiting for the sparrow head face In the form of.

[Ichiman, Ichiman, Ichiman,Niman,Sanman,Shima,Goman,Rokuman,Nanaman,Hachiman,Kuman,Kuman,Kuman]
() ((Ichiman, Ichiman, Ichiman), (Sanman,Shima,Goman), (Rokuman,Nanaman,Hachiman), (Kuman, Kuman, Kuman)) (Niman,)
() ((Ichiman, Ichiman, Ichiman), (Niman,Sanman,Shima), (Goman,Rokuman,Nanaman), (Kuman, Kuman, Kuman)) (Hachiman,)
(Kuman, Kuman) ((Ichiman,Niman,Sanman), (Shima,Goman,Rokuman), (Nanaman,Hachiman, Kuman)) (Ichiman, Ichiman)
(Ichiman, Ichiman) ((Ichiman,Niman,Sanman), (Rokuman,Nanaman,Hachiman), (Kuman, Kuman, Kuman)) (Shima,Goman)
(Kuman, Kuman) ((Ichiman, Ichiman, Ichiman), (Niman,Sanman,Shima), (Goman,Rokuman,Nanaman)) (Hachiman, Kuman)
(Kuman, Kuman) ((Ichiman, Ichiman, Ichiman), (Shima,Goman,Rokuman), (Nanaman,Hachiman, Kuman)) (Niman,Sanman)
(Kuman, Kuman) ((Ichiman, Ichiman, Ichiman), (Niman,Sanman,Shima), (Nanaman,Hachiman, Kuman)) (Goman,Rokuman)
(Ichiman, Ichiman) ((Sanman,Shima,Goman), (Rokuman,Nanaman,Hachiman), (Kuman, Kuman, Kuman)) (Ichiman,Niman)
(Ichiman, Ichiman) ((Ichiman,Niman,Sanman), (Shima,Goman,Rokuman), (Nanaman,Hachiman,Kuman)) (Kuman,Kuman)
() ((Ichiman, Ichiman, Ichiman), (Niman,Sanman,Shima), (Rokuman,Nanaman,Hachiman), (Kuman, Kuman, Kuman)) (Goman,)
(Ichiman, Ichiman) ((Ichiman,Niman,Sanman), (Shima,Goman,Rokuman), (Kuman, Kuman, Kuman)) (Nanaman,Hachiman)

Does this really suit you?

Recommended Posts

Discrimination of mahjong waiting form
Discrimination of prime numbers
Distinguishing the agari shape of mahjong