Add user authentication and game persistence with SQLite

- User registration/login with bcrypt password hashing
- SQLite database with goose migrations and sqlc-generated queries
- Games and players persisted to database, resumable after restart
- Guest play still supported alongside authenticated users
- Auth UI components (login/register forms, auth header, guest banner)
This commit is contained in:
Ryan Hamamura
2026-01-14 16:59:40 -10:00
parent 03dcfdbf85
commit b264d8990b
18 changed files with 1121 additions and 5 deletions

View File

@@ -16,9 +16,17 @@ type PlayerSession struct {
Sync Syncable
}
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)
}
type GameStore struct {
games map[string]*GameInstance
gamesMu sync.RWMutex
games map[string]*GameInstance
gamesMu sync.RWMutex
persister Persister
}
func NewGameStore() *GameStore {
@@ -27,21 +35,68 @@ func NewGameStore() *GameStore {
}
}
func (gs *GameStore) SetPersister(p Persister) {
gs.persister = p
}
func (gs *GameStore) Create() *GameInstance {
id := GenerateID(4)
gi := NewGameInstance(id)
gi.persister = gs.persister
gs.gamesMu.Lock()
gs.games[id] = gi
gs.gamesMu.Unlock()
if gs.persister != nil {
gs.persister.SaveGame(gi.game)
}
go gi.run()
return gi
}
func (gs *GameStore) Get(id string) (*GameInstance, bool) {
gs.gamesMu.RLock()
defer gs.gamesMu.RUnlock()
gi, ok := gs.games[id]
return gi, ok
gs.gamesMu.RUnlock()
if ok {
return gi, true
}
// Try to load from database
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,
players: make(map[PlayerID]Syncable),
leave: make(chan PlayerID, 5),
done: make(chan struct{}),
persister: gs.persister,
}
gs.gamesMu.Lock()
gs.games[id] = gi
gs.gamesMu.Unlock()
go gi.run()
return gi, true
}
func GenerateID(size int) string {
@@ -58,6 +113,7 @@ type GameInstance struct {
leave chan PlayerID
done chan struct{}
dirty bool
persister Persister
}
func NewGameInstance(id string) *GameInstance {
@@ -79,14 +135,17 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
gi.gameMu.Lock()
defer gi.gameMu.Unlock()
var slot int
// Assign player to an open slot
if gi.game.Players[0] == nil {
ps.Player.Color = 1 // Red
gi.game.Players[0] = ps.Player
slot = 0
} else if gi.game.Players[1] == nil {
ps.Player.Color = 2 // Yellow
gi.game.Players[1] = ps.Player
gi.game.Status = StatusInProgress
slot = 1
} else {
return false // Game is full
}
@@ -95,6 +154,11 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
gi.players[ps.Player.ID] = ps.Sync
gi.playersMu.Unlock()
if gi.persister != nil {
gi.persister.SaveGamePlayer(gi.game.ID, ps.Player, slot)
gi.persister.SaveGame(gi.game)
}
gi.dirty = true
return true
}
@@ -138,6 +202,10 @@ func (gi *GameInstance) DropPiece(col int, playerColor int) bool {
gi.game.SwitchTurn()
}
if gi.persister != nil {
gi.persister.SaveGame(gi.game)
}
gi.dirty = true
return true
}