package connect4 import ( "context" "sync" "github.com/ryanhamamura/games/db/repository" "github.com/ryanhamamura/games/player" ) type PlayerSession struct { Player *Player } type Store struct { games map[string]*Instance gamesMu sync.RWMutex queries *repository.Queries notifyFunc func(gameID string) } func NewStore(queries *repository.Queries) *Store { return &Store{ games: make(map[string]*Instance), queries: queries, } } func (s *Store) SetNotifyFunc(f func(gameID string)) { s.notifyFunc = f } func (s *Store) makeNotify(gameID string) func() { return func() { if s.notifyFunc != nil { s.notifyFunc(gameID) } } } func (s *Store) Create() *Instance { id := player.GenerateID(4) gi := NewInstance(id) gi.queries = s.queries gi.notify = s.makeNotify(id) s.gamesMu.Lock() s.games[id] = gi s.gamesMu.Unlock() if s.queries != nil { gi.save() //nolint:errcheck } return gi } func (s *Store) Get(id string) (*Instance, bool) { s.gamesMu.RLock() gi, ok := s.games[id] s.gamesMu.RUnlock() if ok { return gi, true } if s.queries == nil { return nil, false } g, err := loadGame(s.queries, id) if err != nil || g == nil { return nil, false } players, _ := loadGamePlayers(s.queries, id) for _, p := range players { switch p.Color { case 1: g.Players[0] = p case 2: g.Players[1] = p } } gi = &Instance{ game: g, queries: s.queries, notify: s.makeNotify(id), } s.gamesMu.Lock() s.games[id] = gi s.gamesMu.Unlock() return gi, true } func (s *Store) Delete(id string) error { s.gamesMu.Lock() delete(s.games, id) s.gamesMu.Unlock() if s.queries != nil { return s.queries.DeleteGame(context.Background(), id) } return nil } type Instance struct { game *Game gameMu sync.RWMutex notify func() queries *repository.Queries } func NewInstance(id string) *Instance { return &Instance{ game: NewGame(id), notify: func() {}, } } func (gi *Instance) ID() string { gi.gameMu.RLock() defer gi.gameMu.RUnlock() return gi.game.ID } func (gi *Instance) Join(ps *PlayerSession) bool { gi.gameMu.Lock() defer gi.gameMu.Unlock() var slot int if gi.game.Players[0] == nil { ps.Player.Color = 1 gi.game.Players[0] = ps.Player slot = 0 } else if gi.game.Players[1] == nil { ps.Player.Color = 2 gi.game.Players[1] = ps.Player gi.game.Status = StatusInProgress slot = 1 } else { return false } if gi.queries != nil { gi.savePlayer(ps.Player, slot) //nolint:errcheck gi.save() //nolint:errcheck } gi.notify() return true } func (gi *Instance) GetGame() *Game { gi.gameMu.RLock() defer gi.gameMu.RUnlock() return gi.game } func (gi *Instance) GetPlayerColor(pid player.ID) int { gi.gameMu.RLock() defer gi.gameMu.RUnlock() for _, p := range gi.game.Players { if p != nil && p.ID == pid { return p.Color } } return 0 } func (gi *Instance) CreateRematch(s *Store) *Instance { gi.gameMu.Lock() defer gi.gameMu.Unlock() if !gi.game.IsFinished() || gi.game.RematchGameID != nil { return nil } newGI := s.Create() newID := newGI.ID() gi.game.RematchGameID = &newID if gi.queries != nil { if err := gi.save(); err != nil { s.Delete(newID) //nolint:errcheck gi.game.RematchGameID = nil return nil } } gi.notify() return newGI } func (gi *Instance) DropPiece(col int, playerColor int) bool { gi.gameMu.Lock() defer gi.gameMu.Unlock() row, ok := gi.game.DropPiece(col, playerColor) if !ok { return false } if gi.game.CheckWin(row, col) { for _, p := range gi.game.Players { if p != nil && p.Color == playerColor { gi.game.Winner = p break } } } else if gi.game.CheckDraw() { // Status already set by CheckDraw } else { gi.game.SwitchTurn() } if gi.queries != nil { gi.save() //nolint:errcheck } gi.notify() return true }