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
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
}
}

View File

@ -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())
}
}

View File

@ -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()

View File

@ -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