Developer

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

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

目次

  • 状態遷移処理
  • さいごに

状態遷移処理

以上で、テキサスホールデムポーカーアプリを実装する上で重要なパーツの説明はあらかた済んだはずだ。 あとは、それらを適切に画面に配置し、適切なタイミングで適切な関数を呼び出していけばアプリとして成立する。
そのためには、今がどの状態であるのかをきちんと管理し、正しく状態遷移を行う必要がある。
GUI アプリはイベントドリブンな作りになっているのが普通だ。 ユーザがボタンを押すなどの操作を行うと、そのボタン押下のためのイベント処理関数が呼ばれ、 そこで適切な処理が行われる。その時、常に同じ処理を行うとは限らず、状態により処理を分けることがある。
本アプリでは、どのラウンドかを示す state と、 ラウンド中でカード移動中なのか、カードをめくっている状態なのかなどを表す sub_state を変数として使用し(下記コード参照)現在の状態を保持している。

enum {      # 状態
    INIT = 0,
    PRE_FLOP,
    FLOP,
    TURN,
    RIVER,
    SHOW_DOWN,
    ROUND_FINISHED,
}
enum {              # sub_state
    READY = 0,              # カード移動・オープン処理等が終わっている
    CARD_MOVING,            # カード移動中
    CARD_OPENING,           # カードオープン中
    CHIPS_COLLECTING,       # プレイヤーベットチップを中央に移動中
    CHIPS_COLLECTED,        # プレイヤーベットチップを中央に移動中終了
    INITIALIZED,
}
var state = INIT        # 状態
var sub_state = READY   # サブ状態

以下は、各フレームごとにコールされる _process(delta) のコードの一部だ。

func _process(delta):
    if state == SHOW_DOWN || state == ROUND_FINISHED:
        return
    if sub_state != 0: return       # カード移動処理などが終わっていない場合
    if nix == HUMAN_IX && is_folded[nix]:       # ユーザプレイヤー手番 && Folded の場合
        next_player()
    elif state == INIT:
        next_round()        # 次のラウンドに遷移
    elif state >= PRE_FLOP && nix >= 0:
        .....
                    if nix == HUMAN_IX:     # 人間の手番
                        # 行動ボタン表示テキスト設定
                        .....
                        sub_state = INITIALIZED     # 何度も初期化しないように
                        return      # 次のプレイヤーに遷移しないように
                    else:
                        if g.ai_type == g.AI_HONEST:        # AI タイプ判定
                            do_AI_action_honest(nix, max_raise)     # 正直AIをコール
                        else:
                            # スモールブラフAIをコール
                            do_AI_action_small_bluff(nix, max_raise)

状態をチェックし、次のラウンドに遷移したり、AI の行動選択関数をコールしたりしている。

各ラウンドの最初には、デッキからプレイヤーまたは共有カードとしてテーブル中央にカードが配布される。 配布アニメーションが終わると、配布された人間の手札または共有カードがオープンされる。
移動・オープン処理が終わるとその旨のシグナルが発行され、それらに接続された以下の関数が呼ばれる。

func on_moving_finished():
    n_moving -= 1
    if n_moving == 0:       # 全カード移動終了した場合
        print("on_moving_finished")
        sub_state = CARD_OPENING    # 移動が終わったら、人間・共有カードをオープン
        if state == PRE_FLOP:       # フロップであれば
            n_opening = 2
            players_card1[HUMAN_IX].do_open()       # 人間のカードをオープン
            players_card2[HUMAN_IX].do_open()
        elif state == FLOP:
            .....
func on_opening_finished():
    n_opening -= 1
    if n_opening == 0:      # 全カードをオープンし終わった場合
        sub_state = READY
        if state == PRE_FLOP:
            show_user_hand(0)       # 人間の手役名表示
            players[HUMAN_IX].add_child(players_card1[HUMAN_IX])    
            players[HUMAN_IX].add_child(players_card2[HUMAN_IX])
        elif state == FLOP:
            .....

最初に移動・オープン中のカード枚数をデクリメントし、それが0になれば全てのカードの処理が終わったと判断する。 on_moving_finished() では、状態をチェックし、プリフロップであれば人間の手札を、それ以外であれば共有カードをオープンする。
それぞれのカードオープンが終わると on_opening_finished() がコールされるので、必要な処理を淡々とこなしていく。

このようにイベントドリブンなコードは、逐次処理的コードとは異なり、 処理関数がどの順番にコールされるかコードを見てもすぐにわからないので、理解するのが難しいかもしれない。 が、GUI要素などを他に依存することなく部品化できて便利なので、ぜひマスターしてほしいテクニックのひとつだ。

さいごに

テキサスホールデムポーカーアプリのGUIとAIを Godot で実装し、それらを解説した。 GUI要素は依存性なく部品化され、他から容易に利用できるものになっていると考えている。 AI はかなり原始的なものであったが、一応ちゃんとプレイできるものであり、 ポーカーAIアプリに興味ある人には最初のとっかかりとして参考になったのではないかと思っている。

なお、次の連載では、本格的なポーカーAIについて解説予定だ。 ただし、ノーリミットN人ホールデムAIは実装難易度が非常に高いので、クーン・ポーカーを題材にする。 乞うご期待だ。

TechProjin Godot入門 関連連載リンク

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

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