マルチテーブルトーナメント(MTT)の実装方法

確率論

概要

 テキサスホールデムなどポーカーはゲームのルールは複雑だが、個人での開発でもゲームの実装が可能であるし、それをやってみようと思い立ってしまったのがここ最近の筆者であった(ゲームのルールなどもパブリックドメインであり著作権や特許権などの問題も無いようである)。商業的なポーカーのゲームだと多くの場合、マルチテーブルトーナメント(MTT)というゲームの機能が実装されている。日本のゲームであれば、エムホールデムやポーカーチェイスなどのゲームをプレイしている人であれば、どういったルールのゲームであるかは知っているだろう。

 このマルチテーブルトーナメント(MTT)という機能の実装が、筆者にはなかなかに困難なものであるように思える。この辺りの機能が個人(趣味)と組織(事業)を分ける壁になってきそうである。筆者は個人開発なので大掛かりな機能は実現が難しいのであるが、MTTの実装までやって、”壁を超えてやったぜ”と言えるように、なんとか挑戦したいという気になっている。

 本稿は、実装に向けての頭の整理のために、こうやって実装したらいいんじゃないかという筆者の考えをまとめたものである。自分で考えたもの(ChatGPTは使ったけど)であるため、効率が良いアプローチではないかもしれないが、一応一通りメモしておこうと思う。

マルチプレイ機能

 マルチテーブルトーナメントの前に、複数のプレイヤーでデータを共有しながらポーカーをプレイするためには、どのようにすればよいかを先に簡単に述べておこう。シングルプレイでNPCを相手にゲームをプレイする場合は、基本的にスマホ内部の処理だけで完結できる。そのため、クライアントの機能だけ実装すればよいことになる。一方で、マルチプレイでポーカーをプレイする場合は、手札や共通カードの情報、各プレイヤーのアクションなどを複数の端末でリアルタイムで共有する必要がある。

 データを複数のスマホで共有するには、スマホとは別にサーバを立てる必要があり、サーバとの通信を経由してスマホ間でデータの共有を行う。サーバを立てればいいと簡単に書いたが、おそらく実際にサービスを始める際はレンタルサーバーを借りるかAWSやGoogle Cloudなどのクラウドサービスを使うかする必要がありそうである。月に5000~10000円ぐらいかかりそうということで、趣味とはいえ痛い出費になりそうである。(個人でもプロバイダーとの契約で固定IPアドレスでファイアウォールに穴をあけてもいい場合は自宅のPCをサーバにすることもできそうだが、これは適切な構成であるようには思えないし、普通にアプリの審査とかで落とされそう・・・)

 データを共有するためには通信処理を行う必要があるが、これはWebSocketと言われるインターネットと同じ仕組みの通信方法で処理する。ホームページのデータをダウンロードして表示する処理は非同期処理であるが、同じプロトコルで同期処理できるようにしたものがWebSocketであるらしい。実装自体はそれほど難しくなく、サーバー開発の本を片手に実装してみたら1週間ぐらいで複数の端末でデータを共有してポーカーをプレイできるようにプログラムの開発ができてしまった。ここまでの機能ができていることを前提にマルチテーブルトーナメントをどのように実装すればよいかを以下では考察していく。

数千の同時接続をどのように扱うか

 マルチテーブルトーナメントは人数が多いと数千人から1万人ぐらいが参加することがもあるようである(エムホールデムやポーカーチェイスでも1000人前後の人が参加していることが多いように見える)。もちろん、筆者が開発するゲームはそんな人数が参加する見込みは皆無であるけど、技術的にどうやって処理しているのかは興味が湧く部分である。負荷分散の仕組みが必要でありそうであり、複数のサーバーで並列処理する場合、なんらかの特殊な処理が必要なのではないかという推測をした。

 というわけで、ChatGPTに数千の同時接続を処理するマルチテーブルトーナメントはどのように負荷分散すればよいか、仕組みを教えてほしいと質問してみた。回答は想定していた回答とは異なる内容であり、

 ”数千接続ぐらいであれば、そこそこのスペックのサーバー1台で楽勝です”

とのことだった。ポーカーはアクション性が低く、CPUやメモリをほとんど消費しないため、通信の接続数が最もボトルネックになるらしいが、これはサーバーのスペックとはそれほど関係がなく大体1~3万接続ぐらいまでは対応できるらしい。ちなみに、ポーカーよりアクション性が高いMMORPGなどでも1台のサーバで3000~5000ぐらいの同時接続を運用するのが一般的であるらしい。ポーカーでは、1万人がプレイするMTTぐらいであれば1台のサーバに集約して運用することができるようである。

 並列処理が必要な気がしていたのに、いらないと言われてしまって当てが外れたのであるが、一応負荷分散するとしたらどのような構成になるか聞いてみた。いくつか構成を教えてもらったが、基本は

 ”LB + スティッキー + 軽量バックプレーン”

という構成であるらしい。LBはロードバランサーのことでユーザIDやIPアドレスに応じて、接続先のサーバを選択して自動で分散処理をする仕組み、スティッキーはユーザごとに接続するサーバを固定すること、バックプレーンはサーバ間で通信を行いメモリのデータを共有する仕組みであるらしい(Redis Pub/SubやElastiCasheという仕組みを使うらしい)。

 基本的にはクライアントアプリケーションはどのサーバに接続するかということは意識せず、サーバ同士のデータ共有側(バックプレーン)で操作をする。例えば、テーブルを解体して、新しくテーブルを結合するような処理がサーバを跨いでしまう場合は、バックプレーン側でこのユーザを別のサーバに移動(切断+再接続)などの指示を出すイメージであるらしい。指示を受けたサーバがクライアントに通知、クライアント側で古い接続の切断、新しいサーバへの再接続という流れで実現可能とのことだった。

 まあ、聞くだけ聞いたけど、筆者の場合はむしろサーバは1台で十分という気がしてしまった。数千接続が1台で問題ないならば、筆者のアプリで問題になることはまずないだろう・・・

機能一覧

 インフラ面は余計なことをいろいろ考えたが杞憂でありそうなので、MTTに必要そうな機能を洗い出していこう。特徴的な機能は以下の通りである。

  1. 時間:募集開始時間、ゲーム開始時間、登録受付(再エントリー)終了時間
  2. スタックやブラインドのルール
  3. 時間経過によるストラクチャーレベルアップ(ブラインド、アンティ)
  4. 再エントリー可能回数
  5. 1テーブル当たりのプレイヤー数のルール(3人以上9人以下など)
  6. スタックの分布や残りプレイヤー数に応じたTable Rebalancing
  7. プレイヤーの操作時間の管理(1アクションごとの時間、延長の可不可)
  8. 準リアルタイムによる統計情報の表示、最終的なランキングの掲示

大体こんなところだろうか。サーバは1台でよさそうと書いたが、以下のような場合のルールも考える必要があるかもしれない。

  1. 途中退場:プレイヤーがギブアップして途中退場する場合の扱い
  2. 操作の放置:操作せずに時間切れをする場合、何回まで許容するか
  3. 通信の切断:通信が切断された場合に再接続を許可するか、する場合はどの程度の時間まで許容するか。
  4. 通信が切れているプレイヤーの取り扱い:Check or Foldによる自動操作やどの程度の時間で自動操作にするか。一定時間経過時に退場扱いにするなど。

ソーシャルゲームなどでは賞金の構造なども定義する必要があるだろうが、筆者のゲームではチップには何の価値もないため、自己満足以外の報酬はない(まあ、ソーシャルゲームも本当は自己満足以外何もない気もするけれど・・・)ので無視していいだろう。ただし、最終的なランキング結果はデータベースに保存して一定期間参照できるようにした方が良いかもしれない。個々の項目は適当なルールを定めて、地道に実装していけばよさそうであるが、最後にTable Rebalancingのアルゴリズムだけ簡単に考察して終わりにしよう。

Table Rebalancing

 マルチテーブルトーナメントでは、スタックをすべて失ったプレイヤーから脱落していくという性質があるため、テーブルの人数が規定数以下になった場合はテーブルの再構成を行う。あまり多くの頻度で再構成を行うと対戦相手の戦略を読むということが不可能になり、ただの運ゲー化してしまうため、可能な限りは既存のテーブルを維持するように構成した方が良いだろう。また、スタックに差がありすぎるとスタックが多いプレイヤーが有利になってしまうため、再構成する場合、同じテーブルに含めるプレイヤーはスタック(順位)が近いプレイヤーを選択する方が良いということになる。

 上記の条件を考慮して、Table Rebalancingのアルゴリズムを試しに考えてみよう。

  1. テーブルのプレイヤー数が規定数を下回った場合、テーブルを解散し、プレイヤーを待機リストに追加する。
  2. 待機リストのプレイヤー数がテーブルの規定数を上回っており、かつ、スタックの差が一定の範囲内である場合(最大と最小で5倍以内など)、スタックが多い順に待機リストからプレイヤーを抽出して新しくテーブルを作成する。
  3. 待機リストにはスタックの条件ではじかれたプレイヤーと端数のプレイヤーが残っているので、既存のテーブルで空きがあるテーブルにアサインする。スタックが少ないプレーヤーは空きがあるテーブルのうち、最大のスタックを持つプレイヤーのスタックが最小のテーブルにアサインする(min-maxルール)。
  4. これでもアサインできないプレイヤーは条件を満たすまで待機リストに保持する。
  5. 各テーブルにおいてラウンドが終了するたびに1.~4.の処理を実行する。

注意点として、2.で新しく作ったテーブルは3.の処理が終わるまで開始しない方が良いかもしれない。スタックの条件ではじいたけど、min-maxルールでアサインしたら結局そのテーブルにアサインされる可能性もあるためである。実際に実装してみて不自然に感じるようであれば、後々修正するようにしようと思う(注:ブログ公開時点で筆者はまだこの機能を実装していない)。