A minimal, fully-working boilerplate for lobby-based multiplayer on Plot: join by room code, see who's present, play a series of rounds, and rank everyone on a scoreboard at the end.
Clone it and you have the bones every party game (Jackbox, skribbl, trivia) and every turn-based game shares — because both are really the same thing: a lobby + rounds + scoreboard. The one round bundled here is deliberately trivial — "closest guess": each round picks a secret number 1–100, everyone guesses, the closest guess wins the round's points. Swap that round for your own game and keep the engine.
src/
logic.ts Pure engine: State, pickTarget, closestPlayer, advance, ranking (no I/O)
logic.test.ts Vitest unit tests for the pure engine
handler.ts Authoritative @plot/handler room (the round state machine)
main.ts Thin DOM client — a view over room.currentState
lobby.css Minimal styling
index.html #app mount
The split is the whole point:
handler.tsis the state machine. It runs server-side at 5 ticks/sec, ownsctx.state, and decides every transition (lobby → round → intermission → over) by delegating to the pure reducers inlogic.ts. Timing is counted in ticks (state.tickvsstate.deadlineTick), never wall-clock, so it's deterministic.main.tsis a thin view. It never decides game rules. It pollsroom.currentState, renders the current phase, and sends player intents (setName/start/guess) on theeventchannel.
npm install
npm run devThen open the printed URL in two browser tabs (each tab is a player) or
share the room code with a friend. Append ?room=YOURCODE to the URL to pick a
room; otherwise everyone lands in LOBBY1.
Set your app key (create an app at https://plot.ws) via a .env.local file:
VITE_PLOT_APP_KEY=your_app_key_here
# Optional: point at a local/self-hosted Plot instead of the hosted API.
VITE_PLOT_API_URL=http://localhost:8787Both are read in src/main.ts. With no key set it falls back to demo, which
is enough to typecheck/build/test offline.
npm run typecheck # tsc --noEmit
npm run build # vite build
npm test # vitest runThe engine is lobby → round → intermission → over. To build your game, replace
just the round — the rest stays:
- Party (Jackbox / skribbl): make the round a prompt → submit → vote flow
(drawings, captions, answers). Tally votes in
advanceinstead of distance. - Trivia: the round is a question; score the first/most-correct answers.
- Turn-based (board / card game): add a
turnOrder: string[]andactiveIdxtoState, accept amovemessage only from the active player inhandler.ts, and rotate the index. A turn is just a round with one actor. - More rounds, longer timers, more points: tune the constants at the top of
logic.ts(MAX_ROUNDS,ROUND_SECONDS,ROUND_POINTS).
Because logic.ts is pure, every rule you add is unit-testable without a
network — see logic.test.ts for the pattern.
@plot/client and @plot/handler are vendored under vendor/ and wired as
file: dependencies so this starter runs offline. Once the packages are
published, replace the file: entries in package.json with normal versions:
npm install @plot/client @plot/handler…and delete the vendor/ directory.
- Client:
npm run buildemits a staticdist/— host it on any static host (Cloudflare Pages, Netlify, GitHub Pages, an S3 bucket, …). - Handler:
handler.tsis your authoritative room. Deploy it to Plot and pointVITE_PLOT_APP_KEYat the matching app. See the Plot docs at https://plot.ws for the deploy command for your account.
MIT — see LICENSE.
