games: the buy-in and the rake each player sees are their own, not the table's
The two-browser pass found it: at a table two humans share, the felt quoted each of them the pair's total. "Bought in for 200" to a player who put in 100, and a session-rake line that climbed on a pot the other one won. Both were table totals the view read straight off the engine — correct while a table had one human, wrong the moment it had two. Fixed along the border it already draws: bought_in is border accounting, so it comes from the viewer's own game_seats.staked; session rake is a within-table event, so it rides a new per-seat Seat.Paid beside the audit's table-total s.Paid. And top-up never grew game_seats.staked, so the storage invariant drifted by every top-up and the felt under-reported the buy-in — it does now. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
@@ -109,8 +109,8 @@ func viewHoldem(g holdem.State, viewer int) holdemView {
|
||||
ToAct: g.ToAct,
|
||||
Phase: string(g.Phase),
|
||||
Stack: g.Seats[viewer].Stack,
|
||||
BoughtIn: g.BoughtIn,
|
||||
Rake: g.Paid, // the part players actually paid, not the part the table lifted
|
||||
BoughtIn: g.BoughtIn, // a table total; the caller overrides it with this seat's own stake
|
||||
Rake: g.Seats[viewer].Paid, // this seat's rake alone, not the table's — see the ledger line on the felt
|
||||
Payout: g.Payout,
|
||||
}
|
||||
for _, p := range g.Side {
|
||||
@@ -543,6 +543,11 @@ func (s *Server) handleHoldemMove(w http.ResponseWriter, r *http.Request) {
|
||||
acting := seats[seat]
|
||||
acting.Away = false
|
||||
acting.LastSeen = time.Now().Unix()
|
||||
// A top-up is more real money crossing the border, so the seat's staked row —
|
||||
// the record of what they brought and can still take home — has to grow with
|
||||
// it. Without this the storage invariant (stacks + pot == staked - rake) drifts
|
||||
// by every top-up, and the felt under-reports what they bought in for.
|
||||
acting.Staked += topped
|
||||
|
||||
t.State, t.Phase, t.HandNo, t.Deadline = st.State, st.Phase, st.HandNo, st.Deadline
|
||||
if err := storage.CommitTable(storage.TableCommit{
|
||||
|
||||
@@ -87,6 +87,36 @@ func TestHoldemJoinTakesAnOpenSeat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// The felt tells each player "you bought in for X" — their own stake, not the
|
||||
// table's. The engine's BoughtIn is the sum across every human (the audit wants
|
||||
// that), so a two-human table would quote both of them the pair's total if the
|
||||
// view read it straight. It reads each seat's own staked row from storage instead.
|
||||
func TestBoughtInIsPerPlayerNotTheTableTotal(t *testing.T) {
|
||||
s := newCasino(t)
|
||||
fund(t, 5000) // reala
|
||||
fundUser(t, bobPlayer, 5000)
|
||||
|
||||
call(t, s, s.handleHoldemSit, as(t, s, "reala", "POST", "/api/games/holdem/sit",
|
||||
map[string]any{"tier": "low", "bots": 2, "buyin": 500}))
|
||||
tableID, _ := storage.TableOf(testPlayer)
|
||||
call(t, s, s.handleHoldemSit, as(t, s, "bob", "POST", "/api/games/holdem/sit",
|
||||
map[string]any{"table": tableID, "buyin": 500}))
|
||||
|
||||
for _, who := range []string{testPlayer, bobPlayer} {
|
||||
v, err := s.table(who)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if v.Holdem == nil {
|
||||
t.Fatalf("%s has no table", who)
|
||||
}
|
||||
if v.Holdem.BoughtIn != 500 {
|
||||
t.Errorf("%s is shown bought-in %d, want their own 500 — not the table's 1000",
|
||||
who, v.Holdem.BoughtIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Leaving a shared table gives you your stack and hands your chair back to the
|
||||
// house; the table stays open for the people still on it. Only the last human out
|
||||
// closes the felt.
|
||||
|
||||
@@ -303,7 +303,7 @@ func (s *Server) table(user string) (tableView, error) {
|
||||
if live.TableID == "" {
|
||||
return s.dropUnreadable(user, v, fmt.Errorf("holdem row with no table"))
|
||||
}
|
||||
t, _, err := storage.LoadTable(live.TableID)
|
||||
t, tableSeats, err := storage.LoadTable(live.TableID)
|
||||
if errors.Is(err, storage.ErrNoSuchTable) {
|
||||
// The table closed under them (reaped, or the last hand cashed them out).
|
||||
// Their claim is stale; clear it so they can sit down again.
|
||||
@@ -321,6 +321,15 @@ func (s *Server) table(user string) (tableView, error) {
|
||||
return s.dropUnreadable(user, v, err)
|
||||
}
|
||||
hv := viewHoldem(g, seat)
|
||||
// bought_in is a per-player figure — "you bought in for X" — but the engine's
|
||||
// BoughtIn is the table's total across every human. The player's own stake is
|
||||
// border accounting, which lives in storage; take it from their seat row.
|
||||
for _, ts := range tableSeats {
|
||||
if ts.Seat == seat {
|
||||
hv.BoughtIn = ts.Staked
|
||||
break
|
||||
}
|
||||
}
|
||||
v.Holdem = &hv
|
||||
default:
|
||||
return s.dropUnreadable(user, v, fmt.Errorf("unknown game %q", live.Game))
|
||||
|
||||
Reference in New Issue
Block a user