playableintelligence/game-creatorMIT5 files

Add Multiplayer

Add real-time or turn-based multiplayer to an existing browser game using PartyKit (Cloudflare Durable Objects). Scaffolds a room-based server, a NetworkManager client, EventBus events, GameState fields, Constants, and extends render_game_to_text(). Use when the user says "add multiplayer", "make this multiplayer", "add real-time co-op", "add online play", "scaffold multiplayer", or "add netcode". Do NOT use for solo leaderboards (use monetize-game), single-player AI opponents (use add-feature), or peer-to-peer/WebRTC games.

Specification
Skill ID
playableintelligence/game-creator/add-multiplayer
Publisher
playableintelligence
Repository
game-creator
Installs
0
Files
5
License
MIT
Requires
Requires an existing Phaser 3 or Three.js game with game-creator architecture (EventBus, GameState, Constants.js, render_game_to_text). Requires Node.js and a Cloudflare account for `npx partykit deploy` (free tier is sufficient).
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

playableintelligence/game-creator/add-multiplayerInstalls these files
  • SKILL.md
  • architecture.md
  • client-integration.md
  • deploy.md
  • partykit-server.md

What this skill tells the agent

Add Multiplayer (PartyKit / Cloudflare Durable Objects)

Add real-time or turn-based multiplayer to an existing single-player browser game. This skill scaffolds:

  1. A PartyKit server (one Durable Object per room) deployed to Cloudflare's edge.
  2. A client `NetworkManager` wired through EventBus that mirrors the existing playfun.js external-service pattern.
  3. Additive edits to EventBus, GameState, Constants, and render_game_to_text() — single-player gameplay must remain identical when the server is unreachable.

The default state is "single-player works." If the WebSocket connection fails, NetworkManager swallows the error and the game runs locally as before. When connected, remote players appear via network:player-joined and synchronize via network:state-received.

Reference Files

  • architecture.md — event taxonomy, GameState schema, NetworkManager contract, Phaser vs Three.js placement notes.
  • partykit-server.md — server templates (realtime.ts and turn-based.ts), state shape, broadcast helpers, rate limiting.
  • client-integration.md — MultiplayerClient, NetworkManager, RemotePlayerRegistry source, EventBus/GameState/Constants append patterns, render_game_to_text extension.
  • deploy.md — npx partykit dev and npx partykit deploy walkthrough, capturing the deployed URL, .env handling, and client redeploy.

Core Principles

These are rules, not guidelines:

  1. Single-player must work offline. With the server unreachable, the game must boot, play, and reset normally. NetworkManager catches all connection errors and emits network:disconnected instead of throwing.
  2. Additive edits only. Append to EventBus.js, GameState.js, Constants.js, main.js, and render_game_to_text() under a // === Multiplayer === banner. Never rename, remove, or change existing fields.
  3. EventBus is the only seam. NetworkManager talks to the rest of the game through events — no direct imports from scenes, systems, or entities into NetworkManager (or vice versa).
  4. Server is authoritative, but tolerant. The PartyKit room owns the canonical room state. Clients send intents; the server validates and broadcasts. In realtime mode validation is light (last-write-wins). In turn-based mode validation is strict (rejects out-of-turn moves).
  5. Backend-agnostic client API. All partysocket calls go through MultiplayerClient. If a future user wants Colyseus or fly.io+ws, only MultiplayerClient.js changes — game code does not.
  6. Default room is `'lobby'`. No matchmaking UI in v1. Users override by emitting multiplayer:join-room with a custom room id.

Prerequisites

  • An existing Phaser 3 or Three.js game scaffolded with this plugin (has src/core/EventBus.js, src/core/GameState.js, src/core/Constants.js, src/main.js with window.render_game_to_text()).
  • Node.js 18+.
  • A Cloudflare account for npx partykit deploy (the CLI walks the user through login on first deploy; free tier is sufficient for prototyping).

Instructions

The user wants to add multiplayer to the game at $ARGUMENTS (or the current directory if no path given). Optional --mode=realtime (default) or --mode=turn-based chooses the server template.

Step 0: Locate and read the game

Parse $ARGUMENTS for the game path and --mode flag. If no path, use cwd. Verify it's a game by reading package.json and confirming Phaser or Three.js dependency.

Read these files in full before touching anything:

  • package.json — engine + scripts.
  • src/main.js — orchestrator, window.render_game_to_text(), window.advanceTime().
  • src/core/EventBus.js — exact event names already in use.
  • src/core/GameState.js — current state shape and reset() semantics.
  • src/core/Constants.js — config block conventions.
  • progress.md if present — pipeline context.

Then tell the creator one sentence confirming what you saw:

Game is `<engine>` with `<N>` events and a <player|bird|ship> entity. I'll add a multiplayer layer that broadcasts the local <entity>'s state at TICK_RATE_HZ and renders remote players from server broadcasts. Single-player will continue to work when the server is offline.

Step 1: Choose sync mode

Pick the server template:

ModeWhen to useWire model
realtime (default)Action games, runners, dodgers, platformers, anything with continuous movementLocal setInterval at TICK_RATE_HZ broadcasts the local entity's {x, y, [z], score, state}; server fans out; clients render last-known remote state
turn-basedCard games, board games, puzzles, anything with discrete movesEventBus events (player:moved, player:played-card) forward as {type, payload} messages; server validates and broadcasts; clients apply on network:state-received

If the user did not pass --mode, infer from the game's existing events. If you see continuous-position events (bird:flap, player:moved, position-updating physics), use realtime. If you see discrete actions (card:played, move:submitted), use turn-based. State the choice and proceed.

Step 2: Scaffold the server

Create a sibling multiplayer-server/ directory inside the game project. See partykit-server.md for the full template content.

Create:

  • multiplayer-server/partykit.json — manifest with name (use the game's directory name), main: "src/server.ts", compatibilityDate.
  • multiplayer-server/package.json — partykit dep, dev/deploy scripts.
  • multiplayer-server/tsconfig.json — minimal TypeScript config that PartyKit accepts.
  • multiplayer-server/src/server.ts — paste the appropriate template from partykit-server.md (realtime or turn-based).
  • multiplayer-server/.gitignore — node_modules, .partykit.

Run cd multiplayer-server && npm install to install partykit (which provides partysocket for the client too via npm workspaces, but we'll add partysocket explicitly to the client).

Step 3: Scaffold the client

Create three new files. See client-integration.md for the full source.

  • src/multiplayer/MultiplayerClient.js — backend-agnostic interface around partysocket (connect, send, onMessage, disconnect, isConnected).
  • src/multiplayer/RemotePlayerRegistry.js — Map<playerId, RemotePlayer> with upsert, remove, prune(staleMs), list().
  • src/systems/NetworkManager.js — wires MultiplayerClient ↔ EventBus, owns the broadcast tick (in realtime mode), handles reconnect with exponential backoff, emits network:* events.

Add partysocket to the game's package.json deps:

cd <game-path> && npm install partysocket

Step 4: Append to existing core files

Make additive edits only. See architecture.md for full schemas and client-integration.md for the exact append blocks.

`src/core/EventBus.js` — append under // === Multiplayer === banner:

// === Multiplayer ===
NETWORK_CONNECTED: 'network:connected',
NETWORK_DISCONNECTED: 'network:disconnected',
NETWORK_PLAYER_JOINED: 'network:player-joined',
NETWORK_PLAYER_LEFT: 'network:player-left',
NETWORK_STATE_RECEIVED: 'network:state-received',
MULTIPLAYER_JOIN_ROOM: 'multiplayer:join-room',
MULTIPLAYER_LEAVE_ROOM: 'multiplayer:leave-room',

`src/core/GameState.js` — append a multiplayer field with persistent (roomId, playerId) and transient (connected, remotePlayers) parts. Update reset() to clear only the transient parts so rejoin works after a game restart.

`src/core/Constants.js` — append a MULTIPLAYER block with SERVER_URL (filled by Step 6), DEFAULT_ROOM, MAX_PLAYERS, TICK_RATE_HZ, reconnect backoff, stale-player threshold, PROTOCOL_VERSION. No magic numbers — every value is a named constant.

Use this skill in a project

Open any RiverX project, search the Skills panel for playableintelligence/game-creator/add-multiplayer, and install it in one click.

Use this skill