parameterize lobby by exchange types

This commit is contained in:
Branden J Brown 2024-02-04 09:25:09 -06:00
parent 72b0cd1023
commit 82aba814e6
4 changed files with 82 additions and 78 deletions

View File

@ -1,43 +1,43 @@
package lobby package lobby
import ( import "context"
"context"
"github.com/google/uuid" // matchMade is a message sent from the challenger to the dealer to indicate
// to the latter that they have matched.
"git.sunturtle.xyz/studio/shotgun/player" type matchMade[ID, T any] struct {
"git.sunturtle.xyz/studio/shotgun/serve" match chan ID
) carry T
type GameID = serve.GameID
type matchMade struct {
match chan GameID
chall player.ID
} }
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. // Lobby is a matchmaking service. ID is the type of a game ID. T is the type of an exchange from one player
type Lobby struct { // 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 is dealers waiting for a match. It MUST be unbuffered.
matches chan match matches chan match[ID, T]
} }
func New() *Lobby { // New creates a matchmaking lobby.
return &Lobby{ func New[ID, T any]() *Lobby[ID, T] {
matches: make(chan match), return &Lobby[ID, T]{
matches: make(chan match[ID, T]),
} }
} }
// Queue waits for a match and returns a unique ID for it. // Queue waits for a match. Both recipients of the match receive the same
func (l *Lobby) Queue(ctx context.Context, p player.ID) (id GameID, chall player.ID, deal bool) { // 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 { select {
case m := <-l.matches: case m := <-l.matches:
// We found a dealer waiting for a match. // We found a dealer waiting for a match.
r := matchMade{ r := matchMade[ID, T]{
match: make(chan GameID, 1), match: make(chan ID, 1),
chall: p, carry: p,
} }
// We don't need to check the context here because the challenger // We don't need to check the context here because the challenger
// channel is buffered and we have exclusive send access on it. // 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. // We do need to check the context here in case they disappeared.
select { select {
case <-ctx.Done(): case <-ctx.Done():
return GameID{}, player.ID{}, false return zid, zero, false
case id := <-r.match: case id := <-r.match:
return id, p, false return id, p, false
} }
default: // do nothing default: // do nothing
} }
// We're a new dealer. // We're a new dealer.
m := make(match, 1) m := make(match[ID, T], 1)
select { select {
case <-ctx.Done(): case <-ctx.Done():
return GameID{}, player.ID{}, false return zid, zero, false
case l.matches <- m: case l.matches <- m:
// Our match is submitted. Move on. // Our match is submitted. Move on.
case m := <-l.matches: case m := <-l.matches:
// We might have become a dealer at the same time someone else did. // 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 // We created our match, but we can try to get theirs as well and
// never send ours, since l.matches is unbuffered. // never send ours, since l.matches is unbuffered.
r := matchMade{ r := matchMade[ID, T]{
match: make(chan GameID, 1), match: make(chan ID, 1),
chall: p, carry: p,
} }
m <- r m <- r
select { select {
case <-ctx.Done(): case <-ctx.Done():
return GameID{}, player.ID{}, false return zid, zero, false
case id := <-r.match: case id := <-r.match:
return id, p, false return id, p, false
} }
} }
select { select {
case <-ctx.Done(): case <-ctx.Done():
return GameID{}, player.ID{}, false return zid, zero, false
case r := <-m: case r := <-m:
// Got our challenger. Create the game and send the ID back. // 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. // Don't need to check context because the match channel is buffered.
r.match <- id r.match <- id
return id, r.chall, true return id, r.carry, true
} }
} }

View File

@ -5,48 +5,50 @@ import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"github.com/google/uuid"
"git.sunturtle.xyz/studio/shotgun/lobby" "git.sunturtle.xyz/studio/shotgun/lobby"
"git.sunturtle.xyz/studio/shotgun/player"
) )
func TestQueue(t *testing.T) { func TestQueue(t *testing.T) {
const N = 1000 // must be even const N = 10000 // must be even
games := make([]lobby.GameID, 0, N) games := make([]int, 0, N)
ch := make(chan lobby.GameID) ch := make(chan int)
l := lobby.New() pc := make(chan int, N)
for i := 0; i < 100; i++ { l := lobby.New[int, int]()
games = games[:0] var dealers, challs atomic.Int32
var dealers, challs atomic.Int32 for i := 0; i < N; i++ {
for i := 0; i < N; i++ { i := i
i := i new := func() int { return i }
go func() { go func() {
id, _, deal := l.Queue(context.Background(), player.ID{UUID: uuid.UUID{uint8(i), uint8(i >> 8)}}) id, j, deal := l.Queue(context.Background(), new, i)
if deal { if deal {
dealers.Add(1) dealers.Add(1)
} else { } else {
challs.Add(1) 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)
} }
} ch <- id
// The number of dealers must match the number of challengers. pc <- j
if dealers.Load() != challs.Load() { }()
t.Errorf("%d dealers != %d challengers", dealers.Load(), challs.Load()) }
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())
}
} }

View File

@ -8,6 +8,7 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/google/uuid"
"gitlab.com/zephyrtronium/sq" "gitlab.com/zephyrtronium/sq"
"nhooyr.io/websocket" "nhooyr.io/websocket"
@ -42,11 +43,11 @@ func main() {
return return
} }
s := Server{ s := Server{
l: lobby.New(), l: lobby.New[uuid.UUID, player.ID](),
creds: db, creds: db,
sessions: db, sessions: db,
pp: map[player.ID]*websocket.Conn{}, pp: map[player.ID]*websocket.Conn{},
j: map[lobby.GameID]chan person{}, j: map[uuid.UUID]chan person{},
} }
r := chi.NewRouter() r := chi.NewRouter()

View File

@ -9,6 +9,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/google/uuid"
"nhooyr.io/websocket" "nhooyr.io/websocket"
"nhooyr.io/websocket/wsjson" "nhooyr.io/websocket/wsjson"
@ -19,14 +20,14 @@ import (
) )
type Server struct { type Server struct {
l *lobby.Lobby l *lobby.Lobby[uuid.UUID, player.ID]
creds db creds db
sessions db sessions db
mu sync.Mutex mu sync.Mutex
pp map[player.ID]*websocket.Conn pp map[player.ID]*websocket.Conn
j map[lobby.GameID]chan person j map[uuid.UUID]chan person
} }
type db interface { type db interface {
@ -207,10 +208,10 @@ func (s *Server) joinAndServe(p person) {
stop() stop()
} }
}() }()
id, chall, deal := s.l.Queue(ctx, p.id) id, chall, deal := s.l.Queue(ctx, uuid.New, p.id)
<-ch <-ch
stop() stop()
if id == (lobby.GameID{}) { if id == uuid.Nil {
// Context canceled. // Context canceled.
p.conn.Close(websocket.StatusTryAgainLater, "sorry, queue is empty...") p.conn.Close(websocket.StatusTryAgainLater, "sorry, queue is empty...")
return return