Files
games/connect4/store.go
Ryan Hamamura c6885a069b
All checks were successful
CI / Deploy / test (pull_request) Successful in 14s
CI / Deploy / lint (pull_request) Successful in 25s
CI / Deploy / deploy (pull_request) Has been skipped
refactor: rename Go module from c4 to games
Rename module path github.com/ryanhamamura/c4 to
github.com/ryanhamamura/games across go.mod, all source files,
and golangci config.
2026-03-02 20:41:20 -10:00

226 lines
3.8 KiB
Go

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