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, and it's about that joke.
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.
Under the hood it's a small Python service. It deals a prompt, fills the blanks from answer packs, and hands back 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/web.
Peer Pressure: multiplayer
A deck of cards you read alone is a toy. The game is the table. So the second thing I built was Peer Pressure, the multiplayer mode.
- 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.
The part I'm proudest of is the boring part: making it hold up when real people play.
- 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, on the $20 plan of each.
That wasn't a principled stance on AI vendors. It was cost efficiency. Each plan has a usage limit that resets every five hours, so I split the work between them and handed off whenever one needed the other, or I just ran out of five-hour use. That only works if either agent can pick up where the other stopped.
What made that work was a file called HANDOFF.md. It doesn't list steps. It describes the state the project should be in: what's done, what's true, what's broken, and what "finished" looks like for the current piece of work. When one agent needed the other, or I ran out of my five hours, the agent on its way out updated the file, and the next one read it and carried on.
It turns out that's the same idea as Kubernetes or Terraform: you declare the state you want, and the controller works out the steps. The agents were the controllers. My job was to keep the declared state honest, review what came back, and handle anything that touched production myself: deploys, releases, secrets.
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/web
- 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. Make bad decisions.