refactor: deduplicate persistence, add upsert queries, throttle snake saves #4

Merged
ryan merged 3 commits from refactor/game-efficiency into main 2026-03-03 05:02:04 +00:00
15 changed files with 351 additions and 485 deletions

View File

@@ -18,6 +18,9 @@ jobs:
with:
go-version: "1.25"
- name: Generate templ
run: go tool templ generate
- name: Run tests
run: go test ./...
@@ -30,6 +33,9 @@ jobs:
with:
go-version: "1.25"
- name: Generate templ
run: go tool templ generate
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest

View File

@@ -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 = ?;

View File

@@ -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';

View File

@@ -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

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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()

View File

@@ -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,
})
}
}

View File

@@ -2,80 +2,61 @@ package game
import (
"context"
"database/sql"
"github.com/ryanhamamura/c4/db/repository"
"github.com/rs/zerolog/log"
)
// 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
}
func (gi *GameInstance) save() error {
err := saveGame(gi.queries, gi.game)
if err != nil {
return err
log.Error().Err(err).Str("game_id", gi.game.ID).Msg("failed to save game")
}
return gs.queries.UpdateGame(ctx, updateGameParams(g))
return err
}
func (gs *GameStore) loadGame(id string) (*Game, error) {
row, err := gs.queries.GetGame(context.Background(), id)
func (gi *GameInstance) savePlayer(player *Player, slot int) error {
err := saveGamePlayer(gi.queries, gi.game.ID, player, slot)
if err != nil {
return nil, err
log.Error().Err(err).Str("game_id", gi.game.ID).Int("slot", slot).Msg("failed to save game player")
}
return gameFromRow(row)
return err
}
func (gs *GameStore) loadGamePlayers(id string) ([]*Player, error) {
rows, err := gs.queries.GetGamePlayers(context.Background(), id)
if err != nil {
return nil, 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 playersFromRows(rows), nil
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,
})
}
// 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 +66,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 +95,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 +114,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)

View File

@@ -49,7 +49,7 @@ func (gs *GameStore) Create() *GameInstance {
gs.gamesMu.Unlock()
if gs.queries != nil {
gs.saveGame(gi.game) //nolint:errcheck
gi.save() //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
gi.savePlayer(ps.Player, slot) //nolint:errcheck
gi.save() //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 := gi.save(); 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
gi.save() //nolint:errcheck
}
gi.notify()

View File

@@ -62,16 +62,14 @@ func (si *SnakeGameInstance) countdownPhase() {
si.game.Status = StatusInProgress
if si.queries != nil {
si.saveSnakeGame(si.game) //nolint:errcheck
si.save() //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
si.save() //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) {
si.save() //nolint:errcheck
lastSave = time.Now()
}
si.gameMu.Unlock()

View File

@@ -2,108 +2,67 @@ package snake
import (
"context"
"database/sql"
"github.com/ryanhamamura/c4/db/repository"
"github.com/rs/zerolog/log"
)
// Persistence methods on SnakeStore (used during Get to hydrate from DB).
func (si *SnakeGameInstance) save() error {
err := saveSnakeGame(si.queries, si.game)
if err != nil {
log.Error().Err(err).Str("game_id", si.game.ID).Msg("failed to save snake game")
}
return err
}
func (ss *SnakeStore) saveSnakeGame(sg *SnakeGame) error {
ctx := context.Background()
func (si *SnakeGameInstance) savePlayer(player *Player) error {
err := saveSnakePlayer(si.queries, si.game.ID, player)
if err != nil {
log.Error().Err(err).Str("game_id", si.game.ID).Msg("failed to save snake player")
}
return err
}
// 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 +72,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 +112,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 +127,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)

View File

@@ -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
si.save() //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
si.savePlayer(player) //nolint:errcheck
si.save() //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
si.save() //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)
}