【風船割りチキンレース1.0】作りました【開発ノート】

開発

「何回でもシコシコしてよくてでも最低一回はシコってしなきゃいけなくて限界に達した人が負けっていうゲーム」というWebゲームを作りました。
元ネタは「かぐや様は告らせたい」に出てきたやつです。

名前だけ見るとだいぶ怪しいですが、このゲームでいう「シコってする」は、空気入れで風船に空気を入れることです。ルールを一言で表すなら、複数人で遊べる風船割りチキンレースです。

・プレイ:

何回でもシコシコしてよくてでも最低一回はシコってしなきゃいけなくて限界に達した人が負けっていうゲーム
空気入れで風船を膨らませる、オンライン対応チキンレース。爆発させた人が負け。

この記事では、ゲームの企画、マルチプレイの仕組み、確率の持ち方、スマートフォン向けUI、デプロイまでを振り返ります。

どんなゲームか

プレイヤーは自分のターンに「膨らませる」ボタンを押します。毎ターン最低1回は押さなければならず、2回目からは、そのまま欲張るか次の人へパスするかを選べます。

押すたびに風船は少しずつ大きくなり、爆発する確率も上がります。爆発させたプレイヤーが負けです。

確率の上昇量は、ゲームが進むほど大きくなります。

  • 1〜50回目:1回につき0.01%上昇
  • 51〜149回目:1回につき0.1%上昇
  • 150回目以降:1回につき0.2%上昇

さらに、各ターンの開始時には3%の確率でハプニングが発生します。

  • 「強制」:ランダムに指定された2〜5回を押すまでパスできない
  • 「パワフルな空気入れ」:そのターンの確率上昇量が2倍になる
  • 「クソデカい空気入れ」:1回で1〜5%上昇し、そのまま次の人へ手番が移る

通常のチキンレースに、ときどき計画を壊すイベントを入れることで、終盤まで展開が固定されないようにしました。

技術構成

今回は、個人開発でも運用しやすく、リアルタイム対戦を比較的少ない構成で実現できる組み合わせを選びました。

役割使用技術
フロントエンドNext.js 16 / React 19 / TypeScript
データベースSupabase Postgres
リアルタイム同期Supabase Realtime
QRコード生成qrcode
テストNode.js Test Runner / tsx
ホスティングRender
ソース管理GitHub

ソロプレイはブラウザ内のReact stateだけで進行します。マルチプレイでは、部屋・プレイヤー・手番・現在の爆発確率をSupabaseへ保存し、更新をRealtimeで各参加者へ配信しています。

マルチプレイで大事にしたこと

判定をブラウザ任せにしない

マルチプレイでは、「誰の手番か」「パスできる状態か」「今回爆発したか」といった重要な判定をPostgresの関数にまとめました。

ブラウザは inflate_balloonpass_turn といったRPCを呼び出すだけです。データベース側では、プレイヤートークン、現在の手番、最低プッシュ回数などを検証してから状態を更新します。

膨らませる処理では対象の部屋を FOR UPDATE でロックしています。複数回のタップや通信タイミングの重なりが起きても、同じ状態を同時に更新しにくい構造にしました。

プレイヤーごとのトークン

このゲームにはアカウント登録がありません。その代わり、部屋への参加時にプレイヤー固有のランダムトークンを生成し、ブラウザのlocalStorageへ保存します。

データベースにはトークンそのものではなくハッシュを保存し、RPC実行時に照合します。気軽に参加できる体験を維持しつつ、別の参加者が他人の手番を操作しにくいようにしています。

Realtimeは表示の同期に使う

部屋やプレイヤーの状態が更新されると、Supabase Realtimeから各ブラウザへ変更が届きます。ゲームの正解となる状態はデータベースに置き、Realtimeはその変更をすばやく画面へ反映するために使いました。

これにより、参加者の入室、手番の移動、爆発、再戦などが全員の画面へ同期されます。

確率は整数で持つ

爆発確率は、小数ではなくbasis point単位の整数で管理しています。

たとえば、0.01%は 1、1%は 100、100%は 10000 です。小数を何度も足す方式にすると丸め誤差を意識する必要がありますが、整数なら50回目・51回目・150回目といった境界も明確に扱えます。

爆発判定は、今回の上昇量を加えたあとの確率で毎回行います。表示時だけ100で割り、小数点以下2桁へ整形しています。

この仕様はテストでも境界値を固定して確認しています。

  • 50回目までは0.01%ずつ
  • 51回目から0.1%ずつ
  • 150回目から0.2%ずつ
  • パワフル時は上昇量が2倍
  • クソデカ時は1〜5%
  • ハプニング判定は3%未満

ランダム処理そのものを完全に予測するテストは難しいため、乱数関数を外から渡せる形にして、特定の値を返すテスト用関数で分岐を確認しました。

300個のランダムニックネーム

ゲーム開始前の入力をできるだけ軽くするため、ニックネームを自動で決めるサイコロボタンを用意しました。

候補には「ちくわ」「ぬるま湯」「改札口」「自称プロ」「野次馬」など、食べ物、動物、日用品、場所、状態を混ぜた日本語300語を収録しています。強そうな名前に寄せず、有象無象が集まった感じを狙いました。

ここもテストで、候補がちょうど300語あることと、重複がないことを確認しています。

QRコードで参加の手間を減らす

部屋を作ると、部屋コードを含むURLのQRコードが表示されます。近くにいる人はQRコードを読み取るだけで、部屋コードが入力された状態のトップ画面を開けます。

また、トップ画面にもゲーム自体を共有するQRボタンを追加しました。「この場で遊んでもらう」までの手順をできるだけ短くするための機能です。

SNSで共有されたときに何のページかわかるよう、Open GraphとX向けのタイトル・説明・共有画像も設定しています。長いタイトルを共有画像へ入れる作業は、ゲーム本体とは別方向の難しさがありました。

スマートフォンの一画面へ収める

このゲームは、PCよりも友人同士がスマートフォンを持ち寄って遊ぶ場面を想定しています。そのため、トップ画面は375×667の表示領域でも縦スクロールが発生しないように調整しました。

長いタイトルは、ブラウザ任せで折り返すと中途半端な位置で切れてしまいます。そこで、意味のまとまりに合わせて7行へ固定しました。

画面内に必要な操作を残すため、説明文を削り、ニックネーム入力、マルチプレイ、ソロプレイ、部屋参加に情報を絞っています。高さには 100dvh を使い、ナビゲーション・操作部分・フッターをグリッドで配置しました。

ゲーム画面では、パスボタンを途中から表示する方式をやめ、常に同じ位置へ表示しています。まだパスできないときは暗い色とロックマークで無効状態を示し、最低回数を押すと解除されます。ボタンが突然現れてレイアウトが動くことを防ぎ、誤タッチもしにくくなりました。

SEはWeb Audio APIで作った

「膨らませる」を押したときの空気入れの音と、風船が割れたときの音はWeb Audio APIで生成しています。

短い効果音のために音声ファイルを追加せず、オシレーターやノイズを組み合わせてブラウザ内で鳴らす方式です。ゲームの容量を増やしにくく、音の長さや変化もコードから調整できます。

ソロプレイのCPU

マルチプレイの動作確認を一人でもできるよう、プレイヤー1人とCPU3人のソロモードも作りました。

CPUも最低回数を守って風船を膨らませます。条件を満たしたあとは、一定の確率でパスするか、さらに押すかを決めます。高度なAIではありませんが、待ち時間を少しランダムにすることで、機械的すぎないテンポにしています。

Renderへのデプロイ

GitHubの main ブランチとRenderを接続し、push後に自動デプロイされる構成にしました。

RenderではNext.js用のビルド・起動コマンドを使い、SupabaseのProject URLとanon keyは環境変数として設定します。リポジトリには値そのものを含めず、必要な変数名だけを .env.examplerender.yaml に記載しています。

データベース側の変更はSQLマイグレーションとして残しました。初期スキーマとは別に、ハプニング率を3%へ変更する追加マイグレーションも用意しています。コードと本番データベースの仕様がずれないようにするためです。

開発中に難しかったこと

一番難しかったのは、派手な画面を作ることよりも、ゲームの状態をどこで正しく管理するかでした。

ブラウザだけでマルチプレイの判定をすると、連打や通信遅延で状態が食い違う可能性があります。そこで、マルチプレイのルールはPostgres関数へ集約し、フロントエンドは操作と表示を担当する形にしました。

もう一つは、長いタイトルとスマートフォン画面の両立です。タイトル自体がゲームの個性なので短くはできません。一方で、説明を詰め込みすぎると遊び始めるまでにスクロールが必要になります。最終的には、タイトルの改行を固定し、ゲーム開始に不要な説明を大胆に削ることで収めました。

また、最初はハプニング率を1%にしていましたが、実際のプレイでは存在を感じにくかったため3%へ変更しました。数字を決めて終わりではなく、遊んだときのテンポに合わせて調整することの大切さを感じました。

テストと確認

公開前には次の確認を行っています。

  • ゲームロジックの自動テスト
  • ESLint
  • Renderと同じNext.js本番ビルド
  • 375×667でトップ画面に縦スクロールがないこと
  • QR共有モーダルと問い合わせ先の表示
  • パスボタンが最初はロックされ、必要回数後に解除されること
  • Supabaseで追加マイグレーションが正常に実行できること

確率ゲームでは、見た目が動いているだけでは境界のバグを見落としやすいため、50・51・149・150回目のような切り替わり地点を自動テストにしています。

今後やりたいこと

現時点でも一通り遊べますが、今後は次のような改善を考えています。

  • ルームの自動削除や退出処理の強化
  • 通信が切れたプレイヤーへの復帰案内
  • 戦績や連勝記録
  • ハプニングの追加
  • CPUごとの性格付け
  • 効果音のオン・オフを保存
  • 実際のプレイデータを見ながら確率バランスを調整
  • E2Eテストの追加

おわりに

ルールは「押すか、逃げるか」だけですが、最低1回の制約、少しずつ上がる爆発確率、ランダムなハプニングを組み合わせることで、短時間でも盛り上がるゲームになりました。

コメント

タイトルとURLをコピーしました