initial migration
This commit is contained in:
commit
0f9a0faf06
161
coordinator.go
Normal file
161
coordinator.go
Normal file
@ -0,0 +1,161 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.saintnet.tech/tomecraft/tome_game"
|
||||
. "git.saintnet.tech/tomecraft/tome_lib"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Coordinator struct {
|
||||
Matches map[uuid.UUID]*Session
|
||||
MatchLock *sync.Mutex
|
||||
PlayerQueueChan chan uuid.UUID
|
||||
PlayerPool map[MMRPlayer]bool
|
||||
CallbackChan map[uuid.UUID]chan uuid.UUID
|
||||
}
|
||||
|
||||
func NewCoordinator() *Coordinator {
|
||||
return &Coordinator{
|
||||
Matches: make(map[uuid.UUID]*Session),
|
||||
MatchLock: &sync.Mutex{},
|
||||
PlayerQueueChan: make(chan uuid.UUID),
|
||||
PlayerPool: make(map[MMRPlayer]bool),
|
||||
CallbackChan: make(map[uuid.UUID]chan uuid.UUID),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Coordinator) Start() {
|
||||
go MatchMaker(c)
|
||||
go MatchCleaner(c)
|
||||
go QueueCleaner(c)
|
||||
}
|
||||
|
||||
func (c *Coordinator) Coordinate(cmd *SessionCommand) *SessionCommandResult {
|
||||
switch cmd.Command {
|
||||
case SessionCmdQuery:
|
||||
c.CallbackChan[cmd.ID] = make(chan uuid.UUID)
|
||||
c.PlayerQueueChan <- cmd.ID
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
select {
|
||||
case m := <-c.CallbackChan[cmd.ID]:
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: m,
|
||||
Result: SessionRespFound,
|
||||
}
|
||||
case <-ticker.C:
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespError,
|
||||
}
|
||||
}
|
||||
case SessionCmdJoin:
|
||||
m, exists := c.Matches[cmd.MatchID]
|
||||
if !exists {
|
||||
log.Printf("player %v tried to join non-existent match %v", cmd.ID, cmd.MatchID)
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespJoinError,
|
||||
}
|
||||
}
|
||||
resp := m.Join(cmd.ID)
|
||||
if m.p1 != uuid.Nil && m.p2 != uuid.Nil {
|
||||
log.Printf("Starting game for %v and %v\n", m.p1, m.p2)
|
||||
m.Game = tome_game.NewGame()
|
||||
m.Active = true
|
||||
}
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: m.ID,
|
||||
Result: resp,
|
||||
}
|
||||
case SessionCmdReady:
|
||||
m, exists := c.Matches[cmd.MatchID]
|
||||
if !exists {
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespError,
|
||||
}
|
||||
}
|
||||
if m.p1 != uuid.Nil && m.p2 != uuid.Nil {
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: m.ID,
|
||||
Result: SessionRespReady,
|
||||
}
|
||||
} else {
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespError,
|
||||
}
|
||||
}
|
||||
case SessionCmdLeave:
|
||||
m, exists := c.Matches[cmd.MatchID]
|
||||
if exists && m.PlayerIn(cmd.ID) {
|
||||
m.Leave(cmd.ID)
|
||||
} else {
|
||||
for k, _ := range c.PlayerPool {
|
||||
if k.Id == m.ID {
|
||||
delete(c.PlayerPool, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespLeft,
|
||||
}
|
||||
case SessionCmdPlay:
|
||||
m, exists := c.Matches[cmd.MatchID]
|
||||
if !exists || !m.PlayerIn(cmd.ID) || m.Game == nil {
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespError,
|
||||
}
|
||||
}
|
||||
resp := m.Play(cmd.ID, cmd.GameCommand)
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: m.ID,
|
||||
Result: SessionRespPlayed,
|
||||
GameResult: resp,
|
||||
}
|
||||
case SessionCmdPoll:
|
||||
m, exists := c.Matches[cmd.MatchID]
|
||||
if exists {
|
||||
_, exists = m.Broadcasts[cmd.ID]
|
||||
if !exists {
|
||||
log.Printf("%v has opted in to polling", cmd.ID)
|
||||
m.Broadcasts[cmd.ID] = make(chan SessionResp, 10)
|
||||
}
|
||||
select {
|
||||
case res := <-m.Broadcasts[cmd.ID]:
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: m.ID,
|
||||
Result: res,
|
||||
}
|
||||
default:
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: m.ID,
|
||||
Result: SessionRespBroadcastNone,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &SessionCommandResult{
|
||||
ID: cmd.ID,
|
||||
MatchID: uuid.Nil,
|
||||
Result: SessionRespError,
|
||||
}
|
||||
}
|
193
match.go
Normal file
193
match.go
Normal file
@ -0,0 +1,193 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"git.saintnet.tech/tomecraft/tome_lib"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type MMRPlayer struct {
|
||||
Id uuid.UUID
|
||||
MMR int
|
||||
QueueTime time.Time
|
||||
}
|
||||
|
||||
func MatchMaker(c *Coordinator) {
|
||||
//in the future the int will be MMR or something, but for now it's just a set
|
||||
for {
|
||||
select {
|
||||
case p := <-c.PlayerQueueChan:
|
||||
//add player to pool
|
||||
c.PlayerPool[MMRPlayer{
|
||||
Id: p,
|
||||
MMR: 0,
|
||||
QueueTime: time.Now()}] = true
|
||||
log.Printf("Player %v has queued", p)
|
||||
if len(c.PlayerPool) < 2 {
|
||||
continue
|
||||
} else {
|
||||
//fancy matchmaking math to guarantee a fair game
|
||||
var p1, p2 MMRPlayer
|
||||
for key, _ := range c.PlayerPool {
|
||||
p1 = key
|
||||
break
|
||||
}
|
||||
delete(c.PlayerPool, p1)
|
||||
for key, _ := range c.PlayerPool {
|
||||
p2 = key
|
||||
break
|
||||
}
|
||||
delete(c.PlayerPool, p2)
|
||||
m := NewSession()
|
||||
log.Printf("Creating match %v for %v and %v", m.ID, p1, p2)
|
||||
m.Active = false
|
||||
c.MatchLock.Lock()
|
||||
c.Matches[m.ID] = m
|
||||
c.MatchLock.Unlock()
|
||||
c.CallbackChan[p1.Id] <- m.ID
|
||||
c.CallbackChan[p2.Id] <- m.ID
|
||||
go MatchWatcher(m)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func QueueCleaner(c *Coordinator) {
|
||||
ticker := time.NewTicker(5 * time.Minute)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
for v, _ := range c.PlayerPool {
|
||||
if time.Now().After(v.QueueTime.Add(time.Minute * 5)) {
|
||||
log.Printf("Removing player %v from pool", v.Id)
|
||||
delete(c.PlayerPool, v) //probably should just flag
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MatchCleaner(c *Coordinator) {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
c.MatchLock.Lock()
|
||||
for _, v := range c.Matches {
|
||||
if v.Game == nil && v.Active {
|
||||
log.Println("clearing match with no game")
|
||||
delete(c.Matches, v.ID)
|
||||
v.Watcher <- true
|
||||
}
|
||||
if time.Now().After(v.LastMove.Add(time.Minute * 10)) {
|
||||
log.Println("clearing stale match")
|
||||
v.Game = nil
|
||||
v.Active = false
|
||||
delete(c.Matches, v.ID)
|
||||
v.Watcher <- true
|
||||
}
|
||||
}
|
||||
|
||||
c.MatchLock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func MatchWatcher(m *Session) {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
old_turn := -1
|
||||
var old_board *tome_lib.Board
|
||||
old_sen_hand := -1
|
||||
old_sco_hand := -1
|
||||
old_sen_life := -1
|
||||
old_sco_life := -1
|
||||
sen_ready := false
|
||||
sco_ready := false
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if m.Active && m.Game != nil && len(m.Broadcasts) > 0 {
|
||||
if m.Game.Status == tome_lib.StatusLobby && !sen_ready && m.Game.SentinalPlayer.Ready {
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastSenReady)
|
||||
}
|
||||
sen_ready = true
|
||||
}
|
||||
if m.Game.Status == tome_lib.StatusLobby && !sco_ready && m.Game.ScourgePlayer.Ready {
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastScoReady)
|
||||
}
|
||||
sco_ready = true
|
||||
}
|
||||
if m.Game.Status == tome_lib.StatusSentinalWin {
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastSenWin)
|
||||
}
|
||||
}
|
||||
if m.Game.Status == tome_lib.StatusScourgeWin {
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastScoWin)
|
||||
}
|
||||
}
|
||||
if m.p1 == uuid.Nil && sen_ready {
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastSenLeft)
|
||||
}
|
||||
sen_ready = false
|
||||
}
|
||||
if m.p2 == uuid.Nil && sco_ready {
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastScoLeft)
|
||||
}
|
||||
sco_ready = false
|
||||
}
|
||||
if old_board == nil || old_board.Sentinal != m.Game.GameBoard.Sentinal || old_board.Scourge != m.Game.GameBoard.Scourge || old_sen_hand != len(m.Game.SentinalPlayer.Hand) || old_sco_hand != len(m.Game.ScourgePlayer.Hand) || old_sen_life != m.Game.SentinalPlayer.Life || old_sco_life != m.Game.ScourgePlayer.Life {
|
||||
if old_board == nil {
|
||||
old_board = m.Game.GameBoard
|
||||
} else {
|
||||
old_board.Sentinal = m.Game.GameBoard.Sentinal
|
||||
old_board.Scourge = m.Game.GameBoard.Scourge
|
||||
}
|
||||
old_sen_hand = len(m.Game.SentinalPlayer.Hand)
|
||||
old_sco_hand = len(m.Game.ScourgePlayer.Hand)
|
||||
old_sen_life = m.Game.SentinalPlayer.Life
|
||||
old_sco_life = m.Game.ScourgePlayer.Life
|
||||
for _, v := range m.Broadcasts {
|
||||
addBroadcast(v, tome_lib.SessionRespBroadcastUpdate)
|
||||
}
|
||||
}
|
||||
if old_turn != m.Game.CurrentTurn {
|
||||
old_turn = m.Game.CurrentTurn
|
||||
if old_turn == tome_lib.SentinalID {
|
||||
for k, v := range m.pMap {
|
||||
if v == tome_lib.SentinalID {
|
||||
addBroadcast(m.Broadcasts[k], tome_lib.SessionRespBroadcastSenTurn)
|
||||
}
|
||||
}
|
||||
} else if old_turn == tome_lib.ScourgeID {
|
||||
for k, v := range m.pMap {
|
||||
if v == tome_lib.ScourgeID {
|
||||
addBroadcast(m.Broadcasts[k], tome_lib.SessionRespBroadcastScoTrun)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case <-m.Watcher:
|
||||
close(m.Watcher)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addBroadcast(pipe chan tome_lib.SessionResp, brd tome_lib.SessionResp) {
|
||||
select {
|
||||
case pipe <- brd:
|
||||
default:
|
||||
log.Println("broadcasts buffer full, discarding")
|
||||
}
|
||||
}
|
108
session.go
Normal file
108
session.go
Normal file
@ -0,0 +1,108 @@
|
||||
package coordinator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
. "git.saintnet.tech/tomecraft/tome_game"
|
||||
. "git.saintnet.tech/tomecraft/tome_lib"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
ID uuid.UUID
|
||||
p1 uuid.UUID
|
||||
p2 uuid.UUID
|
||||
pMap map[uuid.UUID]int
|
||||
Active bool
|
||||
Game *Game
|
||||
Watcher chan bool
|
||||
Broadcasts map[uuid.UUID]chan SessionResp
|
||||
LastMove time.Time
|
||||
}
|
||||
|
||||
func NewSession() *Session {
|
||||
return &Session{
|
||||
ID: uuid.New(),
|
||||
p1: uuid.Nil,
|
||||
p2: uuid.Nil,
|
||||
pMap: make(map[uuid.UUID]int),
|
||||
Active: false,
|
||||
Game: nil,
|
||||
Watcher: make(chan bool),
|
||||
Broadcasts: make(map[uuid.UUID]chan SessionResp),
|
||||
LastMove: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Join(id uuid.UUID) SessionResp {
|
||||
if s.p1 == uuid.Nil {
|
||||
s.p1 = id
|
||||
s.pMap[id] = SentinalID
|
||||
for _, v := range s.Broadcasts {
|
||||
v <- SessionRespBroadcastSenJoin
|
||||
}
|
||||
return SessionRespJoined1
|
||||
} else if s.p2 == uuid.Nil {
|
||||
s.p2 = id
|
||||
s.pMap[id] = ScourgeID
|
||||
for _, v := range s.Broadcasts {
|
||||
v <- SessionRespBroadcastScoJoin
|
||||
}
|
||||
return SessionRespJoined2
|
||||
} else {
|
||||
return SessionRespJoinError
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Leave(id uuid.UUID) {
|
||||
if id == s.p1 {
|
||||
s.p1 = uuid.Nil
|
||||
} else if id == s.p2 {
|
||||
s.p2 = uuid.Nil
|
||||
}
|
||||
delete(s.Broadcasts, id)
|
||||
if s.p1 == uuid.Nil && s.p2 == uuid.Nil {
|
||||
s.Game = nil
|
||||
} else if s.Game.Status != StatusDraw || s.Game.Status != StatusScourgeWin || s.Game.Status != StatusSentinalWin {
|
||||
s.Game.Status = StatusStop
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Play(id uuid.UUID, cmd *Command) *CommandResult {
|
||||
if s.pMap[id] != cmd.PlayerID {
|
||||
return nil
|
||||
}
|
||||
if cmd.Type != SessionCmdPoll {
|
||||
s.LastMove = time.Now()
|
||||
}
|
||||
res := s.Game.Parse(cmd)
|
||||
return res
|
||||
}
|
||||
|
||||
func (s *Session) PlayerIn(id uuid.UUID) bool {
|
||||
_, exists := s.pMap[id]
|
||||
return exists
|
||||
}
|
||||
|
||||
type SessionCommand struct {
|
||||
ID uuid.UUID `json:"player_id"`
|
||||
MatchID uuid.UUID `json:"match_id"`
|
||||
Command SessionCmd `json:"command"`
|
||||
GameCommand *Command `json:"game_command,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SessionCommand) String() string {
|
||||
return fmt.Sprintf("%v %v %v\n", s.ID, s.Command, s.GameCommand)
|
||||
}
|
||||
|
||||
type SessionCommandResult struct {
|
||||
ID uuid.UUID `json:"player_id"`
|
||||
MatchID uuid.UUID `json:"match_id"`
|
||||
Result SessionResp `json:"result"`
|
||||
GameResult *CommandResult `json:"game_result,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SessionCommandResult) String() string {
|
||||
return fmt.Sprintf("%v %v %v\n", s.ID, s.Result, s.GameResult)
|
||||
}
|
Loading…
Reference in New Issue
Block a user