iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
👾

Designing a DEBUG-ZERO Server with Claude Code: Discovering Technologies I Didn't Know

に公開

Introduction

I am currently designing an online multiplayer server for my custom card game, DEBUG-ZERO (a game where you aim for a target value of zero using number cards and arithmetic operations).

I recently started working with Cloudflare Workers, but I wasn't confident in the design for "how to handle real-time communication." So, I decided to let Claude Code handle the design process.

As a result, I ended up with an architecture I wouldn't have reached on my own and learned about several technologies I hadn't known before. I have summarized that process here.

The First Hurdle: Workers are Stateless

When I told Claude Code, "I want to build a WebSocket multiplayer game using Cloudflare Workers," this was the first point it raised.

Workers are stateless execution environments that boot up per request, so you need a different mechanism to manage long-lived WebSocket connections.

Once it pointed that out, it made perfect sense, but it wasn't something I had considered initially.

  • I need to hold "who is connected" somewhere while the connection is active.
  • I need to share the game state (hands, field, turn) across multiple connections.

Since Workers are ephemeral by design, it told me that Durable Objects are required here.

Learning About Durable Objects for the First Time

To be honest, I only learned about Durable Objects through this design process.

When I researched them, I found they are persistent objects bound to Workers that guarantee the exact same instance for a specific ID. I understood that they are perfect for use cases like "keeping game state for a room ID" and that Cloudflare specifically designed them for this kind of use case.

Here is an image of the structure Claude Code proposed:

The policy is to spin up one Durable Object per room. This ensures the game state is isolated, so states don't get mixed up as the number of rooms increases.

"Separate Connection Management from Game Logic"

This was another separation that Claude Code strongly advocated for.

If you write WebSocket connection handling and game domain logic in the same place, it will become painful later. Let's introduce a MessageRouter to separate the two.

ws/
├── ConnectionManager  Mapping between connection ID and player ID
├── MessageRouter      Routes incoming messages to each service
└── Broadcaster        Broadcasting to specific players, everyone, or spectators

game/
├── GameEngine         Core of game state transitions (applyAction)
├── ActionValidator    Checks validity of actions
└── PhaseController    Controls phase transitions

The MessageRouter checks the type of message and sends it to the appropriate service. The game logic side doesn't need to know about WebSockets. I don't think I would have done this without being told.

Message Design: The visibility Field

It proposed separating message types into client:* (requests from clients) and server:* (notifications from the server) namespaces.

// Client -> Server (Request)
interface ClientMessage {
  id: string;          // UUID for duplication detection
  type: ClientMessageType;
  roomId: string;
  senderId: string;
  payload: unknown;
}

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

// Server -> Client (Notification)
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";

I found the visibility field particularly insightful. It expresses the control of "only send the hand to the player themselves" at the message design level. Since the Broadcaster checks this to narrow down the recipients, the game logic side doesn't have to worry about who receives what.

Communication Sequence Flow

There are three main flows: connection, action submission, and reconnection.

Settling for Full Sync on Reconnection

Regarding state synchronization upon reconnection, Claude Code suggested, "Delta synchronization tends to become complicated, so let's start with full synchronization first."

When a client reconnects after losing their connection, you send the entire current game state. It's simple, and it prevents turns from being skipped due to connection drops. The decision to deal with delta synchronization later felt very reasonable.

Combining with Hono

I plan to use Hono as the framework. I already knew about its compatibility with Workers, but Claude Code pointed out that writing WebSocket upgrade logic is also straightforward. I'll leave routing to Hono and delegate WebSocket handling to a separate module.

Summary

Here is what I learned from designing while letting Claude Code take the lead:

  • Durable Objects are necessary to realize real-time multiplayer in Cloudflare Workers. I wouldn't have noticed this on my own.
  • Separating connection management and game logic by using a MessageRouter allows both to remain independent of each other.
  • Including visibility in messages allows for the design of controls like "information shown only to the player themselves," such as a hand of cards.
  • Settling for full synchronization simplifies handling unstable network conditions.

My biggest discovery this time was that letting AI take charge of the design has the side benefit of "learning things I didn't know." I'm going to start implementation now, so I plan to write a follow-up once I've gained more experience with Durable Objects.

References

Discussion