30b0e8debbfce65c18e5a3960a6f18d3198cec09
21
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
927ed84163 |
games: UNO becomes a table you sit at, and the pot that pays whoever goes out first
Phase D backend: UNO is now a session like hold'em, not a single stake. You sit
with a buy-in stack, ante into a pot each hand, and leave with what's in front of
you. The engine lost its `You` constant and its measured multiples: ApplyMove
takes the acting seat, New takes a seat list, a Tier carries an ante instead of a
Base, and a hand settles by moving the pot to the winner (less rake, and never
when a bot takes it) rather than paying a multiple. A mercy kill puts a seat out
of the hand, not out of the game — the last one standing takes the pot.
The redaction moved to the web layer, where hold'em's already lives: the engine
now stamps every seat's hand onto its events, and viewUno/viewUnoEvents strip
everything that isn't the viewer's own. TestUnoViewNeverLeaksAnotherSeatsCards is
the wall. unoTable implements tableGame; /uno/{sit,move,leave,tables,stream,chat,
say} mirror hold'em, with stream/chat/say now shared game-agnostic handlers.
The frontend is not done: uno.js still calls the retired solo endpoint, so the
page renders but is not yet playable. All engine and web tests are green.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
|
||
|
|
f8b07d8e6c |
games: the buy-in and the rake each player sees are their own, not the table's
The two-browser pass found it: at a table two humans share, the felt quoted each of them the pair's total. "Bought in for 200" to a player who put in 100, and a session-rake line that climbed on a pot the other one won. Both were table totals the view read straight off the engine — correct while a table had one human, wrong the moment it had two. Fixed along the border it already draws: bought_in is border accounting, so it comes from the viewer's own game_seats.staked; session rake is a within-table event, so it rides a new per-seat Seat.Paid beside the audit's table-total s.Paid. And top-up never grew game_seats.staked, so the storage invariant drifted by every top-up and the felt under-reported the buy-in — it does now. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
5139385350 |
games: the poker table others can walk up to, and the one that empties when they leave
Phase C's handler cutover: hold'em now runs on the shared-table runtime instead of the solo game_live_hands blob. Solo is just a table nobody else has joined. - holdem implements tableGame (name/timeout/stacks). timeout auto-checks-or-folds a walked-away seat and marks it away; the audit is per-hand, with each pot's rake on the winner's row alone so HouseTake cannot 4x itself. - New endpoints: sit opens a table (or joins an open bot seat), leave gets you up (LeaveTable + CloseTable behind the last human), plus tables (lobby), stream (SSE), chat and say. The move path loads the player's table, applies at their seat, commits under the version guard, and fans an SSE nudge. - Engine grows Vacate/Occupy (a human leaving/joining between hands) and TableSeats (a named human + bots). The view carries your_seat, since a shared table has no seat-zero-is-you convention. - Storage grows OpenSoloTable (stake+claim+create+seat in one tx), PlayerSeat, and the abandoned-table reaper (AbandonedTables/ReapTable) — the seated-player counterpart to the session reaper, since a walked-away stack is inside a blob the session reaper cannot see. upsertSeat preserves last_seen so an auto-fold never refreshes an away player's clock. Not deployed, and the felt is not rewired yet: the frontend still assumes seat zero is you, so this is browser-unverified. Solo sit/deal/play/leave and two-human join/leave/reaper are covered by tests; the whole suite is green. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
5b381b03ff |
games: the poker engine learns there can be more than one of you
Phase C, the engine half: hold'em becomes multiway, and the redaction that was a bug-in-one-handler becomes the security boundary the plan warned it would. - const You is gone. A table is a list of seats and which are human is a per-seat property, not the fixed index zero. New(tier, []SeatConfig, ...) seats the ring; SoloSeats builds the old one-human-plus-bots shape the solo handler still opens. - ApplyMove(state, seat, move) — seat identity enters the engine in exactly one place; every helper below already worked on indices. The advance loop stops at any human (not just seat 0), so one request plays the bots and hands control back at whichever person is next to act. - deal() now emits every seat's hole cards. The engine cannot redact a stream it doesn't know the audience of, so it stops trying: the view layer builds each viewer's redacted copy. viewHoldem/viewHoldemEvents take a viewerSeat. - Rake attributed to Paid whenever a *human* wins, not just seat 0 — real house income is rake off any player's pot, and bot pots are house-vs-house. - Bust is per-seat: at a solo table it still ends the session (PhaseDone), at a shared one a busted human just goes Out and the table plays on. Tests, three ways, all green: - the solo suite unchanged as a regression guard (a test-local You=0 alias); - TestMultiwayChipsAreConserved — 100 games, two humans at seats 0 and 2, chips counted after every move, proving the reshape actually plays; - TestHoldemViewNeverLeaksAnotherSeatsCards — renders every seat's view and event stream at every street and greps for anyone else's cards. Mutation-tested: undo the redaction and it fails on the preflop deal. No handlers rewired yet — the solo path still calls New(SoloSeats(...)) and renders for seat 0, so nothing a player sees has changed. The table cutover is next. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
a5b7e41929 |
games: the card the dealer never turned over, and the bet that came back doubled
Bust every hand and the dealer doesn't draw, which is right, but it was also not turning over: reveal is only emitted by dealerPlay, and busting out skips the dealer entirely. The browser kept the hole card face down while the settled state printed the dealer's whole total under it. Emit the reveal on that path. And standing your bet back up after a reload read the hand's bet straight off the settled state, which a double has already doubled. Reload, double 200, and the next hand starts with 400 on the spot. Plus: the share card was hand-writing a Content-Length that ServeContent overwrites anyway, and serving a zero-byte 200 for a room with no card. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
6f34a89622 |
games: the hand that becomes two, and the bet that has to follow it
Blackjack has a split. It was the last rule missing from a game that has been live for a week, and it is the only move in blackjack that takes chips out of your stack *after* the cards are out — which is most of what there is to get wrong about it. So the state stops pretending. State.Player is gone; there is a slice of Hands, each with its own cards, its own bet, its own outcome and its own payout, and an Active index the player works left to right. Settle runs per hand and rakes per hand: netting them against each other first would mean a player who won one and lost one paid no rake at all, which is not a rake, it's a discount for splitting. The web layer takes the second bet before the move and hands it straight back if the engine refuses — the same shape double already used, except double was staking st.Bet, the whole table's stake, which was the same number as the hand's until today and is now emphatically not. DoubleCost/SplitCost are the active hand's, and the felt would have found this by charging you 300 to double the third hand of a split. The rules that cost money if you guess them: split aces get one card each and no say (a pair of aces is otherwise the best hand in the game, forever), 21 on a split hand is twenty-one and not a natural (it does not pay 3:2 — the test that pins this is the most expensive one in the file), same rank rather than same value (a king and a queen are not a pair), four hands maximum, double after split allowed, and if every hand busts the dealer does not turn over. A live hand outlives a deploy, so State.UnmarshalJSON still reads the old blobs: "player" with no "hands" becomes one hand holding the whole stake. Without it, a player mid-hand at restart is a player whose cards vanished — which is not a decode error, and would not have looked like one. On the felt a hand is now a box with its own spot, and a split is a card lifting out of one hand into a new one with a second stack of chips flying after it from your pile. Verified in a browser against a real pair: chips 4738 -> 4638 on the split, two hands played out, one push and one loss, "Down on the deal. -100", 4738 back. Three hands stack without collision at 390px. Settled hands come back to full brightness — dimming means "not your turn", and when the deal is over they are the thing you are reading. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
39ed293f4f |
games: the word you owe the table, and the hand you were already holding
Three things, and the first one was a bug. Your own hand didn't move until the lap ended. bump() keeps the bots' fans honest and has always refused seat zero, and nothing else touched yours — so a +4 landing on you at the top of a lap put four backs into your hand and then nothing, and the cards themselves turned up seconds later when the script finished and paint() finally ran. You spent the whole lap looking at a hand you no longer held. The engine now stamps your hand onto every event that changes it (Event.Hand, seat zero only, which is the one hand the browser is already entitled to see) and the table redraws as the cards land. Measured in the running app: 2 -> 3 cards at 414ms into a 1791ms lap. You couldn't call UNO, and not because the button was missing: going down to one card *was* the call. discard() fired the uno event by itself, which made it a thing that happened to you rather than a thing you did, and a rule nobody can fail is not a rule. So now you say it or you don't (Move.Uno), and if you don't, every bot still in the game gets one look at you before any of them plays — because a bot that has moved on is a bot that has stopped watching your hand. It runs the other way too, and that half is the fun one: a bot forgets often enough to be worth watching for, and when it does it says *nothing*. No event, no badge, no tell on the felt except the count beside its fan reading "1 card". Catch it and it takes two; call a seat that had nothing to hide and you take two yourself, which is what stops the catch button from being a thing you simply mash. Which cards owe the call is the engine's answer, not a count of your hand: No Mercy's "discard all" takes every card of its colour with it, so a six-card hand can land on one, and a browser subtracting one from six walks you into a catch it never warned you about. And the room was silent. Every sound in here is *made* — an oscillator, a burst of filtered noise, an envelope — the same bargain the weather engine takes with its clouds. A card is a slap of noise through a bandpass, a chip is two detuned sines with a knock on the front, a win is four notes going up. No asset files, no round trips, and a sound can be pitched and detuned per call instead of being the same wav three hundred times. Hooked into the FX layer rather than into the games, so every table that throws a chip or turns a card got it at once. The multiples moved, and the test that exists to catch that caught it. The naive strategy now calls UNO, because calling is a button and not a strategy — what these tiers price is bad card play, not a player who ignores the felt shouting at them — and on that footing the normal tables come back to where they were (40.1 / 28.5 / 23.1). No Mercy Full House did not: it was paying a *negative* house edge, which is the house paying you to sit down. Re-priced 3.8 -> 3.5. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
03524aefbc |
games: the seat you sit in, and the seat you are left in
Six-handed, the felt printed CO on three seats at once. Position walked the table with nextIn, which steps over folded seats, while the seat count it walked against still included them — so every muck slid the anchors round and the labels landed somewhere new. Folding the small blind relabelled it the cutoff. The two walks are a pair and they are easy to confuse. nextIn asks who is still in the betting; a fold takes you out of it. Position needs the other question — who was dealt in — because where you sit is decided when the button moves and does not change because somebody threw their hand away. So nextDealt, which skips only the seats that are not in the hand at all, and a note at both of them saying which is which. The bots never read this. They use InPosition, which really does want the last seat still live, and which is deliberately not this function. So the policy is untouched and the money never moved — the only thing this ever broke was the badge on the plate, which is precisely why nothing caught it. TestPositionsDoNotMoveWhenSeatsFold deals six-handed, asserts the table prints each of BTN/SB/BB/UTG/MP/CO exactly once, then folds the seats out from under it one at a time and asserts nobody's label moves. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
8db8845feb |
games: no mercy on the felt, and the bill that went to the wrong window
The engine has been able to play No Mercy since
|
||
|
|
aca523e511 |
games: no mercy, and the multiples nobody re-measured
No Mercy UNO as a rules dial on the existing tier, not a fourth table: 168 cards, draw-until-playable, draw-stacking, and the twenty-five card mercy kill. Six tiers now; a normal game never runs a line of the new code. The engine is the whole of it so far — the felt hasn't been touched, so there is no way to play this in a browser yet. Two things worth knowing. The normal tiers were mispriced, and had been for a while. They were set against a naive win rate of 43/32/27%; it now measures 40.3/29.2/23.3%. The bots got better at some point after the multiples were written down and nobody re-ran the measurement — which the plan explicitly warns about, because the bots and the tiers are a pair. Table and Full House had been charging an 18–19% house edge instead of the 8% they were meant to. All six tiers are repriced off a fresh measurement, and TestTheMultiplesAreStillPriced now fails the build if they drift again. It is the test the normal tiers never had, which is how they drifted. And No Mercy is *easier* than UNO, at every table size, so it pays less. The mercy rule does not care whose hand hits twenty-five: it kills bots too, and every bot it buries is one fewer seat that can beat you to the last card. A deck built to be merciless turns out to be merciless mostly to the table. The rake test used to assert a payout of 214, which was the 2.2x duel written down as a number. It failed on a rake that was entirely correct. It derives the arithmetic from the tier now: the rule is that the house takes its cut of the profit and never touches the stake, and that holds at any multiple. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
4bc38859d4 |
games: the bots come back from school
The 20M-hand policy the trainer was running on millenia, collected. 4,159 nodes, which is barely more than the 300k-hand placeholder had — the info-set abstraction is coarse, so what twenty million hands bought is better-converged strategies at the same decision points, not a bigger tree. The heads-up hit rate is 94% and chips still conserve across a hundred sessions of real hands. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
b96879d25c |
games: the short stack that could win money it never matched
A review pass, and it found the one that would have cost somebody real chips. Side pots were only ever cut in runout() — the path taken when the betting stops because nobody is left able to bet. But a hand reaches a showdown with an all-in player in it and the betting having finished perfectly normally: a short stack shoves, two players who still have chips behind call, and then keep betting past them street after street to the river. Nothing was cut. One pot, everybody eligible, and the short stack takes the lot — every chip the deep players put in after they were already all-in, money that could never have been lost to them. All-in for 100 against two players who each put in 500, and the best hand collects 1,100 instead of the 300 it was playing for. Chip conservation never saw it. The chips balance perfectly; they just land in the wrong seat. And every browser session went through runout(), because a player shoving is what ends the betting. It took reading the code. Also from the review: play() dereferenced a table it had just been handed as null, the top-up button offered chips the wallet could not cover, and the trainer's ETA was sixty thousand hands optimistic on the first line it printed. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ |
||
|
|
903c5accdb |
games: the rake you pay, and the rake the table lifts
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 |
||
|
|
e6c1bd3b54 |
games: the poker table opens, and the bots go back to school
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 |
||
|
|
6e20883e5d |
games: the table that couldn't end, and the lock that let go too early
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 |
||
|
|
79c857023f |
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 |
||
|
|
3e9b93af55 |
games: the clock beats the walk button, and the rack isn't betting
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. |
||
|
|
c62d736223 | games: a ladder you climb against the clock | ||
|
|
5ca056bf20 |
games: you buy the deck, and win it back a card at a time
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. |
||
|
|
fe2195e85f |
games: a gallows you can bet on
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. |
||
|
|
8310b30439 |
games: a deck you can seed and a blackjack you can replay
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. |