マガジン

対戦ゲームの投了・時間切れ・無効試合の扱い

遊びをつくる

執筆にClaudeを使用しています。

ゲームを遊んだ具体的な場面を三枚の絵札にし、作者へ届く封筒へ添える情景。
記事のテーマをもとに制作したAI生成イラスト。

対戦ゲームを実装するとき、勝敗の決め方を「どちらかがゼロになったら負け」だけで済ませてしまうと、後から必ず問題が起きます。片方のプレイヤーが画面を閉じた、時間内に操作しなかった、通信が途絶した——こうした「正常ではない終わり方」をどう処理するかは、ゲームの公平感と継続率に直結します。 この記事では、投了・時間切れ・無効試合という三つのシナリオそれぞれについて、ゲームロジックとして何を決めておくべきか、実装の順序、そして失敗しやすい判断ポイントを具体的に整理します。通信方式(WebSocketかポーリングか)の選び方よりも、その上に乗るゲームルールとステート管理の話に絞ります。

この記事の目次
  1. 投了をゲームのルールとして正しく定義する
  2. 時間切れの設計:どのタイマーをどこで管理するか
  3. 無効試合の定義と処理方針
  4. 切断と意図的な離脱を区別する
  5. 実装の順序と優先度
  6. 次の行動

投了をゲームのルールとして正しく定義する

投了は「自発的な敗北宣言」ですが、これを曖昧にしたまま実装すると「本当に投了したのか、誤操作か」という問題が残ります。最初に決めておく項目は三つです。

投了できるタイミング:自分の手番中だけか、相手の手番中でも許可するか。チェスや囲碁系なら相手の手番中でも投了を受け付けるのが自然です。アクションゲームなら任意のタイミングで受け付ける設計が多いです。どちらにするかで、投了ボタンをアクティブにする条件が変わります。

投了の確定フロー:「投了ボタン → 確認ダイアログ → 確定」の二段階を挟むかどうか。ランキングに影響するゲームでは確認ステップがほぼ必須です。確認なしにすると誤タップによるクレームが起きやすくなります。

投了後の状態遷移:投了した側は即座に「敗北」として扱い、相手側には「勝利」を通知します。このとき、サーバー側でゲームのステートを「終了済み」に書き換える処理が先で、クライアントへの通知はその後という順序を守ることが重要です。逆にすると、ネットワーク遅延のタイミングで両者が異なる状態を見る可能性があります。

実装上のよくある失敗は、投了のリクエストを受け取ったとき、相手が同時に操作していたケースを考慮しないことです。たとえば「投了ボタンを押した瞬間に相手の攻撃が届いてゲームが終わった」という競合を防ぐには、ゲーム終了を判定するロジックを一箇所に集め、最初に届いたイベントだけを有効とする排他制御(ロック)が必要です。

時間切れの設計:どのタイマーをどこで管理するか

時間切れは「計測」「通知」「判定」の三層に分けて考えると整理しやすいです。

計測はサーバーで行う

クライアント側のタイマーはあくまで表示用です。ブラウザはタブが非アクティブになるとタイマーの精度が下がり、意図的に遅延させることもできます。「残り時間」の正解値はサーバーが持ち、クライアントはそれを表示するだけという設計にします。

具体的には、手番が切り替わるたびにサーバーが「この手番の開始時刻」をタイムスタンプで記録します。プレイヤーが操作を送信してきたとき、現在時刻 − 手番開始時刻 > 制限時間 であれば、その操作は無効にして時間切れとして処理します。

通知はポーリングか、プッシュか

ポーリング(一定間隔でサーバーに状態を問い合わせる)では、問い合わせ間隔の分だけ時間切れの検出が遅れます。残り時間が数秒の局面で数秒のラグが出るのは体験として問題になります。WebSocketやServer-Sent Eventsを使えば、サーバーから即座に「時間切れになった」と通知できます。どちらを選ぶかはインフラの制約次第ですが、時間切れの演出精度を重視するならプッシュ方式が適しています。

時間切れの判定タイミング

時間切れを「操作が来たときに判定する」方式だと、両者が操作しなかった場合にゲームが永遠に終わらない問題が起きます。これを防ぐには、サーバー側に定期実行のジョブを置き、「最終操作から制限時間が過ぎているゲームを強制終了する」処理を走らせます。この処理の実行間隔が長いと、終了が遅れてリソースが残り続けます。1〜5秒程度の間隔で動かすのが現実的な目安です。

無効試合の定義と処理方針

無効試合とは、「どちらかが勝利した」でも「どちらかが投了した」でもなく、試合そのものを成立させられなかったケースです。主に三つの状況が当てはまります。

  1. ゲーム開始直後に片方が切断した:ゲームが実質的に始まっていない状態でのランキング反映は不公平です。「X手以上進んだ試合だけを有効」などの閾値を設けることが一般的です。
  2. 通信障害で双方の操作が届かなかった:どちらのせいでもない中断です。この場合に「切断した側を負けにする」ルールを適用すると、意図しない敗北が積み重なり不満が出やすいです。
  3. 不正な入力が繰り返し送られてきた:バリデーションエラーが連続する場合、試合を強制終了してどちらの勝敗にもしない判断もあります。

無効試合を設計するときに最初に決めるべきは「無効試合はランキングに影響させるか」という点です。影響させない場合、ゲーム終了時に勝敗フラグを書き込むのではなく「無効」フラグを書き込む別のパスが必要になります。最初から勝敗の二択しか想定していないデータ構造だと、後から無効試合を追加するときに大きな改修になります。設計段階で試合の結果を win / lose / draw / resign / timeout / void のように列挙型(enum)で管理しておくと、後から追加しやすくなります。

切断と意図的な離脱を区別する

切断と投了は発生原因が違いますが、ゲームエンジン側からは「接続が切れた」という同じシグナルに見えます。これを区別するかどうかで、ペナルティの設計が変わります。

よく使われる方法は猶予時間(リコネクト猶予)の設定です。切断を検知してから一定時間(例:30秒)以内に再接続すれば試合を継続できるようにします。この時間が過ぎた場合に初めて「切断による不戦敗」として扱います。猶予時間中は相手側も待機させる必要があるため、「あなたの対戦相手が切断しました。30秒待っています」という表示を出し、相手にも状況を伝えることが必要です。

猶予時間の長さはゲームの種類によります。ターン制のゲームでは30〜60秒でも許容されますが、リアルタイムアクションでは体験が壊れます。アクション系では猶予を短くするか、切断した時点で試合終了とするほうが自然な場合もあります。

また、意図的な離脱(投了せずにブラウザを閉じる行為)を繰り返すプレイヤーへのペナルティを設ける場合は、「投了」「切断後タイムアウト」「切断後リコネクト」を別々のカウンターで記録しておく必要があります。この記録がないと、全部を同じ扱いにするしかなくなります。

実装の順序と優先度

実際に作るときの手順として、次の順序が安全です。

  1. ゲーム終了の結果を表現するデータ構造(上述の列挙型)を最初に定義する
  2. 正常な勝敗(ゲームロジックによる終了)を実装して動かす
  3. 投了を追加する(ステート遷移と競合制御を含む)
  4. 時間切れを追加する(サーバー側タイマーと強制終了ジョブ)
  5. 切断検知と猶予時間を追加する
  6. 無効試合の判定を追加する

4と5を先にやりたくなりますが、正常終了のロジックが固まっていないと、異常系の処理が正常系に干渉して原因不明のバグが出ます。異常系は正常系の後に積み上げる順序を守るのが、デバッグコストを下げる最大のポイントです。

次の行動

まず自分のゲームで「試合が終わりうるケース」をすべて書き出し、それぞれに void を含む結果フラグが必要かどうかを確認してください。その一覧ができたら、投了のステート遷移だけを先に実装して動作確認し、時間切れ・切断の順に追加していくと問題が起きても原因を特定しやすくなります。実装したゲームをアソバースに投稿する際は、ZIPファイルでまとめてアップロードするか、外部ホスティングのURLを登録する方法が使えます。