They are different numbers and the felt was quoting the wrong one. Every raked
pot has chips lifted off it, whoever wins — that has to stay true or the table
stops balancing. But the bots' chips are not real, so a pot a bot wins costs
you nothing, and the counter under your stack was climbing anyway while you sat
there folding.
Rake is now every chip off the table (so the chips conserve) and Paid is the
part that came out of a pot you won, which is the only part that is money and
the only part worth telling you about. A chop costs you half of it. The audit
log takes Paid too: the house's income is what it made off the player, not what
it lifted off a bot.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
Phase 4. Hold'em, and it's the only table in the casino that is a session
rather than a game: you buy in, play as many hands as you like, and leave with
what's in front of you. So the live row spans hands and chips cross the border
exactly twice. Everything in between is inside the engine.
The bots move inside ApplyMove, as UNO's do, which is what keeps poker off a
socket: shove all-in and the flop, turn, river, showdown and payout all come
back in one response, as a script the felt plays back.
The CFR policy the plan called "the single highest-value asset in either repo"
was never read. Not once, in the whole life of the game: the trainer wrote its
info-set keys under IP/OOP and the runtime looked them up under BTN/SB/BB, so
every lookup missed and fell silently through to a pot-odds heuristic. Nothing
looked broken, because a policy miss is not an error. And it was the wrong
policy anyway — ten big blinds deep, trained on a tree where a call always ends
the street, which is not poker. So the trainer is rewritten to play the real
engine through the real reducer, at every stack depth the table deals, and the
trainer and the table now build the key with the same function so they cannot
drift apart again. A test fails if the bots stop finding themselves in it.
Three money bugs, and the tests earned their keep. Chip conservation across a
hundred sessions caught an uncalled bet that minted chips. A var-init ordering
trap meant every card was identical, every showdown tied and every bot believed
it held exactly 50% equity. And the browser caught the rake being silently
zero — the tier said 5 meaning percent, the casino handed it 0.05 meaning a
fraction, and integer division took the house's cut down to nothing.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
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
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
The trivia ladder handled a walk before it looked at the clock, so the
timeout only ever bit if the browser volunteered it. Sit on a question,
look it up, answer if you find it and walk if you don't, and you never
lose a ladder. The clock is now the first thing that happens to a move.
The house's chip rack was wired up as bet buttons on blackjack and
hangman: it's four spans with data-chip on them and nothing said the
handler only wanted the real ones. Clicking the house's money raised
your bet.
Hangman had two definitions of "a letter you'd guess" — unicode in the
engine, ASCII in the renderer — and a phrase with an accent in it would
have had no tile to fill and no key to fill it with. One definition now.
Plus: trivia's countdown no longer freezes at zero when the server turns
down a timeout report it was early for, questions whose wrong answer
decodes into the right one are dropped at the door, and hangman bets on
PeteFX's spot like every other table instead of its own copy of it.
Solitaire, Vegas rules — the only shape solitaire has ever had as a
gambling game. You don't win or lose the deal: the stake buys the deck
outright, and every card you get home to a foundation pays a fifty-second
of the tier's multiple back. Cash the board whenever you like and keep
what you've banked, so a board that has gone dead is a decision rather
than a wall. No undo: the stake is spent the moment the deck is bought,
and an undo would be a way to walk a losing board backwards until it wins.
Three deals, and the two dials are the whole difficulty of Klondike.
Patient draws one with unlimited passes and pays 1.4x, so it takes 38
cards home to get square. Vegas draws three, three times round, 2.2x,
square at 24. Cutthroat draws three and gives you one pass, 3.4x, square
at 16 — most of those boards never clear, and you're ahead long before
they would.
internal/games/klondike is the same pure reducer as the other two, and
Pays() is one function for the same reason hangman's is. Two fuzzers hold
the deck together: no sequence of moves can lose or duplicate a card, and
the board stays well-formed. They earned their keep immediately — the
first thing they caught was a recycle that reversed the waste. It flips as
a block, so the card drawn first comes out first, and reversing it would
have dealt a different game on every pass and quietly broken the seed in
the audit log.
The browser never sees the stock or a face-down card, which here is most
of the deck rather than blackjack's one hole card: a column sends how many
cards are under it, never which.
The table re-renders and animates the difference. Blackjack plays back a
script because a hand only ever grows at one end; solitaire moves runs
from anywhere to anywhere and an auto-finish moves eleven cards at once,
so a script of "append this card there" would be a second engine over here
and it would be the one that's wrong. Instead the board on screen is
always exactly the board the server says exists, and each card is played
from where it just was to where it now is. The events supply only what a
diff can't: where a newly-revealed card came from, and what the board is
worth.
The rules are mirrored in JS on purpose, and only to light up the columns
a held card can go to. Being shown where a card goes is the game teaching
you; being told no after you commit is the game scolding you. The server
still decides, and a disagreement snaps the board back to what it says.
Two things came out into the open rather than being copied, which is the
rule this room runs on: casino-cards.js (the deck — faces, pips, the flip)
and PeteFX.spot() (the pile of chips and the number under it, which now
owns the rule that the number is a readout of the pile). Blackjack uses
both.
Not yet driven in a browser.
Hangman, and it plays for chips — which the plan had down as a free game, on
the grounds that trivia has no euro coupling in gogobee. But a free game in a
casino reads as a demo, so it stakes like everything else.
The idea that makes it a casino game rather than hangman with a wager stapled
on: the gallows is the payout meter. A wrong guess draws a limb *and* takes a
tenth off what a win is worth, because those are the same event and showing
them as one is the entire reason to bet on this. Short phrases pay 2.6x (fewer
letters, less to go on), long ones 1.6x — the floor is 1x, so a win never hands
back less than the stake, and the rake still comes out of winnings only.
State.Pays() is the number the felt quotes and the number settle() lands on.
They were briefly two sums, and the table spent an afternoon advertising a
pre-rake payout it didn't honour.
Two things the storage layer had already decided for us, and one it hadn't:
game_live_hands is keyed on the player, so "one game at a time" holds across
games for free (a live hangman 409s a blackjack deal, stake intact). But
table() unmarshalled every live row as a blackjack hand, which does not fail on
a hangman row — it quietly yields an empty hand. It dispatches on the game now.
commit() is the settle path both games share, and casinoRoutes() the one route
list, since the dev rig wires its own mux and a second copy is a copy that stops
including the newest game.
Driven in a browser, win and loss: 200 at 2.34x paid 455 and the bar landed on
it; six wrong took the stake and no more; a reload mid-phrase brought back the
board, the limbs, the multiple, the spent keys and the chips on the spot. The
browser found the two bugs a Go test can't — a lives counter under the house
rack, and a word wrapping early because the rack's clearance was on the whole
column instead of the one row beside it.
The first two pieces of games.parodia.dev, both pure: no HTTP, no timers, no
euros, nothing that knows a player's name.
cards/ is the shared deck gogobee never had — blackjack carried its own, UNO
carried another, hold'em leaned on a third-party one. The RNG is threaded rather
than the package global, so a hand is reproducible from its seed. That's what
makes the engine testable, and what lets a disputed hand be dealt again exactly
as it fell.
blackjack/ is ApplyMove(state, move) -> (state, events, error), where an error
means the move was illegal and nothing else. gogobee's engine *was* the message
sender, so its errors meant "the send failed"; there was no seam to test against.
State is a plain value, so a hand survives a redeploy.
House terms match the Matrix table — six decks, 3:2, dealer hits soft 17 — plus a
5% rake. The rake comes off winnings only: a push returns the stake untouched and
a loss is never charged for the privilege.