Ship a turn-based game

You need: multiplayer and auth composed.

Add it

The example games are what to copy, Using Multiplayer is the API, sessions and lifecycle is the arc a game runs through, and Durable Objects is what holds it.

pithy add multiplayer
pithy migrate

add writes the Durable Object binding and its class-migration tag into every environment, and puts the session class export in your Worker entry. Both halves matter — a binding without the tag deploys a Worker whose class does not exist.

Define the game

multiplayer({
  games: [
    { key: "tictactoe", kind: "connect-n", rules: { rows: 3, cols: 3, connect: 3 } },
  ],
})

That is the whole game. connect-n is a shipped model, so you are configuring one, not writing one.

Same model, different games:

{ key: "connect4", kind: "connect-n", rules: { rows: 7, cols: 6, connect: 4 } }
{ key: "gomoku",  kind: "connect-n", rules: { rows: 15, cols: 15, connect: 5 } }

Play it

POST /multiplayer/games/tictactoe
→ { "sessionId": "…" }

The creator is the first member. The second player joins:

POST /multiplayer/sessions/<id>/join

The roster filling is what moves the session from open to active.

Then take a turn:

POST /multiplayer/sessions/<id>/action
{ "row": 1, "col": 1 }

The body is whatever the model defines, forwarded untouched. For connect-n that is a cell.

Read the board

GET /multiplayer/sessions/<id>

Your redacted view. For a fully-open game like this one, everybody sees the same thing — which is what a game with nothing to hide looks like through the redaction seam.

Once terminal:

GET /multiplayer/sessions/<id>/result

Live play

GET /multiplayer/sessions/<id>/socket

A hibernation-safe WebSocket. A session waiting on a player’s turn bills no duration, which is the whole reason turn-based play is the quadrant this serves.

Add a deadline

{ key: "tictactoe", kind: "connect-n", rules: { … }, turnTimeoutMs: 60_000 }

Alarm-enforced, never a timer. A move that does not land in time moves the session to abandoned.

What you did not have to build

Membership bound to an authenticated user. A lifecycle. The hidden-state boundary. The deadline. A durable result row in your own D1, joinable beside your own tables. A one-way leaderboard publish when you want it.

That is the same for every game. Only the rules differ.

Next

Publish the result to a board — see publish results to a leaderboard.

Get players in without sharing session ids by hand — see get players into a session.

Write a game the three models do not cover — see write your own game model.

ESC