ポーカーの役を高速判定するアルゴリズムの作り方

トランプ52枚から5枚を抜き取って、その5枚でできる役を判定するプログラムを作ります。

このページではサンプルコードをC#で書いていますが、あくまで考え方の説明ですので、python、java、その他もろもろに自由に書き換えられます。

役の種類

判定したい役は以下の9種類です。

  • ストレートフラッシュ
  • フォー・オブ・ア・カインド
  • フルハウス
  • フラッシュ
  • ストレート
  • スリー・オブ・ア・カインド
  • ツーペア
  • ワンペア
  • ハイカード
https://www.poker.org/poker-hands-ranking-chart/

ロイヤルフラッシュはAハイのストレートフラッシュと同等なので分けて判定はしません。

基本的な関数の作り方

引数にカード5枚、その役であるかどうかを真偽値で返します。

public static bool IsRoyalFlush(int i0, int i1, int i2, int i3, int i4) {
    //内容
}

public static bool IsRoyalFlush(int[] i) {
    if(i.Length != 5){
        throw new Exception("length must be 5");
    }
    //内容
}

5枚バラバラに渡してもいいですし、配列として一気に渡してもいいですが、配列として渡すと配列長が5である保証ができないので、確認を挟んだ方が安全です。

引数はこの例ではintで書いていますが、カスタムObjectを定義してそれを使っても構いません。

役の判定

実装が簡単な順に書いています。

フラッシュ

全てのスートが同じであるかどうかを見れば良いので、これが一番簡単です。

もちろん \({}_5 \mathrm{C}_{2}=10\) 通りを全走査する必要はなく、どれか1枚でも違うスートがあればその時点でfalseを返せばよく、走査は最大4回で済みます。

public static bool IsFlush(int i0, int i1, int i2, int i3, int i4) {
    int[] cards = { i0, i1, i2, i3, i4 };
    for (int i = 0; i < 4; ++i) {
        if (GetCardSuit(cards[i]) != GetCardSuit(cards[i+1]))
            return false;
    }
    return true;
}

ペア系の役(フォー・オブ・ア・カインド、フルハウス、スリー・オブ・ア・カインド、ツーペア、ワンペア)

まず、以下のような関数を用意します。

private static int CountSameRankLine(int i0, int i1, int i2, int i3, int i4) {
    int count = 0;
    int[] cards = { i0, i1, i2, i3, i4 };
    for(int i = 0; i < 5; ++i) {
        for(int j = i; j < 5; ++j) {
            if (GetCardRank(cards[i]) == GetCardRank(cards[j]))
                ++count;
        }
    }
    return count;
}

カードが5枚あり、そこから2枚取る組み合わせ数は \({}_5 \mathrm{C}_{2}=10\) 通りです。そのそれぞれについて、カードの数字が同じであるかどうかを判定します。

10通りのうち、カードの数字が同じである組み合わせ数
フォー・オブ・ア・カインド6
フルハウス4
スリー・オブ・ア・カインド3
ツーペア2
ワンペア1
その他(ストレート、フラッシュなど)0

すると、ペア系の役はこの組み合わせ数が全てバラバラになります。ですので、ペア系の役であるかどうかは全てこの関数で判定できることになります。

        public static bool IsQuads(int i0, int i1, int i2, int i3, int i4) {
            return CountSameRankLine(i0, i1, i2, i3, i4) == 6;
        }

        public static bool IsFullHouse(int i0, int i1, int i2, int i3, int i4) {
            return CountSameRankLine(i0, i1, i2, i3, i4) == 4;
        }

        public static bool IsTrips(int i0, int i1, int i2, int i3, int i4) {
            return CountSameRankLine(i0, i1, i2, i3, i4) == 3;
        }

        public static bool IsTwoPairs(int i0, int i1, int i2, int i3, int i4) {
            return CountSameRankLine(i0, i1, i2, i3, i4) == 2;
        }

        public static bool IsOnePair(int i0, int i1, int i2, int i3, int i4) {
            return CountSameRankLine(i0, i1, i2, i3, i4) == 1;
        }

なのでこれでOKです。

ストレート

連続した5つの数字であるかどうかを見ればよいのですが、TJQKA、A2345を含む一方でJQKA2、QKA23、KA234を含めてはいけないので、一番厄介です。また引数としてカードの数字が小さい順に渡されるかどうかは保証されないので、ソートの必要があります。

手順としては、

  1. 渡されたカードを数字が小さい順にソートする(ここではAを14としてソートする)
  2. 配列の最後(5番目)の要素を除く4つについて、差が1であるかどうかを判定する(差が1でないものがあればその時点でfalseを返す)
  3. 4番目の要素と5番目の要素の差が1である or 4番目の要素が5で5番目の要素がA(14) であればtrue、そうでなければfalseを返す

2番目と3番目の判定がこのような形になるのは、A2345の形のストレートはソートすると{2, 3, 4, 5, 14}の形になってしまうためです。ポーカーにおいて、ストレート以外ではAを1として考えることはないので、コードをややこしくしないためにもAは14として扱った方がいいと思います。

実装としては以下のような形になります。

public static bool IsStraight(int i0, int i1, int i2, int i3, int i4) {
    int[] cards = { GetCardRank(i0), GetCardRank(i1), GetCardRank(i2), GetCardRank(i3), GetCardRank(i4) };
    Array.Sort(cards);
    for (int i = 0; i < 3; ++i) {
        if (cards[i + 1] - cards[i] != 1)
            return false;
    }
    return cards[4] - cards[3] == 1 || (cards[3] == 5 && cards[4] == 14);
}

※これより早く簡潔な実装があればぜひ教えてください。

ストレートフラッシュ

public static bool IsStraightFlush(int i0, int i1, int i2, int i3, int i4) {
    return IsFlush(i0, i1, i2, i3, i4) && IsStraight(i0, i1, i2, i3, i4);
}

ストレートでもあり、フラッシュでもあればストレートフラッシュになりますので、これで良いです。

ハイカード

上記のどれにも当てはまらなければハイカードです。

(コードは省略します)

より細かく判定する(ツーペア同士の強弱を判定する、など)

より細かく見る場合はソートがほぼ必須です。コードは省略します。

ペア系ではない役

ペア系ではない役の場合は逆順ソートするだけで済みます。この時、Aを14扱いすることに注意します。ストレートは5連続の数字であることが保証されているため一番上のカードの強さだけ見ればよいですが、フラッシュやハイカードは全てのカードの強さを見る必要があります。

ペア系の役

役に絡んでいる部分と、絡んでいない部分(いわゆる「キッカー」)を見る必要があります。いずれにしてもまずはペア系ではない役と同様に逆順ソートします。

フォー・オブ・ア・カインド、フルハウス

配列の1番目、3番目、5番目の数字を見ます。

フォー・オブ・ア・カインドの場合、ソートするとキッカーは必ず1番目か5番目の要素になります。3番目の要素の数字が4枚組のカードの数字で、1番目か5番目の要素のうちそれと一致しない方がキッカーです。

フルハウスの場合、3番目の要素は必ず3枚組の数字になります。1番目か5番目の要素のうちそれと一致しない方が2枚組の数字です。

スリー・オブ・ア・カインド

3番目の要素だけは常に3枚組になっているカードの数字であることが保証されています。逆順ソートした配列について以下の要素間の確認をするとこのような結果になります。

3番目の要素=2番目の要素3番目の要素=4番目の要素結果 {キッカー1、キッカー2}
truetrue{1, 5}
truefalse{4, 5}
falsetrue{1, 2}
どちらもfalseであるパターンはありません。

なので、実装としては

  • キッカー1は、3番目の要素=4番目の要素であれば1番目の要素、そうでなければ4番目の要素
  • キッカー2は、3番目の要素=2番目の要素であれば5番目の要素、そうでなければ2番目の要素

となります。

ツーペア、ワンペア

ソートした配列を先頭から順に確認していき、ペアが見つかったらその要素を抜き取ることでキッカーだけが残ります。