shotgun/server.go

89 lines
1.9 KiB
Go

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")