diff --git a/lobby/lobby.go b/lobby/lobby.go index 9a3cdc6..c4637b1 100644 --- a/lobby/lobby.go +++ b/lobby/lobby.go @@ -1,51 +1,87 @@ package lobby import ( - "sync" + "context" + + "github.com/google/uuid" - "git.sunturtle.xyz/studio/shotgun/game" "git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/serve" ) type GameID = serve.GameID -// Lobby is a set of active games. +type matchMade struct { + match chan GameID + chall player.ID +} + +type match chan matchMade + +// Lobby is a matchmaking service. type Lobby struct { - mu sync.Mutex - // games is the set of all active games in the lobby. - games map[GameID]*game.Game // matches is dealers waiting for a match. It MUST be unbuffered. matches chan match } func New() *Lobby { return &Lobby{ - games: make(map[GameID]*game.Game), matches: make(chan match), } } -// Game returns the game with the given ID. -func (l *Lobby) Game(id GameID) *game.Game { - l.mu.Lock() - defer l.mu.Unlock() - return l.games[id] -} - -// Start begins a new game in the lobby. -// The caller must be able to distinguish the dealer's and challenger's conns -// in order to provide correct game start DTOs to each. -func (l *Lobby) Start(id GameID, dealer, challenger player.ID) { - g := game.New(dealer, challenger) - l.mu.Lock() - defer l.mu.Unlock() - l.games[id] = g -} - -// Finish removes a game from the lobby. -func (l *Lobby) Finish(id GameID) { - l.mu.Lock() - defer l.mu.Unlock() - delete(l.games, id) +// Queue waits for a match and returns a unique ID for it. +func (l *Lobby) Queue(ctx context.Context, p player.ID) (id GameID, chall player.ID, deal bool) { + select { + case m := <-l.matches: + // We found a dealer waiting for a match. + r := matchMade{ + match: make(chan GameID, 1), + chall: p, + } + // We don't need to check the context here because the challenger + // channel is buffered and we have exclusive send access on it. + m <- r + // We do need to check the context here in case they disappeared. + select { + case <-ctx.Done(): + return GameID{}, player.ID{}, false + case id := <-r.match: + return id, p, false + } + default: // do nothing + } + // We're a new dealer. + m := make(match, 1) + select { + case <-ctx.Done(): + return GameID{}, player.ID{}, false + case l.matches <- m: + // Our match is submitted. Move on. + case m := <-l.matches: + // We might have become a dealer at the same time someone else did. + // We created our match, but we can try to get theirs as well and + // never send ours, since l.matches is unbuffered. + r := matchMade{ + match: make(chan GameID, 1), + chall: p, + } + m <- r + select { + case <-ctx.Done(): + return GameID{}, player.ID{}, false + case id := <-r.match: + return id, p, false + } + } + select { + case <-ctx.Done(): + return GameID{}, player.ID{}, false + case r := <-m: + // Got our challenger. Create the game and send the ID back. + id := GameID(uuid.New()) + // Don't need to check context because the match channel is buffered. + r.match <- id + return id, r.chall, true + } } diff --git a/lobby/match_test.go b/lobby/lobby_test.go similarity index 64% rename from lobby/match_test.go rename to lobby/lobby_test.go index 6d35c61..d632194 100644 --- a/lobby/match_test.go +++ b/lobby/lobby_test.go @@ -2,6 +2,7 @@ package lobby_test import ( "context" + "sync/atomic" "testing" "git.sunturtle.xyz/studio/shotgun/lobby" @@ -15,10 +16,17 @@ func TestQueue(t *testing.T) { l := lobby.New() for i := 0; i < 100; i++ { games = games[:0] + var dealers, challs atomic.Int32 for i := 0; i < N; i++ { i := i go func() { - ch <- l.Queue(context.Background(), player.ID{uint8(i), uint8(i >> 8)}) + id, _, deal := l.Queue(context.Background(), player.ID{uint8(i), uint8(i >> 8)}) + if deal { + dealers.Add(1) + } else { + challs.Add(1) + } + ch <- id }() } for i := 0; i < N; i++ { @@ -34,16 +42,9 @@ func TestQueue(t *testing.T) { t.Errorf("game %v appears %d times", id, c) } } - // Every game should have two different players. - for _, id := range games { - g := l.Game(id) - if g == nil { - t.Errorf("game %v was created but doesn't exist", g) - continue - } - if g.CurrentPlayer().Is(g.Opponent()) { - t.Errorf("game %v matched with self", id) - } + // The number of dealers must match the number of challengers. + if dealers.Load() != challs.Load() { + t.Errorf("%d dealers != %d challengers", dealers.Load(), challs.Load()) } } } diff --git a/lobby/match.go b/lobby/match.go deleted file mode 100644 index 9167533..0000000 --- a/lobby/match.go +++ /dev/null @@ -1,70 +0,0 @@ -package lobby - -import ( - "context" - - "git.sunturtle.xyz/studio/shotgun/player" - "github.com/google/uuid" -) - -type match struct { - dealer player.ID - chall chan player.ID - match chan GameID -} - -// Queue waits for a match. -// This may cause a new match to be created. -func (l *Lobby) Queue(ctx context.Context, p player.ID) GameID { - select { - case <-ctx.Done(): - return GameID{} - case m := <-l.matches: - // We found a dealer waiting for a match. - // We don't need to check the context here because the challenger - // channel is buffered and we have exclusive send access on it. - m.chall <- p - // We do need to check the context here in case they disappeared. - select { - case <-ctx.Done(): - return GameID{} - case id := <-m.match: - return id - } - default: // do nothing - } - // We're a new dealer. - m := match{ - dealer: p, - chall: make(chan player.ID, 1), - match: make(chan GameID, 1), - } - select { - case <-ctx.Done(): - return GameID{} - case l.matches <- m: - // Our match is submitted. Move on. - case m := <-l.matches: - // We might have become a dealer at the same time someone else did. - // We created our match, but we can try to get theirs as well and - // never send ours, since l.matches is unbuffered. - m.chall <- p - select { - case <-ctx.Done(): - return GameID{} - case id := <-m.match: - return id - } - } - select { - case <-ctx.Done(): - return GameID{} - case chall := <-m.chall: - // Got our challenger. Create the game and send the ID back. - id := uuid.New() - l.Start(id, p, chall) - // Don't need to check context because the match channel is buffered. - m.match <- id - return id - } -}