package game import ( "crypto/rand" "encoding/hex" "sync" ) type PlayerSession struct { Player *Player } type Persister interface { SaveGame(g *Game) error LoadGame(id string) (*Game, error) SaveGamePlayer(gameID string, player *Player, slot int) error LoadGamePlayers(gameID string) ([]*Player, error) DeleteGame(id string) error } type GameStore struct { games map[string]*GameInstance gamesMu sync.RWMutex persister Persister notifyFunc func(gameID string) } func NewGameStore() *GameStore { return &GameStore{ games: make(map[string]*GameInstance), } } func (gs *GameStore) SetPersister(p Persister) { gs.persister = p } 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.persister = gs.persister gi.notify = gs.makeNotify(id) gs.gamesMu.Lock() gs.games[id] = gi gs.gamesMu.Unlock() if gs.persister != nil { gs.persister.SaveGame(gi.game) } 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.persister == nil { return nil, false } game, err := gs.persister.LoadGame(id) if err != nil || game == nil { return nil, false } players, _ := gs.persister.LoadGamePlayers(id) for _, p := range players { if p.Color == 1 { game.Players[0] = p } else if p.Color == 2 { game.Players[1] = p } } gi = &GameInstance{ game: game, persister: gs.persister, 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.persister != nil { return gs.persister.DeleteGame(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() persister Persister } 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.persister != nil { gi.persister.SaveGamePlayer(gi.game.ID, ps.Player, slot) gi.persister.SaveGame(gi.game) } 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.persister != nil { if err := gi.persister.SaveGame(gi.game); err != nil { gs.Delete(newID) 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.persister != nil { gi.persister.SaveGame(gi.game) } gi.notify() return true }