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
This commit is contained in:
prosolis
2026-07-14 16:37:49 -07:00
parent 5b381b03ff
commit 5139385350
12 changed files with 1537 additions and 122 deletions

View File

@@ -1,6 +1,18 @@
package web
import "pete/internal/storage"
import (
"errors"
"pete/internal/storage"
)
// errNotDue is a timeout that turned out to have nothing to do. The clock scanned
// a table as expired, took the lock, and found — on decoding the state — that the
// seat to act is not in fact a waiting human: a real move landed in the instant
// between the scan and the lock and had not yet bumped the version the clock
// checked. It is not an error, it is the race resolving the right way, so the
// clock swallows it silently rather than logging.
var errNotDue = errors.New("games: nothing to time out")
// The table runtime: the game-agnostic half of a shared table.
//
@@ -69,4 +81,9 @@ type tableGame interface {
// landed in the same instant the clock fired and the version had not yet been
// bumped when the clock scanned.
timeout(state []byte, seats []storage.Seat) (step, []storage.Seat, error)
// stacks reports the chips in front of each seat, index-aligned with the
// table's seat rows, so the abandoned-table reaper can cash out a walked-away
// player without knowing how their game is played.
stacks(state []byte) ([]int64, error)
}