package snake import ( "crypto/rand" "encoding/hex" "sync" ) type PubSub interface { Publish(subject string, data []byte) error } type Persister interface { SaveSnakeGame(sg *SnakeGame) error LoadSnakeGame(id string) (*SnakeGame, error) SaveSnakePlayer(gameID string, player *Player) error LoadSnakePlayers(gameID string) ([]*Player, error) DeleteSnakeGame(id string) error } type SnakeStore struct { games map[string]*SnakeGameInstance gamesMu sync.RWMutex persister Persister pubsub PubSub } func NewSnakeStore() *SnakeStore { return &SnakeStore{ games: make(map[string]*SnakeGameInstance), } } func (ss *SnakeStore) SetPersister(p Persister) { ss.persister = p } func (ss *SnakeStore) SetPubSub(ps PubSub) { ss.pubsub = ps } func (ss *SnakeStore) makeNotify(gameID string) func() { return func() { if ss.pubsub != nil { ss.pubsub.Publish("snake."+gameID, nil) } } } func (ss *SnakeStore) Create(width, height int) *SnakeGameInstance { id := generateID(4) sg := &SnakeGame{ ID: id, State: &GameState{ Width: width, Height: height, }, Players: make([]*Player, 8), Status: StatusWaitingForPlayers, } si := &SnakeGameInstance{ game: sg, notify: ss.makeNotify(id), persister: ss.persister, store: ss, } ss.gamesMu.Lock() ss.games[id] = si ss.gamesMu.Unlock() if ss.persister != nil { ss.persister.SaveSnakeGame(sg) } return si } func (ss *SnakeStore) Get(id string) (*SnakeGameInstance, bool) { ss.gamesMu.RLock() si, ok := ss.games[id] ss.gamesMu.RUnlock() if ok { return si, true } if ss.persister == nil { return nil, false } sg, err := ss.persister.LoadSnakeGame(id) if err != nil || sg == nil { return nil, false } players, _ := ss.persister.LoadSnakePlayers(id) if sg.Players == nil { sg.Players = make([]*Player, 8) } for _, p := range players { if p.Slot >= 0 && p.Slot < 8 { sg.Players[p.Slot] = p } } si = &SnakeGameInstance{ game: sg, notify: ss.makeNotify(id), persister: ss.persister, store: ss, } ss.gamesMu.Lock() ss.games[id] = si ss.gamesMu.Unlock() return si, true } func (ss *SnakeStore) Delete(id string) error { ss.gamesMu.Lock() si, ok := ss.games[id] delete(ss.games, id) ss.gamesMu.Unlock() if ok && si != nil { si.Stop() } if ss.persister != nil { return ss.persister.DeleteSnakeGame(id) } return nil } // ActiveGames returns metadata of games that can be joined. // Copies game data to avoid holding nested locks. func (ss *SnakeStore) ActiveGames() []*SnakeGame { ss.gamesMu.RLock() instances := make([]*SnakeGameInstance, 0, len(ss.games)) for _, si := range ss.games { instances = append(instances, si) } ss.gamesMu.RUnlock() var games []*SnakeGame for _, si := range instances { si.gameMu.RLock() g := si.game if g.Status == StatusWaitingForPlayers || g.Status == StatusCountdown { games = append(games, g) } si.gameMu.RUnlock() } return games } type SnakeGameInstance struct { game *SnakeGame gameMu sync.RWMutex pendingDir [8]*Direction notify func() persister Persister store *SnakeStore stopCh chan struct{} loopOnce sync.Once } func (si *SnakeGameInstance) ID() string { si.gameMu.RLock() defer si.gameMu.RUnlock() return si.game.ID } // GetGame returns a snapshot of the game state safe for concurrent read. func (si *SnakeGameInstance) GetGame() *SnakeGame { si.gameMu.RLock() defer si.gameMu.RUnlock() return si.game.snapshot() } func (si *SnakeGameInstance) GetPlayerSlot(pid PlayerID) int { si.gameMu.RLock() defer si.gameMu.RUnlock() for i, p := range si.game.Players { if p != nil && p.ID == pid { return i } } return -1 } func (si *SnakeGameInstance) Join(player *Player) bool { si.gameMu.Lock() defer si.gameMu.Unlock() if si.game.Status == StatusInProgress || si.game.Status == StatusFinished { return false } slot := -1 for i, p := range si.game.Players { if p == nil { slot = i break } } if slot == -1 { return false } player.Slot = slot si.game.Players[slot] = player if si.persister != nil { si.persister.SaveSnakePlayer(si.game.ID, player) si.persister.SaveSnakeGame(si.game) } si.notify() if si.game.PlayerCount() >= 2 { si.startOrResetCountdownLocked() } return true } // SetDirection buffers a direction change for the given slot. // The write happens under the game lock to avoid a data race with the game loop. func (si *SnakeGameInstance) SetDirection(slot int, dir Direction) { if slot < 0 || slot >= 8 { return } si.gameMu.Lock() defer si.gameMu.Unlock() if si.game.State != nil && slot < len(si.game.State.Snakes) { s := si.game.State.Snakes[slot] if s != nil && s.Alive && !ValidateDirection(s.Dir, dir) { return } } si.pendingDir[slot] = &dir } func (si *SnakeGameInstance) Stop() { if si.stopCh != nil { select { case <-si.stopCh: default: close(si.stopCh) } } } func (si *SnakeGameInstance) CreateRematch() *SnakeGameInstance { si.gameMu.Lock() if !si.game.IsFinished() || si.game.RematchGameID != nil { si.gameMu.Unlock() return nil } // Capture state needed, then release lock before calling store.Create // (which acquires gamesMu) to avoid lock ordering deadlock. width := si.game.State.Width height := si.game.State.Height si.gameMu.Unlock() newSI := si.store.Create(width, height) newID := newSI.ID() si.gameMu.Lock() // Re-check after reacquiring lock if si.game.RematchGameID != nil { si.gameMu.Unlock() return newSI } si.game.RematchGameID = &newID if si.persister != nil { si.persister.SaveSnakeGame(si.game) } si.gameMu.Unlock() si.notify() return newSI } func generateID(size int) string { b := make([]byte, size) rand.Read(b) return hex.EncodeToString(b) }