Bad Decisions: a joke that got multiplayer
I made a joke based on a picture on Discord. Thirteen days later (two minutes short, if you're counting, and I was), it had real-time multiplayer on the web. This is the first post on this blog because apparently that is how project scope works now.
What it is
Bad Decisions is a party card game engine. If you've played Cards Against Humanity you already know the shape of it: a prompt with blanks, a hand of answers, and a table of people choosing the worst possible combination.
The engine is a small Python service. It deals a prompt, fills the blanks from answer packs, and returns the finished sentence. The game calls that a consequence. It ships with three packs:
- base: the Cards Against Humanity downloadable main game
- coffee: Cards Against Coffee, a pack of tech and caffeine jokes
- maha: the MAHA Pack
You can try it right now, with no account: bytes.coffee/bad-decisions/.
Peer Pressure: multiplayer
Reading randomly assembled cards by yourself gets old quickly. Peer Pressure turned it into the table game I had in mind.
- Pick a room code. The first person to use it creates the room and becomes the host.
- Invite at least two friends. Everyone gets a private hand of ten cards.
- One player is the Responsible Adult for the round. Everyone else submits answers, which show up anonymously.
- The Responsible Adult picks a winner, the table sees the consequence, and the role rotates to the next seat.
Rooms are deliberately temporary. Each room is its own small SQLite file on the server. Rooms expire on their own, and when the host ends one it's deleted outright. No accounts, no passwords, no email: joining sends a display name and a room code, and you get back a session token that lives in your browser.
Most of the work went into making a room hold together when real people click at the same time, retry requests, or wander away mid-round.
- Every room has a revision number. Every change to the room bumps it, and every action a client sends says which revision it was looking at. If you act on stale state (say, trying to submit after the round already moved on), the server refuses with
stale_revisioninstead of guessing. The client resyncs and decides whether the action still makes sense. - Every action carries a request ID. If the response gets lost and the client retries, the server recognizes the ID and replays the original answer instead of applying it twice.
- The table repairs itself. If the Responsible Adult wanders off, the role passes to the next connected player. If that player had already submitted an answer, it goes back into their hand so they never judge their own card.
Regret: the terminal client
Not everyone wants a browser tab. So there's Regret, a command-line client:
brew install bytesandcoffee/tap/regret
regret # deal a round from every pack
regret deal --packs coffee # stick to one pack
regret deal --prompt-packs coffee --answer-packs maha
regret --list-packs
regret together badidea # join a Peer Pressure room
regret together badidea --tui # ...in a full-screen Textual UI
regret together joins the same rooms as the web app, so half the table can be in a browser and the other half in a terminal. Line mode is plain prompts that work over SSH or in tmux, anywhere. --tui gives you a full-screen interface built with Textual.
The whole thing sits behind a public HTTP API with OpenAPI docs, so if you want to build a Discord bot, an e-ink daily draw, or a button that deals a round when you press it, the contract is already written down.
How I built it: two agents and a handoff file
I built Bad Decisions with Claude Code and OpenAI Codex, using the $20 plan for each. The choice was practical: each plan had a usage limit that reset every five hours. I handed the work over whenever one ran out.
The handoff depended on a file called HANDOFF.md. It described the project state: completed work, current assumptions, known breakage, and the finish line for the current task. The outgoing agent updated it. The next agent read it and carried on.
The idea resembles Kubernetes or Terraform: declare the state you want and let a controller work out the steps. I kept that state honest, reviewed the result, and handled production access myself.
A few things I learned:
- Write down the state, not the plan. Plans go stale the moment work starts. A description of where you want to end up stays useful.
- Let them review each other. Having one agent review what the other wrote is cheap, and it's a different set of eyes.
- Keep the dangerous verbs for yourself. The agents don't push, publish, or deploy unless I ask them to, and they don't read secrets. Those rules are written into the repo's agent instructions.
Try it
- Play: bytes.coffee/bad-decisions/
- Multiplayer: bytes.coffee/bad-decisions/peerpressure
- Terminal:
brew install bytesandcoffee/tap/regret - Source: github.com/BytesAndCoffee/bad-decisions
It's an unofficial fan project, and the humor is exactly as mature as you'd expect. Bring friends you trust.
Comments