commit 0f9a0faf06215265a786075bf261627b8a90f80d Author: Steve Date: Fri Oct 1 14:00:29 2021 -0400 initial migration diff --git a/coordinator.go b/coordinator.go new file mode 100644 index 0000000..0a3c696 --- /dev/null +++ b/coordinator.go @@ -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, + } +} diff --git a/match.go b/match.go new file mode 100644 index 0000000..b1fb9b8 --- /dev/null +++ b/match.go @@ -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") + } +} diff --git a/session.go b/session.go new file mode 100644 index 0000000..b7e6656 --- /dev/null +++ b/session.go @@ -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) +}