Compare commits

...

2 commits

Author SHA1 Message Date
nak
f867753c4a More logic for disconnects 2026-03-18 07:50:25 +00:00
nak
9c86fc57b7 Logic for disconnected players 2026-03-18 07:38:45 +00:00
2 changed files with 106 additions and 22 deletions

View file

@ -413,6 +413,17 @@ func (m *Manager) HandleWSMessage(username string, msg map[string]interface{}) {
case "player:bust":
// Handled in table.go via broadcast, no extra routing needed
case "player:disconnect:stand":
username, _ := msg["username"].(string)
stack, _ := msg["stack"].(float64)
if username != "" && stack > 0 && m.AdjustBalance != nil {
if _, err := m.AdjustBalance(username, int64(stack)); err != nil {
log.Printf("[poker] post-hand disconnect refund error for %s: %v", username, err)
} else {
log.Printf("[poker] post-hand refunded %d chips to %s", int64(stack), username)
}
}
case "admin:position":
// Admin tablet sends position so server knows where to spawn the table
// Store temporarily in Redis for the next table:spawn request
@ -460,3 +471,35 @@ func jsonError(w http.ResponseWriter, msg string, code int) {
w.WriteHeader(code)
json.NewEncoder(w).Encode(map[string]string{"error": msg})
}
func (m *Manager) StandDisconnected(username string) {
m.mu.RLock()
var seated *Table
for _, t := range m.tables {
t.mu.RLock()
for _, s := range t.Seats {
if s.Username == username {
seated = t
break
}
}
t.mu.RUnlock()
if seated != nil {
break
}
}
m.mu.RUnlock()
if seated == nil {
return
}
stack, refundNow := seated.Disconnect(username)
if refundNow && stack > 0 && m.AdjustBalance != nil {
if _, err := m.AdjustBalance(username, stack); err != nil {
log.Printf("[poker] disconnect refund error for %s: %v", username, err)
} else {
log.Printf("[poker] refunded %d chips to %s on disconnect", stack, username)
}
}
}

View file

@ -155,6 +155,34 @@ func (t *Table) Stand(username string) (int64, error) {
return stack, nil
}
func (t *Table) Disconnect(username string) (int64, bool) {
t.mu.Lock()
seat := t.findSeat(username)
if seat == nil {
t.mu.Unlock()
return 0, false
}
if t.Hand.Phase == PhaseWaiting {
stack := seat.Stack
*seat = Seat{}
t.mu.Unlock()
t.broadcastState()
return stack, true // safe to refund immediately
}
// Mid-hand — mark sitout and let the hand clean up
seat.SitOut = true
if t.Seats[t.Hand.ActionOn].Username == username {
if t.cancelTurn != nil {
t.cancelTurn()
}
}
t.mu.Unlock()
t.broadcastState()
return 0, false // refund will happen via player:disconnect:stand after hand ends
}
// 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 +474,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 +483,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 +502,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 +510,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 +527,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 +539,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 +555,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 +668,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.