parameterize lobby by exchange types
This commit is contained in:
parent
72b0cd1023
commit
82aba814e6
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
5
main.go
5
main.go
@ -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()
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user