maybe joining handler exists now
This commit is contained in:
		
							
								
								
									
										2
									
								
								game.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								game.go
									
									
									
									
									
								
							| @@ -21,7 +21,7 @@ type action struct { | ||||
|  | ||||
| // gameActor is an actor that updates a game's state and relays changes to all | ||||
| // 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 | ||||
| 	// definitely expired. | ||||
| 	ctx, stop := context.WithTimeoutCause(ctx, 4*time.Hour, errGameExpired) | ||||
|   | ||||
							
								
								
									
										79
									
								
								server.go
									
									
									
									
									
								
							
							
						
						
									
										79
									
								
								server.go
									
									
									
									
									
								
							| @@ -8,24 +8,19 @@ import ( | ||||
| 	"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 | ||||
| 	rooms map[lobby.GameID]*room | ||||
| } | ||||
| 	l *lobby.Lobby | ||||
|  | ||||
| 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. | ||||
| 	pp map[player.ID]*websocket.Conn | ||||
| } | ||||
|  | ||||
| type person struct { | ||||
| @@ -33,27 +28,35 @@ type person struct { | ||||
| 	id   player.ID | ||||
| } | ||||
|  | ||||
| func (s *Server) room(id serve.GameID) *room { | ||||
| func (s *Server) person(p player.ID) person { | ||||
| 	s.mu.Lock() | ||||
| 	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) { | ||||
| 	s.mu.Lock() | ||||
| 	defer s.mu.Unlock() | ||||
| 	r := s.rooms[id] | ||||
| 	if r == nil { | ||||
| 		r = &room{} | ||||
| 		s.rooms[id] = r | ||||
| 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 | ||||
| 	} | ||||
| 	r.pp = append(r.pp, p) | ||||
| } | ||||
|  | ||||
| func (s *Server) close(id serve.GameID) { | ||||
| 	s.mu.Lock() | ||||
| 	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. | ||||
| @@ -63,26 +66,40 @@ func (s *Server) Queue(w http.ResponseWriter, r *http.Request) { | ||||
| 	if p == (player.ID{}) { | ||||
| 		panic("missing player ID") | ||||
| 	} | ||||
| 	conn, err := websocket.Accept(w, r, nil) | ||||
| 	person, err := s.accept(w, r, p) | ||||
| 	if err != nil { | ||||
| 		http.Error(w, err.Error(), http.StatusBadRequest) | ||||
| 		return | ||||
| 	} | ||||
| 	person := person{conn: conn, id: p} | ||||
| 	go s.joinAndServe(person) | ||||
| 	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) | ||||
| 	id, chall, deal := s.l.Queue(ctx, p.id) | ||||
| 	stop() | ||||
| 	if game == (lobby.GameID{}) { | ||||
| 	if id == (lobby.GameID{}) { | ||||
| 		// 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 | ||||
| 	} | ||||
| 	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") | ||||
|   | ||||
		Reference in New Issue
	
	Block a user