Developer

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

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

目次

  • はじめに
  • 画面構成要素
  • タイトルバー・テーブル・背景
  • 移動可能チップ

はじめに

本稿では、下図のような「テキサスホールデムポーカー」アプリの実装について解説する。

日本人であればテキサスホールデムポーカー(以下、ホールデムと略す)を知らない・プレイしたことがない人がほとんどだと思う。 ホールデムはポーカーのプレイ方法のひとつで、ワールドワイドには最もポピュラーなものだ。 とくに、カジノでポーカーと言えば、ほぼ確実にホールデムなので、将来カジノデビューし、ポーカーをプレイしてみたいと思っている人は、 この機会に覚えてみるのもいいかもしれない。

で、ホールデムでは、最初は上図のように各プレイヤーに2枚の手札が配られる。当然、手札はそのプレイヤーだけが見ることが出来、 他のプレイヤーの手札は見ることが出来ない。手札だけの状態を「プリフロップ」と呼び、ここで最初の賭けが行われる。 賭けが成立すると、デッキから場に3枚のカード、これを共有カード(英語ではコミュニティカード)と呼ぶ、が出される。 この状態を「フロップ」と呼び、2回めの賭けが行われる。ついで4枚目(ターン)、5枚目(リバー)が場に出され、 それぞれ賭けを行う。これら、プリフロップ、フロップ、ターン、リバーをラウンドと呼ぶ。 全ラウンドの賭けが終わったら、各プレイヤーが手をさらす(これをショーダウンと呼ぶ)。 手札2枚と共有カード5枚、合計7枚から5枚を選び、役を作り、 強弱を判定し、最も強い役のプレイヤーが勝ちとなり、場に出されたチップ全てを得ることができる。
誰かが掛け金を上げた(これをレイズと呼ぶ)場合、降りることをフォールドという。同じ掛け金を出すことをコールと呼ぶ。 降りた人はそのハンドでの勝負に参加することはできない。
また、カジノでは「リングゲーム」と言って、参加者が自由にテーブルに出入り可能な方式を取る。 それとは異なり優勝者を決める大会では「トーナメント」と言い、途中でのテーブルの出入りは不可で、 テーブルに持ち込んだチップがなくなると破産(バースト)で試合を抜け、最後まで残った人が優勝・参加費を総取りできるという方式もある。
本アプリでは、トータル6人で行うリングゲームのみとする。手前の一人が人間プレイヤーで、残りの5人はAIだ。つまりAI対戦ということだ。
ただし、本アプリで解説する AI は非常に単純なもので、その強さは限定的だ。

テキサスホールデムポーカーに関するより詳しい解説や戦略については、解説サイトが多くあるのでそれらを参照されたし。
個人的には ポーカー道 をお勧めする。

本アプリのソースコード全体は https://github.com/vivisuke/HoldemPoker を参照のこと。

また、実際にアプリを触ってみたい方は http://vivi.dyndns.org/Godot/HoldemPoker/ 参照してみるといいだろう。

画面構成要素

本章では、画面を構成する要素について説明する。
背景・テーブル・タイトルバーといった静的なオブジェクトについて説明し、 ついで、ポーカーチップ、カード、行動表示パネルなどの動きのあるオブジェクトについて説明する。
チップ・カードは、処理終了時にシグナルを発行するので、外部依存関係が無く、 部品化に適したものになっている(シグナルに関しては後述する)。

■タイトルバー・テーブル・背景

全体背景は、グレイ系で中央を明るくグラデーションをつけた画像(下図)をペイントソフトで作成し、それを表示している。

これまで作成してきたアプリでは単色の背景だったので ColorRect を使用していたが、 単色でなくグラデーションがあるとなんとなく雰囲気が出るような気がして、このような画像を作成・使用した。
画像を表示するために、ノードクラスとしては TextureRect を使用している。

テーブルも下図の画像を用意し、TextureRect を使ってそれを表示している。

タイトルバーは ColorRect を使用し、 _draw() をオーバライドし、角を丸くし、縁取りをし、影を付けている。この部分はこれまで作ってきたアプリと同様だ。

const RADIUS = 5
func _draw():
    var style_box = StyleBoxFlat.new()      # 影、ボーダなどを描画するための矩形スタイルオブジェクト
    style_box.bg_color = color   # 矩形背景色
    style_box.border_color = Color.green
    style_box.set_border_width_all(2)
    style_box.set_corner_radius_all(RADIUS)
    style_box.shadow_offset = Vector2(0, 4)     # 影オフセット
    style_box.shadow_size = 8                   # 影(ぼかし)サイズ
    draw_style_box(style_box, Rect2(Vector2(0, 0), self.rect_size))      # style_box に設定した矩形を描画

■移動可能チップ

チップは、Sprite ノードで、下図の画像を貼り付けたものだ(実際の画像サイズは 16x16px)。

チップは、ラウンド終了時にプレイヤー下部から中央に、ショーダウン後に中央から勝者プレイヤーに移動するアニメーションを行う。
Godot にはアニメーションを行うための専用クラスがあるのだが、筆者にはいまいち使い心地がよくなかったので、 オブジェクトを移動する機構をスクリプトで自作した。

Chip.move_to(移動先, 移動時間) により、現在位置から第一引数の指定移動先位置まで、第2引数の時間(単位:秒)をかけて移動する。

移動処理自体は Chip._process(delta) で行い、 移動が終了した時は、moving_finished シグナルを発行する。

シグナルとは、GUI要素などを依存関係を作らずに部品化するためのもので、 実行時にシグナルが発行されたときに呼ばれる関数を connect() を使って動的に結合することができる。
ボタンオブジェクトをすでに使ったことがある人は、エディタ右側のノードタブで pressed を処理関数に結合したことがあると思う。 実はボタンが押下されると pressed シグナルが発行されるので、それをエディタで処理関数に結合していたというわけだ。

以下に、移動処理の実装コードを示す。

signal moving_finished

var moving = false              # 移動中フラグ
var move_dur = 0.0              # 移動所要時間(単位:秒)
var move_elapsed = 0.0          # 移動経過時間(単位:秒)
var src_pos = Vector2(0, 0)     # 移動元位置
var dst_pos = Vector2(0, 0)     # 移動先位置

func move_to(dst : Vector2, dur : float):
    src_pos = get_position()
    dst_pos = dst
    move_dur = dur
    move_elapsed = 0.0
    moving = true
func _process(delta):
    if moving:      # 移動処理中
        move_elapsed += delta   # 経過時間
        move_elapsed = min(move_elapsed, move_dur)  # 行き過ぎ防止
        var r = move_elapsed / move_dur             # 位置割合
        set_position(src_pos * (1.0 - r) + dst_pos * r)     # 位置更新
        if move_elapsed == move_dur:        # 移動終了の場合
            moving = false
            emit_signal("moving_finished")  # 移動終了シグナル発行

move_to() がコールされると、移動中フラグをONにし、現在位置、移動先位置、移動所要時間を保存する。 移動処理自体は、各フレームごとに呼ばれる _process() で行う。経過時間を更新し、オブジェクトの位置を計算し更新する。 目的位置まで移動した場合は、移動中フラグをOFFにし、moving_finished シグナルを発行(emit)している。