Wrap free persistence functions in instance methods for cleaner call sites (gi.save() instead of saveGame(gi.queries, gi.game)). Methods log errors via zerolog before returning them.
233 lines
4.0 KiB
Go
233 lines
4.0 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 {
|
|
gi.save() //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 := loadGame(gs.queries, id)
|
|
if err != nil || g == nil {
|
|
return nil, false
|
|
}
|
|
|
|
players, _ := loadGamePlayers(gs.queries, 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.savePlayer(ps.Player, slot) //nolint:errcheck
|
|
gi.save() //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.save(); 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.save() //nolint:errcheck
|
|
}
|
|
|
|
gi.notify()
|
|
return true
|
|
}
|