{
  "name": "app-realtime-reference",
  "title": "Real-Time Multiplayer Reference - Raw Colyseus Client",
  "description": "Raw Colyseus client reference: connecting without the kit, room discovery via REST, relay and state room message patterns, turn-based games, and the safe state-room initialization boilerplate",
  "guid": "sk_plat_rtrf",
  "category": "App services",
  "requiredTools": [
    "realtime_room"
  ],
  "content": "# Real-Time Multiplayer Reference - Raw Colyseus Client\n\nThe long tail of [app-realtime](app-realtime.md): driving a room with the **raw Colyseus client** instead of the `@gipity/realtime` kit.\n\n**Read [app-realtime](app-realtime.md) first, and prefer the kit.** `gipity add realtime` wraps everything on this page - `onStateChange` diffing, tokens, reconnection, presence, lobby + match rooms - behind a tested, engine-agnostic API. Reach for the raw client only when you need something the kit does not expose, or to understand what it does internally.\n\nRooms must still be provisioned before any client can connect, and the realtime build loop still ends with a concurrent two-client check - both are covered in [app-realtime](app-realtime.md).\n\n## Quick start (raw client)\n\n1. Provision a room (see \"Provisioning a room\" in [app-realtime](app-realtime.md)) - e.g. the agent tool:\n```\nrealtime_room action=create name=game-lobby room_type=state auth_level=public max_clients=50\n```\n\n2. In your app's HTML, load the realtime client from CDN (every raw-client API is documented on this page - do not search the web for external docs). Pin the exact version - the state-room API below is written for `0.16.22`:\n```html\n<script src=\"https://unpkg.com/colyseus.js@0.16.22/dist/colyseus.js\"></script>\n```\n\n3. Connect to the room:\n\n**IMPORTANT:** The token endpoint is on the API server, NOT the app host. You MUST use the absolute URL `https://a.gipity.ai/api/token` - never a relative path like `/api/token`. It is a POST request and the token is nested under `data`.\n\n```js\n// Get app token - MUST be absolute URL to API server, POST with app GUID\nconst resp = await fetch('https://a.gipity.ai/api/token', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ app: '<PROJECT_GUID>' })\n});\nconst { data: { token } } = await resp.json();\n// ✗ WRONG: fetch('/api/token')           - relative URL hits app host, not API\n// ✗ WRONG: fetch('https://a.gipity.ai/api/token')  with GET - must be POST\n// ✗ WRONG: const { token } = await ...   - token is inside data: { data: { token } }\n\nconst client = new Colyseus.Client(\"wss://rt.gipity.ai\");\nconst room = await client.joinOrCreate(\"state\", {\n  app: \"<PROJECT_GUID>\",\n  room: \"game-lobby\",\n  token\n});\n```\n\n## Room Discovery (Lobby / Matchmaking)\n\nThe client library does **NOT** have a room listing function. To list rooms, use the REST endpoint:\n\n```js\n// List available rooms (uses the app token from step 1)\nconst roomsResp = await fetch(\n  'https://rt.gipity.ai/rooms?room=game-lobby&token=' + encodeURIComponent(token)\n);\nconst { rooms } = await roomsResp.json();\n// rooms = [{ roomId, clients, maxClients, metadata }, ...]\n```\n\n### Lobby Pattern - Join Existing or Create New\n```js\nconst roomsResp = await fetch(\n  'https://rt.gipity.ai/rooms?room=game-lobby&token=' + encodeURIComponent(token)\n);\nconst { rooms } = await roomsResp.json();\n\n// Find a room that isn't full\nconst available = rooms.find(r => r.clients < r.maxClients);\nlet room;\nif (available) {\n  // Join existing room by ID\n  room = await client.joinById(available.roomId, {\n    app: \"<PROJECT_GUID>\", room: \"game-lobby\", token\n  });\n} else {\n  // No room available - create a new one\n  room = await client.joinOrCreate(\"state\", {\n    app: \"<PROJECT_GUID>\", room: \"game-lobby\", token\n  });\n}\n```\n\n> **Note:** The `room` query param is optional - omit it to list all rooms for your app. Add `&scope=<value>` to list only one scope's instances.\n> **Never use** `client.getAvailableRooms()` - it does not exist in the client library.\n> Raw join options also accept `scope` (opaque instance partition key - same semantics as the kit's `scope` config, documented in [app-realtime](app-realtime.md)).\n\n## Relay Room Patterns\n\n```js\n// Send a typed message\nroom.send(\"chat\", { user: \"Alice\", text: \"Hello!\" });\nroom.send(\"move\", { x: 10, y: 20 });\n\n// Receive messages by type\nroom.onMessage(\"chat\", (msg) => {\n  console.log(msg.user + \": \" + msg.text);\n});\nroom.onMessage(\"move\", (msg) => {\n  movePlayer(msg.x, msg.y);\n});\n```\n\n## State Room Patterns\n\n**IMPORTANT - how to read state-room state.** The Colyseus client (`colyseus.js@0.16.22`) does **not** expose `.onAdd()` / `.onChange()` / `.onRemove()` callbacks on the `players` and `data` maps - those were removed after 0.14, and calling them throws `TypeError: ... is not a function`. Instead, react to state inside `room.onStateChange` - it fires with the **full state** on every server update - and diff it against what you have already seen. The maps are also `undefined` on a fresh room, so always guard before reading them.\n\nThe canonical diff loop for both maps lives in \"State room - safe initialization boilerplate\" at the bottom of this page - copy it from there rather than re-deriving it. The sections here cover the messages you send and the APIs that do not exist.\n\n### Players (auto-tracked)\n\nThe server tracks joined players in `room.state.players` (each record carries `displayName`). Detect joins and leaves by diffing that map inside `room.onStateChange` (see the boilerplate); set custom per-player fields with a message:\n\n```js\n// Set custom player data (e.g. score, position)\nroom.send(\"set_player_data\", { data: JSON.stringify({ score: 100 }) });\n\n// ✗ WRONG - .onAdd is not a function in colyseus.js 0.16:\n// state.players.onAdd((player, sid) => { ... })\n```\n\n### Shared Data (key-value, auto-synced)\n\nAny client can set a key and every client receives it. Values are JSON strings - compare them raw to spot a change, then parse. Detect changes and deletions by diffing `room.state.data` (see the boilerplate):\n\n```js\n// Set shared data (any client can set, all clients receive)\nroom.send(\"set_data\", { key: \"gameState\", value: JSON.stringify({ round: 1, phase: \"playing\" }) });\nroom.send(\"delete_data\", { key: \"oldKey\" });\n\n// ✗ WRONG - .onChange is not a function in colyseus.js 0.16:\n// state.data.onChange((value, key) => { ... })\n```\n\n### Custom Messages (broadcast to all)\n```js\n// Any unrecognized message type is broadcast to all other clients\nroom.send(\"explosion\", { x: 50, y: 30, radius: 10 });\nroom.onMessage(\"explosion\", (data) => renderExplosion(data));\n```\n\n## Turn-Based Game Pattern\n\nUse a state room with a `currentTurn` key:\n```js\n// Host sets initial turn\nroom.send(\"set_data\", { key: \"currentTurn\", value: JSON.stringify(room.sessionId) });\nroom.send(\"set_data\", { key: \"board\", value: JSON.stringify(Array(9).fill(null)) });\n\n// On each move, update board + advance turn\nfunction makeMove(index) {\n  const board = JSON.parse(room.state.data.get(\"board\"));\n  board[index] = mySymbol;\n  room.send(\"set_data\", { key: \"board\", value: JSON.stringify(board) });\n  room.send(\"set_data\", { key: \"currentTurn\", value: JSON.stringify(opponentSessionId) });\n}\n\n// React to board/turn changes by diffing data on every update\nconst seen = new Map();\nroom.onStateChange((state) => {\n  if (!state.data) return;\n  state.data.forEach((value, key) => {\n    if (seen.get(key) === value) return;   // unchanged\n    seen.set(key, value);\n    if (key === \"board\") renderBoard(JSON.parse(value));\n    if (key === \"currentTurn\") updateTurnIndicator(JSON.parse(value));\n  });\n});\n```\n\n## State room - safe initialization boilerplate\n\nThe canonical diff loop. Copy-paste this as your starting point for any state room app - it covers player join/leave and shared-data change/delete in one `onStateChange` handler:\n```js\n// 1. Get token\nconst resp = await fetch('https://a.gipity.ai/api/token', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({ app: '<PROJECT_GUID>' })\n});\nconst { data: { token } } = await resp.json();\n\n// 2. Connect (or use Room Discovery to join an existing room - see above)\nconst client = new Colyseus.Client(\"wss://rt.gipity.ai\");  // from CDN loaded above\nconst room = await client.joinOrCreate(\"state\", {\n  app: \"<PROJECT_GUID>\",\n  room: \"my-room\",\n  token\n});\n\n// 3. React to state by diffing it. onStateChange fires with the full state\n//    on every server update; colyseus.js 0.16 has NO .onAdd/.onChange\n//    callbacks on the maps - diff against what you have seen instead.\nconst knownPlayers = new Set();\nconst dataSeen = new Map();   // key -> last raw JSON string\nroom.onStateChange((state) => {\n  // Players\n  if (state.players) {\n    const present = new Set();\n    state.players.forEach((player, sessionId) => {\n      present.add(sessionId);\n      if (!knownPlayers.has(sessionId)) {\n        knownPlayers.add(sessionId);\n        // Handle player join\n      }\n    });\n    for (const sessionId of [...knownPlayers]) {\n      if (!present.has(sessionId)) {\n        knownPlayers.delete(sessionId);\n        // Handle player leave\n      }\n    }\n  }\n  // Shared data - values are JSON strings\n  if (state.data) {\n    state.data.forEach((value, key) => {\n      if (dataSeen.get(key) !== value) {\n        dataSeen.set(key, value);\n        const parsed = JSON.parse(value);\n        // Handle data change\n      }\n    });\n    for (const key of [...dataSeen.keys()]) {\n      if (!state.data.has(key)) {\n        dataSeen.delete(key);\n        // Handle data delete\n      }\n    }\n  }\n});\n\n// 4. Send messages\nroom.send(\"set_data\", { key: \"myKey\", value: JSON.stringify({ foo: \"bar\" }) });\n\n// 5. Cleanup on leave\nroom.onLeave((code) => {\n  console.log(\"Left room, code:\", code);\n});\n```\n\n## Related\n- [app-realtime](app-realtime.md) - the main guide: the `@gipity/realtime` kit, party/lobby flows, presence, provisioning, and how to verify multiplayer across concurrent clients\n- [realtime-scheduled-app](realtime-scheduled-app.md) - end-to-end recipe combining presence + messages with a database and a scheduled poster\n"
}
