simplify matchmaking

This commit is contained in:
Branden J Brown 2024-01-27 09:43:55 -06:00
parent cc2b54eac3
commit f2ba9849f1
3 changed files with 77 additions and 110 deletions

View File

@ -1,51 +1,87 @@
package lobby package lobby
import ( import (
"sync" "context"
"github.com/google/uuid"
"git.sunturtle.xyz/studio/shotgun/game"
"git.sunturtle.xyz/studio/shotgun/player" "git.sunturtle.xyz/studio/shotgun/player"
"git.sunturtle.xyz/studio/shotgun/serve" "git.sunturtle.xyz/studio/shotgun/serve"
) )
type GameID = serve.GameID type GameID = serve.GameID
// Lobby is a set of active games. type matchMade struct {
match chan GameID
chall player.ID
}
type match chan matchMade
// Lobby is a matchmaking service.
type Lobby struct { type Lobby struct {
mu sync.Mutex
// games is the set of all active games in the lobby.
games map[GameID]*game.Game
// 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
} }
func New() *Lobby { func New() *Lobby {
return &Lobby{ return &Lobby{
games: make(map[GameID]*game.Game),
matches: make(chan match), matches: make(chan match),
} }
} }
// Game returns the game with the given ID. // Queue waits for a match and returns a unique ID for it.
func (l *Lobby) Game(id GameID) *game.Game { func (l *Lobby) Queue(ctx context.Context, p player.ID) (id GameID, chall player.ID, deal bool) {
l.mu.Lock() select {
defer l.mu.Unlock() case m := <-l.matches:
return l.games[id] // We found a dealer waiting for a match.
r := matchMade{
match: make(chan GameID, 1),
chall: p,
} }
// We don't need to check the context here because the challenger
// Start begins a new game in the lobby. // channel is buffered and we have exclusive send access on it.
// The caller must be able to distinguish the dealer's and challenger's conns m <- r
// in order to provide correct game start DTOs to each. // We do need to check the context here in case they disappeared.
func (l *Lobby) Start(id GameID, dealer, challenger player.ID) { select {
g := game.New(dealer, challenger) case <-ctx.Done():
l.mu.Lock() return GameID{}, player.ID{}, false
defer l.mu.Unlock() case id := <-r.match:
l.games[id] = g return id, p, false
}
default: // do nothing
}
// We're a new dealer.
m := make(match, 1)
select {
case <-ctx.Done():
return GameID{}, player.ID{}, 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,
}
m <- r
select {
case <-ctx.Done():
return GameID{}, player.ID{}, false
case id := <-r.match:
return id, p, false
}
}
select {
case <-ctx.Done():
return GameID{}, player.ID{}, false
case r := <-m:
// Got our challenger. Create the game and send the ID back.
id := GameID(uuid.New())
// Don't need to check context because the match channel is buffered.
r.match <- id
return id, r.chall, true
} }
// Finish removes a game from the lobby.
func (l *Lobby) Finish(id GameID) {
l.mu.Lock()
defer l.mu.Unlock()
delete(l.games, id)
} }

View File

@ -2,6 +2,7 @@ package lobby_test
import ( import (
"context" "context"
"sync/atomic"
"testing" "testing"
"git.sunturtle.xyz/studio/shotgun/lobby" "git.sunturtle.xyz/studio/shotgun/lobby"
@ -15,10 +16,17 @@ func TestQueue(t *testing.T) {
l := lobby.New() l := lobby.New()
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
games = games[:0] games = games[:0]
var dealers, challs atomic.Int32
for i := 0; i < N; i++ { for i := 0; i < N; i++ {
i := i i := i
go func() { go func() {
ch <- l.Queue(context.Background(), player.ID{uint8(i), uint8(i >> 8)}) id, _, deal := l.Queue(context.Background(), player.ID{uint8(i), uint8(i >> 8)})
if deal {
dealers.Add(1)
} else {
challs.Add(1)
}
ch <- id
}() }()
} }
for i := 0; i < N; i++ { for i := 0; i < N; i++ {
@ -34,16 +42,9 @@ func TestQueue(t *testing.T) {
t.Errorf("game %v appears %d times", id, c) t.Errorf("game %v appears %d times", id, c)
} }
} }
// Every game should have two different players. // The number of dealers must match the number of challengers.
for _, id := range games { if dealers.Load() != challs.Load() {
g := l.Game(id) t.Errorf("%d dealers != %d challengers", dealers.Load(), challs.Load())
if g == nil {
t.Errorf("game %v was created but doesn't exist", g)
continue
}
if g.CurrentPlayer().Is(g.Opponent()) {
t.Errorf("game %v matched with self", id)
}
} }
} }
} }

View File

@ -1,70 +0,0 @@
package lobby
import (
"context"
"git.sunturtle.xyz/studio/shotgun/player"
"github.com/google/uuid"
)
type match struct {
dealer player.ID
chall chan player.ID
match chan GameID
}
// Queue waits for a match.
// This may cause a new match to be created.
func (l *Lobby) Queue(ctx context.Context, p player.ID) GameID {
select {
case <-ctx.Done():
return GameID{}
case m := <-l.matches:
// We found a dealer waiting for a match.
// We don't need to check the context here because the challenger
// channel is buffered and we have exclusive send access on it.
m.chall <- p
// We do need to check the context here in case they disappeared.
select {
case <-ctx.Done():
return GameID{}
case id := <-m.match:
return id
}
default: // do nothing
}
// We're a new dealer.
m := match{
dealer: p,
chall: make(chan player.ID, 1),
match: make(chan GameID, 1),
}
select {
case <-ctx.Done():
return GameID{}
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.
m.chall <- p
select {
case <-ctx.Done():
return GameID{}
case id := <-m.match:
return id
}
}
select {
case <-ctx.Done():
return GameID{}
case chall := <-m.chall:
// Got our challenger. Create the game and send the ID back.
id := uuid.New()
l.Start(id, p, chall)
// Don't need to check context because the match channel is buffered.
m.match <- id
return id
}
}