games: a blackjack table you can actually sit down at
The engine, the escrow and the wire were all in place; nothing had a browser on the end of it. This is that end: a lobby, a table, and the five endpoints between them. The browser holds no game. It sends intents and gets back a view — the cards it is entitled to see, and the script of how they arrived, one event per card off the shoe. The dealer's hole card is not in the payload at all until the reveal, because a field the client is told to ignore is a field somebody reads in devtools. The shoe lives in game_live_hands, which also means a redeploy mid-hand no longer costs a player their stake: the hand is still there when they come back. The money is ordered so nothing can be spent twice. The stake leaves the stack in the same statement that checks it exists, before a card is dealt. Every new hand is seated with a plain INSERT, so a double-clicked Deal is decided by the primary key rather than by a read that raced — it loses, gets its chips back, and the hand in progress is untouched. A double takes its raise up front and hands it straight back if the engine refuses the move. Cards are dealt rather than swapped in — they fly out of the shoe and turn over, which was a requirement and not a flourish. The faces and the chips are still plain; that's next.
This commit is contained in:
@@ -8,7 +8,7 @@ This plan reuses that seam wholesale and does not invent a second one.
|
||||
|
||||
---
|
||||
|
||||
## 0. Progress — last updated 2026-07-13
|
||||
## 0. Progress — last updated 2026-07-14
|
||||
|
||||
A multi-session build. This section is the handover; read it before anything else.
|
||||
|
||||
@@ -58,17 +58,49 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
gogobee crash replays as a no-op: 13 tests across both repos, including a fake
|
||||
Pete that offers the same row three times and a player who is charged once.
|
||||
|
||||
- **Identity.** `preferred_username` now rides in the signed session, and
|
||||
`SessionUser.MatrixUser(server)` maps it to `@user:parodia.dev`. The session cookie
|
||||
takes an opt-in `web.auth.cookie_domain`, so a sign-in on news is a sign-in on games;
|
||||
the OAuth round-trip cookie deliberately stays host-only, and the redirect_uri is
|
||||
derived per-request so a login that starts on games comes back to games. A Host we
|
||||
don't own is never echoed into a redirect. *(pete `cb84e1d`)*
|
||||
- **Blackjack, playable end to end.** `game_live_hands` (the hand in progress,
|
||||
engine state and all, so a redeploy mid-hand is survivable), the session-authed play
|
||||
surface (`internal/web/games_play.go`), the lobby and table pages, and the dealing
|
||||
animation. Driven in a real browser: chips staked before the deal, hole card withheld
|
||||
from the payload until the reveal, payout settled back into the stack.
|
||||
|
||||
### Next, in order
|
||||
|
||||
1. **Identity.** `PreferredUsername` into the OIDC claims struct and the signed cookie;
|
||||
cookie `Domain: ".parodia.dev"` so a news session travels to games. Add the games
|
||||
redirect URI to the `pete` app in Authentik.
|
||||
2. **Frontend.** Host branching in the mux, lobby + blackjack table, animated dealing.
|
||||
Nothing yet *opens* an escrow row from a browser — `RequestBuyIn`/`RequestCashOut`
|
||||
have no HTTP surface, on purpose: they need a signed-in Matrix identity, which is
|
||||
step 1.
|
||||
1. **Make the table lively.** The mechanics are all there and the dealing animates, but
|
||||
the presentation is thin: card faces are a rank and a small suit (no pips, no corner
|
||||
indices), chips never physically move, and nothing celebrates. This is a *stated
|
||||
requirement*, not polish — see the decisions above. Ideas already sketched: chips that
|
||||
fly to a bet spot and back on a win, cards landing with weight, a dealer beat before
|
||||
drawing out, a burst on a natural.
|
||||
2. **Deploy.** Add the `games.parodia.dev` redirect URI to the `pete` app in Authentik,
|
||||
point Caddy at the same port, set `[web.games]` + `web.auth.cookie_domain` on the
|
||||
server. Nothing else is host-specific.
|
||||
3. Then Phase 2 (trivia, hangman), 3 (UNO), 4 (hold'em) as below.
|
||||
|
||||
### How the browser half fits together
|
||||
|
||||
- `GET /games` (lobby), `GET /games/blackjack` (table) — signed-in only. On the games
|
||||
host, the mux prefixes `/games` onto the path, so the lobby is that host's `/`. Shared
|
||||
paths (`/api/`, `/auth/`, `/static/`) mean the same thing on every host and are left
|
||||
alone.
|
||||
- `GET /api/games/table`, `POST /api/games/{buyin,cashout}`,
|
||||
`POST /api/games/blackjack/{deal,move}` — session-authed, JSON, all returning the same
|
||||
`tableView` so the money and the felt can never disagree.
|
||||
- **The browser never sees the shoe.** The dealer's hole card is *absent* from the
|
||||
payload — not flagged hidden — until the reveal, and the deck lives only in
|
||||
`game_live_hands`. The response carries the engine's events (one per card off the
|
||||
shoe), which is what the table plays back as an animation.
|
||||
- Money order-of-operations: stake leaves the stack *before* the hand is dealt, in the
|
||||
same statement that checks it's there; the hand is *seated* (a plain INSERT on the
|
||||
primary key) before it can settle, which is what makes a double-clicked Deal a 409 with
|
||||
the stake refunded rather than a silently overwritten hand.
|
||||
|
||||
### Notes for whoever picks this up
|
||||
|
||||
- SQLite runs at `MaxOpenConns(1)` in *both* repos. Any `db.Get().Exec` inside an
|
||||
|
||||
Reference in New Issue
Block a user