WIP: Add multiplayer Snake game
N-player (2-8) real-time Snake game alongside Connect 4. Lobby has tabs to switch between games. Players join via invite link with 10-second countdown. Game loop runs at tick-based intervals with NATS pub/sub for state sync. Keyboard input not yet working (Datastar keydown binding issue still under investigation).
This commit is contained in:
142
db/persister.go
142
db/persister.go
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/ryanhamamura/c4/db/gen"
|
||||
"github.com/ryanhamamura/c4/game"
|
||||
"github.com/ryanhamamura/c4/snake"
|
||||
)
|
||||
|
||||
type GamePersister struct {
|
||||
@@ -138,3 +139,144 @@ func (p *GamePersister) DeleteGame(id string) error {
|
||||
ctx := context.Background()
|
||||
return p.queries.DeleteGame(ctx, id)
|
||||
}
|
||||
|
||||
// SnakePersister implements snake.Persister
|
||||
type SnakePersister struct {
|
||||
queries *gen.Queries
|
||||
}
|
||||
|
||||
func NewSnakePersister(q *gen.Queries) *SnakePersister {
|
||||
return &SnakePersister{queries: q}
|
||||
}
|
||||
|
||||
func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
ctx := context.Background()
|
||||
|
||||
boardJSON := "{}"
|
||||
if sg.State != nil {
|
||||
boardJSON = sg.State.ToJSON()
|
||||
}
|
||||
|
||||
var gridWidth, gridHeight sql.NullInt64
|
||||
if sg.State != nil {
|
||||
gridWidth = sql.NullInt64{Int64: int64(sg.State.Width), Valid: true}
|
||||
gridHeight = sql.NullInt64{Int64: int64(sg.State.Height), Valid: true}
|
||||
}
|
||||
|
||||
_, err := p.queries.GetSnakeGame(ctx, sg.ID)
|
||||
if err == sql.ErrNoRows {
|
||||
_, err = p.queries.CreateSnakeGame(ctx, gen.CreateSnakeGameParams{
|
||||
ID: sg.ID,
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
GridWidth: gridWidth,
|
||||
GridHeight: gridHeight,
|
||||
})
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var winnerUserID sql.NullString
|
||||
if sg.Winner != nil && sg.Winner.UserID != nil {
|
||||
winnerUserID = sql.NullString{String: *sg.Winner.UserID, Valid: true}
|
||||
}
|
||||
|
||||
rematchGameID := sql.NullString{}
|
||||
if sg.RematchGameID != nil {
|
||||
rematchGameID = sql.NullString{String: *sg.RematchGameID, Valid: true}
|
||||
}
|
||||
|
||||
return p.queries.UpdateSnakeGame(ctx, gen.UpdateSnakeGameParams{
|
||||
Board: boardJSON,
|
||||
Status: int64(sg.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
RematchGameID: rematchGameID,
|
||||
ID: sg.ID,
|
||||
})
|
||||
}
|
||||
|
||||
func (p *SnakePersister) LoadSnakeGame(id string) (*snake.SnakeGame, error) {
|
||||
ctx := context.Background()
|
||||
row, err := p.queries.GetSnakeGame(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
state, err := snake.GameStateFromJSON(row.Board)
|
||||
if err != nil {
|
||||
state = &snake.GameState{}
|
||||
}
|
||||
if row.GridWidth.Valid {
|
||||
state.Width = int(row.GridWidth.Int64)
|
||||
}
|
||||
if row.GridHeight.Valid {
|
||||
state.Height = int(row.GridHeight.Int64)
|
||||
}
|
||||
|
||||
sg := &snake.SnakeGame{
|
||||
ID: row.ID,
|
||||
State: state,
|
||||
Players: make([]*snake.Player, 8),
|
||||
Status: snake.Status(row.Status),
|
||||
}
|
||||
|
||||
if row.RematchGameID.Valid {
|
||||
sg.RematchGameID = &row.RematchGameID.String
|
||||
}
|
||||
|
||||
return sg, nil
|
||||
}
|
||||
|
||||
func (p *SnakePersister) SaveSnakePlayer(gameID string, player *snake.Player) error {
|
||||
ctx := context.Background()
|
||||
|
||||
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 p.queries.CreateSnakePlayer(ctx, gen.CreateSnakePlayerParams{
|
||||
GameID: gameID,
|
||||
UserID: userID,
|
||||
GuestPlayerID: guestPlayerID,
|
||||
Nickname: player.Nickname,
|
||||
Color: int64(player.Slot + 1),
|
||||
Slot: int64(player.Slot),
|
||||
})
|
||||
}
|
||||
|
||||
func (p *SnakePersister) LoadSnakePlayers(gameID string) ([]*snake.Player, error) {
|
||||
ctx := context.Background()
|
||||
rows, err := p.queries.GetSnakePlayers(ctx, gameID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
players := make([]*snake.Player, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
player := &snake.Player{
|
||||
Nickname: row.Nickname,
|
||||
Slot: int(row.Slot),
|
||||
}
|
||||
|
||||
if row.UserID.Valid {
|
||||
player.UserID = &row.UserID.String
|
||||
player.ID = snake.PlayerID(row.UserID.String)
|
||||
} else if row.GuestPlayerID.Valid {
|
||||
player.ID = snake.PlayerID(row.GuestPlayerID.String)
|
||||
}
|
||||
|
||||
players = append(players, player)
|
||||
}
|
||||
|
||||
return players, nil
|
||||
}
|
||||
|
||||
func (p *SnakePersister) DeleteSnakeGame(id string) error {
|
||||
ctx := context.Background()
|
||||
return p.queries.DeleteSnakeGame(ctx, id)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user