games: the felt that knows which seat is yours, and the rail you can talk on

Phase C frontend: the hold'em felt runs on the shared-table runtime.

- holdem.js reads view.your_seat instead of assuming seat zero — every "you"
  test (layout, your cards, the burst on a pot you win, the verdict) is keyed on
  it now, so a joiner at seat 2 sees their own hand at the bottom.
- Leaving is its own endpoint, and a bust closes a solo table; play() animates a
  session-ending hand (the last showdown) before the felt clears.
- A live table: one EventSource per seated player. The server pushes a nudge on
  every table change and a chat line as it is said; a nudge refetches the player's
  own redacted view (a hole card must never ride a frame that fans to the table),
  and a frame that lands mid-animation is held until the script finishes.
- Chat on the felt (a _chat panel, messages only) and a lobby that lists tables
  with a seat going spare. Two-cookie dev rig (reala + bob), with the turn clock
  and reaper live under it.

Browser-confirmed for solo: sit renders your seat and the rail, a hand deals and
conserves to the chip (bought in 100, 100 in front), chat sends. The two-browser
multiplayer pass (join, live sync between windows, shared-table conservation) is
still owed before this deploys.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
prosolis
2026-07-14 16:49:27 -07:00
parent 5139385350
commit 4ad96dcb5e
5 changed files with 301 additions and 44 deletions

View File

@@ -35,13 +35,18 @@ func TestDevCasino(t *testing.T) {
s := newCasino(t)
fund(t, 5000)
fundUser(t, bobPlayer, 5000)
seedTriviaBank(t)
payload, _ := json.Marshal(SessionUser{
Sub: "sub-1", Username: "reala", Name: "Reala",
Exp: time.Now().Add(24 * time.Hour).Unix(),
})
cookie := s.auth.sign(payload)
// The full table runtime, so the turn clock and the reaper are live under the
// browser exactly as in production.
s.StartTableClock(context.Background())
cookie := devCookie(s, "reala", "Reala")
// A second player, so a shared table can be reviewed — hold'em is multiplayer
// now, and one browser cannot see two people at the felt. Plant this cookie in
// a second browser profile (or a private window) to sit down as Bob.
bobCookie := devCookie(s, "bob", "Bob")
staticSub, err := fs.Sub(staticFS, "static")
if err != nil {
@@ -57,19 +62,31 @@ func TestDevCasino(t *testing.T) {
t.Fatal(err)
}
// Written to a file, not printed: `go test` buffers stdout, and the browser
// driver needs the cookie while the server is still running.
// driver needs the cookie while the server is still running. The second cookie
// rides alongside it, newline-separated, for the second browser.
if out := os.Getenv("PETE_DEV_COOKIE_FILE"); out != "" {
if err := os.WriteFile(out, []byte(cookie), 0o600); err != nil {
if err := os.WriteFile(out, []byte(cookie+"\n"+bobCookie), 0o600); err != nil {
t.Fatal(err)
}
}
fmt.Printf("\nCASINO http://localhost%s/games\nCOOKIE %s=%s\n\n", addr, sessionCookie, cookie)
fmt.Printf("\nCASINO http://localhost%s/games\nCOOKIE %s=%s\nBOB %s=%s\n\n",
addr, sessionCookie, cookie, sessionCookie, bobCookie)
srv := &http.Server{Handler: mux, ReadHeaderTimeout: 5 * time.Second}
t.Cleanup(func() { _ = srv.Close() })
_ = srv.Serve(ln)
}
// devCookie mints a signed session for a player the rig has funded, so the felt
// can be driven as them.
func devCookie(s *Server, username, name string) string {
payload, _ := json.Marshal(SessionUser{
Sub: "sub-" + username, Username: username, Name: name,
Exp: time.Now().Add(24 * time.Hour).Unix(),
})
return s.auth.sign(payload)
}
// seedTriviaBank puts enough questions in the bank to deal a ladder of each
// difficulty.
//