Files
Catacombs/game/lobby.go
tolelom afe4ee1056 feat: add structured logging with log/slog and panic recovery
Replace log.Printf/Println with slog.Info/Error/Warn across the codebase.
Initialize slog with JSON handler in main.go. Add panic recovery defer
in SSH session handler. Add structured game event logging (room created,
player joined, game started, game over, player inactive removed).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 13:18:06 +09:00

242 lines
5.1 KiB
Go

package game
import (
"fmt"
"log/slog"
"math/rand"
"sync"
"github.com/tolelom/catacombs/config"
)
type RoomStatus int
const (
RoomWaiting RoomStatus = iota
RoomPlaying
)
type LobbyPlayer struct {
Name string
Class string // empty until class selected
Fingerprint string
Ready bool
}
type LobbyRoom struct {
Code string
Name string
Players []LobbyPlayer
Status RoomStatus
Session *GameSession
}
type OnlinePlayer struct {
Name string
Fingerprint string
InRoom string // room code, empty if in lobby
}
type Lobby struct {
mu sync.RWMutex
cfg *config.Config
rooms map[string]*LobbyRoom
online map[string]*OnlinePlayer // fingerprint -> player
activeSessions map[string]string // fingerprint -> room code (for reconnect)
}
func NewLobby(cfg *config.Config) *Lobby {
return &Lobby{
cfg: cfg,
rooms: make(map[string]*LobbyRoom),
online: make(map[string]*OnlinePlayer),
activeSessions: make(map[string]string),
}
}
func (l *Lobby) Cfg() *config.Config {
return l.cfg
}
func (l *Lobby) RegisterSession(fingerprint, roomCode string) {
l.mu.Lock()
defer l.mu.Unlock()
l.activeSessions[fingerprint] = roomCode
}
func (l *Lobby) UnregisterSession(fingerprint string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.activeSessions, fingerprint)
}
func (l *Lobby) GetActiveSession(fingerprint string) (string, *GameSession) {
l.mu.RLock()
defer l.mu.RUnlock()
code, ok := l.activeSessions[fingerprint]
if !ok {
return "", nil
}
room, ok := l.rooms[code]
if !ok || room.Session == nil {
return "", nil
}
// Check if this player is still in the session
for _, p := range room.Session.GetState().Players {
if p.Fingerprint == fingerprint {
return code, room.Session
}
}
return "", nil
}
func (l *Lobby) PlayerOnline(fingerprint, name string) {
l.mu.Lock()
defer l.mu.Unlock()
l.online[fingerprint] = &OnlinePlayer{Name: name, Fingerprint: fingerprint}
}
func (l *Lobby) PlayerOffline(fingerprint string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.online, fingerprint)
}
func (l *Lobby) ListOnline() []*OnlinePlayer {
l.mu.RLock()
defer l.mu.RUnlock()
result := make([]*OnlinePlayer, 0, len(l.online))
for _, p := range l.online {
result = append(result, p)
}
return result
}
func (l *Lobby) InvitePlayer(roomCode, fingerprint string) error {
l.mu.Lock()
defer l.mu.Unlock()
p, ok := l.online[fingerprint]
if !ok {
return fmt.Errorf("player not online")
}
if p.InRoom != "" {
return fmt.Errorf("player already in a room")
}
// Store the invite as a pending field
p.InRoom = "invited:" + roomCode
return nil
}
func (l *Lobby) CreateRoom(name string) string {
l.mu.Lock()
defer l.mu.Unlock()
code := generateCode()
for l.rooms[code] != nil {
code = generateCode()
}
l.rooms[code] = &LobbyRoom{
Code: code,
Name: name,
Status: RoomWaiting,
}
slog.Info("room created", "code", code, "name", name)
return code
}
func (l *Lobby) JoinRoom(code, playerName, fingerprint string) error {
l.mu.Lock()
defer l.mu.Unlock()
room, ok := l.rooms[code]
if !ok {
return fmt.Errorf("room %s not found", code)
}
if len(room.Players) >= l.cfg.Game.MaxPlayers {
return fmt.Errorf("room %s is full", code)
}
if room.Status != RoomWaiting {
return fmt.Errorf("room %s already in progress", code)
}
room.Players = append(room.Players, LobbyPlayer{Name: playerName, Fingerprint: fingerprint})
slog.Info("player joined", "room", code, "player", playerName)
return nil
}
func (l *Lobby) SetPlayerClass(code, fingerprint, class string) {
l.mu.Lock()
defer l.mu.Unlock()
if room, ok := l.rooms[code]; ok {
for i := range room.Players {
if room.Players[i].Fingerprint == fingerprint {
room.Players[i].Class = class
}
}
}
}
func (l *Lobby) SetPlayerReady(code, fingerprint string, ready bool) {
l.mu.Lock()
defer l.mu.Unlock()
if room, ok := l.rooms[code]; ok {
for i := range room.Players {
if room.Players[i].Fingerprint == fingerprint {
room.Players[i].Ready = ready
}
}
}
}
func (l *Lobby) AllReady(code string) bool {
l.mu.RLock()
defer l.mu.RUnlock()
room, ok := l.rooms[code]
if !ok || len(room.Players) == 0 {
return false
}
for _, p := range room.Players {
if !p.Ready {
return false
}
}
return true
}
func (l *Lobby) GetRoom(code string) *LobbyRoom {
l.mu.RLock()
defer l.mu.RUnlock()
return l.rooms[code]
}
func (l *Lobby) ListRooms() []*LobbyRoom {
l.mu.RLock()
defer l.mu.RUnlock()
result := make([]*LobbyRoom, 0, len(l.rooms))
for _, r := range l.rooms {
result = append(result, r)
}
return result
}
func (l *Lobby) StartRoom(code string) {
l.mu.Lock()
defer l.mu.Unlock()
if room, ok := l.rooms[code]; ok {
room.Status = RoomPlaying
slog.Info("game started", "room", code, "players", len(room.Players))
}
}
func (l *Lobby) RemoveRoom(code string) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.rooms, code)
}
func generateCode() string {
const letters = "ABCDEFGHJKLMNPQRSTUVWXYZ"
b := make([]byte, 4)
for i := range b {
b[i] = letters[rand.Intn(len(letters))]
}
return string(b)
}