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 words file. 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 file (words.txt). 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.txt is committed. Buy a copy and
index the tiles (about 15 minutes), or bring any word list you like.
- One word per line, prefixed with one or more part-of-speech markers listing every
part of speech the word can be, e.g.
[noun][verb] play. Valid markers:[noun][verb][adjective][adverb][pronoun][preposition][conjunction][interjection][other](punctuation, suffix tiles, articles). - Blank lines are ignored; a line starting with
#is a comment. The parser is forgiving: an unknown marker is dropped with a warning and an unmarked word defaults to[other]. - Duplicate words are fine; each word line becomes a distinct tile keyed
"<index>|<word>". The tags are served to clients per tile key in theposfield of the draw/tiles responses, so players can browse their pile grouped by part of speech.
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 (the word's index in the words file) 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. Part-of-speech tags never ride in the token itself: the draw/tiles responses carry them in a separateposmap keyed by tile key. - 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; word/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.txt
and (optionally) prompts file as described under Configuration.