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:
@@ -49,6 +49,7 @@ var (
|
||||
ErrUnknownTier = errors.New("holdem: no such table")
|
||||
ErrBadBuyIn = errors.New("holdem: that isn't a legal buy-in")
|
||||
ErrTableFull = errors.New("holdem: too many seats")
|
||||
ErrSeatTaken = errors.New("holdem: that seat is taken")
|
||||
)
|
||||
|
||||
// rakeCapBB caps the rake on any one pot at three big blinds, which is what a
|
||||
@@ -321,13 +322,98 @@ func New(t Tier, seats []SeatConfig, rakePct float64, seed1, seed2 uint64) (Stat
|
||||
// The human is seat zero and takes buyIn; the bots take the table maximum.
|
||||
// SoloSeats is exported for the handler that still opens a solo table. See New.
|
||||
func SoloSeats(t Tier, bots int, buyIn int64) []SeatConfig {
|
||||
seats := []SeatConfig{{Name: "You", Stack: buyIn}}
|
||||
for i := 0; i < bots; i++ {
|
||||
return TableSeats(t, "You", bots, buyIn)
|
||||
}
|
||||
|
||||
// TableSeats builds a table of one named human and n bots. It is what a player
|
||||
// opening their own table gets: a chair with their name on it, and the rest of
|
||||
// the felt filled with the house's regulars so the table is never empty. The
|
||||
// human takes seat zero and their buy-in; each bot takes the table maximum in
|
||||
// house chips, which are not real money and get rebought when a bot runs dry.
|
||||
func TableSeats(t Tier, human string, bots int, buyIn int64) []SeatConfig {
|
||||
seats := []SeatConfig{{Name: human, Stack: buyIn}}
|
||||
for i := 0; i < bots && i < len(botNames); i++ {
|
||||
seats = append(seats, SeatConfig{Name: botNames[i], Bot: true, Stack: t.MaxBuy})
|
||||
}
|
||||
return seats
|
||||
}
|
||||
|
||||
// freeBotName picks a regular not already sitting at the table, so vacating a
|
||||
// seat never puts two Marjories on the felt. It falls back to a generic name if
|
||||
// every regular is somehow taken, which a six-max table cannot actually manage.
|
||||
func (s *State) freeBotName() string {
|
||||
used := make(map[string]bool, len(s.Seats))
|
||||
for i := range s.Seats {
|
||||
used[s.Seats[i].Name] = true
|
||||
}
|
||||
for _, n := range botNames {
|
||||
if !used[n] {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return "The House"
|
||||
}
|
||||
|
||||
// Vacate turns a human's chair back into the house's and returns the stack that
|
||||
// goes home with them. It is how a shared table survives a player getting up: the
|
||||
// seat keeps its place and its chips — which become house money, rebought like
|
||||
// any bot's when they run low — so the others play on without a hole in the ring.
|
||||
//
|
||||
// The chips left in the seat are not a leak. The only real money at the table is
|
||||
// in the human seats; the moment a seat is a bot's, its stack is house chips that
|
||||
// nobody's balance is counting. What the player actually takes home is the
|
||||
// returned stack, credited by the runtime in the same transaction that saves this
|
||||
// state — see storage.LeaveTable.
|
||||
//
|
||||
// It refuses while a hand is live, because a seat with chips in the pot cannot be
|
||||
// emptied without stranding them. PhaseHandOver is the only phase Leave was ever
|
||||
// legal from.
|
||||
func (s *State) Vacate(seat int) (int64, error) {
|
||||
if seat < 0 || seat >= len(s.Seats) {
|
||||
return 0, ErrUnknownMove
|
||||
}
|
||||
if s.Phase == PhaseBetting {
|
||||
return 0, ErrHandLive
|
||||
}
|
||||
p := &s.Seats[seat]
|
||||
if p.Bot {
|
||||
return 0, ErrUnknownMove
|
||||
}
|
||||
home := p.Stack
|
||||
p.Bot = true
|
||||
p.Name = s.freeBotName()
|
||||
return home, nil
|
||||
}
|
||||
|
||||
// Occupy seats a human in a chair a bot was keeping warm, with the buy-in they
|
||||
// brought. It is the join half of Vacate: a player sitting down at somebody
|
||||
// else's table takes an open seat rather than opening a felt of their own.
|
||||
//
|
||||
// Like Vacate it is a between-hands move — you cannot sit into a live hand — and
|
||||
// the buy-in has to be legal for the table, because the chips are already off the
|
||||
// player's stack by the time this runs and an illegal seat would strand them.
|
||||
func (s *State) Occupy(seat int, name string, buyIn int64) error {
|
||||
if seat < 0 || seat >= len(s.Seats) {
|
||||
return ErrUnknownMove
|
||||
}
|
||||
if s.Phase == PhaseBetting {
|
||||
return ErrHandLive
|
||||
}
|
||||
if !s.Seats[seat].Bot {
|
||||
return ErrSeatTaken
|
||||
}
|
||||
if buyIn < s.Tier.MinBuy || buyIn > s.Tier.MaxBuy {
|
||||
return ErrBadBuyIn
|
||||
}
|
||||
p := &s.Seats[seat]
|
||||
p.Bot = false
|
||||
p.Name = name
|
||||
p.Stack = buyIn
|
||||
p.State = Out // between hands; the next deal brings them in
|
||||
s.BoughtIn += buyIn
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyMove is the whole engine. It plays the acting seat's move, then every bot
|
||||
// behind them, deals whatever streets that finishes, and stops when the action
|
||||
// reaches a human — the same one again, or another at the table — or when the
|
||||
|
||||
Reference in New Issue
Block a user