package web 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. // // A shared table has two writers where a solo game had one — an HTTP move, and a // turn clock acting for whoever walked away — and the whole job of this layer is // to let those two coexist without either trusting the other. The rule that makes // that work is the database's version column: every write is conditional on the // version its writer read, so the two can race freely and exactly one wins. // // Everything specific to a game — how a move advances it, whose turn it is now, // what a settled hand pays — lives behind the tableGame interface. The clock, the // lock discipline and the SSE publish do not know whether they are driving poker // or UNO, and that is what lets Phase B ship before any engine is multiway. // turnSeconds is how long a human has to act before the clock acts for them. Long // enough to read the table and think, short enough that a walked-away player does // not hold three others hostage. const turnSeconds = 30 // bootGrace is how far the turn clock shoves every live deadline out on boot. A // deploy takes the in-memory clock with it, so without this the first tick after // a restart would find every deadline in the casino already past and auto-act the // whole room at once. const bootGrace = 30 // step is what a game hands back after a move or a timeout: the new state, ready // to persist, plus everything the runtime needs to settle and to schedule. // // The chips are inside State — a hand ending moves the pot within the blob and // credits nobody — so there is no payout field. What comes out is the state, the // audit of any hand that just ended, and the clock's next deadline. type step struct { // State is the engine's whole state, marshalled, ready for the table blob. State []byte // Phase is lifted out of the state so the lobby can read it without decoding. Phase string // HandNo is the hand this state is on. It advances when a new hand is dealt, // and it is the audit key now that a seed no longer reproduces a shared hand. HandNo int64 // Deadline is when the clock must next act, or 0 for none. It is nonzero only // when the turn has landed on a *present* human: a bot resolves inside the move // and an away human is auto-acted on sight, so neither is ever waited for. Deadline int64 // Audit is the per-seat record of a hand that ended in this step. Empty if no // hand settled. Audit []storage.Hand // Events is the script the felt plays back — the same shape every solo game // already returns. It is what the SSE frame and the acting player both animate. Events any } // tableGame is everything the runtime needs from an engine to run it at a shared // table. Each multiplayer game implements it; the clock and the handlers hold it // as an interface so they stay game-agnostic. type tableGame interface { // name is the storage key and the lobby label: "holdem", "uno", "blackjack". name() string // timeout acts for the human whose clock has expired — check if the rules // allow it, fold otherwise — and advances the table as far as the next // decision, exactly as a real move would. seats is the current roster, so the // engine can mark the timed-out player away. // // It returns ErrNotDue (via a nil step, see runClockTable) if, on decode, the // seat to act is not in fact a waiting human — which happens when a real move // 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) }