麻雀のアガリ形の判別
天和の確率が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_に丸投げ。
#!/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枚のやつが雀頭になるように書いてみた。
判定部分はクラスに切り出すのめんどくさかったのでクロージャで。
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
天和のチェック
親の配牌でアガリの形になってればいいので、まぁこうなるよね。
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回くらいしか試行してない。
書いたら満足した。
役とか待ちとかの判別まで入れるならたぶん書き直さないと駄目だなコレ…
おまけ
デバッグ用のアレ。
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])
気になる人は
自分の手で確かみてみろ!