refactor: remove persister abstraction layer
Inline persistence logic directly into game stores and handlers: - game/persist.go: DB mapping methods on GameStore and GameInstance - snake/persist.go: DB mapping methods on SnakeStore and SnakeGameInstance - Chat persistence inlined into c4game handlers - Delete db/persister.go (GamePersister, SnakePersister, ChatPersister) - Stores now take *repository.Queries directly instead of Persister interface
This commit is contained in:
157
game/persist.go
Normal file
157
game/persist.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
)
|
||||
|
||||
// Persistence methods on GameStore (used during Get to hydrate from DB).
|
||||
|
||||
func (gs *GameStore) saveGame(g *Game) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := gs.queries.GetGame(ctx, g.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = gs.queries.CreateGame(ctx, repository.CreateGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gs.queries.UpdateGame(ctx, updateGameParams(g))
|
||||
}
|
||||
|
||||
func (gs *GameStore) loadGame(id string) (*Game, error) {
|
||||
row, err := gs.queries.GetGame(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return gameFromRow(row)
|
||||
}
|
||||
|
||||
func (gs *GameStore) loadGamePlayers(id string) ([]*Player, error) {
|
||||
rows, err := gs.queries.GetGamePlayers(context.Background(), id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return playersFromRows(rows), nil
|
||||
}
|
||||
|
||||
// Persistence methods on GameInstance (used during gameplay mutations).
|
||||
|
||||
func (gi *GameInstance) saveGame(g *Game) error {
|
||||
ctx := context.Background()
|
||||
|
||||
_, err := gi.queries.GetGame(ctx, g.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = gi.queries.CreateGame(ctx, repository.CreateGameParams{
|
||||
ID: g.ID,
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return gi.queries.UpdateGame(ctx, updateGameParams(g))
|
||||
}
|
||||
|
||||
func (gi *GameInstance) saveGamePlayer(gameID string, player *Player, slot int) error {
|
||||
var userID, guestPlayerID sql.NullString
|
||||
if player.UserID != nil {
|
||||
userID = sql.NullString{String: *player.UserID, Valid: true}
|
||||
} else {
|
||||
guestPlayerID = sql.NullString{String: string(player.ID), Valid: true}
|
||||
}
|
||||
|
||||
return gi.queries.CreateGamePlayer(context.Background(), repository.CreateGamePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
Nickname: player.Nickname,
|
||||
Color: int64(player.Color),
|
||||
Slot: int64(slot),
|
||||
})
|
||||
}
|
||||
|
||||
// Shared helpers for domain ↔ DB mapping.
|
||||
|
||||
func updateGameParams(g *Game) repository.UpdateGameParams {
|
||||
var winnerUserID sql.NullString
|
||||
if g.Winner != nil && g.Winner.UserID != nil {
|
||||
winnerUserID = sql.NullString{String: *g.Winner.UserID, Valid: true}
|
||||
}
|
||||
|
||||
var winningCells sql.NullString
|
||||
if wc := g.WinningCellsToJSON(); wc != "" {
|
||||
winningCells = sql.NullString{String: wc, Valid: true}
|
||||
}
|
||||
|
||||
var rematchGameID sql.NullString
|
||||
if g.RematchGameID != nil {
|
||||
rematchGameID = sql.NullString{String: *g.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return repository.UpdateGameParams{
|
||||
Board: g.BoardToJSON(),
|
||||
CurrentTurn: int64(g.CurrentTurn),
|
||||
Status: int64(g.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
WinningCells: winningCells,
|
||||
RematchGameID: rematchGameID,
|
||||
ID: g.ID,
|
||||
}
|
||||
}
|
||||
|
||||
func gameFromRow(row repository.Game) (*Game, error) {
|
||||
g := &Game{
|
||||
ID: row.ID,
|
||||
CurrentTurn: int(row.CurrentTurn),
|
||||
Status: GameStatus(row.Status),
|
||||
}
|
||||
|
||||
if err := g.BoardFromJSON(row.Board); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if row.WinningCells.Valid {
|
||||
g.WinningCellsFromJSON(row.WinningCells.String)
|
||||
}
|
||||
|
||||
if row.RematchGameID.Valid {
|
||||
g.RematchGameID = &row.RematchGameID.String
|
||||
}
|
||||
|
||||
return g, nil
|
||||
}
|
||||
|
||||
func playersFromRows(rows []repository.GamePlayer) []*Player {
|
||||
players := make([]*Player, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
player := &Player{
|
||||
Nickname: row.Nickname,
|
||||
Color: int(row.Color),
|
||||
}
|
||||
|
||||
if row.UserID.Valid {
|
||||
player.UserID = &row.UserID.String
|
||||
player.ID = PlayerID(row.UserID.String)
|
||||
} else if row.GuestPlayerID.Valid {
|
||||
player.ID = PlayerID(row.GuestPlayerID.String)
|
||||
}
|
||||
|
||||
players = append(players, player)
|
||||
}
|
||||
return players
|
||||
}
|
||||
@@ -1,40 +1,32 @@
|
||||
package game
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"sync"
|
||||
|
||||
"github.com/ryanhamamura/c4/db/repository"
|
||||
)
|
||||
|
||||
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
|
||||
games map[string]*GameInstance
|
||||
gamesMu sync.RWMutex
|
||||
queries *repository.Queries
|
||||
notifyFunc func(gameID string)
|
||||
}
|
||||
|
||||
func NewGameStore() *GameStore {
|
||||
func NewGameStore(queries *repository.Queries) *GameStore {
|
||||
return &GameStore{
|
||||
games: make(map[string]*GameInstance),
|
||||
games: make(map[string]*GameInstance),
|
||||
queries: queries,
|
||||
}
|
||||
}
|
||||
|
||||
func (gs *GameStore) SetPersister(p Persister) {
|
||||
gs.persister = p
|
||||
}
|
||||
|
||||
func (gs *GameStore) SetNotifyFunc(f func(gameID string)) {
|
||||
gs.notifyFunc = f
|
||||
}
|
||||
@@ -50,14 +42,14 @@ func (gs *GameStore) makeNotify(gameID string) func() {
|
||||
func (gs *GameStore) Create() *GameInstance {
|
||||
id := GenerateID(4)
|
||||
gi := NewGameInstance(id)
|
||||
gi.persister = gs.persister
|
||||
gi.queries = gs.queries
|
||||
gi.notify = gs.makeNotify(id)
|
||||
gs.gamesMu.Lock()
|
||||
gs.games[id] = gi
|
||||
gs.gamesMu.Unlock()
|
||||
|
||||
if gs.persister != nil {
|
||||
gs.persister.SaveGame(gi.game)
|
||||
if gs.queries != nil {
|
||||
gs.saveGame(gi.game)
|
||||
}
|
||||
|
||||
return gi
|
||||
@@ -72,28 +64,28 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) {
|
||||
return gi, true
|
||||
}
|
||||
|
||||
if gs.persister == nil {
|
||||
if gs.queries == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
game, err := gs.persister.LoadGame(id)
|
||||
if err != nil || game == nil {
|
||||
g, err := gs.loadGame(id)
|
||||
if err != nil || g == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
players, _ := gs.persister.LoadGamePlayers(id)
|
||||
players, _ := gs.loadGamePlayers(id)
|
||||
for _, p := range players {
|
||||
if p.Color == 1 {
|
||||
game.Players[0] = p
|
||||
g.Players[0] = p
|
||||
} else if p.Color == 2 {
|
||||
game.Players[1] = p
|
||||
g.Players[1] = p
|
||||
}
|
||||
}
|
||||
|
||||
gi = &GameInstance{
|
||||
game: game,
|
||||
persister: gs.persister,
|
||||
notify: gs.makeNotify(id),
|
||||
game: g,
|
||||
queries: gs.queries,
|
||||
notify: gs.makeNotify(id),
|
||||
}
|
||||
|
||||
gs.gamesMu.Lock()
|
||||
@@ -108,8 +100,8 @@ func (gs *GameStore) Delete(id string) error {
|
||||
delete(gs.games, id)
|
||||
gs.gamesMu.Unlock()
|
||||
|
||||
if gs.persister != nil {
|
||||
return gs.persister.DeleteGame(id)
|
||||
if gs.queries != nil {
|
||||
return gs.queries.DeleteGame(context.Background(), id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -121,10 +113,10 @@ func GenerateID(size int) string {
|
||||
}
|
||||
|
||||
type GameInstance struct {
|
||||
game *Game
|
||||
gameMu sync.RWMutex
|
||||
notify func()
|
||||
persister Persister
|
||||
game *Game
|
||||
gameMu sync.RWMutex
|
||||
notify func()
|
||||
queries *repository.Queries
|
||||
}
|
||||
|
||||
func NewGameInstance(id string) *GameInstance {
|
||||
@@ -158,9 +150,9 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
if gi.persister != nil {
|
||||
gi.persister.SaveGamePlayer(gi.game.ID, ps.Player, slot)
|
||||
gi.persister.SaveGame(gi.game)
|
||||
if gi.queries != nil {
|
||||
gi.saveGamePlayer(gi.game.ID, ps.Player, slot)
|
||||
gi.saveGame(gi.game)
|
||||
}
|
||||
|
||||
gi.notify()
|
||||
@@ -196,8 +188,8 @@ func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance {
|
||||
newID := newGI.ID()
|
||||
gi.game.RematchGameID = &newID
|
||||
|
||||
if gi.persister != nil {
|
||||
if err := gi.persister.SaveGame(gi.game); err != nil {
|
||||
if gi.queries != nil {
|
||||
if err := gi.saveGame(gi.game); err != nil {
|
||||
gs.Delete(newID)
|
||||
gi.game.RematchGameID = nil
|
||||
return nil
|
||||
@@ -230,8 +222,8 @@ func (gi *GameInstance) DropPiece(col int, playerColor int) bool {
|
||||
gi.game.SwitchTurn()
|
||||
}
|
||||
|
||||
if gi.persister != nil {
|
||||
gi.persister.SaveGame(gi.game)
|
||||
if gi.queries != nil {
|
||||
gi.saveGame(gi.game)
|
||||
}
|
||||
|
||||
gi.notify()
|
||||
|
||||
Reference in New Issue
Block a user