pithy add multiplayerNo required prerequisites. Add auth if it is not already there — sessions bind to an authenticated user, so without it every route denies.
What lands in your repo
apps/<worker>/pithy.config.ts gains the registration, with an example game you are meant to replace:
multiplayer({
games: [
{
key: "tic-tac-toe",
kind: "connect-n",
rules: { rows: 3, cols: 3, connect: 3 },
},
],
basePath: "/multiplayer",
}),That example is tic-tac-toe: the smallest game that actually plays, on the built-in grid model.
apps/<worker>/wrangler.jsonc gains two bindings, in every environment stanza:
| Binding | Type | What it is |
|---|---|---|
DB | d1 | Where durable session results are written |
SESSIONS | durable_object | The live game session itself |
And your Worker’s entry module gains a line, which is the part worth knowing about:
export { MultiplayerSession } from "@pithy-sh/multiplayer/src/session/durableObject";A Durable Object is two halves and only one of them is config. The binding entry names a class, and wrangler resolves that name against the module your main names — a Worker whose entry does not export the class is refused at deploy with Your Worker depends on the following Durable Objects, which are not exported in your entrypoint file.
So add writes the export too, under a comment saying what the block is. Delete a line from it and the Worker stops deploying; pithy remove takes it back out; pithy upgrade reports one that is missing.
The class migration tag goes into the wrangler config at the same time. A tag is written once and never revisited, which is why a Durable Object binding is one of the three kinds declinedBindings refuses to decline.
One migration runs, creating the session-result tables.
Declaring a game
Each entry is a game your app runs, keyed by a name your client passes when it creates a session.
games: [
{ key: "connect-four", kind: "connect-n", rules: { rows: 6, cols: 7, connect: 4 } },
{ key: "duel", kind: "battle", rules: { rounds: 3 } },
],kind selects the pattern helper. Three ship — connect-n for turn-based grids, battle for simultaneous choose-then-reveal, and a wagering table — and each is a reusable layer rather than a special case.
A game key is stored on every session result, so renaming one orphans its history.
Writing your own game
The three built-in kinds are the ones that ship, not the ones that are possible. A GameModel of your own registers alongside them, and the pattern helpers exist to be the layer you build on rather than the boundary you stop at.
Two things the model gives you that are hard to add later:
Hidden state. The model distinguishes what the session knows from what each player is told, so a projection to one player omits the cards in the other player’s hand. A design that assumed shared state cannot be retrofitted into this.
N players. Not a two-player assumption with a workaround.
What a resolved session flows into
Nothing automatically, and deliberately. A resolved session is a durable result in your own D1, and what happens next is one call of yours:
await rating.record({ game: "duel", outcome: result.outcome });
await leaderboard.submit({ board: "wins", userId: winner, score: total });For a wagering game, the settlement goes through ledger — the stake was already held when the bet was placed, so resolving is a capture or a release rather than a debit that might fail.
Getting players into a session
This capability gives you an authoritative session once players are in it. **How they get there is **matchmaking — a room code, a direct invite, a friend, or an open queue, all four ending at a session id.
Composing both needs no wiring: a matchmaking game entry declares the session it mints, and the two line up by key.
Check it worked
pithy doctor reports multiplayer under the Worker’s health, and the bindings line checks both halves of the Durable Object — the config entry and the export. A missing export is the one drift a config read alone cannot see, and it is a failed deploy rather than a runtime error.
In dev, create a session for the example game and play a move. pithy dev runs the Durable Object locally through Miniflare.