ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

ポーカー#2 役の認識(Unity2019.4.4f1)

f:id:tomo_mana:20211209123333p:plain
今回はポーカーの役(Hands)を認識できるようにします。


※12/13注:ループするストレートの判定条件が間違っていました(10・J・Q・K・Aのみ有効で、J・Q・K・A・2のようなループはしない。Aが一番強いから)条件判定は第4回で修正します。

役が揃ったら並べ替える機能までは実装していません(そのうちに?)

ソートできる環境を準備する

役を見つけるには、ソート機能が欠かせません。後述しますが、フラッシュ以外の役を見つけるには、数字の昇順ソートが必要です。

ドローした時のカードは、マークとナンバーを一元的に扱うため、0から51までの一つの乱数で作りました。

f:id:tomo_mana:20211204103023p:plain
カードの絵柄を乱数で表す

カードのマーク(図の青字)は値を13で割った値、ナンバー(赤字)は値を13で割った余りで取得できます。

f:id:tomo_mana:20211204103129p:plain
マーク(青)とナンバー(赤)


弱い順に並べる

ポーカーで、カードのナンバーは 1 を除いて13が一番強く、2が一番弱いです。このため、マークも数字が大きいものが強く、数字が小さいものが弱くなるようにします。

ポーカーでもっとも強いマークはスペードです。そのため、マークの優劣を数字の大小で比較できるように、マークをクラブ、ダイヤ、ハート、スペードの順に定義します。

string[] mark = {"club", "diamond", "heart", "spade"};

役の認識

一般的なポーカーの役は以下の通りです。

役(日本語) (英語) マーク ナンバー 組み合わせ数 確率
ストレートフラッシュ Straight flush すべて一致 ストレート 40 0.0015
フォーカード Four of a kind (Don't care) 同一ナンバー×4 624 0.024
フルハウス Full house (Don't care) スリーカード+ツーペア 3,744 0.14
フラッシュ Flush すべて一致 (Don't care) 5,108 0.2
ストレート Straight (Don't care) ストレート 10,200 0.39
スリーカード Three of a kind (Don't care) 同一ナンバー×3 54,912 2.1
ツーペア Two pair (Don't care) (同一ナンバー×2)×2 123,552 4.75
ワンペア Pair (Don't care) 同一ナンバー×2 1,098,240 42.25
ノーペア High Card (Don't care) (Don't care) 1,302,540 50

Wikipediaから参照しました。
ポーカー・ハンドの一覧 - Wikipedia


判定は以下のとおりです。役のうち、フラッシュ(flush)とストレート(straight)は、他の役には無い特別な認識方法になります。マークで判断するのはフラッシュだけなので、先に判定します。

f:id:tomo_mana:20211202234143p:plain
役の決定フロー

フラッシュ判定

フラッシュは、5枚のマークが全て一致するかどうかなので、2-5枚目のカードが、全て1枚目のマークと一致するかで判断できます。

f:id:tomo_mana:20211202203853p:plain
フラッシュ判定

ストレート判定

ストレートは、マークを除いて昇順にソートされたカードにおいて、一番小さいものから一番大きいものまで1ずつ変化しています。

f:id:tomo_mana:20211204134128p:plain
ストレートの特徴

フローにすると以下になります。

f:id:tomo_mana:20211202224934p:plain
ストレート判定

ループするストレート

しかし、13と1を含むストレート(ここではループストレートと呼ぶことにします)は、途中に段差があって正しく検出できません。

f:id:tomo_mana:20211204135444p:plain
段差

この場合は、段差がある場所の前後で条件を満たし、かつ両端が1と13かどうかで見分けができます。

f:id:tomo_mana:20211204135525p:plain
ループストレート条件

ループストレートになるカードの条件は、最大と最小で以下になります。

f:id:tomo_mana:20211204135613p:plain
ループストレート最大最小
f:id:tomo_mana:20211202230009p:plain
ループストレート判定

ペア判定

ペアの判断は同じ数のカードの枚数を数え、組数と一番枚数が多かった組で役を判定します。

f:id:tomo_mana:20211204140740p:plain
ペア判定

フローにすると以下のようになります。少し複雑に見えるかもしれませんが、処理の内容は先ほどの通りです。

f:id:tomo_mana:20211202233307p:plain
ペア数と最大枚数のカウント

組数と一番枚数が多かった組の組み合わせで決まる役は以下の通りです。

f:id:tomo_mana:20211204142508p:plain
ペア系の役判定

コード

役の確認のために、デバッグ用のテキストを追加しています(ボタン下)。

f:id:tomo_mana:20211204141403p:plain
デバッグ用のテキスト(TextMeshPro)

前回作成したスート処理(CardSuit.cs)を修正します。

public class CardSuit : MonoBehaviour
{
    /* 定義・処理の一部を略しています */
    
    [SerializeField]
    TextMeshProUGUI handText = default;
    
    string[] mark = {"club", "diamond", "heart", "spade"};
    int MAX_CARDS = 5;
    
    // ランダムに1枚のカードを取り出す
    int DrawCardNo()
    {
        return (int)UnityEngine.Random.Range(0f, 51f);  // 52枚(ジョーカー含まず)
    }
    // カードをイメージ名に変換
    string GetImageName(int i)
    {
        int card = cards[i];
        return "card_" + mark[(card/13)] + "_" + ((card%13)+1).ToString().PadLeft(2, '0');
    }
    // カードのイラストを変更
    void ChangeCardImage(int i)
    {
        Image image = GetImage( i );
        if( !ReferenceEquals(image, null) ){
            string card = GetImageName( i );
            Texture2D texture = Resources.Load(card) as Texture2D;
            image.sprite = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
        }
    }
    
    // ナンバーのみの配列
    List<int> numberOfCards = new List<int>();
    int[] m_numberOfCards = new int[5];
    void SortWithoutMark()
    {
        for( int i = 0; i < MAX_CARDS; i++ ){
            numberOfCards[i] = cards[i] % 13;
        }
        numberOfCards.Sort();
    }
    
    // 役
    bool flush, straight;
    int max_paircount, pair_num;
    
    bool CheckFlush()
    {
        int mark = cards[0]/13;
        int i, count = 1;
        for( i = 1; i < MAX_CARDS; i++ ){
            if( mark == (cards[i]/13) ){
                count++;
            } else {
                break;
            }
        }
        if( count == MAX_CARDS ){
            return true;
        }
        return false;
    }
    
    bool CheckStraight()
    {
        int i, a, b, count = 1;
        
        // ストレート判定
        for( i = 1; i < MAX_CARDS; i++ ){
            if( (numberOfCards[i-1] + 1) != numberOfCards[i] ){
                break;
            }
            count++;
        }
        if( count == MAX_CARDS ){
            return true;
        }
        // ループストレート判定(ストレートから継続)
        b = i;
        a = (b+1) % MAX_CARDS;
        if( (numberOfCards[0] == 0) && (numberOfCards[4] == 12) ){
            for( ; i < MAX_CARDS; i++ ){
                if( (numberOfCards[b] + 1) != numberOfCards[a] ){
                    if( (b == (MAX_CARDS-1)) && (a == 0) ){
                        count++;
                    }
                    break;
                }
                count++;
                b = a;
                a = (a+1) % MAX_CARDS;
            }
        }
        if( count == MAX_CARDS ){
            return true;
        }
        return false;
    }
    
    bool CheckPair()
    {
        max_paircount = pair_num = 0;
        int i, count = 1;
        bool fin = false;
        for( i = 1; i < MAX_CARDS; i++ ){
            if( numberOfCards[i-1] == numberOfCards[i] ){
                count++;
                fin = false;
            } else {
                fin = true;
            }
            if( fin || (i == (MAX_CARDS-1)) ){
                if( count > 1 ){
                    if( max_paircount < count ){
                        max_paircount = count;
                    }
                    pair_num++;
                    count = 1;
                }
            }
        }
        if( (max_paircount == 0) && (pair_num == 0) ){
            return false;
        } else {
            return true;
        }
    }
    
    enum HAND_NAME {
        STRAIGHT_FLUSH = 0,
        FLUSH,
        STRAIGHT,
        FOUR_OF_A_KIND,
        THREE_OF_A_KIND,
        TWO_PAIR,
        PAIR,
        FULL_HOUSE,
        HIGH_CARD,
        BUG,
        HAND_MAX
    };
    string[] hand_name = new string[]  {
        "Straight flush",
        "Flush",
        "Straight",
        "Four of a kind",
        "Three of a kind",
        "Two pair",
        "Pair",
        "Full house",
        "High card",
        "bug",
    };
    
    HAND_NAME ConfirmHand()
    {
        HAND_NAME hand = HAND_NAME.HIGH_CARD;
        
        if( flush || straight || (pair_num > 0) ){
            if( flush && straight ){
                hand = HAND_NAME.STRAIGHT_FLUSH;
            } else
            if( flush ){
                hand = HAND_NAME.FLUSH;
            } else
            if( straight ){
                hand = HAND_NAME.STRAIGHT;
            } else {
                switch( pair_num ){
                    case 1:
                        switch( max_paircount ){
                            case 4:  hand = HAND_NAME.FOUR_OF_A_KIND; break;
                            case 3:  hand = HAND_NAME.THREE_OF_A_KIND; break;
                            case 2:  hand = HAND_NAME.PAIR; break;
                            default: hand = HAND_NAME.BUG; break;
                        }
                        break;
                    case 2:
                        switch( max_paircount ){
                            case 3:  hand = HAND_NAME.FULL_HOUSE; break;
                            case 2:  hand = HAND_NAME.TWO_PAIR; break;
                            default: hand = HAND_NAME.BUG; break;
                        }
                        break;
                    default:
                        hand = HAND_NAME.BUG; break;
                }
            }
        }
        return hand;
    }
    
    HAND_NAME CheckHands()
    {
        // フラッシュ判定
        flush = CheckFlush();
        // マーク無視で並び替え
        SortWithoutMark();
        // ストレート判定
        straight = CheckStraight();
        // ペア判定
        if( (!flush) && (!straight) ){
            CheckPair();
        }
        // 役の最終決定
        return ConfirmHand();
    }
    
    // トリガ
    public void ChangeCards( bool force )
    {
        int i;
        HAND_NAME hand;
        
        // カードのドロー
        for( i = 0; i < 5; i++ ){
            if( (force | GetToggle( i ).isOn) ){
                cards[i] = DrawCardNo();
            }
        }
        // 役の決定
        hand = CheckHands();
        // 役を表示する
        if( !ReferenceEquals(handText, null) ){
            handText.text = hand_name[(int)hand];
        }
        // イメージ
        for( i = 0; i < 5; i++ ){
            ChangeCardImage( i );
            GetToggle( i ).isOn = false;
        }
    }
}

プレイ画面

tomo-mana.hatenablog.com

次回
イベント重複チェック、揃いそうな役の推測

本記事に使用しているトランプの絵柄は、いらすとやさんの素材を使用させていただいています。ありがとうございます。
トランプのイラスト(54枚まとめ) | かわいいフリー素材集 いらすとや