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.
This commit is contained in:
prosolis
2026-07-14 01:19:05 -07:00
parent d29a311eff
commit fe2195e85f
19 changed files with 2678 additions and 81 deletions

View File

@@ -147,9 +147,55 @@ A multi-session build. This section is the handover; read it before anything els
that adventure news already set. Restarted, it logs
`pete games: escrow loop started interval=3s`.
- **Hangman, and it plays for chips.** *(2026-07-14. This revises §7's "Phase 2 —
no escrow": the decision was that a free game in a casino reads as a demo, so
hangman stakes chips like everything else and reuses the money path whole.)*
- **The gallows is the payout meter.** You pick a tier, stake, and get six
lives. Every wrong guess draws a limb *and* takes a tenth off the base
multiple — one event, shown as one event. Short phrases pay 2.6×, medium 2.0×,
long 1.6× (short is hardest: fewer letters, less to go on). Floored at 1×, so
a win never hands back less than the stake, and the rake still comes out of
winnings only.
- `internal/games/hangman` — the same pure reducer as blackjack, phrases
embedded (`phrases.txt`, 205 of them, video-game flavoured, lifted from
gogobee). `State.Pays()` is the number the felt quotes *and* the number
settle() lands on: they were briefly two sums and the table advertised a
pre-rake payout it didn't honour. One function now, and a test that walks a
game asserting the quote equals the payout at every step.
- **The browser never sees the phrase.** Cells carry the letter or an empty
string — not the letter with a hidden flag — and the phrase itself is only
added to the payload once the game is over and it decides nothing.
- Two things the storage layer already gave us for free, and one it didn't:
`game_live_hands` is keyed on the *player*, so "one game at a time" holds
across games with no new code (a live hangman 409s a blackjack deal). But
`table()` used to unmarshal any live row as a blackjack hand — which does not
*fail* on a hangman row, it just silently yields an empty hand. It now
dispatches on `live.Game`.
- `commit()` in games_play.go is the shared settle path (seat → pay → audit →
clear → touch). Both games go through it so neither re-derives an ordering
that took a while to get right. `casinoRoutes()` is likewise the single route
list, because devcasino_test.go has to wire its own mux and a second copy is
a copy that stops including the newest game.
- Driven in a real browser, win and loss: a 200 stake at 2.34× paid 455 and the
bar landed on it; six wrong took the stake and nothing more; a reload
mid-phrase brought back the board, the limbs, the multiple, the spent keys and
the stake on the spot. Two layout bugs only the browser could show: the lives
counter ran under the house rack, and the board wrapped a word early because
the rack's clearance padding was on the whole column instead of the one row
level with it.
### Next, in order
1. Phase 2 (trivia, hangman), 3 (UNO), 4 (hold'em) as below.
1. Phase 2's other half: **trivia**. Decided but not built: the question bank is
**prefetched from OpenTDB into a local table** (a per-question fetch in a web
game loop is a latency and rate-limit problem gogobee never had), through
`internal/safehttp`. It stakes chips too. The shape that fits the room is a
**ladder**: stake once, answer a run of questions, each right answer compounds
the multiple and a wrong one loses the lot, with the option to walk. Note the
real risk — trivia answers are googlable, so a tight per-question clock is the
only thing making a slow-but-correct answer worth less than a fast one. Score
the clock server-side; the browser's countdown is decoration.
2. Phase 3 (UNO), Phase 4 (hold'em) as below.
Still open on the table itself, none of it blocking: **split** isn't implemented (the
engine has no move for it), the felt is roomy at desktop widths with only one seat on