From e95d526266caa2b9e5ea1b8cd9e0c23de487cb57 Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Sun, 21 Jan 2024 19:53:08 -0600 Subject: [PATCH] implement matchmaking lobby Fixes #2. --- game/player.go | 6 ++++ lobby/lobby.go | 8 ++++-- lobby/match.go | 70 +++++++++++++++++++++++++++++++++++++++++++++ lobby/match_test.go | 49 +++++++++++++++++++++++++++++++ 4 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 lobby/match.go create mode 100644 lobby/match_test.go diff --git a/game/player.go b/game/player.go index 5501579..cd0a4a7 100644 --- a/game/player.go +++ b/game/player.go @@ -41,6 +41,12 @@ func (p *Player) DTO() serve.Player { return r } +// Is returns whether the player has the same ID as other. +// This exists for tests. +func (p *Player) Is(other *Player) bool { + return p.id == other.id +} + type CuffState uint8 const ( diff --git a/lobby/lobby.go b/lobby/lobby.go index 93a70b5..9a3cdc6 100644 --- a/lobby/lobby.go +++ b/lobby/lobby.go @@ -15,11 +15,14 @@ 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), + games: make(map[GameID]*game.Game), + matches: make(chan match), } } @@ -33,12 +36,11 @@ func (l *Lobby) Game(id GameID) *game.Game { // 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) GameID { +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 - return id } // Finish removes a game from the lobby. diff --git a/lobby/match.go b/lobby/match.go new file mode 100644 index 0000000..9167533 --- /dev/null +++ b/lobby/match.go @@ -0,0 +1,70 @@ +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 + } +} diff --git a/lobby/match_test.go b/lobby/match_test.go new file mode 100644 index 0000000..6d35c61 --- /dev/null +++ b/lobby/match_test.go @@ -0,0 +1,49 @@ +package lobby_test + +import ( + "context" + "testing" + + "git.sunturtle.xyz/studio/shotgun/lobby" + "git.sunturtle.xyz/studio/shotgun/player" +) + +func TestQueue(t *testing.T) { + const N = 1000 // must be even + games := make([]lobby.GameID, 0, N) + ch := make(chan lobby.GameID) + l := lobby.New() + for i := 0; i < 100; i++ { + games = games[:0] + for i := 0; i < N; i++ { + i := i + go func() { + ch <- l.Queue(context.Background(), player.ID{uint8(i), uint8(i >> 8)}) + }() + } + for i := 0; i < N; i++ { + games = append(games, <-ch) + } + // Every unique game ID should appear exactly twice. + counts := make(map[lobby.GameID]int, N/2) + for _, id := range games { + counts[id]++ + } + for id, c := range counts { + if c != 2 { + 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) + } + } + } +}