package game import ( "crypto/rand" "encoding/hex" "sync" "time" ) type Syncable interface { Sync() } type PlayerSession struct { Player *Player Sync Syncable } 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 } func NewGameStore() *GameStore { return &GameStore{ games: make(map[string]*GameInstance), } } func (gs *GameStore) SetPersister(p Persister) { gs.persister = p } func (gs *GameStore) Create() *GameInstance { id := GenerateID(4) gi := NewGameInstance(id) gi.persister = gs.persister gs.gamesMu.Lock() gs.games[id] = gi gs.gamesMu.Unlock() if gs.persister != nil { gs.persister.SaveGame(gi.game) } go gi.run() 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 } // Try to load from database 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, players: make(map[PlayerID]Syncable), leave: make(chan PlayerID, 5), done: make(chan struct{}), persister: gs.persister, } gs.gamesMu.Lock() gs.games[id] = gi gs.gamesMu.Unlock() go gi.run() return gi, true } func (gs *GameStore) Delete(id string) error { gs.gamesMu.Lock() gi, ok := gs.games[id] if ok { delete(gs.games, id) close(gi.done) } 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 players map[PlayerID]Syncable playersMu sync.RWMutex leave chan PlayerID done chan struct{} dirty bool persister Persister } func NewGameInstance(id string) *GameInstance { return &GameInstance{ game: NewGame(id), players: make(map[PlayerID]Syncable), leave: make(chan PlayerID, 5), done: make(chan struct{}), } } 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 // Assign player to an open slot if gi.game.Players[0] == nil { ps.Player.Color = 1 // Red gi.game.Players[0] = ps.Player slot = 0 } else if gi.game.Players[1] == nil { ps.Player.Color = 2 // Yellow gi.game.Players[1] = ps.Player gi.game.Status = StatusInProgress slot = 1 } else { return false // Game is full } gi.playersMu.Lock() gi.players[ps.Player.ID] = ps.Sync gi.playersMu.Unlock() if gi.persister != nil { gi.persister.SaveGamePlayer(gi.game.ID, ps.Player, slot) gi.persister.SaveGame(gi.game) } gi.dirty = true 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) RegisterSync(playerID PlayerID, sync Syncable) { gi.playersMu.Lock() gi.players[playerID] = sync gi.playersMu.Unlock() gi.dirty = true } 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.dirty = true 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.dirty = true return true } func (gi *GameInstance) run() { ticker := time.NewTicker(100 * time.Millisecond) defer ticker.Stop() for { select { case pid := <-gi.leave: gi.playersMu.Lock() delete(gi.players, pid) gi.playersMu.Unlock() case <-ticker.C: gi.publish() case <-gi.done: return } } } func (gi *GameInstance) publish() { gi.gameMu.Lock() if !gi.dirty { gi.gameMu.Unlock() return } gi.dirty = false gi.playersMu.RLock() syncers := make([]Syncable, 0, len(gi.players)) for _, sync := range gi.players { syncers = append(syncers, sync) } gi.playersMu.RUnlock() gi.gameMu.Unlock() for _, sync := range syncers { sync.Sync() } }