From 9c86fc57b71fe64072ffc18ad56fa034c5819634 Mon Sep 17 00:00:00 2001 From: nak Date: Wed, 18 Mar 2026 07:38:45 +0000 Subject: [PATCH] Logic for disconnected players --- poker/table.go | 87 +++++++++++++++++++++++++++++++++++++------------- 1 file changed, 65 insertions(+), 22 deletions(-) diff --git a/poker/table.go b/poker/table.go index 2a40c7e..7237cb7 100644 --- a/poker/table.go +++ b/poker/table.go @@ -155,6 +155,36 @@ func (t *Table) Stand(username string) (int64, error) { return stack, nil } +func (t *Table) Disconnect(username string) (bool, error) { + t.mu.Lock() + seat := t.findSeat(username) + if seat == nil { + t.mu.Unlock() + return false, nil // not seated, nothing to do + } + + if t.Hand.Phase == PhaseWaiting { + // Safe to remove immediately + stack := seat.Stack + *seat = Seat{} + t.mu.Unlock() + t.broadcastState() + return true, nil // caller should refund stack + } + + // Mid-hand — mark sitout, hand will clean up at end + seat.SitOut = true + // If it's their turn, cancel the turn timer so the auto-fold fires immediately + if t.Seats[t.Hand.ActionOn].Username == username { + if t.cancelTurn != nil { + t.cancelTurn() + } + } + t.mu.Unlock() + t.broadcastState() + return false, nil // not safe to refund yet +} + // TopUp adds chips to a seated player's stack (between hands only). func (t *Table) TopUp(username string, amount int64) error { t.mu.Lock() @@ -446,7 +476,6 @@ func (t *Table) advance() { } func (t *Table) doShowdown() { - // Build hands for non-folded players remaining := make(map[string][]string) for username, cards := range t.hands { if !t.folded[username] { @@ -456,12 +485,10 @@ func (t *Table) doShowdown() { winners := Winners(remaining, t.Hand.Community) - // Broadcast all remaining hands + winners revealedHands := make(map[string][]string) for _, w := range winners { revealedHands[w.Username] = remaining[w.Username] } - // Also include any players who chose to reveal t.mu.RLock() for _, seat := range t.Seats { if seat.Revealed && seat.Username != "" { @@ -477,7 +504,6 @@ func (t *Table) doShowdown() { winnerDescs[w.Username] = w.Desc } - // Split pot among winners share := t.Hand.Pot / int64(len(winners)) remainder := t.Hand.Pot % int64(len(winners)) t.mu.Lock() @@ -486,7 +512,7 @@ func (t *Table) doShowdown() { if seat != nil { extra := int64(0) if i == 0 { - extra = remainder // first winner gets remainder chips + extra = remainder } seat.Stack += share + extra } @@ -503,25 +529,10 @@ func (t *Table) doShowdown() { }) } - // Brief pause then reset time.Sleep(5 * time.Second) t.mu.Lock() t.Hand.Phase = PhaseWaiting - // Remove players with no chips - for _, s := range t.Seats { - if s.Username != "" && s.Stack == 0 { - log.Printf("[poker] %s busted out of table %s", s.Username, t.Config.ID) - // Return to wallet handled by manager on bust event - if t.Broadcast != nil { - t.Broadcast(map[string]interface{}{ - "type": "player:bust", - "username": s.Username, - "tableId": t.Config.ID, - }) - } - *s = Seat{} - } - } + t.cleanupSeats() t.mu.Unlock() t.broadcastState() t.mu.Lock() @@ -530,7 +541,6 @@ func (t *Table) doShowdown() { } func (t *Table) awardPot() { - // Find the only remaining active player for _, s := range t.Seats { if s.Username != "" && !s.Folded { s.Stack += t.Hand.Pot @@ -547,7 +557,10 @@ func (t *Table) awardPot() { } t.Hand.Pot = 0 time.Sleep(2 * time.Second) + t.mu.Lock() t.Hand.Phase = PhaseWaiting + t.cleanupSeats() + t.mu.Unlock() t.broadcastState() t.maybeStartHand() } @@ -657,6 +670,36 @@ func (t *Table) bettingComplete() bool { return true } +func (t *Table) cleanupSeats() { + for _, s := range t.Seats { + if s.Username == "" { + continue + } + if s.Stack == 0 { + log.Printf("[poker] %s busted", s.Username) + if t.Broadcast != nil { + t.Broadcast(map[string]interface{}{ + "type": "player:bust", + "username": s.Username, + "tableId": t.Config.ID, + }) + } + *s = Seat{} + } else if s.SitOut { + log.Printf("[poker] standing disconnected player %s with %d chips", s.Username, s.Stack) + if t.Broadcast != nil { + t.Broadcast(map[string]interface{}{ + "type": "player:disconnect:stand", + "username": s.Username, + "stack": s.Stack, + "tableId": t.Config.ID, + }) + } + *s = Seat{} + } + } +} + // ── State broadcast ────────────────────────────────────────────── // PublicState returns the table state safe to send to all clients.