Files
games/game/store.go
Ryan Hamamura e68e4b48f5
All checks were successful
Deploy c4 / deploy (push) Successful in 45s
fix: resolve nil pubsub preventing live game updates
v.PubSub() was captured at startup before v.Start() initialized NATS,
so both stores held nil and notify() silently no-oped. Replace the
PubSub interface with a callback that evaluates v.PubSub() lazily at
call time.
2026-02-20 12:37:28 -10:00

240 lines
4.2 KiB
Go

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
}