導入
私にとって、人生の大半を占めているもの、それは麻雀である。
麻雀にハマってから約10年。麻雀以上に楽しいものが見つかったことは、いまだかつて一度もない。
麻雀だけで食っていく道はないかと、学生時代に何度も考えたが、どうも険しそうな道しか見つからない。
紆余曲折あって、仕事は麻雀ではなく、エンジニアをしている。
そんな麻雀厨の私がこの度興味を持ったのは、麻雀AIだ。
近年、ついに囲碁の世界でコンピュータが人間を超えたことは有名な話であるが、
囲碁以外に、コンピュータでは難しいと言われていた不完全情報ゲームであるポーカーの世界でも、コンピュータが人間を圧倒し始めている。
そして今、麻雀業界においても、その兆しは見え始めている。
オンライン麻雀ゲーム最高峰の「天鳳」では、水上さんが開発している「爆打」、ドワンゴが開発している「NAGA」、そして今年開発された「Suphx」(開発者不明)の3つの麻雀AIが有名だ。
私もそれらに引けを取らないような麻雀AIを作成してみたいと思った。
麻雀AI作成方法
人間を超える強さを持つ麻雀AIを作成するにはどうしたらいいのか。
麻雀AIの作成方法は、大きく4通りほど考えられる。*1
- 打牌の条件式を組み込む
- 評価関数を作成する
- シミュレーションをする
- 牌譜から機械学習を行う
麻雀を打たせるプログラムを作成するには、最初は1,2あたりがとっつきやすそうではある。
しかし、本格的に条件式を作成し、評価関数を作成しだしたらきりがなさそうなのと、
自分の頭で条件を考えてプログラミングする以上は、自分を能力を超える麻雀AIは作れなさそうである。
3のシュミレーションは、処理に時間がかかる等のデメリットはありつつも、人間を超える上では不可欠な要素な気がする。
4は強者の牌譜から学習することで、少なくとも自分よりは強い麻雀AIが作成できそうだが、牌譜から学ぶ以上、人間を大きく超えることはなさそうだ。
実装
麻雀AI作成方法はどうであれ、モチベーション的にもまずは手を動かしてみないと始まらないということで、とりあえず思うがままに書いてみた。
将来的にディープラーニングの技術を使いたくなることが目に見えており、
ディープラーニングは、Chainerというフレームワークが有名なようなので、
Chainerの言語であるPythonで書いてみた。
(Chainer Tutorialというサイトで、微積、線形代数あたりの数学や、機械学習、データ分析入門が体系的に学べられそうなので、こちらも別途勉強予定 https://tutorials.chainer.org/ja/tutorial.html )
まずはクラスを作ってみる。
とりあえず最低限必要なクラスは、「卓」と「雀士」でいいでしょう。
「卓」に牌136枚を混ぜさせて、
「雀士」は、配牌を取り、リー牌をして、1牌ツモって、1牌を切る。それだけ。
以下実装。
import random
class Taku:
def __init__(self):
self.yama = list(['1m', '1m', '1m', '1m', '2m', '2m', '2m', '2m', '3m', '3m', '3m', '3m', '4m', '4m', '4m', '4m', '5m', '5m', '5m', '5m', '6m', '6m', '6m', '6m', '7m', '7m', '7m', '7m', '8m', '8m', '8m', '8m', '9m', '9m', '9m', '9m', '1p', '1p', '1p', '1p', '2p', '2p', '2p', '2p', '3p', '3p', '3p', '3p', '4p', '4p', '4p', '4p', '5p', '5p', '5p', '5p', '6p', '6p', '6p', '6p', '7p', '7p', '7p', '7p', '8p', '8p', '8p', '8p', '9p', '9p', '9p', '9p', '1s', '1s', '1s', '1s', '2s', '2s', '2s', '2s', '3s', '3s', '3s', '3s', '4s', '4s', '4s', '4s', '5s', '5s', '5s', '5s', '6s', '6s', '6s', '6s', '7s', '7s', '7s', '7s', '8s', '8s', '8s', '8s', '9s', '9s', '9s', '9s','1z','1z','1z','1z','2z','2z','2z','2z','3z','3z','3z','3z','4z','4z','4z','4z', '5z', '5z', '5z', '5z', '6z', '6z', '6z', '6z', '7z', '7z', '7z', '7z'])
random.shuffle(self.yama)
class Janshi:
def __init__(self):
self.tehai = []
self.sutehai = []
def get_haipai(self, yama):
self.tehai = yama[0:13]
del yama[0:13]
return self.tehai
def riipai(self):
self.tehai = sorted(self.tehai)
manzu = [s for s in self.tehai if 'm' in s]
pinzu = [s for s in self.tehai if 'p' in s]
souzu = [s for s in self.tehai if 's' in s]
zihai = [s for s in self.tehai if 'z' in s]
self.tehai = manzu + pinzu + souzu + zihai
def tsumo(self, yama):
hai = yama[0]
del yama[0]
self.tehai.append(hai)
return hai
def dahai(self):
# ここで打牌選択のロジック
# 一旦右端を切る
hai = self.tehai[13]
del self.tehai[13]
self.sutehai.append(hai)
return hai
麻雀AIを作成する場合、上記で「一旦右端を切る」と処理を端折った「打牌(dahai)」のロジックの構築が、99.9%を占めることになるはずである。
しかし今はとりあえず動けばいいので、このままで。
作成したクラスを使って、一人麻雀をしてみる。
taku = Taku()
yama = taku.yama
print('山 最初')
print(yama)
print('')
janshi1 = Janshi()
janshi1.get_haipai(yama)
print('手牌')
print(janshi1.tehai)
print('')
print('リー牌後 手牌')
janshi1.riipai()
print(janshi1.tehai)
print('')
for i in range(18):
print('%s順目 手牌'%(i + 1))
tsumohai = janshi1.tsumo(yama)
print(janshi1.tehai)
print('ツモ ' + tsumohai)
janshi1.riipai()
dahai = janshi1.dahai()
print('打 ' + dahai)
print(janshi1.tehai)
print('')
print('捨て牌')
print(janshi1.sutehai)
print('')
print('山 最後')
print(yama)
開発環境をまだ全く構築していないので、
paiza.io( https://paiza.io/ )に貼り付けて実行しました。
出力結果
山 最初
['4z', '2m', '9p', '9p', '7z', '9s', '7p', '3m', '1m', '8s', '3z', '5p', '5m', '4z', '6m', '3s', '1s', '6p', '4m', '7m', '8s', '7z', '1m', '6z', '3p', '2p', '8m', '6p', '5z', '8s', '8m', '1z', '3s', '8m', '5z', '7m', '6m', '5z', '1z', '4p', '7s', '7z', '5p', '5p', '1p', '1s', '6m', '3s', '3m', '6s', '4s', '8s', '2z', '4s', '4p', '6s', '4p', '5s', '4z', '9s', '9m', '5s', '1s', '2m', '5m', '4s', '6s', '7s', '8m', '1p', '5s', '7m', '3p', '2p', '2s', '3m', '6z', '2z', '3z', '2p', '2p', '1m', '9p', '4m', '3z', '9m', '7s', '1s', '7m', '2z', '4m', '7s', '8p', '6s', '6p', '3s', '4p', '7p', '1z', '5p', '6z', '9s', '5m', '5z', '6p', '3p', '1m', '6z', '7z', '5m', '8p', '3p', '4z', '1p', '3z', '9p', '4s', '6m', '2s', '4m', '2s', '1z', '2z', '8p', '9m', '2s', '7p', '1p', '2m', '8p', '7p', '3m', '2m', '9m', '9s', '5s']
手牌
['4z', '2m', '9p', '9p', '7z', '9s', '7p', '3m', '1m', '8s', '3z', '5p', '5m']
リー牌後 手牌
['1m', '2m', '3m', '5m', '5p', '7p', '9p', '9p', '8s', '9s', '3z', '4z', '7z']
1順目 手牌
['1m', '2m', '3m', '5m', '5p', '7p', '9p', '9p', '8s', '9s', '3z', '4z', '7z', '4z']
ツモ 4z
打 7z
['1m', '2m', '3m', '5m', '5p', '7p', '9p', '9p', '8s', '9s', '3z', '4z', '4z']
2順目 手牌
['1m', '2m', '3m', '5m', '5p', '7p', '9p', '9p', '8s', '9s', '3z', '4z', '4z', '6m']
ツモ 6m
打 4z
['1m', '2m', '3m', '5m', '6m', '5p', '7p', '9p', '9p', '8s', '9s', '3z', '4z']
3順目 手牌
['1m', '2m', '3m', '5m', '6m', '5p', '7p', '9p', '9p', '8s', '9s', '3z', '4z', '3s']
ツモ 3s
打 4z
['1m', '2m', '3m', '5m', '6m', '5p', '7p', '9p', '9p', '3s', '8s', '9s', '3z']
4順目 手牌
['1m', '2m', '3m', '5m', '6m', '5p', '7p', '9p', '9p', '3s', '8s', '9s', '3z', '1s']
ツモ 1s
打 3z
['1m', '2m', '3m', '5m', '6m', '5p', '7p', '9p', '9p', '1s', '3s', '8s', '9s']
5順目 手牌
['1m', '2m', '3m', '5m', '6m', '5p', '7p', '9p', '9p', '1s', '3s', '8s', '9s', '6p']
ツモ 6p
打 9s
['1m', '2m', '3m', '5m', '6m', '5p', '6p', '7p', '9p', '9p', '1s', '3s', '8s']
6順目 手牌
['1m', '2m', '3m', '5m', '6m', '5p', '6p', '7p', '9p', '9p', '1s', '3s', '8s', '4m']
ツモ 4m
打 8s
['1m', '2m', '3m', '4m', '5m', '6m', '5p', '6p', '7p', '9p', '9p', '1s', '3s']
7順目 手牌
['1m', '2m', '3m', '4m', '5m', '6m', '5p', '6p', '7p', '9p', '9p', '1s', '3s', '7m']
ツモ 7m
打 3s
['1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '1s']
8順目 手牌
['1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '1s', '8s']
ツモ 8s
打 8s
['1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '1s']
9順目 手牌
['1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '1s', '7z']
ツモ 7z
打 7z
['1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '1s']
10順目 手牌
['1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '1s', '1m']
ツモ 1m
打 1s
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p']
11順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '6z']
ツモ 6z
打 6z
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p']
12順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '5p', '6p', '7p', '9p', '9p', '3p']
ツモ 3p
打 9p
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '3p', '5p', '6p', '7p', '9p']
13順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '3p', '5p', '6p', '7p', '9p', '2p']
ツモ 2p
打 9p
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '2p', '3p', '5p', '6p', '7p']
14順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '2p', '3p', '5p', '6p', '7p', '8m']
ツモ 8m
打 7p
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p']
15順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p', '6p']
ツモ 6p
打 6p
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p']
16順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p', '5z']
ツモ 5z
打 5z
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p']
17順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p', '8s']
ツモ 8s
打 8s
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p']
18順目 手牌
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '2p', '3p', '5p', '6p', '8m']
ツモ 8m
打 6p
['1m', '1m', '2m', '3m', '4m', '5m', '6m', '7m', '8m', '8m', '2p', '3p', '5p']
捨て牌
['7z', '4z', '4z', '3z', '9s', '8s', '3s', '8s', '7z', '1s', '6z', '9p', '9p', '7p', '6p', '5z', '8s', '6p']
山 最後
['1z', '3s', '8m', '5z', '7m', '6m', '5z', '1z', '4p', '7s', '7z', '5p', '5p', '1p', '1s', '6m', '3s', '3m', '6s', '4s', '8s', '2z', '4s', '4p', '6s', '4p', '5s', '4z', '9s', '9m', '5s', '1s', '2m', '5m', '4s', '6s', '7s', '8m', '1p', '5s', '7m', '3p', '2p', '2s', '3m', '6z', '2z', '3z', '2p', '2p', '1m', '9p', '4m', '3z', '9m', '7s', '1s', '7m', '2z', '4m', '7s', '8p', '6s', '6p', '3s', '4p', '7p', '1z', '5p', '6z', '9s', '5m', '5z', '6p', '3p', '1m', '6z', '7z', '5m', '8p', '3p', '4z', '1p', '3z', '9p', '4s', '6m', '2s', '4m', '2s', '1z', '2z', '8p', '9m', '2s', '7p', '1p', '2m', '8p', '7p', '3m', '2m', '9m', '9s', '5s']
考察
今の所、マンズ、ピンズ、ソウズ、字牌 の順番に並べ替えて、ただ右端を切り続けるロジックなので、マンズの染めを狙い続けることしかできないが、18順右端を切り続けるだけでも、意外と良い形になるんだなってことが改めてわかった。
そして稀にしっかりテンパイもするのも面白い。
Pythonについては、初めて触ったけどPHPが書ければ特につまずかずにスラスラ書けるなあという感想。
今後
これで最低限一人麻雀をする環境は整ったので、これから打牌ロジック、役、リーチあたりを実装してみようと思う。
参考
*1 麻雀AI作成の4パターンについては、paoさんのブログを参考にさせていただきました。 : http://mjpao.blog.fc2.com/blog-entry-3.html