The multiplayer game server for Quipnotes, a party game where players arrange word tiles into a "ransom note" answering a prompt, then a rotating judge picks a favorite. This Go + Gin service is the source of truth for game state: it hosts many concurrent games at once (Jackbox-style, each a 4-digit code) and pushes live updates to every screen over WebSockets.
Quipnotes is a mash-up of two games we love — Ransom Notes by Very Special Games and Quiplash by Jackbox Games — hence the name. The best in-person experience is still to buy Ransom Notes and use its cards; this project makes it playable on phones, no table full of tiny tiles required.
Quipnotes is three independently-versioned projects (each its own git repo):
| Project | Role | Stack |
|---|---|---|
quipnotes (this repo) |
Game server — owns all state, one HTTP/WS API per game code | Go + Gin |
quipnotesclient |
Player client — join with a name + code, draw tiles, submit notes, judge | Vue 3 + Vite |
quipnotesmanager |
Host/manager client — start a game, show the code, drive the board | Vue 3 + Vite |
Everything the server exposes is scoped to a 4-digit game code. State lives in memory only — restarting the server wipes every game.
cp .env.example .env # then edit — see Configuration below
go run . # serves on :8081 (override with PORT)Requires Go 1.21+. The server panics on startup if .env is missing or
WORDS_FILE_PATH doesn't point to a readable CSV. Once running, the interactive API docs
are at http://localhost:8081/swagger/index.html.
go build -o quipnotes # build a binary
go test ./... # run the test suite
swag init # regenerate Swagger docs in docs/ from handler annotationsSet via .env (see .env.example):
| Variable | Required | Default | Purpose |
|---|---|---|---|
WORDS_FILE_PATH |
yes | — | Path to the word-tile CSV. Missing/unreadable → panic on boot. |
PROMPTS_FILE_PATH |
no | built-in bank | Path to the prompts file. Unset/unreadable → log a warning and use a built-in family-friendly bank. |
PORT |
no | 8081 |
Port to bind. |
GIN_MODE |
no | debug |
release in production to quiet Gin. |
Ransom Notes' word tiles are proprietary, so no words.csv is committed. Buy a copy and
index the tiles (about 15 minutes), or bring any word list you like.
- One column, no header row — one word per line.
- Duplicate words are fine; each row becomes a distinct tile keyed
"<rowIndex>|<word>".
Prompts are the round cards a player writes a note about. Supply your own list, or skip it
entirely and the server falls back to a small built-in family-friendly bank so it still
boots with a playable game. See data/prompts.example.txt.
- One prompt per line — line-based (not CSV), so prompts may contain commas.
- Blank lines are ignored; a line starting with
#is a comment. - Family-friendly mode: prefix a line with the
[adult]marker to rate it adult. Adult prompts are withheld from games a host starts in family-friendly mode (a lobby toggle), and the marker is stripped from the text. Every other line is family-friendly.
# A comment. Everything below is a prompt, one per line.
The worst possible thing to say on a first date
A terrible name for a boat
[adult] Whisper something seductive to your date during a movie
Play runs in rounds — one active prompt each:
- The host starts a game (
POST /games→ a 4-digit code) and shares the code; players join from their phones. - A round begins when the next prompt is drawn (
POST /games/:code/rounds). It's pushed live to every screen. - Each round rotates a judge (round-robin, so everyone judges once per cycle before repeating). The judge sits out; everyone else submits one note.
- Judging opens automatically once every non-judge has submitted (or the judge forces it), then the judge flips notes face-up and picks a favorite — the author scores a point. A round with fewer than 2 players is judge-less and plays freely.
- Anyone eligible draws the next prompt, which clears the board and starts the next round.
- In-memory, multi-game. A package-level
game.Registry(internal/game/game.go) maps each 4-digit code to agame.Manager— one game with its own players, tile pool, submitted notes, prompt deck, scores, and judging state, each behind its own mutex. No database. - Tile wire format
"<id>|<word>". The numeric id (CSV row index) keeps duplicate words distinct across the whole protocol. Notes are stored as ordered token lists — tile keys plus a reserved"\n"break token for line breaks — never flattened to a string. - WebSockets for push.
GET /games/:code/eventsstreams round/judging/roster events and replays the current round's lifecycle on connect so refreshes and late joins recover mid-round. Player actions still go over the REST endpoints; the socket is push-only.
The routes, handlers (with Swagger annotations), and the full event catalog live in
internal/game/router.go; pure game logic in game.go; CSV/prompt loading in words.go
and prompts.go; the WebSocket hub in hub.go. See CLAUDE.md for a
detailed map of the state model and every endpoint. Interactive API docs are served at
/swagger/index.html.
Production runs in Docker behind Caddy. See DEPLOY.md for the full setup.
Note the known port mismatch: main.go binds :8081 while the base
Dockerfile/docker-compose.yaml reference 8080 — verify ports before relying on the
container.
- CORS allows all origins with credentials — a local-development posture only.
- Each sub-project is its own git repo. Branch off the latest
masterand open a PR rather than committing straight tomaster.
The code in this repository is licensed under the MIT License.
The Ransom Notes word tiles and prompt cards are proprietary to Very Special Games and
are not covered by this license — none are committed here. Supply your own words.csv
and (optionally) prompts file as described under Configuration.