From bc6488f063eda2d943ae9dff2d4ea74d07c7b6b6 Mon Sep 17 00:00:00 2001 From: Ryan Hamamura <58859899+ryanhamamura@users.noreply.github.com> Date: Mon, 2 Mar 2026 16:56:29 -1000 Subject: [PATCH] refactor: deduplicate persistence, add upsert queries, throttle snake saves - Replace Create+Get+Update with UpsertGame/UpsertSnakeGame queries - Extract free functions (saveGame, loadGame, etc.) from duplicated receiver methods on Store and Instance types - Remove duplicate generateID from snake package, reuse game.GenerateID - Throttle snake game DB writes to every 2s instead of every tick - Fix double-lock in c4game chat handler - Update all code for sqlc pointer types (*string instead of sql.NullString) --- db/queries/games.sql | 20 ++-- db/queries/snake_games.sql | 19 ++-- db/repository/chat.sql.go | 16 +-- db/repository/games.sql.go | 133 +++++++++--------------- db/repository/models.go | 74 +++++++------ db/repository/snake_games.sql.go | 136 +++++++++--------------- db/repository/users.sql.go | 18 ++-- features/c4game/handlers.go | 3 - features/lobby/handlers.go | 16 ++- game/persist.go | 150 +++++++++------------------ game/store.go | 14 +-- snake/loop.go | 18 ++-- snake/persist.go | 172 ++++++++++--------------------- snake/store.go | 23 ++--- 14 files changed, 318 insertions(+), 494 deletions(-) diff --git a/db/queries/games.sql b/db/queries/games.sql index c81c146..5f1befe 100644 --- a/db/queries/games.sql +++ b/db/queries/games.sql @@ -1,16 +1,18 @@ --- name: CreateGame :one -INSERT INTO games (id, board, current_turn, status) -VALUES (?, ?, ?, ?) -RETURNING *; +-- name: UpsertGame :exec +INSERT INTO games (id, board, current_turn, status, game_type, winner_user_id, winning_cells, rematch_game_id) +VALUES (?, ?, ?, ?, 'connect4', ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET + board = excluded.board, + current_turn = excluded.current_turn, + status = excluded.status, + winner_user_id = excluded.winner_user_id, + winning_cells = excluded.winning_cells, + rematch_game_id = excluded.rematch_game_id, + updated_at = CURRENT_TIMESTAMP; -- name: GetGame :one SELECT * FROM games WHERE id = ?; --- name: UpdateGame :exec -UPDATE games -SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP -WHERE id = ?; - -- name: DeleteGame :exec DELETE FROM games WHERE id = ?; diff --git a/db/queries/snake_games.sql b/db/queries/snake_games.sql index b5356c3..656975b 100644 --- a/db/queries/snake_games.sql +++ b/db/queries/snake_games.sql @@ -1,16 +1,17 @@ --- name: CreateSnakeGame :one -INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode, snake_speed) -VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?, ?) -RETURNING *; +-- name: UpsertSnakeGame :exec +INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode, snake_speed, winner_user_id, rematch_game_id, score) +VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?, ?, ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET + board = excluded.board, + status = excluded.status, + winner_user_id = excluded.winner_user_id, + rematch_game_id = excluded.rematch_game_id, + score = excluded.score, + updated_at = CURRENT_TIMESTAMP; -- name: GetSnakeGame :one SELECT * FROM games WHERE id = ? AND game_type = 'snake'; --- name: UpdateSnakeGame :exec -UPDATE games -SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, score = ?, updated_at = CURRENT_TIMESTAMP -WHERE id = ? AND game_type = 'snake'; - -- name: DeleteSnakeGame :exec DELETE FROM games WHERE id = ? AND game_type = 'snake'; diff --git a/db/repository/chat.sql.go b/db/repository/chat.sql.go index 1694e02..a06fcfb 100644 --- a/db/repository/chat.sql.go +++ b/db/repository/chat.sql.go @@ -15,11 +15,11 @@ VALUES (?, ?, ?, ?, ?) ` type CreateChatMessageParams struct { - GameID string - Nickname string - Color int64 - Message string - CreatedAt int64 + GameID string `db:"game_id" json:"game_id"` + Nickname string `db:"nickname" json:"nickname"` + Color int64 `db:"color" json:"color"` + Message string `db:"message" json:"message"` + CreatedAt int64 `db:"created_at" json:"created_at"` } func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) error { @@ -40,13 +40,13 @@ ORDER BY created_at DESC, id DESC LIMIT 50 ` -func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]ChatMessage, error) { +func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]*ChatMessage, error) { rows, err := q.db.QueryContext(ctx, getChatMessages, gameID) if err != nil { return nil, err } defer rows.Close() - var items []ChatMessage + var items []*ChatMessage for rows.Next() { var i ChatMessage if err := rows.Scan( @@ -59,7 +59,7 @@ func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]ChatMes ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err diff --git a/db/repository/games.sql.go b/db/repository/games.sql.go index 7883526..c2d4ba6 100644 --- a/db/repository/games.sql.go +++ b/db/repository/games.sql.go @@ -7,63 +7,21 @@ package repository import ( "context" - "database/sql" + "time" ) -const createGame = `-- name: CreateGame :one -INSERT INTO games (id, board, current_turn, status) -VALUES (?, ?, ?, ?) -RETURNING id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id, game_type, grid_width, grid_height, max_players, game_mode, score, snake_speed -` - -type CreateGameParams struct { - ID string - Board string - CurrentTurn int64 - Status int64 -} - -func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, error) { - row := q.db.QueryRowContext(ctx, createGame, - arg.ID, - arg.Board, - arg.CurrentTurn, - arg.Status, - ) - var i Game - err := row.Scan( - &i.ID, - &i.Board, - &i.CurrentTurn, - &i.Status, - &i.WinnerUserID, - &i.WinningCells, - &i.CreatedAt, - &i.UpdatedAt, - &i.RematchGameID, - &i.GameType, - &i.GridWidth, - &i.GridHeight, - &i.MaxPlayers, - &i.GameMode, - &i.Score, - &i.SnakeSpeed, - ) - return i, err -} - const createGamePlayer = `-- name: CreateGamePlayer :exec INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot) VALUES (?, ?, ?, ?, ?, ?) ` type CreateGamePlayerParams struct { - GameID string - UserID sql.NullString - GuestPlayerID sql.NullString - Nickname string - Color int64 - Slot int64 + GameID string `db:"game_id" json:"game_id"` + UserID *string `db:"user_id" json:"user_id"` + GuestPlayerID *string `db:"guest_player_id" json:"guest_player_id"` + Nickname string `db:"nickname" json:"nickname"` + Color int64 `db:"color" json:"color"` + Slot int64 `db:"slot" json:"slot"` } func (q *Queries) CreateGamePlayer(ctx context.Context, arg CreateGamePlayerParams) error { @@ -91,13 +49,13 @@ const getActiveGames = `-- name: GetActiveGames :many SELECT id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id, game_type, grid_width, grid_height, max_players, game_mode, score, snake_speed FROM games WHERE game_type = 'connect4' AND status < 2 ` -func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) { +func (q *Queries) GetActiveGames(ctx context.Context) ([]*Game, error) { rows, err := q.db.QueryContext(ctx, getActiveGames) if err != nil { return nil, err } defer rows.Close() - var items []Game + var items []*Game for rows.Next() { var i Game if err := rows.Scan( @@ -120,7 +78,7 @@ func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) { ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -135,7 +93,7 @@ const getGame = `-- name: GetGame :one SELECT id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id, game_type, grid_width, grid_height, max_players, game_mode, score, snake_speed FROM games WHERE id = ? ` -func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) { +func (q *Queries) GetGame(ctx context.Context, id string) (*Game, error) { row := q.db.QueryRowContext(ctx, getGame, id) var i Game err := row.Scan( @@ -156,20 +114,20 @@ func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) { &i.Score, &i.SnakeSpeed, ) - return i, err + return &i, err } const getGamePlayers = `-- name: GetGamePlayers :many SELECT game_id, user_id, guest_player_id, nickname, color, slot, created_at FROM game_players WHERE game_id = ? ` -func (q *Queries) GetGamePlayers(ctx context.Context, gameID string) ([]GamePlayer, error) { +func (q *Queries) GetGamePlayers(ctx context.Context, gameID string) ([]*GamePlayer, error) { rows, err := q.db.QueryContext(ctx, getGamePlayers, gameID) if err != nil { return nil, err } defer rows.Close() - var items []GamePlayer + var items []*GamePlayer for rows.Next() { var i GamePlayer if err := rows.Scan( @@ -183,7 +141,7 @@ func (q *Queries) GetGamePlayers(ctx context.Context, gameID string) ([]GamePlay ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -201,13 +159,13 @@ WHERE gp.user_id = ? ORDER BY g.updated_at DESC ` -func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) ([]Game, error) { +func (q *Queries) GetGamesByUserID(ctx context.Context, userID *string) ([]*Game, error) { rows, err := q.db.QueryContext(ctx, getGamesByUserID, userID) if err != nil { return nil, err } defer rows.Close() - var items []Game + var items []*Game for rows.Next() { var i Game if err := rows.Scan( @@ -230,7 +188,7 @@ func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) ( ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -257,21 +215,21 @@ ORDER BY g.updated_at DESC ` type GetUserActiveGamesRow struct { - ID string - Status int64 - CurrentTurn int64 - UpdatedAt sql.NullTime - MyColor int64 - OpponentNickname sql.NullString + ID string `db:"id" json:"id"` + Status int64 `db:"status" json:"status"` + CurrentTurn int64 `db:"current_turn" json:"current_turn"` + UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` + MyColor int64 `db:"my_color" json:"my_color"` + OpponentNickname *string `db:"opponent_nickname" json:"opponent_nickname"` } -func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString) ([]GetUserActiveGamesRow, error) { +func (q *Queries) GetUserActiveGames(ctx context.Context, userID *string) ([]*GetUserActiveGamesRow, error) { rows, err := q.db.QueryContext(ctx, getUserActiveGames, userID) if err != nil { return nil, err } defer rows.Close() - var items []GetUserActiveGamesRow + var items []*GetUserActiveGamesRow for rows.Next() { var i GetUserActiveGamesRow if err := rows.Scan( @@ -284,7 +242,7 @@ func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString) ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -295,31 +253,38 @@ func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString) return items, nil } -const updateGame = `-- name: UpdateGame :exec -UPDATE games -SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP -WHERE id = ? +const upsertGame = `-- name: UpsertGame :exec +INSERT INTO games (id, board, current_turn, status, game_type, winner_user_id, winning_cells, rematch_game_id) +VALUES (?, ?, ?, ?, 'connect4', ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET + board = excluded.board, + current_turn = excluded.current_turn, + status = excluded.status, + winner_user_id = excluded.winner_user_id, + winning_cells = excluded.winning_cells, + rematch_game_id = excluded.rematch_game_id, + updated_at = CURRENT_TIMESTAMP ` -type UpdateGameParams struct { - Board string - CurrentTurn int64 - Status int64 - WinnerUserID sql.NullString - WinningCells sql.NullString - RematchGameID sql.NullString - ID string +type UpsertGameParams struct { + ID string `db:"id" json:"id"` + Board string `db:"board" json:"board"` + CurrentTurn int64 `db:"current_turn" json:"current_turn"` + Status int64 `db:"status" json:"status"` + WinnerUserID *string `db:"winner_user_id" json:"winner_user_id"` + WinningCells *string `db:"winning_cells" json:"winning_cells"` + RematchGameID *string `db:"rematch_game_id" json:"rematch_game_id"` } -func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) error { - _, err := q.db.ExecContext(ctx, updateGame, +func (q *Queries) UpsertGame(ctx context.Context, arg UpsertGameParams) error { + _, err := q.db.ExecContext(ctx, upsertGame, + arg.ID, arg.Board, arg.CurrentTurn, arg.Status, arg.WinnerUserID, arg.WinningCells, arg.RematchGameID, - arg.ID, ) return err } diff --git a/db/repository/models.go b/db/repository/models.go index de3f897..8b5fc50 100644 --- a/db/repository/models.go +++ b/db/repository/models.go @@ -5,50 +5,56 @@ package repository import ( - "database/sql" + "time" ) type ChatMessage struct { - ID int64 - GameID string - Nickname string - Color int64 - Message string - CreatedAt int64 + ID int64 `db:"id" json:"id"` + GameID string `db:"game_id" json:"game_id"` + Nickname string `db:"nickname" json:"nickname"` + Color int64 `db:"color" json:"color"` + Message string `db:"message" json:"message"` + CreatedAt int64 `db:"created_at" json:"created_at"` } type Game struct { - ID string - Board string - CurrentTurn int64 - Status int64 - WinnerUserID sql.NullString - WinningCells sql.NullString - CreatedAt sql.NullTime - UpdatedAt sql.NullTime - RematchGameID sql.NullString - GameType string - GridWidth sql.NullInt64 - GridHeight sql.NullInt64 - MaxPlayers int64 - GameMode int64 - Score int64 - SnakeSpeed int64 + ID string `db:"id" json:"id"` + Board string `db:"board" json:"board"` + CurrentTurn int64 `db:"current_turn" json:"current_turn"` + Status int64 `db:"status" json:"status"` + WinnerUserID *string `db:"winner_user_id" json:"winner_user_id"` + WinningCells *string `db:"winning_cells" json:"winning_cells"` + CreatedAt *time.Time `db:"created_at" json:"created_at"` + UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` + RematchGameID *string `db:"rematch_game_id" json:"rematch_game_id"` + GameType string `db:"game_type" json:"game_type"` + GridWidth *int64 `db:"grid_width" json:"grid_width"` + GridHeight *int64 `db:"grid_height" json:"grid_height"` + MaxPlayers int64 `db:"max_players" json:"max_players"` + GameMode int64 `db:"game_mode" json:"game_mode"` + Score int64 `db:"score" json:"score"` + SnakeSpeed int64 `db:"snake_speed" json:"snake_speed"` } type GamePlayer struct { - GameID string - UserID sql.NullString - GuestPlayerID sql.NullString - Nickname string - Color int64 - Slot int64 - CreatedAt sql.NullTime + GameID string `db:"game_id" json:"game_id"` + UserID *string `db:"user_id" json:"user_id"` + GuestPlayerID *string `db:"guest_player_id" json:"guest_player_id"` + Nickname string `db:"nickname" json:"nickname"` + Color int64 `db:"color" json:"color"` + Slot int64 `db:"slot" json:"slot"` + CreatedAt *time.Time `db:"created_at" json:"created_at"` +} + +type Session struct { + Token string `db:"token" json:"token"` + Data []byte `db:"data" json:"data"` + Expiry float64 `db:"expiry" json:"expiry"` } type User struct { - ID string - Username string - PasswordHash string - CreatedAt sql.NullTime + ID string `db:"id" json:"id"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"password_hash"` + CreatedAt *time.Time `db:"created_at" json:"created_at"` } diff --git a/db/repository/snake_games.sql.go b/db/repository/snake_games.sql.go index 1c80c2b..c6dc522 100644 --- a/db/repository/snake_games.sql.go +++ b/db/repository/snake_games.sql.go @@ -7,69 +7,21 @@ package repository import ( "context" - "database/sql" + "time" ) -const createSnakeGame = `-- name: CreateSnakeGame :one -INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode, snake_speed) -VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?, ?) -RETURNING id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id, game_type, grid_width, grid_height, max_players, game_mode, score, snake_speed -` - -type CreateSnakeGameParams struct { - ID string - Board string - Status int64 - GridWidth sql.NullInt64 - GridHeight sql.NullInt64 - GameMode int64 - SnakeSpeed int64 -} - -func (q *Queries) CreateSnakeGame(ctx context.Context, arg CreateSnakeGameParams) (Game, error) { - row := q.db.QueryRowContext(ctx, createSnakeGame, - arg.ID, - arg.Board, - arg.Status, - arg.GridWidth, - arg.GridHeight, - arg.GameMode, - arg.SnakeSpeed, - ) - var i Game - err := row.Scan( - &i.ID, - &i.Board, - &i.CurrentTurn, - &i.Status, - &i.WinnerUserID, - &i.WinningCells, - &i.CreatedAt, - &i.UpdatedAt, - &i.RematchGameID, - &i.GameType, - &i.GridWidth, - &i.GridHeight, - &i.MaxPlayers, - &i.GameMode, - &i.Score, - &i.SnakeSpeed, - ) - return i, err -} - const createSnakePlayer = `-- name: CreateSnakePlayer :exec INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot) VALUES (?, ?, ?, ?, ?, ?) ` type CreateSnakePlayerParams struct { - GameID string - UserID sql.NullString - GuestPlayerID sql.NullString - Nickname string - Color int64 - Slot int64 + GameID string `db:"game_id" json:"game_id"` + UserID *string `db:"user_id" json:"user_id"` + GuestPlayerID *string `db:"guest_player_id" json:"guest_player_id"` + Nickname string `db:"nickname" json:"nickname"` + Color int64 `db:"color" json:"color"` + Slot int64 `db:"slot" json:"slot"` } func (q *Queries) CreateSnakePlayer(ctx context.Context, arg CreateSnakePlayerParams) error { @@ -97,13 +49,13 @@ const getActiveSnakeGames = `-- name: GetActiveSnakeGames :many SELECT id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id, game_type, grid_width, grid_height, max_players, game_mode, score, snake_speed FROM games WHERE game_type = 'snake' AND status < 2 AND game_mode = 0 ` -func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]Game, error) { +func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]*Game, error) { rows, err := q.db.QueryContext(ctx, getActiveSnakeGames) if err != nil { return nil, err } defer rows.Close() - var items []Game + var items []*Game for rows.Next() { var i Game if err := rows.Scan( @@ -126,7 +78,7 @@ func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]Game, error) { ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -141,7 +93,7 @@ const getSnakeGame = `-- name: GetSnakeGame :one SELECT id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id, game_type, grid_width, grid_height, max_players, game_mode, score, snake_speed FROM games WHERE id = ? AND game_type = 'snake' ` -func (q *Queries) GetSnakeGame(ctx context.Context, id string) (Game, error) { +func (q *Queries) GetSnakeGame(ctx context.Context, id string) (*Game, error) { row := q.db.QueryRowContext(ctx, getSnakeGame, id) var i Game err := row.Scan( @@ -162,20 +114,20 @@ func (q *Queries) GetSnakeGame(ctx context.Context, id string) (Game, error) { &i.Score, &i.SnakeSpeed, ) - return i, err + return &i, err } const getSnakePlayers = `-- name: GetSnakePlayers :many SELECT game_id, user_id, guest_player_id, nickname, color, slot, created_at FROM game_players WHERE game_id = ? ORDER BY slot ` -func (q *Queries) GetSnakePlayers(ctx context.Context, gameID string) ([]GamePlayer, error) { +func (q *Queries) GetSnakePlayers(ctx context.Context, gameID string) ([]*GamePlayer, error) { rows, err := q.db.QueryContext(ctx, getSnakePlayers, gameID) if err != nil { return nil, err } defer rows.Close() - var items []GamePlayer + var items []*GamePlayer for rows.Next() { var i GamePlayer if err := rows.Scan( @@ -189,7 +141,7 @@ func (q *Queries) GetSnakePlayers(ctx context.Context, gameID string) ([]GamePla ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -214,20 +166,20 @@ ORDER BY g.updated_at DESC ` type GetUserActiveSnakeGamesRow struct { - ID string - Status int64 - GridWidth sql.NullInt64 - GridHeight sql.NullInt64 - UpdatedAt sql.NullTime + ID string `db:"id" json:"id"` + Status int64 `db:"status" json:"status"` + GridWidth *int64 `db:"grid_width" json:"grid_width"` + GridHeight *int64 `db:"grid_height" json:"grid_height"` + UpdatedAt *time.Time `db:"updated_at" json:"updated_at"` } -func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullString) ([]GetUserActiveSnakeGamesRow, error) { +func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID *string) ([]*GetUserActiveSnakeGamesRow, error) { rows, err := q.db.QueryContext(ctx, getUserActiveSnakeGames, userID) if err != nil { return nil, err } defer rows.Close() - var items []GetUserActiveSnakeGamesRow + var items []*GetUserActiveSnakeGamesRow for rows.Next() { var i GetUserActiveSnakeGamesRow if err := rows.Scan( @@ -239,7 +191,7 @@ func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullSt ); err != nil { return nil, err } - items = append(items, i) + items = append(items, &i) } if err := rows.Close(); err != nil { return nil, err @@ -250,29 +202,43 @@ func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullSt return items, nil } -const updateSnakeGame = `-- name: UpdateSnakeGame :exec -UPDATE games -SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, score = ?, updated_at = CURRENT_TIMESTAMP -WHERE id = ? AND game_type = 'snake' +const upsertSnakeGame = `-- name: UpsertSnakeGame :exec +INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode, snake_speed, winner_user_id, rematch_game_id, score) +VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?, ?, ?, ?, ?) +ON CONFLICT(id) DO UPDATE SET + board = excluded.board, + status = excluded.status, + winner_user_id = excluded.winner_user_id, + rematch_game_id = excluded.rematch_game_id, + score = excluded.score, + updated_at = CURRENT_TIMESTAMP ` -type UpdateSnakeGameParams struct { - Board string - Status int64 - WinnerUserID sql.NullString - RematchGameID sql.NullString - Score int64 - ID string +type UpsertSnakeGameParams struct { + ID string `db:"id" json:"id"` + Board string `db:"board" json:"board"` + Status int64 `db:"status" json:"status"` + GridWidth *int64 `db:"grid_width" json:"grid_width"` + GridHeight *int64 `db:"grid_height" json:"grid_height"` + GameMode int64 `db:"game_mode" json:"game_mode"` + SnakeSpeed int64 `db:"snake_speed" json:"snake_speed"` + WinnerUserID *string `db:"winner_user_id" json:"winner_user_id"` + RematchGameID *string `db:"rematch_game_id" json:"rematch_game_id"` + Score int64 `db:"score" json:"score"` } -func (q *Queries) UpdateSnakeGame(ctx context.Context, arg UpdateSnakeGameParams) error { - _, err := q.db.ExecContext(ctx, updateSnakeGame, +func (q *Queries) UpsertSnakeGame(ctx context.Context, arg UpsertSnakeGameParams) error { + _, err := q.db.ExecContext(ctx, upsertSnakeGame, + arg.ID, arg.Board, arg.Status, + arg.GridWidth, + arg.GridHeight, + arg.GameMode, + arg.SnakeSpeed, arg.WinnerUserID, arg.RematchGameID, arg.Score, - arg.ID, ) return err } diff --git a/db/repository/users.sql.go b/db/repository/users.sql.go index 4654293..84a3af8 100644 --- a/db/repository/users.sql.go +++ b/db/repository/users.sql.go @@ -16,12 +16,12 @@ RETURNING id, username, password_hash, created_at ` type CreateUserParams struct { - ID string - Username string - PasswordHash string + ID string `db:"id" json:"id"` + Username string `db:"username" json:"username"` + PasswordHash string `db:"password_hash" json:"password_hash"` } -func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, error) { +func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (*User, error) { row := q.db.QueryRowContext(ctx, createUser, arg.ID, arg.Username, arg.PasswordHash) var i User err := row.Scan( @@ -30,14 +30,14 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.PasswordHash, &i.CreatedAt, ) - return i, err + return &i, err } const getUserByID = `-- name: GetUserByID :one SELECT id, username, password_hash, created_at FROM users WHERE id = ? ` -func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { +func (q *Queries) GetUserByID(ctx context.Context, id string) (*User, error) { row := q.db.QueryRowContext(ctx, getUserByID, id) var i User err := row.Scan( @@ -46,14 +46,14 @@ func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { &i.PasswordHash, &i.CreatedAt, ) - return i, err + return &i, err } const getUserByUsername = `-- name: GetUserByUsername :one SELECT id, username, password_hash, created_at FROM users WHERE username = ? ` -func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { +func (q *Queries) GetUserByUsername(ctx context.Context, username string) (*User, error) { row := q.db.QueryRowContext(ctx, getUserByUsername, username) var i User err := row.Scan( @@ -62,5 +62,5 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, &i.PasswordHash, &i.CreatedAt, ) - return i, err + return &i, err } diff --git a/features/c4game/handlers.go b/features/c4game/handlers.go index 6d24ed2..8b9d0d7 100644 --- a/features/c4game/handlers.go +++ b/features/c4game/handlers.go @@ -156,9 +156,6 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.Sessio if len(chatMessages) > 50 { chatMessages = chatMessages[len(chatMessages)-50:] } - chatMu.Unlock() - - chatMu.Lock() msgs := make([]components.ChatMessage, len(chatMessages)) copy(msgs, chatMessages) chatMu.Unlock() diff --git a/features/lobby/handlers.go b/features/lobby/handlers.go index 8144543..938c02a 100644 --- a/features/lobby/handlers.go +++ b/features/lobby/handlers.go @@ -2,10 +2,10 @@ package lobby import ( "context" - "database/sql" "fmt" "net/http" "strconv" + "time" "github.com/ryanhamamura/c4/db/repository" lobbycomponents "github.com/ryanhamamura/c4/features/lobby/components" @@ -28,16 +28,24 @@ func HandleLobbyPage(queries *repository.Queries, sessions *scs.SessionManager, var userGames []lobbycomponents.GameListItem if isLoggedIn { ctx := context.Background() - games, err := queries.GetUserActiveGames(ctx, sql.NullString{String: userID, Valid: true}) + games, err := queries.GetUserActiveGames(ctx, &userID) if err == nil { for _, g := range games { isMyTurn := g.Status == 1 && g.CurrentTurn == g.MyColor + opponentName := "" + if g.OpponentNickname != nil { + opponentName = *g.OpponentNickname + } + var lastPlayed time.Time + if g.UpdatedAt != nil { + lastPlayed = *g.UpdatedAt + } userGames = append(userGames, lobbycomponents.GameListItem{ ID: g.ID, Status: int(g.Status), - OpponentName: g.OpponentNickname.String, + OpponentName: opponentName, IsMyTurn: isMyTurn, - LastPlayed: g.UpdatedAt.Time, + LastPlayed: lastPlayed, }) } } diff --git a/game/persist.go b/game/persist.go index fb05716..a4327de 100644 --- a/game/persist.go +++ b/game/persist.go @@ -2,80 +2,43 @@ 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 +// saveGame persists the game state via upsert. +func saveGame(queries *repository.Queries, g *Game) error { + var winnerUserID *string + if g.Winner != nil && g.Winner.UserID != nil { + winnerUserID = g.Winner.UserID } - return gs.queries.UpdateGame(ctx, updateGameParams(g)) + var winningCells *string + if wc := g.WinningCellsToJSON(); wc != "" { + winningCells = &wc + } + + return queries.UpsertGame(context.Background(), repository.UpsertGameParams{ + ID: g.ID, + Board: g.BoardToJSON(), + CurrentTurn: int64(g.CurrentTurn), + Status: int64(g.Status), + WinnerUserID: winnerUserID, + WinningCells: winningCells, + RematchGameID: g.RematchGameID, + }) } -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 +func saveGamePlayer(queries *repository.Queries, gameID string, player *Player, slot int) error { + var userID, guestPlayerID *string if player.UserID != nil { - userID = sql.NullString{String: *player.UserID, Valid: true} + userID = player.UserID } else { - guestPlayerID = sql.NullString{String: string(player.ID), Valid: true} + id := string(player.ID) + guestPlayerID = &id } - return gi.queries.CreateGamePlayer(context.Background(), repository.CreateGamePlayerParams{ + return queries.CreateGamePlayer(context.Background(), repository.CreateGamePlayerParams{ GameID: gameID, UserID: userID, GuestPlayerID: guestPlayerID, @@ -85,36 +48,25 @@ func (gi *GameInstance) saveGamePlayer(gameID string, player *Player, slot int) }) } -// 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 loadGame(queries *repository.Queries, id string) (*Game, error) { + row, err := queries.GetGame(context.Background(), id) + if err != nil { + return nil, err } + return gameFromRow(row) } -func gameFromRow(row repository.Game) (*Game, error) { +func loadGamePlayers(queries *repository.Queries, id string) ([]*Player, error) { + rows, err := queries.GetGamePlayers(context.Background(), id) + if err != nil { + return nil, err + } + return playersFromRows(rows), nil +} + +// Domain ↔ DB mapping helpers. + +func gameFromRow(row *repository.Game) (*Game, error) { g := &Game{ ID: row.ID, CurrentTurn: int(row.CurrentTurn), @@ -125,18 +77,18 @@ func gameFromRow(row repository.Game) (*Game, error) { return nil, err } - if row.WinningCells.Valid { - _ = g.WinningCellsFromJSON(row.WinningCells.String) + if row.WinningCells != nil { + _ = g.WinningCellsFromJSON(*row.WinningCells) } - if row.RematchGameID.Valid { - g.RematchGameID = &row.RematchGameID.String + if row.RematchGameID != nil { + g.RematchGameID = row.RematchGameID } return g, nil } -func playersFromRows(rows []repository.GamePlayer) []*Player { +func playersFromRows(rows []*repository.GamePlayer) []*Player { players := make([]*Player, 0, len(rows)) for _, row := range rows { player := &Player{ @@ -144,11 +96,11 @@ func playersFromRows(rows []repository.GamePlayer) []*Player { 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) + if row.UserID != nil { + player.UserID = row.UserID + player.ID = PlayerID(*row.UserID) + } else if row.GuestPlayerID != nil { + player.ID = PlayerID(*row.GuestPlayerID) } players = append(players, player) diff --git a/game/store.go b/game/store.go index e56e750..ef09aee 100644 --- a/game/store.go +++ b/game/store.go @@ -49,7 +49,7 @@ func (gs *GameStore) Create() *GameInstance { gs.gamesMu.Unlock() if gs.queries != nil { - gs.saveGame(gi.game) //nolint:errcheck + saveGame(gs.queries, gi.game) //nolint:errcheck } return gi @@ -68,12 +68,12 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) { return nil, false } - g, err := gs.loadGame(id) + g, err := loadGame(gs.queries, id) if err != nil || g == nil { return nil, false } - players, _ := gs.loadGamePlayers(id) + players, _ := loadGamePlayers(gs.queries, id) for _, p := range players { switch p.Color { case 1: @@ -152,8 +152,8 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool { } if gi.queries != nil { - gi.saveGamePlayer(gi.game.ID, ps.Player, slot) //nolint:errcheck - gi.saveGame(gi.game) //nolint:errcheck + saveGamePlayer(gi.queries, gi.game.ID, ps.Player, slot) //nolint:errcheck + saveGame(gi.queries, gi.game) //nolint:errcheck } gi.notify() @@ -190,7 +190,7 @@ func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance { gi.game.RematchGameID = &newID if gi.queries != nil { - if err := gi.saveGame(gi.game); err != nil { + if err := saveGame(gi.queries, gi.game); err != nil { gs.Delete(newID) //nolint:errcheck gi.game.RematchGameID = nil return nil @@ -224,7 +224,7 @@ func (gi *GameInstance) DropPiece(col int, playerColor int) bool { } if gi.queries != nil { - gi.saveGame(gi.game) //nolint:errcheck + saveGame(gi.queries, gi.game) //nolint:errcheck } gi.notify() diff --git a/snake/loop.go b/snake/loop.go index cf44f84..b6d4710 100644 --- a/snake/loop.go +++ b/snake/loop.go @@ -62,16 +62,14 @@ func (si *SnakeGameInstance) countdownPhase() { si.game.Status = StatusInProgress if si.queries != nil { - si.saveSnakeGame(si.game) //nolint:errcheck + saveSnakeGame(si.queries, si.game) //nolint:errcheck } si.gameMu.Unlock() si.notify() return } - if si.queries != nil { - si.saveSnakeGame(si.game) //nolint:errcheck - } + // No DB save during countdown ticks — state is transient si.gameMu.Unlock() si.notify() } @@ -98,6 +96,7 @@ func (si *SnakeGameInstance) gamePhase() { defer ticker.Stop() lastInput := time.Now() + lastSave := time.Now() var moveAccum time.Duration for { @@ -124,7 +123,7 @@ func (si *SnakeGameInstance) gamePhase() { if time.Since(lastInput) > inactivityLimit { si.game.Status = StatusFinished if si.queries != nil { - si.saveSnakeGame(si.game) //nolint:errcheck + saveSnakeGame(si.queries, si.game) //nolint:errcheck } si.gameMu.Unlock() si.notify() @@ -175,13 +174,10 @@ func (si *SnakeGameInstance) gamePhase() { alive := AliveCount(state) gameOver := false if si.game.Mode == ModeSinglePlayer { - // Single player ends when the player dies (alive == 0) if alive == 0 { gameOver = true - // No winner in single player - just final score } } else { - // Multiplayer ends when 1 or fewer alive if alive <= 1 { gameOver = true winnerIdx := LastAlive(state) @@ -195,8 +191,10 @@ func (si *SnakeGameInstance) gamePhase() { si.game.Status = StatusFinished } - if si.queries != nil { - si.saveSnakeGame(si.game) //nolint:errcheck + // Throttle DB saves: persist on game over or every 2 seconds + if si.queries != nil && (gameOver || time.Since(lastSave) >= 2*time.Second) { + saveSnakeGame(si.queries, si.game) //nolint:errcheck + lastSave = time.Now() } si.gameMu.Unlock() diff --git a/snake/persist.go b/snake/persist.go index 6ef51d0..0f9d404 100644 --- a/snake/persist.go +++ b/snake/persist.go @@ -2,108 +2,49 @@ package snake import ( "context" - "database/sql" "github.com/ryanhamamura/c4/db/repository" ) -// Persistence methods on SnakeStore (used during Get to hydrate from DB). - -func (ss *SnakeStore) saveSnakeGame(sg *SnakeGame) error { - ctx := context.Background() - +// saveSnakeGame persists the snake game state via upsert. +func saveSnakeGame(queries *repository.Queries, sg *SnakeGame) error { boardJSON := "{}" + var gridWidth, gridHeight *int64 if sg.State != nil { boardJSON = sg.State.ToJSON() + w, h := int64(sg.State.Width), int64(sg.State.Height) + gridWidth, gridHeight = &w, &h } - 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} + var winnerUserID *string + if sg.Winner != nil && sg.Winner.UserID != nil { + winnerUserID = sg.Winner.UserID } - _, err := ss.queries.GetSnakeGame(ctx, sg.ID) - if err == sql.ErrNoRows { - _, err = ss.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{ - ID: sg.ID, - Board: boardJSON, - Status: int64(sg.Status), - GridWidth: gridWidth, - GridHeight: gridHeight, - GameMode: int64(sg.Mode), - SnakeSpeed: int64(sg.Speed), - }) - return err - } - if err != nil { - return err - } - - return ss.queries.UpdateSnakeGame(ctx, updateSnakeGameParams(sg, boardJSON)) + return queries.UpsertSnakeGame(context.Background(), repository.UpsertSnakeGameParams{ + ID: sg.ID, + Board: boardJSON, + Status: int64(sg.Status), + GridWidth: gridWidth, + GridHeight: gridHeight, + GameMode: int64(sg.Mode), + SnakeSpeed: int64(sg.Speed), + WinnerUserID: winnerUserID, + RematchGameID: sg.RematchGameID, + Score: int64(sg.Score), + }) } -func (ss *SnakeStore) loadSnakeGame(id string) (*SnakeGame, error) { - row, err := ss.queries.GetSnakeGame(context.Background(), id) - if err != nil { - return nil, err - } - return snakeGameFromRow(row) -} - -func (ss *SnakeStore) loadSnakePlayers(id string) ([]*Player, error) { - rows, err := ss.queries.GetSnakePlayers(context.Background(), id) - if err != nil { - return nil, err - } - return snakePlayersFromRows(rows), nil -} - -// Persistence methods on SnakeGameInstance (used during gameplay mutations). - -func (si *SnakeGameInstance) saveSnakeGame(sg *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 := si.queries.GetSnakeGame(ctx, sg.ID) - if err == sql.ErrNoRows { - _, err = si.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{ - ID: sg.ID, - Board: boardJSON, - Status: int64(sg.Status), - GridWidth: gridWidth, - GridHeight: gridHeight, - GameMode: int64(sg.Mode), - SnakeSpeed: int64(sg.Speed), - }) - return err - } - if err != nil { - return err - } - - return si.queries.UpdateSnakeGame(ctx, updateSnakeGameParams(sg, boardJSON)) -} - -func (si *SnakeGameInstance) saveSnakePlayer(gameID string, player *Player) error { - var userID, guestPlayerID sql.NullString +func saveSnakePlayer(queries *repository.Queries, gameID string, player *Player) error { + var userID, guestPlayerID *string if player.UserID != nil { - userID = sql.NullString{String: *player.UserID, Valid: true} + userID = player.UserID } else { - guestPlayerID = sql.NullString{String: string(player.ID), Valid: true} + id := string(player.ID) + guestPlayerID = &id } - return si.queries.CreateSnakePlayer(context.Background(), repository.CreateSnakePlayerParams{ + return queries.CreateSnakePlayer(context.Background(), repository.CreateSnakePlayerParams{ GameID: gameID, UserID: userID, GuestPlayerID: guestPlayerID, @@ -113,39 +54,34 @@ func (si *SnakeGameInstance) saveSnakePlayer(gameID string, player *Player) erro }) } -// Shared helpers for domain ↔ DB mapping. - -func updateSnakeGameParams(sg *SnakeGame, boardJSON string) repository.UpdateSnakeGameParams { - var winnerUserID sql.NullString - if sg.Winner != nil && sg.Winner.UserID != nil { - winnerUserID = sql.NullString{String: *sg.Winner.UserID, Valid: true} - } - - var rematchGameID sql.NullString - if sg.RematchGameID != nil { - rematchGameID = sql.NullString{String: *sg.RematchGameID, Valid: true} - } - - return repository.UpdateSnakeGameParams{ - Board: boardJSON, - Status: int64(sg.Status), - WinnerUserID: winnerUserID, - RematchGameID: rematchGameID, - Score: int64(sg.Score), - ID: sg.ID, +func loadSnakeGame(queries *repository.Queries, id string) (*SnakeGame, error) { + row, err := queries.GetSnakeGame(context.Background(), id) + if err != nil { + return nil, err } + return snakeGameFromRow(row) } -func snakeGameFromRow(row repository.Game) (*SnakeGame, error) { +func loadSnakePlayers(queries *repository.Queries, id string) ([]*Player, error) { + rows, err := queries.GetSnakePlayers(context.Background(), id) + if err != nil { + return nil, err + } + return snakePlayersFromRows(rows), nil +} + +// Domain ↔ DB mapping helpers. + +func snakeGameFromRow(row *repository.Game) (*SnakeGame, error) { state, err := GameStateFromJSON(row.Board) if err != nil { state = &GameState{} } - if row.GridWidth.Valid { - state.Width = int(row.GridWidth.Int64) + if row.GridWidth != nil { + state.Width = int(*row.GridWidth) } - if row.GridHeight.Valid { - state.Height = int(row.GridHeight.Int64) + if row.GridHeight != nil { + state.Height = int(*row.GridHeight) } sg := &SnakeGame{ @@ -158,14 +94,14 @@ func snakeGameFromRow(row repository.Game) (*SnakeGame, error) { Speed: int(row.SnakeSpeed), } - if row.RematchGameID.Valid { - sg.RematchGameID = &row.RematchGameID.String + if row.RematchGameID != nil { + sg.RematchGameID = row.RematchGameID } return sg, nil } -func snakePlayersFromRows(rows []repository.GamePlayer) []*Player { +func snakePlayersFromRows(rows []*repository.GamePlayer) []*Player { players := make([]*Player, 0, len(rows)) for _, row := range rows { player := &Player{ @@ -173,11 +109,11 @@ func snakePlayersFromRows(rows []repository.GamePlayer) []*Player { Slot: int(row.Slot), } - 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) + if row.UserID != nil { + player.UserID = row.UserID + player.ID = PlayerID(*row.UserID) + } else if row.GuestPlayerID != nil { + player.ID = PlayerID(*row.GuestPlayerID) } players = append(players, player) diff --git a/snake/store.go b/snake/store.go index 670b9da..c842715 100644 --- a/snake/store.go +++ b/snake/store.go @@ -2,11 +2,10 @@ package snake import ( "context" - "crypto/rand" - "encoding/hex" "sync" "github.com/ryanhamamura/c4/db/repository" + "github.com/ryanhamamura/c4/game" ) type SnakeStore struct { @@ -39,7 +38,7 @@ func (ss *SnakeStore) Create(width, height int, mode GameMode, speed int) *Snake if speed <= 0 { speed = DefaultSpeed } - id := generateID(4) + id := game.GenerateID(4) sg := &SnakeGame{ ID: id, State: &GameState{ @@ -63,7 +62,7 @@ func (ss *SnakeStore) Create(width, height int, mode GameMode, speed int) *Snake ss.gamesMu.Unlock() if ss.queries != nil { - ss.saveSnakeGame(sg) //nolint:errcheck + saveSnakeGame(ss.queries, sg) //nolint:errcheck } return si @@ -82,12 +81,12 @@ func (ss *SnakeStore) Get(id string) (*SnakeGameInstance, bool) { return nil, false } - sg, err := ss.loadSnakeGame(id) + sg, err := loadSnakeGame(ss.queries, id) if err != nil || sg == nil { return nil, false } - players, _ := ss.loadSnakePlayers(id) + players, _ := loadSnakePlayers(ss.queries, id) if sg.Players == nil { sg.Players = make([]*Player, 8) } @@ -207,8 +206,8 @@ func (si *SnakeGameInstance) Join(player *Player) bool { si.game.Players[slot] = player if si.queries != nil { - si.saveSnakePlayer(si.game.ID, player) //nolint:errcheck - si.saveSnakeGame(si.game) //nolint:errcheck + saveSnakePlayer(si.queries, si.game.ID, player) //nolint:errcheck + saveSnakeGame(si.queries, si.game) //nolint:errcheck } si.notify() @@ -294,16 +293,10 @@ func (si *SnakeGameInstance) CreateRematch() *SnakeGameInstance { si.game.RematchGameID = &newID if si.queries != nil { - si.saveSnakeGame(si.game) //nolint:errcheck + saveSnakeGame(si.queries, si.game) //nolint:errcheck } si.gameMu.Unlock() si.notify() return newSI } - -func generateID(size int) string { - b := make([]byte, size) - _, _ = rand.Read(b) - return hex.EncodeToString(b) -}