Files
games/game/store.go
Ryan Hamamura afd8a3e9d0
Some checks failed
CI / Deploy / test (pull_request) Successful in 8s
CI / Deploy / lint (pull_request) Failing after 44s
CI / Deploy / deploy (pull_request) Has been skipped
fix: resolve all linting errors and add SSE compression
- Add brotli compression (level 5) to long-lived SSE event streams
  (HandleGameEvents, HandleSnakeEvents) to reduce wire payload
- Fix all errcheck violations with nolint annotations for best-effort calls
- Fix goimports: separate stdlib, third-party, and local import groups
- Fix staticcheck: add package comments, use tagged switch
- Zero lint issues remaining
2026-03-02 12:38:21 -10:00

233 lines
4.1 KiB
Go

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 {
gs.saveGame(gi.game) //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 := gs.loadGame(id)
if err != nil || g == nil {
return nil, false
}
players, _ := gs.loadGamePlayers(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.saveGamePlayer(gi.game.ID, ps.Player, slot) //nolint:errcheck
gi.saveGame(gi.game) //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.saveGame(gi.game); 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.saveGame(gi.game) //nolint:errcheck
}
gi.notify()
return true
}