A code review of the uno table found the stuck guard had never once fired.
It counted how many bots had passed in a row and wanted more of them than
there are seats — but the bot loop hands the turn back the moment it comes
round to you, so the count could never get there, and your own empty-handed
pass was never in it. A dead table just passed the turn round forever. That
is not an ugly ending, it's a game you cannot finish, and a game you cannot
finish is chips you cannot cash out. So it asks the real question now: is
there anything to draw, and is anyone holding a card that goes.
And the table let go of itself too early. busy came off when the request
landed, not when the script it came back with had finished playing — so for
the seconds a bot lap takes, you could click a card at a board the server
had already moved past. It comes off at the end now, like the other tables.
Also: left: 0 was being dropped on its way out the door, which is the one
number that matters (the seat that just went out), the deck counter didn't
come back after a reshuffle, and hoisting fly() into flyNode() had quietly
flattened the chip arc on every other table in the room.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
Driven in a browser for the first time, which is where three bugs were.
Every visit to /games/uno was a 500: the page was never added to the list
server.go parses into the games template set, so render() answered "unknown
page". The casino tests all call their handlers directly and never go through
render(), so nothing saw it. TestEveryCasinoPageRenders now walks the mux and
asks for every page the casino routes to.
The play script hid the first card that lit up rather than the one you clicked,
so playing any other playable card made an innocent card vanish. And on a phone
the discard sized its box but not its card, which takes its size from --uno-h,
so a full-size card hung out of a small hole and covered the colour in play.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
UNO, played for chips. You stake once, sit down against one to three bots,
and going out first pays the table: 2.2x heads up, 3.6x against a full house.
Anybody else going out first takes the stake. The table size is the tier,
because it is the only dial UNO has.
The bots move inside ApplyMove. A game with opponents is normally where you
reach for a socket, and the plan says solo UNO must not — so one request plays
your move and every bot turn behind it, and hands back the whole lap as a
script the felt plays in order.
The RNG is in the state rather than an argument to it: the bots choose and a
spent deck reshuffles, so the engine needs randomness mid-game, and there is no
generator alive across requests to pass in. The seed rides in the state and each
step derives its own. The game still replays exactly as it fell.
The zero value of Color is Wild, and that is the whole point of it: a wild
played with the colour field missing from the JSON must be refused, not
quietly played as a red one. It was red for an hour.
The browser never sees a bot's card — not the deck, not a hand, not the face of
a card a bot drew, which is most of the deck. Seats cross the wire as a name and
a count.
The multiples are measured, not guessed: playing the first legal card you hold
wins 43/32/27% of the time against these bots, so the tiers price that to lose
about 8% a game and leave good play worth roughly the house's edge.
PeteFX.flyNode is the throw with the chip taken out of it, so a card can be
thrown across the felt the same way. fly() is now that with a chip in it.
Not yet driven in a browser, which in this room means not yet finished.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ