maybe joining handler exists now

This commit is contained in:
Branden J Brown 2024-01-27 13:24:42 -06:00
parent f2ba9849f1
commit ffc39c9f8a
2 changed files with 49 additions and 32 deletions

View File

@ -21,7 +21,7 @@ type action struct {
// gameActor is an actor that updates a game's state and relays changes to all // gameActor is an actor that updates a game's state and relays changes to all
// observers. // observers.
func gameActor(ctx context.Context, g *game.Game, dealer, chall person, join chan person) { func gameActor(ctx context.Context, g *game.Game, dealer, chall person, join <-chan person) {
// Games should generally be on the order of minutes. A four hour game is // Games should generally be on the order of minutes. A four hour game is
// definitely expired. // definitely expired.
ctx, stop := context.WithTimeoutCause(ctx, 4*time.Hour, errGameExpired) ctx, stop := context.WithTimeoutCause(ctx, 4*time.Hour, errGameExpired)

View File

@ -8,24 +8,19 @@ import (
"time" "time"
"nhooyr.io/websocket" "nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson"
"git.sunturtle.xyz/studio/shotgun/game"
"git.sunturtle.xyz/studio/shotgun/lobby" "git.sunturtle.xyz/studio/shotgun/lobby"
"git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/player"
"git.sunturtle.xyz/studio/shotgun/serve" "git.sunturtle.xyz/studio/shotgun/serve"
) )
type Server struct { type Server struct {
l *lobby.Lobby l *lobby.Lobby
mu sync.Mutex
rooms map[lobby.GameID]*room
}
type room struct {
mu sync.Mutex mu sync.Mutex
pp []person pp map[player.ID]*websocket.Conn
// 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 { type person struct {
@ -33,27 +28,35 @@ type person struct {
id player.ID id player.ID
} }
func (s *Server) room(id serve.GameID) *room { func (s *Server) person(p player.ID) person {
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
return s.rooms[id] return person{conn: s.pp[p], id: p}
} }
func (s *Server) join(id serve.GameID, p person) { func (s *Server) accept(w http.ResponseWriter, r *http.Request, p player.ID) (person, error) {
s.mu.Lock() conn, err := websocket.Accept(w, r, nil)
defer s.mu.Unlock() if err != nil {
r := s.rooms[id] return person{}, err
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() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
delete(s.rooms, id) 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.
c.Close(websocket.StatusNormalClosure, "bye")
}
} }
// Queue connects players to games. The connection immediately upgrades. // Queue connects players to games. The connection immediately upgrades.
@ -63,26 +66,40 @@ func (s *Server) Queue(w http.ResponseWriter, r *http.Request) {
if p == (player.ID{}) { if p == (player.ID{}) {
panic("missing player ID") panic("missing player ID")
} }
conn, err := websocket.Accept(w, r, nil) person, err := s.accept(w, r, p)
if err != nil { if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest) http.Error(w, err.Error(), http.StatusBadRequest)
return return
} }
person := person{conn: conn, id: p} s.joinAndServe(person)
go s.joinAndServe(person)
} }
func (s *Server) joinAndServe(p person) { func (s *Server) joinAndServe(p person) {
ctx, stop := context.WithTimeoutCause(context.Background(), 10*time.Minute, errQueueEmpty) ctx, stop := context.WithTimeoutCause(context.Background(), 10*time.Minute, errQueueEmpty)
game := s.l.Queue(ctx, p.id) id, chall, deal := s.l.Queue(ctx, p.id)
stop() stop()
if game == (lobby.GameID{}) { if id == (lobby.GameID{}) {
// Context canceled. // Context canceled.
p.conn.Close(websocket.StatusNormalClosure, "sorry, queue is empty...") 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 := struct {
Game lobby.GameID `json:"game"`
}{
Game: id,
}
if err := wsjson.Write(context.TODO(), p.conn, r); err != nil {
// TODO(zeph): log
s.left(p.id)
return 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") var errQueueEmpty = errors.New("sorry, queue is empty")