Developer

さくさく理解する Godot 入門(ただし2Dに限る)応用編 テキサスホールデムポーカー【第4回】
2022.08.03
Lv1

さくさく理解する Godot 入門(ただし2Dに限る)応用編 テキサスホールデムポーカー【第4回】

目次

  • 手役判定

手役判定

この章では、枚数2枚~7枚の与えられたカード配列を引数とし、それらから作ることのできる最強の役を判定する check_hand(v) の説明を行う。

下図はショーダウンにおいて、手札をさらしている状態だが、プレイヤーパネル下部にポーカー役名 (フルハウス・3オブアカインド)が表示されている。この役判定が check_hand() で行われている。

ショーダウンにおいて、どのプレイヤーが勝ったのかを決めるために、 手持ちカード・共有カードからポーカーの役を判定する必要があるのはもちろんのこと、 共有カードが0,3,4枚である途中のラウンドでも、その時点での手役を表示するために、引数のカード枚数は可変となっている。
また、同じ役であった場合にも強弱を判定可能にする必要があるので、check_hand() が返す値は単なる数値ではなく、 [手役値, ランク1,ランク2,…] という配列にしている。ランクとはカードの数字のことで、 手役により意味のある数値が付加的に追加される。
例えば、5, 5, 5, K, K のフルハウスの場合、[FULL_HOUSE, RANK_5, RANK_K] が返る。
こうしておけば、配列の先頭から順に値を比較することで役の強弱を判定できる。

check_hand() の実装を下記に示す。

enum {      # 手役
    HIGH_CARD = 0,
    ONE_PAIR,
    TWO_PAIR,
    THREE_OF_A_KIND,
    STRAIGHT,
    FLUSH,
    FULL_HOUSE,
    FOUR_OF_A_KIND,
    STRAIGHT_FLUSH,
    ROYAL_FLUSH,
    N_KIND_HAND,
};
# 手役判定
# return: [手役値, ランク1,ランク2,...]
func check_hand(v : Array) -> Array:
    var rcnt = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]      # 各ランク枚数
    var scnt = [0, 0, 0, 0]             # 各スート枚数
    for i in range(v.size()):           # 手札のランク・スートの数を数える
        rcnt[card_to_rank(v[i])] += 1
        scnt[card_to_suit(v[i])] += 1
    var s = -1      # フラッシュの場合のスート
    if scnt[CLUBS] >= 5: s = CLUBS              # スートが5枚以上あればフラッシュ
    elif scnt[DIAMONDS] >= 5: s = DIAMONDS
    elif scnt[HEARTS] >= 5: s = HEARTS
    elif scnt[SPADES] >= 5: s = SPADES
    if s >= CLUBS:      # フラッシュ確定の場合
        var bitmap = 0      # ランクをビット値に変換したものの合計
        for i in v.size():
            if( card_to_suit(v[i]) == s ):      # 同一スートの場合
                bitmap |= 1 << card_to_rank(v[i])
        var mask = 0x1f00       # AKQJT
        for i in range(9):
            if( (bitmap & mask) == mask ):
                return [STRAIGHT_FLUSH, mask]       # ストレートフラッシュ
            mask >>= 1
        if( bitmap == 0x100f ):     # 1 0000 00000 1111 = 5432A
            return [STRAIGHT_FLUSH, 0x0f]       # 5432A よりも 65432 の方が強い
        return add_rank(v, s, FLUSH)            # 単なるフラッシュ
    .....

まずは enum で役名を宣言している。強い役ほど大きい数字になっている。

関数の実装では、まず各ランク・スートの数を数える。どれかのスート枚数が5を超えていればフラッシュが確定となる。
ストレートフラッシュの場合もあるので、再度すべてのカードを調べ、当該スートの場合は、ランクをビットマップにした総和を計算する。 それが 0x1f00, 0x0f80, …, 0x001f, 0x100f のいずれかであればストレートフラッシュ、そうでなければ単なるフラッシュとなる。

add_rank() はフラッシュの場合に、そのスートの数字を大きい順に結果配列に追加する関数だ。実装を下記に示す。

func add_rank(v, s, hand):      # フラッシュの場合に、そのスートの数字を大きい順に結果配列に追加
    var rnk = []
    for i in range(v.size()):
        if( card_to_suit(v[i]) == s ):      # 同一スートの場合
            rnk.push_back(card_to_rank(v[i]))
    rnk.sort()      # 昇順ソート
    var t = [hand]
    for i in range(5):              # 大きいランクから5枚を配列に追加
        t.push_back(rnk[-1-i])      # ランクを降順に格納
    return t

以下は check_hand() 実装の続きだ。

func check_hand(v : Array) -> Array:
    .....
    var threeOfAKindRank1 = -1       # 3 of a Kind の rcnt インデックス
    var threeOfAKindRank2 = -1  # 3 of a Kind の rcnt インデックス その2
    var pairRank1 = -1          # ペアの場合の rcnt インデックス
    var pairRank2 = -1          # ペアの場合の rcnt インデックス その2、pairRank1 > pairRank2 とする
    for r in range(13):
        if( rcnt[r] == 4):
            return [FOUR_OF_A_KIND, r]      # 4 of a kind は他のプレイヤーと同じ数字になることはない
        if( rcnt[r] == 3):
            if( threeOfAKindRank1 < 0 ):
                threeOfAKindRank1 = r
            else:
                threeOfAKindRank2 = r
        elif( rcnt[r] == 2):
            if pairRank1 < 0:
                pairRank1 = r
            elif pairRank2 < 0:
                if pairRank1 > r:
                    pairRank2 = r
                else:
                    pairRank2 = pairRank1
                    pairRank1 = r
            else:
                if r > pairRank1:
                    pairRank2 = pairRank1
                    pairRank1 = r
                if r > pairRank2:
                    pairRank2 = r
    # 3カード*2 もフルハウス
    if( threeOfAKindRank1 >= 0 && (pairRank1 >= 0 || threeOfAKindRank2 >= 0) ):
        return [FULL_HOUSE, threeOfAKindRank1]       # 3 of a kind は他のプレイヤーと同じ数字になることはない
    .....

次にランクごとの枚数を調べ、ペア系の役を探す。
同じランクが4枚揃っていれば、4オブアカインド確定なので、[FOUR_OF_A_KIND, r] を返す。r はランクだ。 まずありえないことだが、複数人が4オブアカインドの場合に、優劣をつけるための情報だ。 もしそうなったら、壮絶なベット合戦が繰り広げられることだろう。
同じランクが3枚、2枚の場合もチェックし、そうであればそのランクを threeOfAKindRank1, 2, pairRank1, pairRank2 に保持する。 そして、3枚が2種類、または3枚と2枚があればフルハウスとなる。

以下が、check_hand() の残りの実装コードだ。

func check_hand(v : Array) -> Array:
    .....
    var bitmap = 0
    var mask = 1
    for i in range(13):
        if( rcnt[i] != 0 ):
            bitmap |= mask
        mask <<= 1
    mask = 0x1f00       #   AKQJT
    for i in range(9):
        if( (bitmap & mask) == mask ):
            return [STRAIGHT, mask]
        mask >>= 1
    if( (bitmap & 0x100f) == 0x100f ):      #   5432A
        return [STRAIGHT, 0x0f]             # 5432A より 65432 の方が強い
    if( threeOfAKindRank >= 0 ):
        return [THREE_OF_A_KIND, threeOfAKindRank]      # 3 of a kind は他のプレイヤーと同じ数字になることはない
    if( pairRank2 >= 0 ):
        #return [TWO_PAIR]
        return add_rank_pair(v, pairRank1, pairRank2, TWO_PAIR)
    if( pairRank1 >= 0 ):
        return add_rank_pair(v, pairRank1, -1, ONE_PAIR)
        #return [ONE_PAIR]
    return add_rank_pair(v, -1, -1, HIGH_CARD)

ストレート、3オブアカインド、2ペア、ワンペアをチェックし、そうであればその役情報を返す。

add_rank_pair() はペアがある場合に、同じ役での強弱判定のための情報を付加する関数だ。
以下にそのコードを示す。

func add_rank_pair(v, p1, p2, hand):    # ペアの場合に、ペア以外の数字を大きい順に結果配列に追加
    var rnk = []
    for i in range(v.size()):
        var r = card_to_rank(v[i])
        if r != p1 && r != p2:      # ペアの数字でない場合
            rnk.push_back(r)
    rnk.sort()      # 昇順ソート
    var t = [hand]
    var n = 5       # 配列に追加する枚数
    if p2 >= 0:     # 2ペアの場合
        t.push_back(p1)
        t.push_back(p2)
        n = 1
    elif p1 >= 0:       # 1ペアの場合
        t.push_back(p1)
        n = 3
    n = min(rnk.size(), n)
    for i in range(n):
        t.push_back(rnk[rnk.size()-1-i])        # ランクを降順に格納
    return t

TechProjin Godot入門 関連連載リンク

Godotで学ぶゲーム制作
さくさく理解するGodot入門 連載目次

標準C++ライブラリの活用でコーディング力UP!
「競技プログラミング風」標準C++ライブラリ 連載目次