package game import ( "context" "crypto/rand" "encoding/hex" "sync" "github.com/ryanhamamura/c4/db/repository" ) type PlayerSession struct { Player *Player } type GameStore struct { games map[string]*GameInstance gamesMu sync.RWMutex queries *repository.Queries notifyFunc func(gameID string) } func NewGameStore(queries *repository.Queries) *GameStore { return &GameStore{ games: make(map[string]*GameInstance), queries: queries, } } func (gs *GameStore) SetNotifyFunc(f func(gameID string)) { gs.notifyFunc = f } func (gs *GameStore) makeNotify(gameID string) func() { return func() { if gs.notifyFunc != nil { gs.notifyFunc(gameID) } } } func (gs *GameStore) Create() *GameInstance { id := GenerateID(4) gi := NewGameInstance(id) gi.queries = gs.queries gi.notify = gs.makeNotify(id) gs.gamesMu.Lock() gs.games[id] = gi gs.gamesMu.Unlock() if gs.queries != nil { gi.save() //nolint:errcheck } return gi } func (gs *GameStore) Get(id string) (*GameInstance, bool) { gs.gamesMu.RLock() gi, ok := gs.games[id] gs.gamesMu.RUnlock() if ok { return gi, true } if gs.queries == nil { return nil, false } g, err := loadGame(gs.queries, id) if err != nil || g == nil { return nil, false } players, _ := loadGamePlayers(gs.queries, id) for _, p := range players { switch p.Color { case 1: g.Players[0] = p case 2: g.Players[1] = p } } gi = &GameInstance{ game: g, queries: gs.queries, notify: gs.makeNotify(id), } gs.gamesMu.Lock() gs.games[id] = gi gs.gamesMu.Unlock() return gi, true } func (gs *GameStore) Delete(id string) error { gs.gamesMu.Lock() delete(gs.games, id) gs.gamesMu.Unlock() if gs.queries != nil { return gs.queries.DeleteGame(context.Background(), id) } return nil } func GenerateID(size int) string { b := make([]byte, size) _, _ = rand.Read(b) return hex.EncodeToString(b) } type GameInstance struct { game *Game gameMu sync.RWMutex notify func() queries *repository.Queries } func NewGameInstance(id string) *GameInstance { return &GameInstance{ game: NewGame(id), notify: func() {}, } } func (gi *GameInstance) ID() string { gi.gameMu.RLock() defer gi.gameMu.RUnlock() return gi.game.ID } func (gi *GameInstance) 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 *GameInstance) GetGame() *Game { gi.gameMu.RLock() defer gi.gameMu.RUnlock() return gi.game } func (gi *GameInstance) GetPlayerColor(pid PlayerID) 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 *GameInstance) CreateRematch(gs *GameStore) *GameInstance { gi.gameMu.Lock() defer gi.gameMu.Unlock() if !gi.game.IsFinished() || gi.game.RematchGameID != nil { return nil } newGI := gs.Create() newID := newGI.ID() gi.game.RematchGameID = &newID if gi.queries != nil { if err := gi.save(); err != nil { gs.Delete(newID) //nolint:errcheck gi.game.RematchGameID = nil return nil } } gi.notify() return newGI } func (gi *GameInstance) 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 }