From 82aba814e6a260c1cd486a19493d184e1cef98dc Mon Sep 17 00:00:00 2001 From: Branden J Brown Date: Sun, 4 Feb 2024 09:25:09 -0600 Subject: [PATCH] parameterize lobby by exchange types --- lobby/lobby.go | 70 ++++++++++++++++++++--------------------- lobby/lobby_test.go | 76 +++++++++++++++++++++++---------------------- main.go | 5 +-- server.go | 9 +++--- 4 files changed, 82 insertions(+), 78 deletions(-) diff --git a/lobby/lobby.go b/lobby/lobby.go index c4637b1..5ab82fe 100644 --- a/lobby/lobby.go +++ b/lobby/lobby.go @@ -1,43 +1,43 @@ package lobby -import ( - "context" +import "context" - "github.com/google/uuid" - - "git.sunturtle.xyz/studio/shotgun/player" - "git.sunturtle.xyz/studio/shotgun/serve" -) - -type GameID = serve.GameID - -type matchMade struct { - match chan GameID - chall player.ID +// matchMade is a message sent from the challenger to the dealer to indicate +// to the latter that they have matched. +type matchMade[ID, T any] struct { + match chan ID + carry T } -type match chan matchMade +// match is a match request. Each match receives one message from the matching +// opponent. It MUST be buffered with a capacity of at least 1. +type match[ID, T any] chan matchMade[ID, T] -// Lobby is a matchmaking service. -type Lobby struct { +// Lobby is a matchmaking service. ID is the type of a game ID. T is the type of an exchange from one player +// to the other when a match is found. +type Lobby[ID, T any] struct { // matches is dealers waiting for a match. It MUST be unbuffered. - matches chan match + matches chan match[ID, T] } -func New() *Lobby { - return &Lobby{ - matches: make(chan match), +// New creates a matchmaking lobby. +func New[ID, T any]() *Lobby[ID, T] { + return &Lobby[ID, T]{ + matches: make(chan match[ID, T]), } } -// 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) { +// Queue waits for a match. Both recipients of the match receive the same +// result of new and the challenger's value of p. +func (l *Lobby[ID, T]) Queue(ctx context.Context, new func() ID, p T) (id ID, chall T, deal bool) { + var zid ID + var zero T select { case m := <-l.matches: // We found a dealer waiting for a match. - r := matchMade{ - match: make(chan GameID, 1), - chall: p, + r := matchMade[ID, T]{ + match: make(chan ID, 1), + carry: p, } // We don't need to check the context here because the challenger // channel is buffered and we have exclusive send access on it. @@ -45,43 +45,43 @@ func (l *Lobby) Queue(ctx context.Context, p player.ID) (id GameID, chall player // We do need to check the context here in case they disappeared. select { case <-ctx.Done(): - return GameID{}, player.ID{}, false + return zid, zero, false case id := <-r.match: return id, p, false } default: // do nothing } // We're a new dealer. - m := make(match, 1) + m := make(match[ID, T], 1) select { case <-ctx.Done(): - return GameID{}, player.ID{}, false + return zid, zero, 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, + r := matchMade[ID, T]{ + match: make(chan ID, 1), + carry: p, } m <- r select { case <-ctx.Done(): - return GameID{}, player.ID{}, false + return zid, zero, false case id := <-r.match: return id, p, false } } select { case <-ctx.Done(): - return GameID{}, player.ID{}, false + return zid, zero, false case r := <-m: // Got our challenger. Create the game and send the ID back. - id := GameID(uuid.New()) + id := new() // Don't need to check context because the match channel is buffered. r.match <- id - return id, r.chall, true + return id, r.carry, true } } diff --git a/lobby/lobby_test.go b/lobby/lobby_test.go index 5626d1c..c4ee692 100644 --- a/lobby/lobby_test.go +++ b/lobby/lobby_test.go @@ -5,48 +5,50 @@ import ( "sync/atomic" "testing" - "github.com/google/uuid" - "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] - var dealers, challs atomic.Int32 - for i := 0; i < N; i++ { - i := i - go func() { - id, _, deal := l.Queue(context.Background(), player.ID{UUID: uuid.UUID{uint8(i), uint8(i >> 8)}}) - if deal { - dealers.Add(1) - } else { - challs.Add(1) - } - ch <- id - }() - } - 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) + const N = 10000 // must be even + games := make([]int, 0, N) + ch := make(chan int) + pc := make(chan int, N) + l := lobby.New[int, int]() + var dealers, challs atomic.Int32 + for i := 0; i < N; i++ { + i := i + new := func() int { return i } + go func() { + id, j, deal := l.Queue(context.Background(), new, i) + if deal { + dealers.Add(1) + } else { + challs.Add(1) } - } - // 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()) + ch <- id + pc <- j + }() + } + for i := 0; i < N; i++ { + games = append(games, <-ch) + } + // Every unique game ID should appear exactly twice. + counts := make(map[int]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 unique challenger info should appear exactly twice. + ps := make(map[int]int, N/2) + for i := 0; i < N; i++ { + ps[<-pc]++ + } + // 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/main.go b/main.go index cf5dcca..58aa96c 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" + "github.com/google/uuid" "gitlab.com/zephyrtronium/sq" "nhooyr.io/websocket" @@ -42,11 +43,11 @@ func main() { return } s := Server{ - l: lobby.New(), + l: lobby.New[uuid.UUID, player.ID](), creds: db, sessions: db, pp: map[player.ID]*websocket.Conn{}, - j: map[lobby.GameID]chan person{}, + j: map[uuid.UUID]chan person{}, } r := chi.NewRouter() diff --git a/server.go b/server.go index 290f661..8045ad9 100644 --- a/server.go +++ b/server.go @@ -9,6 +9,7 @@ import ( "sync" "time" + "github.com/google/uuid" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" @@ -19,14 +20,14 @@ import ( ) type Server struct { - l *lobby.Lobby + l *lobby.Lobby[uuid.UUID, player.ID] creds db sessions db mu sync.Mutex pp map[player.ID]*websocket.Conn - j map[lobby.GameID]chan person + j map[uuid.UUID]chan person } type db interface { @@ -207,10 +208,10 @@ func (s *Server) joinAndServe(p person) { stop() } }() - id, chall, deal := s.l.Queue(ctx, p.id) + id, chall, deal := s.l.Queue(ctx, uuid.New, p.id) <-ch stop() - if id == (lobby.GameID{}) { + if id == uuid.Nil { // Context canceled. p.conn.Close(websocket.StatusTryAgainLater, "sorry, queue is empty...") return