What it does
Two players each get a fake $20 and a few empty roster slots. Items from a category (NBA players, foods, movies, or a list you type yourself) are revealed one at a time, so nobody knows whether something better is coming. Either player opens the bidding, they raise each other, and when one concedes, the other buys it. Every item ends up on someone’s roster.
- No turn order: whoever wants the item opens.
- Running out of money is the point: once a player is broke, the other names the price or lets items go free.
- No accounts: play pass-and-play on one phone, or share a room link and play on two.
- The results sheet is built to be screenshotted and argued about. There’s no score.
How it works
- The rules are one pure function.
applyAction(state, action)takes the game and a move and returns the next game. It knows nothing about Svelte, the DOM or storage. An illegal move returns the state untouched instead of throwing, so a stray double-tap can’t break a game. - Local play runs that reducer in the browser and saves to localStorage.
- Online play sends the same actions to four Supabase Edge Functions (create, join, act, leave), which run the same reducer on the server. Because the shuffled order is the whole game, it never leaves the server.
- Row-level security hides the deck. Clients can’t read the real game tables at all. They read projection tables that hold a redacted copy, with the face-down card sent as
null. The secret can’t leak through a column that doesn’t exist. - Simultaneous moves use a version number. A write only lands if the version still matches. The loser of a race gets the corrected state back, and their screen says “Your opponent got there first.”
- Identity is a per-device token instead of an account, so refreshing mid-game puts you back in your seat.
- Jul 2026Pass-and-play on one phone, with the rules engine and the full content pools.
- Aug 2026Online play: schema, row-level security, Edge Functions and realtime updates. CI added.
- Aug 2026Fixed quitting, the self-award exploit, and invite links that seated nameless guests.
- Sep 2026Keepalive fix, then a cleanup that cut 1,383 lines of stale comments and dead code.
What broke
Testing
I tested at three levels, each for a different reason:
- Unit tests cover the rules engine at 100% of lines and functions. A seeded simulation plays 40 full games for each of the 21 content pools, from timid to reckless bidding, and checks that money is conserved and every item is owned exactly once. That’s 840 drafts per run, and it’s what proves no line of play can strand a player.
- Component tests run in real Chromium, not a simulated DOM, because the accessibility contrast checks need real rendering. axe found two real contrast failures (3.36:1 and 3.94:1), fixed with a dedicated muted-ink color.
- End-to-end tests in Playwright run against the production build, never the dev server. The worst bug this project has had, a crash that left a page rendering perfectly with every button dead, only showed up in built output.
CI runs these as 4 parallel jobs, so a red X says what broke. The end-to-end job boots a local Supabase stack and fails, rather than skips, if the online tests can’t run.
What I’d do next
Online play has been tested in separate browser sessions but not yet on two real phones. Next up: no test yet fires two moves at the exact same moment, end-to-end tests only cover Chromium, and a quit could offer a rematch instead of a dead end.
