games: a table of bots you have to beat to the last card

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
This commit is contained in:
prosolis
2026-07-14 07:07:17 -07:00
parent 3e9b93af55
commit 79c857023f
14 changed files with 3438 additions and 17 deletions

View File

@@ -322,11 +322,65 @@ A multi-session build. This section is the handover; read it before anything els
live games on the felt: no overlap with text, no overlap with the shoe, no
horizontal overflow, desktop geometry unchanged.
- **UNO, and it plays for chips.** *(2026-07-14. Built and tested, **not yet driven
in a browser** — see "Next" below, because in this room that means it is not
finished.)*
- **You beat the table, or you don't.** The user's call between three money
models: stake once, go out first and take the tier's multiple; anybody else
going out first takes the stake. **The table size is the tier**, which is the
one dial UNO actually has: Duel (1 bot, 2.2×), Table (2 bots, 2.9×), Full
House (3 bots, 3.6×). Rake on winnings only, as everywhere.
- **The multiples are measured, not guessed.** A player who just plays the first
legal card they hold goes out first 43% / 32% / 27% of the time against the
bots, so the tiers are priced to make that lose about 8% a game — which leaves
good play (holding the wilds back, dumping the colour you're long in) worth
roughly the house's edge. The measurement is a throwaway test, not in the tree;
re-run it if the bots or the tiers change, because the two are a pair.
- **The bots move inside `ApplyMove`, and that is what keeps solo UNO off a
socket.** One request plays your move *and every bot turn it hands off to*,
and returns the lot as a script of events the felt plays back in order. §7 said
solo first, no sockets; this is what that costs.
- **The RNG is in the state, not an argument.** The bots choose and a spent deck
reshuffles, so the engine needs randomness *mid-game* — but there is no rng
alive across requests to hand it. So the seed rides in the state (which never
leaves the server; the deck is in there too) and each step derives its own
generator from the seed and the step count. Value in, value out, and a game
still replays exactly as it fell.
- **The zero value of `Color` is Wild, deliberately.** It was Red for an hour, and
a wild played with the `color` field simply missing from the JSON went down as
a red one. The zero has to be "no colour named", so the omission is refused
instead of quietly meaning something. This is the kind of bug a rules test
finds and a browser never would.
- **The browser never sees a bot's card.** Not the deck, not a hand, not even the
face of a card a bot drew — that last one is most of the deck, and sending it
would turn counting cards into reading the network tab. Seats cross the wire as
a name and a *count*. There are two walls: the engine only attaches a face to
an event the seat may see it in, and `viewUnoEvents` drops it again anyway.
- `internal/games/uno` — engine, 22 tests. The census one is the load-bearing one:
108 cards, each in exactly one place, asserted after every move of 100 games
played out end to end. It is what would catch a reshuffle that leaks cards (the
wilds go back into the deck as *wilds*, not as the colour they were played as)
or a turn the bots never hand back.
- `PeteFX.flyNode` — the throw, with the chip taken out of it. `fly()` is now that
with a chip in it, because UNO wanted the same arc with a card in it. Extracted
rather than copied, same as `casino-cards.js` and `PeteFX.spot()` before it.
- The felt has no corner free for the house rack (bots along the top, piles in the
middle, your hand at the bottom), so it takes solitaire's **rail** instead:
`data-at="rail"`, off the felt, no collision to check for.
### Next, in order
1. Phase 3 (UNO), Phase 4 (hold'em) as below.
2. Trivia is played but not deployed. Hangman, solitaire and trivia are all still
sitting on main un-deployed — the server runs `StartTriviaBank`, so its bank
1. **Drive UNO in a browser.** It is built, the engine is tested and the handlers are
tested, but nothing on this table has been *looked at* — and every game before it
found bugs in that step that no Go test could see (a white-on-white pill, a rack on
top of a multiplier, a spot printing double the stake). Specifically worth watching:
the bots' turns play back as a readable script rather than a blur; the wild colour
picker; a hand of ten cards at 390px with no sideways overflow; and a reload
mid-game bringing the board back. `PETE_DEV_CASINO=:7788 go test ./internal/web
-run TestDevCasino -timeout 0`, then Playwright.
2. Phase 4 (hold'em) as below.
3. Trivia is played but not deployed. Hangman, solitaire, trivia and now UNO are all
still sitting on main un-deployed — the server runs `StartTriviaBank`, so its bank
fills itself once the binary is out there, but the first player to try a ladder
in the first minute after a deploy gets the 503.