package main import ( "context" "errors" "net/http" "sync" "time" "nhooyr.io/websocket" "git.sunturtle.xyz/studio/shotgun/lobby" "git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/serve" ) type Server struct { l *lobby.Lobby mu sync.Mutex rooms map[lobby.GameID]*room } type room struct { mu sync.Mutex pp []person // NOTE(zeph): since only the players can ever see revealed shells, we // could factor out the players into separate fields to save work on // generating dtos for observers. hold that idea until it's needed. } type person struct { conn *websocket.Conn id player.ID } func (s *Server) room(id serve.GameID) *room { s.mu.Lock() defer s.mu.Unlock() return s.rooms[id] } func (s *Server) join(id serve.GameID, p person) { s.mu.Lock() defer s.mu.Unlock() r := s.rooms[id] if r == nil { r = &room{} s.rooms[id] = r } r.pp = append(r.pp, p) } func (s *Server) close(id serve.GameID) { s.mu.Lock() defer s.mu.Unlock() delete(s.rooms, id) } // Queue connects players to games. The connection immediately upgrades. // This handler MUST be wrapped in [serve.WithPlayerID]. func (s *Server) Queue(w http.ResponseWriter, r *http.Request) { p := serve.PlayerID(r.Context()) if p == (player.ID{}) { panic("missing player ID") } conn, err := websocket.Accept(w, r, nil) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } person := person{conn: conn, id: p} go s.joinAndServe(person) } func (s *Server) joinAndServe(p person) { ctx, stop := context.WithTimeoutCause(context.Background(), 10*time.Minute, errQueueEmpty) game := s.l.Queue(ctx, p.id) stop() if game == (lobby.GameID{}) { // Context canceled. p.conn.Close(websocket.StatusNormalClosure, "sorry, queue is empty...") return } s.join(game, p) // TODO(zeph): need to broadcast to observers, need to listen for updates in a single goroutine, ... } var errQueueEmpty = errors.New("sorry, queue is empty")