games: the felt other people can sit at, and the version that settles the race
Phase B foundation for the multiplayer casino: the shared-table storage layer, the SSE fan-out, and the lock that only ever pretends to be the authority. - game_tables/game_seats/game_chat, plus a nullable table_id on game_live_hands so occupancy stays one row per player — the same primary key that stops a second solo hand stops a second seat. No second uniqueness domain, no split brain, no cash-out-to-zero while sitting on a pot. - The money model the plan sketched turned out simpler than it drew: chips cross the border only at sit-down and get-up, so a hand settles by moving the pot *within* the state blob and credits nobody. That deletes the payout ledger the design called for — there is no money write to make idempotent, only a state write conditional on the version. A replayed settle affects zero rows. - CommitTable/SitDown/LeaveTable each one transaction with the state write in it; the version column is the concurrency authority and the striped in-memory lock is only an optimisation over it, because a mutex does not survive a redeploy. - The SSE hub is a dumb byte fan-out: non-blocking sends (a stalled phone must not hold the table lock and freeze the clock for the room) and never a DB touch after the first read (holding the one connection open bricks the app). - DueTables/PushDeadlines for the turn clock to come; Chat keeps the hand_no it was said during, because at a money table collusion looks like chat. Storage and hub tested, including the version race and the never-block publish. No handlers wired yet, so nothing a player can see has changed. Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
@@ -563,6 +563,12 @@ type LiveHand struct {
|
||||
State []byte
|
||||
Seed1 uint64
|
||||
Seed2 uint64
|
||||
// TableID is set when the player is sitting at a shared table instead of playing
|
||||
// alone. The cards are then in game_tables and State here is empty: this row is
|
||||
// the occupancy claim and nothing else. One row per player either way, which is
|
||||
// the point — the primary key that stops a second solo hand is the same one that
|
||||
// stops a second seat.
|
||||
TableID string
|
||||
}
|
||||
|
||||
// StartLiveHand seats a *new* hand, and refuses if the player is already in one.
|
||||
@@ -609,16 +615,17 @@ func LoadLiveHand(user string) (LiveHand, error) {
|
||||
var h LiveHand
|
||||
var state string
|
||||
var s1, s2 int64
|
||||
var tableID sql.NullString
|
||||
err := Get().QueryRow(
|
||||
`SELECT game, state, seed1, seed2 FROM game_live_hands WHERE matrix_user = ?`, user,
|
||||
).Scan(&h.Game, &state, &s1, &s2)
|
||||
`SELECT game, state, seed1, seed2, table_id FROM game_live_hands WHERE matrix_user = ?`, user,
|
||||
).Scan(&h.Game, &state, &s1, &s2, &tableID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return LiveHand{}, ErrNoLiveHand
|
||||
}
|
||||
if err != nil {
|
||||
return LiveHand{}, fmt.Errorf("games: load live hand: %w", err)
|
||||
}
|
||||
h.State, h.Seed1, h.Seed2 = []byte(state), uint64(s1), uint64(s2)
|
||||
h.State, h.Seed1, h.Seed2, h.TableID = []byte(state), uint64(s1), uint64(s2), tableID.String
|
||||
return h, nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user