ゲーム化!tomo_manaのブログ

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

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

そろいやすいポーカー#6 リーチの認識3(Unity2019.4.4f1)

f:id:tomo_mana:20211228011547p:plain

リーチ表示の第3回目。今回は、どのカードが出たら役が確定するかを予測します。

これまでの修正との関連

第4回は手持ち札はどの役に近いか、第5回はどの手持ち札がリーチなのか、をそれぞれ予測しました。


第4回:手持ち札はどの役に近いか

f:id:tomo_mana:20211228103910p:plain
どの役に近いか(第4回)

そろいやすいポーカー#4 リーチの認識(Unity2019.4.4f1) - ゲーム化!tomo-manaのブログ


第5回:どの手持ち札がリーチなのか

f:id:tomo_mana:20211228104103p:plain
どの手持ち札がリーチなのか(第5回)

そろいやすいポーカー#5 リーチの認識2(Unity2019.4.4f1) - ゲーム化!tomo-manaのブログ


今回(第6回)は、どのカードが出たら役が確定するかを予測します。

f:id:tomo_mana:20211227185604p:plain
今回の修正範囲(#6)

方針

山札の全てのカード(52枚)に対して、以下の役の狙い札にされているかどうかをマークします。

役リーチ 狙い札
ストレートフラッシュ ストレート4 && フラッシュ4
フラッシュ 最頻マーク
ストレート ストレートパターン
ワンペア→スリーカード→フォーカード
ツーペア→フルハウス(役強化)
ペアを構成している同一ナンバー
ワンペア→ツーペア
スリーカード→フルハウス(役追加)
ペアを構成していない同一ナンバー※
イカード→ワンペア(最小役生成) 手持ち札と同じナンバー


以下の構造で保持することにします。

public class 山札の状態 {
    public byte ストレートフラッシュフラグ;
    public byte フラッシュフラグ;
    public byte ストレートフラグ;
    public byte 役強化フラグ;
    public byte 役追加フラグ;
    public byte 最小役生成フラグ;
    
    public void リセット()
    {
        ストレートフラッシュフラグ = 0;
        フラッシュフラグ = 0;
        ストレートフラグ = 0;
        役強化フラグ = 0;
        役生成フラグ = 0;
        最小役生成フラグ = 0;
    }
}
山札の状態[] 狙い札 = new 山札の状態[52];


それぞれの役での狙い札を見ていきます。


フラッシュ
前回(第5回)のフラッシュの図で見た場合、ダイヤが最頻マークになります。リーチするには、最頻マークが手持ち札の過半数(3枚以上)を構成している必要がありました。

f:id:tomo_mana:20211213190923p:plain
フラッシュのリーチ走査(#5)

この場合、狙い札は以下のようになります。

f:id:tomo_mana:20211227205308p:plain
フラッシュのリーチ表示

最大で山札のうち10枚がフラッシュの狙い札になります。

こうして狙い札の数を数えてみると、手持ち札によって、狙い札の数が変わらないのがフラッシュの特徴のようです。この後ストレートを見ていきますが、実際にポーカーをしている時、ストレートよりフラッシュの方が総組合せ数が少ないはずなのに、体感的にストレートより狙いやすく感じるのはこれが原因かと思います。

フラッシュリーチ条件 狙い札の数
フラッシュ3 10枚以下
フラッシュ4 9枚以下


ストレート
次に、ストレートを見ていきます。同じく前回(第5回)のストレートの図で見た場合、5枚中3枚以上がストレートの条件を満たす組み合わせ(ストレートパターン)は、{A, 2, 3, 4, 5} {3, 4, 5, 6, 7} {4, 5, 6, 7, 8} {5, 6, 7, 8, 9} の4通りです。

f:id:tomo_mana:20211213191009p:plain
ストレートのリーチ走査(#5)

この場合、ストレートの狙い札は {2, 4, 6, 9} の4種類、各マーク4種を加味して 4 x 4 =16枚 が対象になります。

f:id:tomo_mana:20211227205500p:plain
ストレートのリーチ表示

色付けしてみると気づくのですが、手持ち札によって、狙い札の数に大きな差が出るのがストレートの特徴です。


なお、最大はうまく説明できませんが、おそらく4種類×4マーク=16枚です。5枚ある手持ち札の中央値にあたるカードから数えて両側±4以内のカードしか、ストレート狙い札になれないためです。

f:id:tomo_mana:20211228135314p:plain
ストレートの狙い札最大

最小は1種類×4マーク=4枚です。Aを含むストレートで、あと1枚でそろう場合がこれに該当します。

f:id:tomo_mana:20211228135441p:plain
ストレートの狙い札最小

なお、あと1枚でストレートとなる場合、狙い札の数は両端待ちで8枚、その他の場合で4枚のため、いずれもフラッシュの方が狙い札の数は多くなります。

f:id:tomo_mana:20211228140257p:plain
ストレート4での待ち


まとめると以下になります。

ストレートリーチ条件 狙い札の数
ストレート3 16枚以下
ストレート4(両面待ち) 8枚以下
ストレート4(その他) 4枚以下


役強化(ワンペア→スリーカード→フォーカード、ツーペア→フルハウス

ここからは、ペア系を見ていきます。

第4回で使った図を使うと、ちょうど図の矢印が予測になります(遷移をあらわします)。

f:id:tomo_mana:20211228164421p:plain
役強化(緑線)、役追加(青線)、最小役生成(紫線)

緑の線は、同じナンバーのカードが増える線(ここでは役強化と呼びます)です。


●ワンペアまたはスリーカードの場合
すでに役を構成しているのと同じナンバーが来たら役強化(ワンペア→スリーカード→フォーカード)。狙い札の数はワンペア→スリーカードで2枚、スリーカード→フォーカードで1枚です。

f:id:tomo_mana:20211228165318p:plain
ワンペア→スリーカード→フォーカードへのリーチ表示

スリーカードの狙い札が少ないことはびっくりですが、ワンペア→スリーカードの場合、捨てる手持ち札が最大で3枚のため、もう少し揃いやすい印象があるのかもしれません。


●ツーペアの場合
すでに役を構成している2組のどちらかと同じナンバーが来たら役強化(ツーペア→フルハウス)。狙い札の数はワンペア→スリーカードの2倍で4枚です。

f:id:tomo_mana:20211228173712p:plain
ツーペア→フルハウスへのリーチ表示


役追加(ワンペア→ツーペア、スリーカード→フルハウス
次に、最小役がある状態で、役を追加する動きを見ていきます。先ほどの図で、青の線は、役になっていないナンバーのカードが増える線(ここでは役追加と呼びます)です。

f:id:tomo_mana:20211228164421p:plain
役強化(緑線)、役追加(青線)、最小役生成(紫線)

上記の図では4のペア形成になっていますが、ここでは便宜的に {A, 7, 8} を持っている場合を示します。狙い札の数は9枚です。

f:id:tomo_mana:20211228190819p:plain
ワンペア→ツーペアへのリーチ表示


最小役生成(ハイカード→ワンペア)
最後に、役が無い状態で、最小の役を構成する動きを見ていきます。先ほどの図で、紫の線は、手持ちの札のどれかが役を構成する線(ここでは最小役生成と呼びます)です。

f:id:tomo_mana:20211228164421p:plain
役強化(緑線)、役追加(青線)、最小役生成(紫線)

手持ち札のどれかを残して、それがワンペアを構成する場合、狙い札はそれぞれ3枚です。通常のポーカーでは、ハイカードが出た時は5枚すべてを交換が定石ですが、それはハイカードが50%、ワンペアが42.25%だからです。通常は全部交換する方が合理的かもしれません。

f:id:tomo_mana:20211227211337p:plain
イカード→ワンペア(最小役生成)へのリーチ表示


以下、複数の役の組み合わせについても触れておきます。


ストレートフラッシュ
通常、ストレートフラッシュが狙えるという心理になるのは、ストレート候補が4枚、かつフラッシュ候補が4枚、といった、かなり好条件の場合に限られると思います(偏見かもしれませんが)。ストレートフラッシュへのリーチの場合、他のストレート狙い札、フラッシュ狙い札とは別格に扱いたいので、この特別な狙い札には、専用の色を分けることにします。

f:id:tomo_mana:20211228003156p:plain
ストレートフラッシュへのリーチ表示

フルハウス
少しこれまでで検討が漏れていたのが、フルハウスへのリーチでした。フルハウスへのリーチは、スリーカードからのリーチと、ツーペアからのリーチで予測のされ方が異なります。

●スリーカード→フルハウスへのリーチ
スリーカードからフルハウスへのリーチは、スリーカード→フォーカードのリーチと、役追加のリーチの両方がかかった状態になります。これは特に違和感はありません。

f:id:tomo_mana:20211228004021p:plain
スリーカード→フルハウスへのリーチ(ペア増加で表現)

●ツーペア→フルハウスへのリーチ
一方、ツーペアからフルハウスへのリーチの場合、役を構成しない残り1枚は、どう考えても捨てるカードです。そのため、何らかのリーチを表示させる必要はないのですが、これまでの考えだと役候補になります。

f:id:tomo_mana:20211228003515p:plain
ツーペア→フルハウスへのリーチ(役強化で表現)

待ちカードの予測(設計)

フラッシュ

int 最大ナンバー = 13;
void フラッシュ狙い札予測()
{
    if( フラッシュリーチ ){
        走査番号 = 最大ナンバー * 最頻マーク;  // 開始点
        for( A~K ){
            if( !ドロー済( 走査番号 ) ){
                狙い札[ 走査番号 ].フラッシュフラグ++;
            }
            走査番号++;
        }
    }
}

ストレート

void ストレート狙い札予測()
{
    if( ストレートリーチ ){
        for( すべてのストレートパターン ){
            if( ストレートパターン[i] >= ストレートリーチ閾値 ){
                for( 手持ち札の枚数分だけ ){
                	狙い中 = true;
                    if( すでに保持しているカード ){
                        狙い中 = false;
                    }
                    if( 狙い中 ){
                        for( すべての同一ナンバーのマークにつき ){
                            if( !ドロー済( no ) ){
                                狙い札[ no ].ストレートフラグ++;
                                if( ストレートフラッシュ条件 ){
                                    狙い札[ no ].ストレートフラッシュフラグ++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

ペア

void UpdateDeckPair()
{
    int no;
    
    for( すべての手持ち札 ){
        no = 手持ち札[i]%MAX_NUMBERS;
        
        if( ペアリーチ ){
            for( すべてのマーク ){
                if( !ドロー済( no ) ){
                    狙い札[ no ].ペアフラグ++;
                }
            }
        } else {
            if( 役強化リーチ ){
                if( ナンバー出現数 > 1 ){
                    for( すべてのマーク ){
                        if( !ドロー済( no ) ){
                            狙い札[ no ].役強化フラグ++;
                        }
                    }
                }
            }
            if( 役追加リーチ ){
                if( ナンバー出現数 == 1 ){
                    if( ペア数 == 1 ){
                        for( すべてのマーク ){
                            if( !ドロー済( no ) ){
                                狙い札[ no ].役追加フラグ++;
                            }
                        }
                    }
                }
            }
        }
    }
}

コード

山札のカード状態の更新(CardSuit.cs)

public class WantNumber {
    public byte straightFlush;
    public byte flush;
    public byte straight;
    public byte over3;
    public byte over2pair;
    public byte pair;
    public void Reset()
    {
        straightFlush = 0;
        flush = 0;
        straight = 0;
        over3 = 0;
        over2pair = 0;
        pair = 0;
    }
}
public class CardSuit : MonoBehaviour
{
    // 追加分のみ
    
    void UpdateDeckFlash()
    {
        int no;
        
        if( r_flush ){
            no = MAX_NUMBERS * maxMarkId;
            for(int i = 0; i < MAX_NUMBERS; i++){
                if( !Drawed( no ) ){
                    wantNum[ no ].flush++;
                }
                no++;
            }
        }
    }
    void CheckFlush()
    {
        /* 前略 */
        
        UpdateDeckFlash();
    }

    void UpdateDeckStraight()
    {
        int i, j, k, l, no;
        bool hint;
        
        if( r_straight ){
            for( i = 0; i < MAX_STRAIGHT_COMBINATION_NUMBER; i++ ){
                if( straightPtnCount[i] >= STRAIGHT_REACHING_THRESHOLD ){
                    for( j = 0; j < MAX_CARDS; j++ ){
                        no = (i + j)%MAX_NUMBERS;
                        hint = true;
                        for( l = 0; l < MAX_CARDS; l++ ){
                            if( cards[l]%MAX_NUMBERS == no ){
                                hint = false;
                            }
                        }
                        if( hint ){
                            for( k = 0; k < MAX_MARKS; k++ ){
                                no = (i + j)%MAX_NUMBERS + k * MAX_NUMBERS;
                                if( !Drawed( no ) ){
                                    wantNum[ no ].straight++;
                                    if( (straightPtnCount[i] == 4) && (k == maxMarkId) && (maxMarkCount == 4) ){
                                        wantNum[ no ].straightFlush++;
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    void CheckStraight()
    {
        /* 前略 */
        
        UpdateDeckStraight();
    }
    
    void UpdateDeckPair()
    {
        int no;
        
        for(int i = 0; i < MAX_CARDS; i++){
            no = cards[i]%MAX_NUMBERS;
            
            // ハイカード→ワンペアへのリーチ(すべて色を付ける)
            if( r_pair ){
                for(int j = 0; j < MAX_MARKS; j++){
                    if( !Drawed( no + MAX_NUMBERS*j ) ){
                        wantNum[ no + MAX_NUMBERS*j ].pair++;
                    }
                }
            } else {
                // スリーカード以上へのリーチ(役強化)
                // ペアを構成していたら色を付ける
                if( r_over3 ){
                    if( numbersCount[ no ] > 1 ){
                        Debug.Log("r_over3 no = " + no + " numbersCount = " + numbersCount[no] );
                        for(int j = 0; j < MAX_MARKS; j++){
                            if( !Drawed( no + MAX_NUMBERS*j ) ){
                                wantNum[ no + MAX_NUMBERS*j ].over3++;
                            }
                        }
                    }
                }
                // 役追加
                // ワンペアまたはスリーカードの場合、ツーペアまたはフルハウスへのリーチ
                if( r_over2pair ){
                    if( numbersCount[ no ] == 1 ){
                        if( pair_num == 1 ){
                            for(int j = 0; j < MAX_MARKS; j++){
                                if( !Drawed( no + MAX_NUMBERS*j ) ){
                                    wantNum[ no + MAX_NUMBERS*j ].over2pair++;
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    bool CheckPair()
    {
        /* 前略 */
        
        UpdateDeckPair();
        
        // 戻り値
        if( (max_paircount == 0) && (pair_num == 0) ){
            return false;
        } else {
            return true;
        }
    }
    
    public void ChangeCards( bool force )
    {
        /* 前略 */
        
        // リーチを山札にも表示(追加)
        drawCtrl.UpdateDeckHints();
    }
}

山札の色分け(DrawControl.cs)

public class DrawControl : MonoBehaviour
{
    // 追加した関数
    public void UpdateDeckHints()
    {
        for( int i = 0; i < TOTAL_CARDS; i++ ){
            if( !cardSuit.Drawed( i ) ){
                WantNumber wn = cardSuit.GetWantNumber( i );
                if( wn.straightFlush != 0 ){
                    GoStraightFlushHinted( i );
                } else
                if( wn.flush != 0 ){
                    GoFlushHinted( i );
                } else
                if( wn.straight != 0 ){
                    GoStraightHinted( i );
                } else
                if( wn.over3 != 0 ){
                    GoOver3OfAKindHinted( i );
                } else
                if( wn.over2pair != 0 ){
                    GoOver2PairHinted( i );
                } else
                if( wn.pair != 0 ){
                    GoPairHinted( i );
                } else {
                    GoNotDrawed( i );
                }
            }
        }
    }
}

プレイ画面

(前回までと同じURLです。最新の状態に更新されています)
tomo-mana.hatenablog.com

次回

リーチがかかった時の表示に、どの役へのリーチを表示ししてて、その発生確率は何パーセントか(何通りあるか)を表示します。ここまで来ると、ポーカーの役が決まるまでのダイナミックな動きが見られるようになります。

思っていたよりも、乱数調整への道は長い!