package main import ( "context" "errors" "log/slog" "net/http" "sync" "time" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" "git.sunturtle.xyz/studio/shotgun/game" "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 pp map[player.ID]*websocket.Conn } type person struct { conn *websocket.Conn id player.ID } func (s *Server) person(p player.ID) person { s.mu.Lock() defer s.mu.Unlock() return person{conn: s.pp[p], id: p} } func (s *Server) accept(w http.ResponseWriter, r *http.Request, p player.ID) (person, error) { conn, err := websocket.Accept(w, r, nil) if err != nil { return person{}, err } slog.Debug("upgraded", "player", p) s.mu.Lock() defer s.mu.Unlock() s.pp[p] = conn return person{conn: conn, id: p}, nil } func (s *Server) left(p player.ID) { s.mu.Lock() // NOTE(zeph): neither map index nor map delete can panic, so this critical // section does not need a defer c := s.pp[p] delete(s.pp, p) s.mu.Unlock() if c != nil { // We don't care about the error here since the connection is leaving. // It's probably already closed anyway. slog.Debug("leaving", "player", p) c.Close(websocket.StatusNormalClosure, "bye") } } // 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") } person, err := s.accept(w, r, p) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } s.joinAndServe(person) } func (s *Server) joinAndServe(p person) { slog.Debug("joining", "player", p.id) ctx, stop := context.WithTimeoutCause(context.Background(), 10*time.Minute, errQueueEmpty) id, chall, deal := s.l.Queue(ctx, p.id) stop() if id == (lobby.GameID{}) { // Context canceled. p.conn.Close(websocket.StatusTryAgainLater, "sorry, queue is empty...") return } if deal { g := game.New(p.id, chall) other := s.person(chall) // TODO(zeph): save the game state s.t. we can provide a join channel go gameActor(ctx, g, p, other, nil) } // Reply with the game ID so they can share. r := serve.GameStart{ ID: id, Dealer: deal, } if err := wsjson.Write(context.TODO(), p.conn, r); err != nil { slog.WarnContext(ctx, "got a game but player dropped", "game", id, "player", p.id) s.left(p.id) return } } var errQueueEmpty = errors.New("sorry, queue is empty")