parameterize lobby by exchange types
This commit is contained in:
parent
72b0cd1023
commit
82aba814e6
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -5,37 +5,35 @@ 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]
|
||||
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, _, 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 {
|
||||
dealers.Add(1)
|
||||
} else {
|
||||
challs.Add(1)
|
||||
}
|
||||
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[lobby.GameID]int, N/2)
|
||||
counts := make(map[int]int, N/2)
|
||||
for _, id := range games {
|
||||
counts[id]++
|
||||
}
|
||||
@ -44,9 +42,13 @@ func TestQueue(t *testing.T) {
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
5
main.go
5
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()
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user