# オンライン対戦は、端末の言うことを何も信用しない：手・持ち時間・勝敗を、サーバーが決める設計

> ログイン無しのオンライン対戦を作った。手の検証をルールエンジンに通す、持ち時間をサーバーだけが持つ、切断は 30 秒待つ、署名つきトークンで名乗りを防ぐ、レーティングは対局前の相手の数字で計算する。コードの実際の値つき。

- URL: https://koshinstudio.com/notes/negaeri-online/
- 公開: 2026-10-17
- 書いた人: Koshin（https://koshinstudio.com/about/）
- サイト: Koshin Studio

  English version (https://koshinstudio.com/en/notes/negaeri-online/)

寝返り将棋 制作の記録 ／ 2026年9月

  寝返り将棋 (https://koshinstudio.com/works/negaeri-shogi/)には、知らない人と対戦するオンライン対戦があります。登録もログインもありません。ボタンを押すと、同じように待っている人と組まれます。

  ネットで対戦するゲームを作るときに、最初に決めたことがあります。端末（クライアント）の言うことは、何も信用しない、です。端末は、持ち主が好きなように書き換えられます。「今の盤はこうです」「私の残り時間は 2 分です」「私が勝ちました」と、嘘を送ることができます。この記事は、そう言われても困らないように、サーバーをどう作ったかの記録です。数字と仕組みは、サーバーのコードに書かれている、実際の値です。

  これは、仕組みの記録です。人どうしの対局での釣り合いは、まだ十分には確かめられていません（ルールを AI 同士の対局で決めた記録 (https://koshinstudio.com/notes/negaeri-balance/)を参照）。

## 原則：サーバーが決める。端末は「やりたい手」を送るだけ

  端末がサーバーに送れるのは、次の 3 種類のメッセージだけです。

- join：この対局に参加します（署名つきの身分証を添えて）。

- move：この手を指します。

- resign：投了します。

  盤の状態も、残り時間も、勝敗も、端末からは送れません。送られてきた手は、サーバーが持っている盤に、ルールエンジンで当てはめて確かめます。このルールエンジンは、画面（端末側）と同じファイルです。ルールの判定が 1 か所にしか書かれていないので、画面で「指せる」手が、サーバーでは「反則」になる、という食い違いが起きません。

## 1 対局 ＝ 1 つの小さなサーバー

  サーバーは Cloudflare Workers で動いています。1 つの対局は、Durable Object（状態を持てる小さなサーバー）1 個が受け持ちます。2 人はそこに WebSocket でつながります。対局の途中経過（手の記録と、残り時間）は、手が確定するたびに、その場で保存します。

  Durable Object は、使われていないと眠ります。眠って起き直したときには、保存してある手の記録を最初から指し直して、盤を復元します。盤そのものを保存するより、手の記録だけを保存しておくほうが、食い違いが起きません。

## 手を受け取ったときに、サーバーがすること

  手が届くと、順番に、こう確かめます。

- 先に、時間切れを見る。手番の側が持ち時間を使い切っていたら、その手は通さず、時間切れ負けにします。

- 手番を見る。相手の番に送ってきた手は、「あなたの手番ではありません」と返します。

- ルールで検証する。反則の手なら、その人の負けにします（相手の勝ち。理由は「反則」）。

- 通った手を反映する。盤を進めて、手を記録に足し、時計を進めて、保存します。

- 両者に知らせる。指された手、裏返った駒の順序、連鎖の数、新しい残り時間を、2 人に送ります。

  反則の手は、黙って捨てずに、負けにしています。

## 持ち時間は、サーバーだけが持つ

  持ち時間は各 3 分、1 手ごとに 5 秒を足す方式です（フィッシャー方式）。時計はサーバーにだけあり、端末に出ている残り時間は、サーバーが送った数字を表示しているだけです。

  // 着手が確定したとき
const used = now - clock.turnStartedAt;
clock.remaining[mover] = Math.max(0, clock.remaining[mover] - used) + INCREMENT_MS;
clock.turnStartedAt = now;
  使った時間を引いて、5 秒を足して、相手の番を始めます。残りが 0 を下回らないように、Math.max(0, …) を挟んでいます。

  もう 1 つの問題は、誰も手を指さないとき、時間切れを誰が見つけるかです。相手が手を送ってこなければ、サーバーは何も動きません。そこで、アラーム（指定した時刻にサーバーを起こす仕組み）を使っています。次に起こす時刻は、「手番の側の持ち時間が尽きる時刻」と「切断の猶予が切れる時刻」の早いほうです。ただし、短すぎる間隔にならないよう、最低でも 1 秒後にしています。

## 切断は、30 秒待つ

  スマートフォンの通信は、よく切れます。電波が一瞬途切れただけで負けにするのは、ひどいので、切断されても30 秒は待ちます。その間、相手には、切断したことと、待つ時間の長さが伝わります。30 秒以内に戻ってくれば、続きから再開します。戻ってくると、サーバーは手の記録と時計の状態を全部送り直す（同期する）ので、画面は途切れた時点に追いつきます。戻らなければ、時間切れ負けです。

  同じ人が、別の画面で同じ対局を開き直した場合は、古いほうの接続を閉じます。2 つの画面から別々の手が届いて、盤が食い違うのを防ぐためです。

## 登録なしで、本人を見分ける

  ログインがないので、サーバーが「あなたは、あの ID の人です」と証明する仕組みが要ります。サーバーが ID を発行し、その ID に秘密鍵で署名したもの（HMAC-SHA256）を付けて、「ID.署名」という形の札（トークン）を渡します。端末は、これを自分の中に保存します。

  他人の ID を名乗るには、その ID の正しい署名が必要です。署名は秘密鍵がないと作れないので、名乗れません。署名の照合には、一致しているかを最後の文字まで見る比較を使っています。普通の比較は、最初に違った文字で打ち切るので、その時間の差から署名を推測される恐れがあるためです。

  この札は、端末に残っている間だけ有効です。端末のデータを消すと、別の人として扱われます。登録がない代わりの、割り切りです。

## マッチング：近いレートから探して、待つほど広げる

  待っている人のレート（実力の数字）が近い相手と組みます。探し始めは±100。待ち時間が 10 秒たつごとに、許す差を100 ずつ広げます。待たせすぎないためと、実力が近い人と当たるようにするための折衷です。

  2 人が組めるかは、どちらか一方でも「自分の許す差」に収まっていれば、組みます。長く待っている側の条件が優先されるので、待ち続けて誰とも組めない、ということが起きにくくなります。待っている人が複数いるときは、長く待っている人から順に見て、いちばんレートの近い相手を選びます。

  先手と後手は、レートの低い側を先手にしています。先手は少し有利なので、弱い側が有利な先手を持つ形になり、差が部分的に埋め合わされます。

## レーティング：Elo を、対局前の数字で

  レーティングは Elo 方式です。最初は 1500。自分の期待勝率は、次の式で出します。

  1 / (1 + 10 ** ((相手のレート - 自分のレート) / 400))
  動く幅（K 値）は、対局数が少ないうちは大きくします。30 局までは 40、100 局までは 20、それ以降は 10。実力に早く近づくためです。

  気をつけたのは、2 人のレートを順に更新しないことです。先に片方を更新してから、もう片方を計算すると、更新後の相手のレートを使ってしまい、不公平になります。両方とも、対局前のレートで計算しています。

  順位表に載るのは、10 局以上遊んだ人だけです。1 局だけ遊んで、運よく高いレートで止めておく、という操作を避けるためです。

## どう確かめているか

- 単体テスト：ルール、AI、サーバー側の持ち時間・レーティング・マッチング・署名を、126 件のテストで確認しています。

- 通しのテスト：ブラウザを 2 つ立ち上げて、実際にオンライン対局を成立させ、サーバーに直接つないだ動作も確かめるテストがあります。

  まだ確かめていないこともあります。実際に知らない人どうしが大勢で遊んだときの挙動と、先手と後手の勝率の差（AI 同士の対局では 61% (https://koshinstudio.com/notes/negaeri-balance/)）が、人間でも同じかどうかです。

## 分かったこと

> 端末の申告は、すべて「お願い」。決めるのは、サーバー。

- 時間も、盤も、勝敗も、サーバーが持つ。端末に持たせた瞬間に、書き換えられます。

- ルールの判定は、画面とサーバーで同じものを使う。別々に書くと、いつか食い違います。

- 切れることを前提にする。スマートフォンの通信は切れます。待つ時間と、戻ってきたときの復元を、先に決めておきます。

- 順に更新すると、不公平になる。レーティングは、両者とも対局前の数字で計算します。

  この記事の数字（持ち時間、猶予、マッチングの幅、K 値、テストの件数）は、寝返り将棋のサーバーのコードと README から書きました。遊べるのはこちら (https://koshinstudio.com/play/negaeri-shogi/)です。

  この話に出てくるもの

- 寝返り将棋 (https://koshinstudio.com/works/negaeri-shogi/) — 6×6 の盤で、挟まれた駒が敵に寝返り、出世する将棋

- 寝返り将棋のルールを、AI 同士の対局で決めた (https://koshinstudio.com/notes/negaeri-balance/) — 同じゲームの、ルールの調整の記録

- 10秒ピッタリのランキングを、公平にする (https://koshinstudio.com/notes/stopwatch-rank/) — 同じ作者の、別のゲームのランキングの設計
