ゲーム化!tomo_manaのブログ

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

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

ポーカー#3 山札の管理(重複防止とシャッフル)(Unity2019.4.4f1)

f:id:tomo_mana:20211209123048p:plain
前回の記事で漏れていた、ドローするカードの重複管理と山札のシャッフルについて。

山札と管理?

先に、カードの重複管理をする機能の名前について。

カードの重複管理は、本来のポーカーであれば山札がそれにあたります。また、山札からドローするとき、(カジノでは)通常はディーラーが配ります。

山札は英語では Deck※ が使われるようです。また、ディーラー役になる機能として、その役割を明確にする意味で、ここではドロー制御(DrawControl)と呼ぶことにします。


※山札に相当する英語は Deck、Stack、Drawpile があるようで(Weblio辞書)、実際にはどれを使うのか迷いましたが、海外のサイトに、以下のような記述を見つけたので、多分 Deck が使われるのではないかと思いました。

Dealer
The player who shuffles the deck and deals the cards.

Poker Terms | How To Play | Official World Series of Poker

先ほどのWeblio辞書でも、Deckは組(ゲーム可能なカードの一揃え)、Stack は山積みにした札、Drawpileはめくり札(DeckとStackの両方の意味がある)の意味でした。しかし Stack はポーカーではユーザーの手持ちの総額(チップ)を表すため、使用できなさそうでした。Deckはデュエリング系カードゲームのイメージがありましたが、ポーカーでも使用するのですね。

山札(Deck)

山札は、ここでは場で使用されたカード番号を重複して振り出さない機能です。


前回までの記事で、52枚のカードは、一つの通し番号で管理しました。

f:id:tomo_mana:20211204103023p:plain
カードの通し番号(0~51)

この52枚のカードについて、ドローされたかどうかを管理する配列を準備します。

enum カード状態 = {
    ドローされていない,
    ドローされた(手持ち),
    捨てた,
};
カード状態[] cardState = new カード状態[52];


この山札の状態を見えるように、場にある全てのカードを俯瞰できる画面を作ります。この画面を Deck と呼ぶことにします。

f:id:tomo_mana:20211205172202p:plain
場にある全てのカードの確認

山札にある全てのカードを表示する

パーツ
数が多いので、最小限の表示にするため、1枚のカードを現すパーツは、Panel に Text を付けただけの単純なものにします。

f:id:tomo_mana:20211205172436p:plain
Hierarchy - 1枚のカードを表すパネル

全体
マークごとに整列させるため、13枚を一セットにした、4段の組を作ります。
(数多いので少し省略してます)

f:id:tomo_mana:20211205172342p:plain
Hierarchy - 山札

カードの状態と色

ドローされていない・ドローされた(手持ち)・捨てた、の状態に、それぞれ色を付けることにします。

Color[] cardColor = new Color[] {
    白,      // ドローされていない
    青,      // ドローされた(手持ち)
    灰色     // 捨てた
};


イメージとしてはこんな感じです。以下は、5枚の手持ち(青)について、すでに2枚が交換された(灰色)ことを表します。

f:id:tomo_mana:20211205232142p:plain
表示イメージ

山札の管理(Dealer、DrawControl)

山札の管理は、山札からカードが正しくドローされ、正しく捨てられたかどうかを管理します(イカサマ防止、、、いや、イカサマするためでもある!?)。

重複チェック

山札からのドローは、コード上は乱数の振り出しで表現されるのでした。

この振り出された乱数について、すでにドローされた、あるいは捨てられたカードと一致するものがあれば、再度乱数を振り出すようにして、一度ドローされたカード、あるいは捨てられたカードが、再びドローされないようにします。

int カード番号;
do {
    // カードを引く
    カード番号 = 乱数振出();
} while( カード状態[カード番号] != カード状態.ドローされていない );

ドロー済にする(カード番号);

シャッフル

しかし、このポーカーは、1回交換でなく、何回でもドローできる仕様にしていました。そうすると、山札のカードは、いつか底をつきます。

この山札の残数を監視し、すべての山札がなくなったら、捨てた山札をシャッフルし、再びドローできるようにします。

int 残数;

// 残数がなくなったらシャッフルする
if( (--残数) == 0 ){
    //山札のシャッフル
    山札のシャッフル();
}

void 山札のシャッフル()
{
    // 山札をシャッフルし、手持ちカード以外すべてドローできるようにする
    for( すべてのカード番号 ){
        if( カード状態[カード番号] == カード状態.捨てた ){
            ドローしていない状態にする( カード番号 );
        }
    }
    残数 = 総数(=52) - 手持ち数(=5);
}


このほか、プレイ画面では、山札の残数と、シャッフルされたかどうかを簡単に表示するためのログを画面に追加していますが、大した機能ではないので、ここでは省略します。

コード

作成したコード(DrawControl.cs)を、先述の山札の表示グループ(Deck)に追加します。

山札の管理(DrawControl.cs)

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class DrawControl : MonoBehaviour
{
    int MAX_CARDS = 5;
    int TOTAL_CARDS = 52;
    
    // カード状態
    enum CARD_STATE {
        NOT_DRAWED = 0,
        DRAWED,
        DISPOSED,
    }
    CARD_STATE[] cardState = new CARD_STATE[52];
    
    // カード状態(表示)
    List<Image> displayedDeck = new List<Image>();
    
    Color[] cardColor = new Color[] {
        Color.white,    // NOT_DRAWED
        Color.blue,     // DRAWED
        Color.grey,     // DISPOSED
    };
    
    // カード管理
    int remain;
    
    void GoNotDrawed(int i)
    {
        cardState[i] = CARD_STATE.NOT_DRAWED;
        displayedDeck[i].color = cardColor[ (int)cardState[i] ];
    }
    void GoDrawed(int i)
    {
        cardState[i] = CARD_STATE.DRAWED;
        displayedDeck[i].color = cardColor[ (int)cardState[i] ];
    }
    void GoDisposed(int i)
    {
        if( (i < 0) || (TOTAL_CARDS <= i) ){
            return;
        }
        cardState[i] = CARD_STATE.DISPOSED;
        displayedDeck[i].color = cardColor[ (int)cardState[i] ];
    }
    
    // シャッフル
    void Shuffle()
    {
        // 山札をシャッフルし、手持ちカード以外すべてドローできるようにする
        for(int i = 0; i < TOTAL_CARDS; i++){
            if( cardState[i] == CARD_STATE.DISPOSED ){
                GoNotDrawed( i );
            }
        }
        remain = TOTAL_CARDS - MAX_CARDS;
    }
    
    void Initialize()
    {
        // 山札を初期化し、全てのカードをドローできるようにする
        for(int i = 0; i < TOTAL_CARDS; i++){
            GoNotDrawed( i );
        }
        remain = TOTAL_CARDS;
    }
    
    // 1枚配る
    public int Draw()
    {
        // 重複チェック
        int cardNo;
        do {
            // カードを引く
            cardNo = (int)UnityEngine.Random.Range(0f, 51f);  // 52枚(ジョーカー含まず)
        } while( cardState[cardNo] != CARD_STATE.NOT_DRAWED );

        // ドロー済にする
        GoDrawed( cardNo );
        
        // 残数がなくなったらシャッフルする
        if( (--remain) == 0 ){
            Shuffle();
        }
        return cardNo;
    }
    // 1枚捨てて1枚配る
    public int DisposeAndDraw(int changedCard)
    {
        GoDisposed( changedCard );
        return Draw();
    }
    
    // Start is called before the first frame update
    void Start()
    {
        // UI初期化
        foreach ( Transform child in gameObject.transform ){
            // マーク全検索
            foreach ( Transform card in child ){
                // ナンバー全検索
                Image image = card.gameObject.GetComponent<Image>();
                displayedDeck.Add( card.GetComponent<Image>() );
            }
        }
        Initialize();
    }
}

上記に加えて、手持ち札(CardSuit.cs)の方も少し修正を入れます。(捨てたカードをカード管理側に通知する)

手持ち札(CardSuit.cs)

public class CardSuit : MonoBehaviour
{
    // ランダムに1枚のカードを取り出す
    int DrawCardNo(int disposeNo)
    {
//      return drawCtrl.Draw();
        return drawCtrl.DisposeAndDraw(disposeNo);
    }
    // トリガ
    public void ChangeCards( bool force )
    {
        // カードのドロー
        for( i = 0; i < 5; i++ ){
            if( (force | GetToggle( i ).isOn) ){
//                cards[i] = DrawCardNo();
                cards[i] = DrawCardNo( cards[i] );
            }
        }
        /* 以下 略 */
    }
}

プレイ画面

tomo-mana.hatenablog.com

次回
揃いそうな役の推測(重複チェックのコード量が結構多かったので回を分けました)

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