【麻雀】【Python】アガリを判定するロジックを考える【アルゴリズム】

以前ちょっとした機会で、麻雀のアガリ判定をするアルゴリズムを作ったことがありました。せっかくなので、忘れないうちに、当時考えた内容を記事にしてみます。

この記事では、以下の2点をまとめてみます。

なお、考えていく上で、以下の3点を前提としています。

前提1

判定する手牌は面前のエリアのみとする。

すなわち、チー、ポン、カン(暗槓、明槓)を除外した手牌を対象に、判定をする。

前提2

入力は、全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,        # 字牌の情報
]
前提3

この記事で紹介するのは、手牌がアガっているかどうかの判定のみを行うアルゴリズムです。

例えば、手牌のアガリ形も取得したい、といった場合には、追加で実装が必要になります。

それでは、まとめていきます。

この記事に記載の内容が、ベストなアルゴリズム・プログラムとは限りません。
一例としての記載ですので、参考にする際は、各環境にあった手法の検討もしてみて下さい。

当記事の麻雀牌画像は「麻雀の雀龍.com(ルール解説あり)」の無料麻雀牌画を利用しています。

目次

麻雀のアガリ判定をするためのアルゴリズム

大まかなフローチャートは以下のようになります。

雀頭、刻子、順子と順に取り除いていき、最終的に、手牌が綺麗に全て取り除けたら、その手牌はアガっていると判定する、といった流れです。

なお、雀頭、刻子、順子と探す順序にこだわりはありません。ただし、全候補に対して後続の処理をするので、候補が多くなりがちな順子は、最後に処理するのが良いでしょう。

こちらの手配を例に、もう少し詳しく説明します。

雀頭の候補を探す

まず、一番初めに、雀頭の候補を探します。

これは、入力された長さ34の配列の各要素をチェックし、要素の大きさが2以上の箇所を探すだけです。

こちらの手牌では、四萬、五萬、六萬、六索、中、が、雀頭候補になりますね。

雀頭の候補を見つけたら、雀頭の候補1つ1つに対して、以下の処理を繰り返し実施していきます。(フローチャートに記載の通りです。)

刻子の候補を探す

まず最初に、雀頭の候補を手牌から除外します。今回は、正しい雀頭「中」を除外してみましょう。手牌は以下のようになります。

この時、誤った雀頭を除外した場合は、全手牌が綺麗に無くなることができず、別の雀頭候補を除外して、以降の処理を再度やり直す、という仕組みです。

今度は、刻子の候補を探します。

これも、長さ34の配列の各要素をチェックし、要素の大きさが3以上の箇所を探すだけですね。

上の手牌では、五萬と六索が候補になります。

雀頭と同様に、単純に刻子の候補を1つずつ除外するだけでは、正しくアガリの判定はできません。以降の作業が重要になるので、注意が必要です。

刻子の候補を見つけたら、刻子候補の全部分集合を作ります。

全部分集合とは、配列の要素から作れる全パターンの集合を網羅した集合です。今回であれば、以下のような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が返ってきます。

こうしてみると、思った以上に簡単に判定できますね。

この記事が気に入ったら
フォローしてね!

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次