feat: remove inactive players after 60s disconnect timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-24 10:59:33 +09:00
parent 46afd82060
commit ce2f03baf5
3 changed files with 57 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ package game
import (
"fmt"
"math/rand"
"time"
"github.com/tolelom/catacombs/dungeon"
"github.com/tolelom/catacombs/entity"
@@ -12,6 +13,13 @@ func (s *GameSession) EnterRoom(roomIdx int) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now()
for _, p := range s.state.Players {
if p.Fingerprint != "" {
s.lastActivity[p.Fingerprint] = now
}
}
s.state.Floor.CurrentRoom = roomIdx
dungeon.UpdateVisibility(s.state.Floor)
room := s.state.Floor.Rooms[roomIdx]

View File

@@ -1,6 +1,7 @@
package game
import (
"fmt"
"sync"
"time"
@@ -68,6 +69,7 @@ type GameSession struct {
actionCh chan playerActionMsg
combatSignal chan struct{}
done chan struct{}
lastActivity map[string]time.Time // fingerprint -> last activity time
}
type playerActionMsg struct {
@@ -84,6 +86,7 @@ func NewGameSession() *GameSession {
actionCh: make(chan playerActionMsg, 4),
combatSignal: make(chan struct{}, 1),
done: make(chan struct{}),
lastActivity: make(map[string]time.Time),
}
}
@@ -105,6 +108,10 @@ func (s *GameSession) StartGame() {
}
s.started = true
s.state.SoloMode = len(s.state.Players) == 1
now := time.Now()
for _, p := range s.state.Players {
s.lastActivity[p.Fingerprint] = now
}
s.mu.Unlock()
s.StartFloor()
go s.combatLoop()
@@ -128,6 +135,36 @@ func (s *GameSession) combatLoop() {
return
}
// Remove players inactive for >60 seconds
s.mu.Lock()
now := time.Now()
changed := false
remaining := make([]*entity.Player, 0, len(s.state.Players))
for _, p := range s.state.Players {
if p.Fingerprint != "" && !p.IsOut() {
if last, ok := s.lastActivity[p.Fingerprint]; ok {
if now.Sub(last) > 60*time.Second {
s.addLog(fmt.Sprintf("%s removed (disconnected)", p.Name))
changed = true
continue
}
}
}
remaining = append(remaining, p)
}
if changed {
s.state.Players = remaining
if len(s.state.Players) <= 1 {
s.state.SoloMode = true
}
if len(s.state.Players) == 0 {
s.state.GameOver = true
s.mu.Unlock()
return
}
}
s.mu.Unlock()
if phase == PhaseCombat {
s.RunTurn()
} else {
@@ -226,9 +263,18 @@ func (s *GameSession) GetState() GameState {
}
func (s *GameSession) SubmitAction(playerID string, action PlayerAction) {
s.mu.Lock()
s.lastActivity[playerID] = time.Now()
s.mu.Unlock()
s.actionCh <- playerActionMsg{PlayerID: playerID, Action: action}
}
func (s *GameSession) TouchActivity(fingerprint string) {
s.mu.Lock()
defer s.mu.Unlock()
s.lastActivity[fingerprint] = time.Now()
}
// BuyItem handles shop purchases
func (s *GameSession) BuyItem(playerID string, itemIdx int) bool {
s.mu.Lock()

View File

@@ -285,6 +285,9 @@ func (m Model) pollState() tea.Cmd {
type tickMsg struct{}
func (m Model) updateGame(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.session != nil && m.fingerprint != "" {
m.session.TouchActivity(m.fingerprint)
}
// Refresh state on every update
if m.session != nil {
m.gameState = m.session.GetState()