以前ちょっとした機会で、麻雀のアガリ判定をするアルゴリズムを作ったことがありました。せっかくなので、忘れないうちに、当時考えた内容を記事にしてみます。
この記事では、以下の2点をまとめてみます。
なお、考えていく上で、以下の3点を前提としています。
判定する手牌は面前のエリアのみとする。
すなわち、チー、ポン、カン(暗槓、明槓)を除外した手牌を対象に、判定をする。
入力は、全34牌が何枚ずつあるかをまとめた長さ34の配列とする。
配列は、萬子・筒子・索子・字牌(東南西北白發中)の枚数を順に並べたものです。
例えば、このような手牌であれば、下に記載の配列として扱います。
# 上の画像を変換した配列
tehai = [
1, 1, 1, 0, 0, 0, 0, 0, 0, # 萬子の情報
0, 0, 0, 2 ,2, 2, 0, 0, 0, # 筒子の情報
0, 0, 0, 0, 1, 1, 1, 0, 0, # 索子の情報
0, 2, 0, 0, 0, 0, 0, # 字牌の情報
]
この記事で紹介するのは、手牌がアガっているかどうかの判定のみを行うアルゴリズムです。
例えば、手牌のアガリ形も取得したい、といった場合には、追加で実装が必要になります。
それでは、まとめていきます。
麻雀のアガリ判定をするためのアルゴリズム
大まかなフローチャートは以下のようになります。
雀頭、刻子、順子と順に取り除いていき、最終的に、手牌が綺麗に全て取り除けたら、その手牌はアガっていると判定する、といった流れです。
なお、雀頭、刻子、順子と探す順序にこだわりはありません。ただし、全候補に対して後続の処理をするので、候補が多くなりがちな順子は、最後に処理するのが良いでしょう。
こちらの手配を例に、もう少し詳しく説明します。
雀頭の候補を探す
まず、一番初めに、雀頭の候補を探します。
これは、入力された長さ34の配列の各要素をチェックし、要素の大きさが2以上の箇所を探すだけです。
こちらの手牌では、四萬、五萬、六萬、六索、中、が、雀頭候補になりますね。
雀頭の候補を見つけたら、雀頭の候補1つ1つに対して、以下の処理を繰り返し実施していきます。(フローチャートに記載の通りです。)
刻子の候補を探す
まず最初に、雀頭の候補を手牌から除外します。今回は、正しい雀頭「中」を除外してみましょう。手牌は以下のようになります。
今度は、刻子の候補を探します。
これも、長さ34の配列の各要素をチェックし、要素の大きさが3以上の箇所を探すだけですね。
上の手牌では、五萬と六索が候補になります。
刻子の候補を見つけたら、刻子候補の全部分集合を作ります。
全部分集合とは、配列の要素から作れる全パターンの集合を網羅した集合です。今回であれば、以下のような2次元配列を作成します。
array = [
[], # 空リストも含まれるので、注意
[五萬],
[六索],
[五萬,六索],
]
こうして作成した刻子候補の2次元配列に対して、以降のループを回していきます。
単純に刻子候補のリストに対してループするのではなく、このような処理を行う理由を補足します。
いくつか、アガリ形を見てみましょう。
例えば、こちらのアガリは、刻子候補はあるものの、刻子として取る箇所は一つもありませんね。
また、こちらのアガリは、刻子候補は3つあり、そのうちの2つが実際、刻子になります。
このように、刻子候補では、雀頭のように、候補のうちの必ず一つが刻子になるとは限りません。
刻子候補から作り得る全ての組み合わせを作成し、それらに対して、一つずつ除外して、後続のループを回していく必要があるわけです。
順子の候補を探す
まず最初に、刻子の候補の組み合わせを手牌から除外します。今回も、正しい刻子「六索」を除外してみましょう。手牌は以下のようになります。
あとは、ここから順子を除外していきます。
順子は、連続する3つの牌を見つけるだけなので、簡単ですね。ただし、萬子、筒子、索子を跨いだ順子を作ってしまわないこと、字牌から順子を作ってしまわないことには注意しましょう。
手牌が綺麗に全てなくなったら、アガリの判定になります。
この時、手牌が0枚より少なくなったり、残ってしまった牌があった場合は、候補の取り方が誤っているため、再度ループをやり直すことになります。
実際のプログラムをPythonで実装
実際に、上のアルゴリズム通りに実装したPythonのコードが以下になります。
def checkAgari(tehai):
# 雀頭候補を作成する
headList = []
for index, hai in enumerate(tehai):
if hai >= 2:
headList.append(index)
# 雀頭候補を1つずつ取り出しループ
for head in headList:
# 除外用に手配をコピー
tehaiCopy = tehai.copy()
# 雀頭候補を除外
tehaiCopy[head] -= 2
# 刻子候補を作成する
kotsuList = []
for index, hai in enumerate(tehaiCopy):
if hai >= 3:
kotsuList.append(index)
# 刻子候補の全部分集合を作る
kotsuAllSubset = [[]]
for kotsu in kotsuList:
kotsuAllSubsetCopy = kotsuAllSubset.copy()
for kotsuSubset in kotsuAllSubsetCopy:
kotsuAllSubset.append([kotsu] + kotsuSubset)
# 刻子候補を1つ取り出しループ
for kotsuSubset in kotsuAllSubset:
# 除外用に手配をコピー
tehaiCopy2 = tehaiCopy.copy()
# 刻子を除外
for kotsu in kotsuSubset:
tehaiCopy2[kotsu] -= 3
# 順子を除外(萬子)
for i in range(0, 6):
haiCount = tehaiCopy2[i]
tehaiCopy2[i] -= haiCount
tehaiCopy2[i + 1] -= haiCount
tehaiCopy2[i + 2] -= haiCount
# 順子を除外(索子)
for i in range(9, 15):
haiCount = tehaiCopy2[i]
tehaiCopy2[i] -= haiCount
tehaiCopy2[i + 1] -= haiCount
tehaiCopy2[i + 2] -= haiCount
# 順子を除外(筒子)
for i in range(18, 24):
haiCount = tehaiCopy2[i]
tehaiCopy2[i] -= haiCount
tehaiCopy2[i + 1] -= haiCount
tehaiCopy2[i + 2] -= haiCount
# 手牌が綺麗に全て除外されたかチェック
result = True
for hai in tehaiCopy2:
if hai != 0:
result = False
if result:
return True
return False
手牌の配列を用意し、checkAgari(手牌)を実行すれば、アガっていればTrueが返ってきます。
こうしてみると、思った以上に簡単に判定できますね。