はしのした


2024-11-29 オンラインビンゴ!

ぷよ橋恒例のビンゴ企画をオンライン対応させたよ、という話。

  りきりきりきりきりきなんなん

ビーンゴー!(元ネタの曲知っている人どれくらいいるんだろ……)

ぷよ橋では、これまでにも数回ビンゴ企画を開いておりまして。 先日開催したぷよ橋8では、5年ぶりに企画を復活させたわけです。 ただ、今まで通りやるだけでは芸がないので、今回はビンゴカードをオンライン化して、各自のスマホのブラウザからもビンゴに参加できるようにしました! ちょうど仕事でも技術的に似たようなことやる必要あったし……

ということで、オンラインビンゴカードシステムをどう作ったか、記事にまとめておこうと思います。

全体的なアーキテクチャ

アーキテクチャ概要

今回のオンラインビンゴカードシステムの全体像を図示します。 簡潔に言えば、フロントエンドは JavaScript(JS)で、バックエンドは Ruby の CGI で、それぞれ書かれています。

司会者は抽選用のページに、参加者はカード表示用のページに、それぞれアクセスします。 ページから読み込まれる JS スクリプトには、抽選用・カード表示用・共通の3つの種類があります。 共通の JS は両方のページから読み込まれますが、残りの JS はそれぞれのページからのみ読み込まれます。 抽選用 JS は、司会者が抽選などの操作を行うと、バックエンドの抽選用 CGI スクリプトを呼び出します。 抽選用 CGI は、どの番号がいつ引かれたのかの情報を含んだルームデータを、テキストファイルとして書き出します。 カード表示用 JS はこのルームデータを読み取って、引かれた番号にマークをつけたり、ビンゴしたかどうかの判定を行います。

この設計は、ビンゴ実施中のサーバへの負荷を極力減らすことを意識したものになっています。 ルームデータへのアクセスの頻度は、参加者の人数に比例します。 しかし、このときサーバがすべきことは、数百バイトほどのテキストファイルを送ることのみです。 CGI スクリプトは、参加者側から呼び出されることはありません。

ただ、このことは同時に、参加者のカードデータをサーバに送ることはできないということも意味します。 司会者側の画面で参加者のリーチやビンゴの状況を確認する、などといった機能もあると楽しそうです。 しかしそこは、今回は設計の時点で割り切って諦めました。

フロントエンドの実装

まずは、フロントエンド部分の JavaScript について。 分量的には、カード表示用が計650行ほど、抽選用が350行ほど、共通が450行ほどで、計1,450行ほどです。 過去の回で使ってたスクリプトは計450行(抽選用300行、カード画像作成用150行)ほどだったので、3倍ほどに増えました……。 作らないといけない画面の数が増えたので、致し方ないところなのですが。

これまで作ってきた抽選ページ(や、ぷよ橋5で作った COOL24+24)は、特にフレームワークなども使わない、素の JavaScript を使っていました。 今回は、もう少しリッチな表現を求めて、Phaser というゲームフレームワークを採用しました。 GUI で開発ができるシーンエディタなどの有料機能もあるようですが、自力でコードを書く分には無料で使えます。 コードを書くのに十分なドキュメントやサンプル、リファレンスマニュアルもしっかり整備されています(ここが足りなくて利用を諦めたフレームワークもあります……)。 最低限の環境を整えるのも簡単で、基本的には 外部の JavaScript を読み込む HTML を用意して、適当な Web サーバ(Python 付属の簡易サーバ機能で十分)経由でブラウザに読ませるだけです。 そんな感じで、試行錯誤こそ必要だったものの、比較的すんなりと開発は進みました。

Phaser では、1つの画面(シーン)は Phaser.Scene を継承したクラスで定義します。 このクラスでは、コンストラクタ、および create() と update() の2つのメソッドを継承します。 コンストラクタでは、一意なシーン名を key に加えて継承元のコンストラクタを呼びます。 create() ではシーンの始めに1度だけ実行される処理、update() ではフレームごとに実行される処理を、それぞれ記述します。 ですので、基本的には create() でシーン上のオブジェクトを用意し、update() の中で入力のチェックとオブジェクトの操作を行えば、最低限動くものは作れることになります。

実際には、ボタンクリックなどの操作をきっかけに処理を行いたい場合があります。 この場合、オブジェクトにイベントリスナを設定することで対応できます。 具体的には、対象のオブジェクトの setInteractive() メソッドでクリック判定の領域を設定したあと、on() メソッドでイベントリスナを設定します。 今回はクリック可能なボタンが必要でしたので、ボタンを Phaser.GameObjects.Layer を継承したクラスとして、共通 JS の方で定義しています。 ボタンをクリックしたときにコールバックされる関数は、シーンのメソッドとして定義した上で、シーンに bind してからボタンのコンストラクタに渡しています。 (と、書いてて気づきましたが、on() メソッドの第3引数に context ってのがあったので、そこでシーンを指定してしまえば、bind は必要なかったかもしれません)

また、ちょっとしたアニメーションや色の変更など、毎回 update() の中で計算して動かすには面倒なものもあります。 これを楽にするための仕組みに Tween があります。 Tween をシーンに登録しておくと、経過時間をもとに一定の計算式でオブジェクトのプロパティを自動更新してくれます。 特に今回は、具体的なオブジェクトへの割り当てが必要ない、Number Tween というものを使いました。 経過時間を 0~1 の形で(あらかじめ指定した時間で割り算して)数えるとともに、フレームごとに onUpdate に登録したコールバックを呼び出してくれます。 また、指定した時間が経過すると、onComplete に登録したコールバックを呼ぶこともできます。 今回、カードを表示するシーンで、選ばれたマスをしばらく虹色で光らせたり、各ラインがビンゴにどれくらい近いかをアニメーションで表示したりといった処理に、この Number Tween を使いました。 その結果、このシーンでの update() の処理は、10秒おきにルームデータを読み込んだり、その結果を1秒おきに反映させたり(後述します)といった処理を書くだけになり、描画周りの処理が完全に分割できました。

カード情報の符号化

1度カードを作成したら、ブラウザをリロードなどしたときも同じカード情報で再開できることが望ましいです。 よって、カードが作成された時点で Cookie にカード情報を保存することにしました。 保存するカード情報は、各マスの番号をそのまま符号化するのも1つの手です。 しかし今回は、カードを再現するのに必要最小限の情報だけをアルファベット大文字(26種類)の文字列に符号化して、それをカード ID とすることとしました。 こうすれば、カード作成時と再開時とで、同じプログラムをそのまま使えます。 また、まさかこんな企画でズルをする人はいないと思いますが、原理上はズルもしにくくなるはずです。

具体的には、必要な情報は以下の通りとなります。

  • 巨大マスの配置される場所(16通り)
  • 巨大マスの番号(↑によって10通り or 20通り)
  • 乱数の種(2^32 通り)

巨大マスに関する情報はそれぞれ1文字で、乱数の種は7文字(∵ 7 x log2(26) ≒ 32.9)で、それぞれ符号化します。 カードの ID は9文字で表現されることになります。

バックエンドの実装

バックエンド部分は、約100行の Ruby スクリプトです。 ビンゴの進行状況はルームごとに管理され、ルームは数字4桁のルーム ID で識別されます。 入力はルーム ID と、そのルームに対して行う操作の種類です。 また、操作の結果として出力・更新されたルームデータを、そのまま出力します。

ルームデータは、以下に示すようなシンプルなテキストファイルです。

11234 1728190154
249 413
314 424
437 439
50 0

ルームデータの各行は、スペースで区切られた2つの数字からなります。 最初の行には、ルーム ID と、そのルームが開設された時刻(UNIX 時間で)が書かれます。 それ以外の行には、抽選された番号と、その時刻(ルームの開設時刻からの秒数)が書かれます。 ただし、ルームを閉鎖した場合には、両方に 0 が書かれます。

抽選操作が行われると、まだ選ばれていない数字を1つ選び、ルームデータの末尾に書き込みます。 このとき、抽選された時刻には、実際の時刻から少し(具体的には12秒)遅れた時間を記録しています。 カード表示用のページでは、1秒おきに現在時刻と抽選された時刻とを比較し、現在時刻が抽選された時刻になったとき初めて抽選結果を画面に反映させます。 これにより、カード表示用のページからルームデータを読み込む頻度を最小限(10秒に1回)に抑えつつ、抽選用のページで抽選された番号がわかるのと同時にカード表示用のページに反映させる、という動作を実現しています。

まとめ

ということで、ぷよ橋8で使われたオンラインビンゴカードシステムの紹介でした。 ソースコード一式はこちらに New BSD ライセンスで置いておきますので、無保証であることを承知いただけるのであれば、ご自由にお使いください。