stet
← 記事一覧
AI

Claude Code と一緒に DEBUG-ZERO のサーバを設計したら、知らなかった技術を知った

Eiichiro Iriguchi  ·  2026.03.21  ·  8分

はじめに

自作のカードゲーム DEBUG-ZERO(数字カードと四則演算で目標値をゼロにするゲーム)のオンライン対戦サーバを設計している。

最近 Cloudflare Workers を触り始めたのだけど、「リアルタイム通信どうすればいいんだろう」という部分の設計に自信がなかったので、Claude Code に設計を任せながら進めてみた。

結果として、自分では辿り着かなかったアーキテクチャが出てきて、知らなかった技術をいくつか知ることになった。その過程を整理する。

最初に壁にぶつかった:Workers はステートレス

「Cloudflare Workers で WebSocket 対戦を作りたい」と Claude Code に伝えたところ、最初に返ってきた指摘がこれだった。

Workers はリクエストごとに起動するステートレスな実行環境なので、WebSocket の長時間接続を管理するには別の仕組みが必要です。

言われてみれば確かに、という話なのだが、自分では最初にそこを考えられていなかった。

  • 接続が続いている間、どこかに「誰が接続しているか」を保持する必要がある
  • ゲームの状態(手札・場・ターン)を複数の接続にまたがって共有する必要がある

Workers 単体では揮発するので、ここで Durable Objects というものが必要だと言われた。

Durable Objects を初めて知った

正直に言うと、Durable Objects はこの設計を通じて初めて知った。

調べてみると、Workers に紐づく永続オブジェクトで、同じ ID に対して常に同一インスタンスが保証される、という仕組みだった。「ルームIDに対してゲーム状態を保持し続ける」という用途にぴったりで、Cloudflare がまさにこのユースケースのために用意していたものだと理解した。

Claude Code が提案してきた構成のイメージはこうなる:

ルームごとに Durable Object を1つ立てる方針。これでゲーム状態が独立するので、ルームが増えても状態が混ざらない。

「接続管理とゲームロジックは分けましょう」

もう一つ、Claude Code が強めに主張してきたのがこの分離だった。

WebSocket の接続ハンドリングとゲームのドメインロジックを一箇所に書くと、後で辛くなります。MessageRouter を挟んで両者を分離しましょう。

ws/
├── ConnectionManager  接続IDとプレイヤーIDのマッピング
├── MessageRouter      受信メッセージを各サービスにルーティング
└── Broadcaster        対象プレイヤー・全員・観戦者への配信

game/
├── GameEngine         ゲーム状態遷移の中核(applyAction)
├── ActionValidator    行動の合法性チェック
└── PhaseController    フェーズ遷移制御

MessageRouter がメッセージの種別を見て適切なサービスに投げる。ゲームロジック側は WebSocket のことを知らなくていい。これは言われなければやらなかったと思う。

メッセージ設計:visibility フィールドが面白かった

メッセージの種別は client:*(クライアントからの要求)と server:*(サーバからの通知)で名前空間を分ける提案が来た。

// クライアント → サーバ(要求)
interface ClientMessage {
  id: string;          // 重複検知用UUID
  type: ClientMessageType;
  roomId: string;
  senderId: string;
  payload: unknown;
}

type ClientMessageType =
  | "client:action"
  | "client:join"
  | "client:leave";

// サーバ → クライアント(通知)
interface ServerMessage {
  id: string;
  type: ServerMessageType;
  roomId: string;
  payload: unknown;
  visibility: "all" | "player" | "spectator";
  targetPlayerId?: string;
}

type ServerMessageType =
  | "server:action_result"
  | "server:state_sync"
  | "server:error";

特に visibility フィールドは「なるほど」と思った。「手札は本人にしか送らない」という制御をメッセージ設計レベルで表現している。Broadcaster がこれを見て配信先を絞るので、ゲームロジック側が配信先を意識しなくて済む。

通信フローのシーケンス

接続・アクション送信・再接続の3つのフロー。

再接続は全量同期で割り切る

再接続時の状態同期について、Claude Code は「差分同期は複雑になりがちなので、まず全量同期から始めましょう」という提案だった。

接続が切れた後に再接続したとき、その時点のゲーム状態を全量で送る。シンプルで、接続が途切れたことで手番がスキップされることもない。差分同期は後から考えればいい、という割り切りは納得感があった。

Hono との組み合わせ

フレームワークは Hono を使う予定。Workers との相性のよさはもともと知っていたが、WebSocket のアップグレード処理も素直に書けるという点は Claude Code に教えてもらった。ルーティングは Hono に任せて、WebSocket のハンドリングは別モジュールに委ねる形にする。

まとめ

Claude Code に設計を任せながら進めた結果、理解できたことをまとめる。

  • Cloudflare Workers でリアルタイム対戦を実現するには Durable Objects が必要。これは自分では気づかなかった
  • 接続管理とゲームロジックの分離は、MessageRouter を挟むことで両者が互いを知らなくてよくなる
  • visibility をメッセージに持たせると、手札のような「本人にだけ見せる情報」の制御が設計に乗る
  • 全量同期で割り切ると、ネットワーク不安定への対処がシンプルになる

AI に設計を任せると「知らなかったことを知る」という副産物がある、というのが今回一番の発見だった。これから実装に入るので、Durable Objects 周りの知見が増えたら続きを書く予定。

参考