{
  "schema": "leumas.docs.page/1",
  "id": "pkg:@leumas/game-server",
  "slug": "engines/game-server",
  "kind": "capabilities",
  "bucket": "package",
  "title": "@leumas/game-server",
  "name": "@leumas/game-server",
  "eyebrow": null,
  "chip": null,
  "summary": "The authoritative host for Leumas games: the 'game' room kind, a worker-thread simulation pool, per-player interest fan-out and match persistence. Node only.",
  "keywords": [
    "game-server",
    "game-server social-service",
    "game-server game-kit",
    "game-server games",
    "authoritative",
    "leumas game server",
    "simulation",
    "room"
  ],
  "audience": "both",
  "funnel": {
    "product": null,
    "cta": null
  },
  "body": "# @leumas/game-server\n\nThe authoritative host for Leumas games. Node only.\n\nTwo halves, deliberately unaware of each other:\n\n- **Transport** — the `'game'` kind on `@leumas/social-service`'s room hub. Lobby, input, rate\n  limiting, disconnect. It owns no simulation.\n- **Simulation** — `createSimRoom` running in worker threads, importing nothing but\n  `@leumas/game-kit` and a game definition. It owns no sockets.\n\n## The four things worth knowing\n\n**1. The simulation is never on the Express event loop.** A room ticking at 60 Hz with a physics step\nin it is a CPU-bound loop, and Node has one thread for JavaScript. Run it inline and every REST\nrequest waits behind it — the same failure `check:async` exists to catch, arriving on a timer instead\nof from an unawaited promise.\n\n**2. `spawn` is the scale-out plan.** A channel is anything with `post`, `onMessage` and `close`.\nThe default is a worker thread; pass a WebSocket to a remote `game-node` and the same room kind runs\nagainst another machine with no change to the kind, the protocol or the game.\n\n**3. A full host refuses rather than degrades.** Past `maxRooms`, `open()` rejects with `capacity`.\nAccepting the room and letting every existing match stutter punishes the players who were already\nthere for the arrival of one more.\n\n**4. Disconnect is not departure.** `onDisconnect` fires the moment a socket closes and says the body\nis unattended while the seat is still held. `onLeave` fires when the reconnect grace expires and says\nthe seat is gone. A shooter needs the first within a couple of hundred milliseconds; without it a\nplayer who alt-F4s stands in the world for a full minute as a free kill.\n\n## Three traps\n\n**Register before `buildHandlers()`.** The hub snapshots its kinds. A kind registered afterwards has\nno frames at all and every message it expects is silently dropped — nothing throws to say so.\n\n**`hostId` is not authority.** The hub migrates it when the host drops. In a server-authoritative\ngame the host chooses lobby settings and when to start, and nothing else. A test asserts `simRoom.js`\nnever mentions it.\n\n**Kick once, not per message.** A flood is thousands of messages a second; kicking on each one after\nthe threshold sends thousands of kick frames back down the same connection, which is more traffic in\nthe flood's direction and does its work for it.\n\n## Wiring\n\n```js\nconst gameKind = { current: null };\nconst host = createSimHost({\n  onOutbound: (roomId, userId, frame, payload) => gameKind.current?.outbound(roomId, userId, frame, payload),\n});\ncreateSocialRealtime({\n  server,\n  authenticate,\n  registerKinds: [(hub) => { gameKind.current = registerGameKind(hub, { host, resolveGame }); }],\n});\n```\n\nThe mutable holder exists because each half needs the other: the host needs somewhere to send, the\nkind needs a host to forward to. One has to be built first, and a holder is cheaper than a circular\nimport.\n\n## Verify\n\n```sh\nnode --test shared/engines/game-server/test/*.test.js   # 22 tests\npnpm smoke:game                                          # a whole match, end to end\npnpm smoke:game --players 8 --seconds 60\n```\n\n`smoke:game` runs the REAL room hub and the REAL game kind; only the worker thread is replaced by an\nin-process channel, so a failure there is a failure in code that ships.\n",
  "source": {
    "path": "shared/engines/game-server/README.md",
    "blobSha": "",
    "commit": "",
    "committedAt": "",
    "provenance": "no-git",
    "bytes": 3503,
    "hash": "7d6cb7608634178bbb217a41a864c66766b40732"
  },
  "urls": {
    "html": "/p/engines/game-server",
    "json": "/docs/engines/game-server.json",
    "md": "/docs/engines/game-server.md"
  },
  "links": {
    "composes": [
      "pkg:@leumas/game-kit",
      "pkg:@leumas/social-service"
    ],
    "usedBy": [],
    "product": [],
    "howTo": [],
    "skills": []
  },
  "exports": null
}
