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)
This commit is contained in:
@@ -1,16 +1,18 @@
|
|||||||
-- name: CreateGame :one
|
-- name: UpsertGame :exec
|
||||||
INSERT INTO games (id, board, current_turn, status)
|
INSERT INTO games (id, board, current_turn, status, game_type, winner_user_id, winning_cells, rematch_game_id)
|
||||||
VALUES (?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, 'connect4', ?, ?, ?)
|
||||||
RETURNING *;
|
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
|
-- name: GetGame :one
|
||||||
SELECT * FROM games WHERE id = ?;
|
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
|
-- name: DeleteGame :exec
|
||||||
DELETE FROM games WHERE id = ?;
|
DELETE FROM games WHERE id = ?;
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
-- name: CreateSnakeGame :one
|
-- name: UpsertSnakeGame :exec
|
||||||
INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode, snake_speed)
|
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, ?, ?)
|
VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?, ?, ?, ?, ?)
|
||||||
RETURNING *;
|
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
|
-- name: GetSnakeGame :one
|
||||||
SELECT * FROM games WHERE id = ? AND game_type = 'snake';
|
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
|
-- name: DeleteSnakeGame :exec
|
||||||
DELETE FROM games WHERE id = ? AND game_type = 'snake';
|
DELETE FROM games WHERE id = ? AND game_type = 'snake';
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ VALUES (?, ?, ?, ?, ?)
|
|||||||
`
|
`
|
||||||
|
|
||||||
type CreateChatMessageParams struct {
|
type CreateChatMessageParams struct {
|
||||||
GameID string
|
GameID string `db:"game_id" json:"game_id"`
|
||||||
Nickname string
|
Nickname string `db:"nickname" json:"nickname"`
|
||||||
Color int64
|
Color int64 `db:"color" json:"color"`
|
||||||
Message string
|
Message string `db:"message" json:"message"`
|
||||||
CreatedAt int64
|
CreatedAt int64 `db:"created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) error {
|
func (q *Queries) CreateChatMessage(ctx context.Context, arg CreateChatMessageParams) error {
|
||||||
@@ -40,13 +40,13 @@ ORDER BY created_at DESC, id DESC
|
|||||||
LIMIT 50
|
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)
|
rows, err := q.db.QueryContext(ctx, getChatMessages, gameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []ChatMessage
|
var items []*ChatMessage
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i ChatMessage
|
var i ChatMessage
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -59,7 +59,7 @@ func (q *Queries) GetChatMessages(ctx context.Context, gameID string) ([]ChatMes
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -7,63 +7,21 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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
|
const createGamePlayer = `-- name: CreateGamePlayer :exec
|
||||||
INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot)
|
INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateGamePlayerParams struct {
|
type CreateGamePlayerParams struct {
|
||||||
GameID string
|
GameID string `db:"game_id" json:"game_id"`
|
||||||
UserID sql.NullString
|
UserID *string `db:"user_id" json:"user_id"`
|
||||||
GuestPlayerID sql.NullString
|
GuestPlayerID *string `db:"guest_player_id" json:"guest_player_id"`
|
||||||
Nickname string
|
Nickname string `db:"nickname" json:"nickname"`
|
||||||
Color int64
|
Color int64 `db:"color" json:"color"`
|
||||||
Slot int64
|
Slot int64 `db:"slot" json:"slot"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateGamePlayer(ctx context.Context, arg CreateGamePlayerParams) error {
|
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
|
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)
|
rows, err := q.db.QueryContext(ctx, getActiveGames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Game
|
var items []*Game
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Game
|
var i Game
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -120,7 +78,7 @@ func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
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 = ?
|
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)
|
row := q.db.QueryRowContext(ctx, getGame, id)
|
||||||
var i Game
|
var i Game
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -156,20 +114,20 @@ func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
|
|||||||
&i.Score,
|
&i.Score,
|
||||||
&i.SnakeSpeed,
|
&i.SnakeSpeed,
|
||||||
)
|
)
|
||||||
return i, err
|
return &i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getGamePlayers = `-- name: GetGamePlayers :many
|
const getGamePlayers = `-- name: GetGamePlayers :many
|
||||||
SELECT game_id, user_id, guest_player_id, nickname, color, slot, created_at FROM game_players WHERE game_id = ?
|
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)
|
rows, err := q.db.QueryContext(ctx, getGamePlayers, gameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GamePlayer
|
var items []*GamePlayer
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GamePlayer
|
var i GamePlayer
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -183,7 +141,7 @@ func (q *Queries) GetGamePlayers(ctx context.Context, gameID string) ([]GamePlay
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -201,13 +159,13 @@ WHERE gp.user_id = ?
|
|||||||
ORDER BY g.updated_at DESC
|
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)
|
rows, err := q.db.QueryContext(ctx, getGamesByUserID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Game
|
var items []*Game
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Game
|
var i Game
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -230,7 +188,7 @@ func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) (
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -257,21 +215,21 @@ ORDER BY g.updated_at DESC
|
|||||||
`
|
`
|
||||||
|
|
||||||
type GetUserActiveGamesRow struct {
|
type GetUserActiveGamesRow struct {
|
||||||
ID string
|
ID string `db:"id" json:"id"`
|
||||||
Status int64
|
Status int64 `db:"status" json:"status"`
|
||||||
CurrentTurn int64
|
CurrentTurn int64 `db:"current_turn" json:"current_turn"`
|
||||||
UpdatedAt sql.NullTime
|
UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
|
||||||
MyColor int64
|
MyColor int64 `db:"my_color" json:"my_color"`
|
||||||
OpponentNickname sql.NullString
|
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)
|
rows, err := q.db.QueryContext(ctx, getUserActiveGames, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetUserActiveGamesRow
|
var items []*GetUserActiveGamesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetUserActiveGamesRow
|
var i GetUserActiveGamesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -284,7 +242,7 @@ func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString)
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -295,31 +253,38 @@ func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString)
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateGame = `-- name: UpdateGame :exec
|
const upsertGame = `-- name: UpsertGame :exec
|
||||||
UPDATE games
|
INSERT INTO games (id, board, current_turn, status, game_type, winner_user_id, winning_cells, rematch_game_id)
|
||||||
SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP
|
VALUES (?, ?, ?, ?, 'connect4', ?, ?, ?)
|
||||||
WHERE id = ?
|
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 {
|
type UpsertGameParams struct {
|
||||||
Board string
|
ID string `db:"id" json:"id"`
|
||||||
CurrentTurn int64
|
Board string `db:"board" json:"board"`
|
||||||
Status int64
|
CurrentTurn int64 `db:"current_turn" json:"current_turn"`
|
||||||
WinnerUserID sql.NullString
|
Status int64 `db:"status" json:"status"`
|
||||||
WinningCells sql.NullString
|
WinnerUserID *string `db:"winner_user_id" json:"winner_user_id"`
|
||||||
RematchGameID sql.NullString
|
WinningCells *string `db:"winning_cells" json:"winning_cells"`
|
||||||
ID string
|
RematchGameID *string `db:"rematch_game_id" json:"rematch_game_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) error {
|
func (q *Queries) UpsertGame(ctx context.Context, arg UpsertGameParams) error {
|
||||||
_, err := q.db.ExecContext(ctx, updateGame,
|
_, err := q.db.ExecContext(ctx, upsertGame,
|
||||||
|
arg.ID,
|
||||||
arg.Board,
|
arg.Board,
|
||||||
arg.CurrentTurn,
|
arg.CurrentTurn,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
arg.WinnerUserID,
|
arg.WinnerUserID,
|
||||||
arg.WinningCells,
|
arg.WinningCells,
|
||||||
arg.RematchGameID,
|
arg.RematchGameID,
|
||||||
arg.ID,
|
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,50 +5,56 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ChatMessage struct {
|
type ChatMessage struct {
|
||||||
ID int64
|
ID int64 `db:"id" json:"id"`
|
||||||
GameID string
|
GameID string `db:"game_id" json:"game_id"`
|
||||||
Nickname string
|
Nickname string `db:"nickname" json:"nickname"`
|
||||||
Color int64
|
Color int64 `db:"color" json:"color"`
|
||||||
Message string
|
Message string `db:"message" json:"message"`
|
||||||
CreatedAt int64
|
CreatedAt int64 `db:"created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
ID string
|
ID string `db:"id" json:"id"`
|
||||||
Board string
|
Board string `db:"board" json:"board"`
|
||||||
CurrentTurn int64
|
CurrentTurn int64 `db:"current_turn" json:"current_turn"`
|
||||||
Status int64
|
Status int64 `db:"status" json:"status"`
|
||||||
WinnerUserID sql.NullString
|
WinnerUserID *string `db:"winner_user_id" json:"winner_user_id"`
|
||||||
WinningCells sql.NullString
|
WinningCells *string `db:"winning_cells" json:"winning_cells"`
|
||||||
CreatedAt sql.NullTime
|
CreatedAt *time.Time `db:"created_at" json:"created_at"`
|
||||||
UpdatedAt sql.NullTime
|
UpdatedAt *time.Time `db:"updated_at" json:"updated_at"`
|
||||||
RematchGameID sql.NullString
|
RematchGameID *string `db:"rematch_game_id" json:"rematch_game_id"`
|
||||||
GameType string
|
GameType string `db:"game_type" json:"game_type"`
|
||||||
GridWidth sql.NullInt64
|
GridWidth *int64 `db:"grid_width" json:"grid_width"`
|
||||||
GridHeight sql.NullInt64
|
GridHeight *int64 `db:"grid_height" json:"grid_height"`
|
||||||
MaxPlayers int64
|
MaxPlayers int64 `db:"max_players" json:"max_players"`
|
||||||
GameMode int64
|
GameMode int64 `db:"game_mode" json:"game_mode"`
|
||||||
Score int64
|
Score int64 `db:"score" json:"score"`
|
||||||
SnakeSpeed int64
|
SnakeSpeed int64 `db:"snake_speed" json:"snake_speed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GamePlayer struct {
|
type GamePlayer struct {
|
||||||
GameID string
|
GameID string `db:"game_id" json:"game_id"`
|
||||||
UserID sql.NullString
|
UserID *string `db:"user_id" json:"user_id"`
|
||||||
GuestPlayerID sql.NullString
|
GuestPlayerID *string `db:"guest_player_id" json:"guest_player_id"`
|
||||||
Nickname string
|
Nickname string `db:"nickname" json:"nickname"`
|
||||||
Color int64
|
Color int64 `db:"color" json:"color"`
|
||||||
Slot int64
|
Slot int64 `db:"slot" json:"slot"`
|
||||||
CreatedAt sql.NullTime
|
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 {
|
type User struct {
|
||||||
ID string
|
ID string `db:"id" json:"id"`
|
||||||
Username string
|
Username string `db:"username" json:"username"`
|
||||||
PasswordHash string
|
PasswordHash string `db:"password_hash" json:"password_hash"`
|
||||||
CreatedAt sql.NullTime
|
CreatedAt *time.Time `db:"created_at" json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,69 +7,21 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"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
|
const createSnakePlayer = `-- name: CreateSnakePlayer :exec
|
||||||
INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot)
|
INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot)
|
||||||
VALUES (?, ?, ?, ?, ?, ?)
|
VALUES (?, ?, ?, ?, ?, ?)
|
||||||
`
|
`
|
||||||
|
|
||||||
type CreateSnakePlayerParams struct {
|
type CreateSnakePlayerParams struct {
|
||||||
GameID string
|
GameID string `db:"game_id" json:"game_id"`
|
||||||
UserID sql.NullString
|
UserID *string `db:"user_id" json:"user_id"`
|
||||||
GuestPlayerID sql.NullString
|
GuestPlayerID *string `db:"guest_player_id" json:"guest_player_id"`
|
||||||
Nickname string
|
Nickname string `db:"nickname" json:"nickname"`
|
||||||
Color int64
|
Color int64 `db:"color" json:"color"`
|
||||||
Slot int64
|
Slot int64 `db:"slot" json:"slot"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) CreateSnakePlayer(ctx context.Context, arg CreateSnakePlayerParams) error {
|
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
|
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)
|
rows, err := q.db.QueryContext(ctx, getActiveSnakeGames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []Game
|
var items []*Game
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i Game
|
var i Game
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -126,7 +78,7 @@ func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]Game, error) {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
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'
|
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)
|
row := q.db.QueryRowContext(ctx, getSnakeGame, id)
|
||||||
var i Game
|
var i Game
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -162,20 +114,20 @@ func (q *Queries) GetSnakeGame(ctx context.Context, id string) (Game, error) {
|
|||||||
&i.Score,
|
&i.Score,
|
||||||
&i.SnakeSpeed,
|
&i.SnakeSpeed,
|
||||||
)
|
)
|
||||||
return i, err
|
return &i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSnakePlayers = `-- name: GetSnakePlayers :many
|
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
|
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)
|
rows, err := q.db.QueryContext(ctx, getSnakePlayers, gameID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GamePlayer
|
var items []*GamePlayer
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GamePlayer
|
var i GamePlayer
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -189,7 +141,7 @@ func (q *Queries) GetSnakePlayers(ctx context.Context, gameID string) ([]GamePla
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -214,20 +166,20 @@ ORDER BY g.updated_at DESC
|
|||||||
`
|
`
|
||||||
|
|
||||||
type GetUserActiveSnakeGamesRow struct {
|
type GetUserActiveSnakeGamesRow struct {
|
||||||
ID string
|
ID string `db:"id" json:"id"`
|
||||||
Status int64
|
Status int64 `db:"status" json:"status"`
|
||||||
GridWidth sql.NullInt64
|
GridWidth *int64 `db:"grid_width" json:"grid_width"`
|
||||||
GridHeight sql.NullInt64
|
GridHeight *int64 `db:"grid_height" json:"grid_height"`
|
||||||
UpdatedAt sql.NullTime
|
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)
|
rows, err := q.db.QueryContext(ctx, getUserActiveSnakeGames, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []GetUserActiveSnakeGamesRow
|
var items []*GetUserActiveSnakeGamesRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetUserActiveSnakeGamesRow
|
var i GetUserActiveSnakeGamesRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(
|
||||||
@@ -239,7 +191,7 @@ func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullSt
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, &i)
|
||||||
}
|
}
|
||||||
if err := rows.Close(); err != nil {
|
if err := rows.Close(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -250,29 +202,43 @@ func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullSt
|
|||||||
return items, nil
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSnakeGame = `-- name: UpdateSnakeGame :exec
|
const upsertSnakeGame = `-- name: UpsertSnakeGame :exec
|
||||||
UPDATE games
|
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)
|
||||||
SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, score = ?, updated_at = CURRENT_TIMESTAMP
|
VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?, ?, ?, ?, ?)
|
||||||
WHERE id = ? AND game_type = 'snake'
|
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 {
|
type UpsertSnakeGameParams struct {
|
||||||
Board string
|
ID string `db:"id" json:"id"`
|
||||||
Status int64
|
Board string `db:"board" json:"board"`
|
||||||
WinnerUserID sql.NullString
|
Status int64 `db:"status" json:"status"`
|
||||||
RematchGameID sql.NullString
|
GridWidth *int64 `db:"grid_width" json:"grid_width"`
|
||||||
Score int64
|
GridHeight *int64 `db:"grid_height" json:"grid_height"`
|
||||||
ID string
|
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 {
|
func (q *Queries) UpsertSnakeGame(ctx context.Context, arg UpsertSnakeGameParams) error {
|
||||||
_, err := q.db.ExecContext(ctx, updateSnakeGame,
|
_, err := q.db.ExecContext(ctx, upsertSnakeGame,
|
||||||
|
arg.ID,
|
||||||
arg.Board,
|
arg.Board,
|
||||||
arg.Status,
|
arg.Status,
|
||||||
|
arg.GridWidth,
|
||||||
|
arg.GridHeight,
|
||||||
|
arg.GameMode,
|
||||||
|
arg.SnakeSpeed,
|
||||||
arg.WinnerUserID,
|
arg.WinnerUserID,
|
||||||
arg.RematchGameID,
|
arg.RematchGameID,
|
||||||
arg.Score,
|
arg.Score,
|
||||||
arg.ID,
|
|
||||||
)
|
)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ RETURNING id, username, password_hash, created_at
|
|||||||
`
|
`
|
||||||
|
|
||||||
type CreateUserParams struct {
|
type CreateUserParams struct {
|
||||||
ID string
|
ID string `db:"id" json:"id"`
|
||||||
Username string
|
Username string `db:"username" json:"username"`
|
||||||
PasswordHash string
|
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)
|
row := q.db.QueryRowContext(ctx, createUser, arg.ID, arg.Username, arg.PasswordHash)
|
||||||
var i User
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -30,14 +30,14 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return &i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
const getUserByID = `-- name: GetUserByID :one
|
||||||
SELECT id, username, password_hash, created_at FROM users WHERE id = ?
|
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)
|
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||||
var i User
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -46,14 +46,14 @@ func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) {
|
|||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return &i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
const getUserByUsername = `-- name: GetUserByUsername :one
|
||||||
SELECT id, username, password_hash, created_at FROM users WHERE username = ?
|
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)
|
row := q.db.QueryRowContext(ctx, getUserByUsername, username)
|
||||||
var i User
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -62,5 +62,5 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User,
|
|||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
)
|
)
|
||||||
return i, err
|
return &i, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,9 +156,6 @@ func HandleGameEvents(store *game.GameStore, nc *nats.Conn, sessions *scs.Sessio
|
|||||||
if len(chatMessages) > 50 {
|
if len(chatMessages) > 50 {
|
||||||
chatMessages = chatMessages[len(chatMessages)-50:]
|
chatMessages = chatMessages[len(chatMessages)-50:]
|
||||||
}
|
}
|
||||||
chatMu.Unlock()
|
|
||||||
|
|
||||||
chatMu.Lock()
|
|
||||||
msgs := make([]components.ChatMessage, len(chatMessages))
|
msgs := make([]components.ChatMessage, len(chatMessages))
|
||||||
copy(msgs, chatMessages)
|
copy(msgs, chatMessages)
|
||||||
chatMu.Unlock()
|
chatMu.Unlock()
|
||||||
|
|||||||
@@ -2,10 +2,10 @@ package lobby
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/ryanhamamura/c4/db/repository"
|
"github.com/ryanhamamura/c4/db/repository"
|
||||||
lobbycomponents "github.com/ryanhamamura/c4/features/lobby/components"
|
lobbycomponents "github.com/ryanhamamura/c4/features/lobby/components"
|
||||||
@@ -28,16 +28,24 @@ func HandleLobbyPage(queries *repository.Queries, sessions *scs.SessionManager,
|
|||||||
var userGames []lobbycomponents.GameListItem
|
var userGames []lobbycomponents.GameListItem
|
||||||
if isLoggedIn {
|
if isLoggedIn {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
games, err := queries.GetUserActiveGames(ctx, sql.NullString{String: userID, Valid: true})
|
games, err := queries.GetUserActiveGames(ctx, &userID)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, g := range games {
|
for _, g := range games {
|
||||||
isMyTurn := g.Status == 1 && g.CurrentTurn == g.MyColor
|
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{
|
userGames = append(userGames, lobbycomponents.GameListItem{
|
||||||
ID: g.ID,
|
ID: g.ID,
|
||||||
Status: int(g.Status),
|
Status: int(g.Status),
|
||||||
OpponentName: g.OpponentNickname.String,
|
OpponentName: opponentName,
|
||||||
IsMyTurn: isMyTurn,
|
IsMyTurn: isMyTurn,
|
||||||
LastPlayed: g.UpdatedAt.Time,
|
LastPlayed: lastPlayed,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
140
game/persist.go
140
game/persist.go
@@ -2,80 +2,43 @@ package game
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/ryanhamamura/c4/db/repository"
|
"github.com/ryanhamamura/c4/db/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Persistence methods on GameStore (used during Get to hydrate from DB).
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
func (gs *GameStore) saveGame(g *Game) error {
|
var winningCells *string
|
||||||
ctx := context.Background()
|
if wc := g.WinningCellsToJSON(); wc != "" {
|
||||||
|
winningCells = &wc
|
||||||
|
}
|
||||||
|
|
||||||
_, err := gs.queries.GetGame(ctx, g.ID)
|
return queries.UpsertGame(context.Background(), repository.UpsertGameParams{
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
_, err = gs.queries.CreateGame(ctx, repository.CreateGameParams{
|
|
||||||
ID: g.ID,
|
ID: g.ID,
|
||||||
Board: g.BoardToJSON(),
|
Board: g.BoardToJSON(),
|
||||||
CurrentTurn: int64(g.CurrentTurn),
|
CurrentTurn: int64(g.CurrentTurn),
|
||||||
Status: int64(g.Status),
|
Status: int64(g.Status),
|
||||||
|
WinnerUserID: winnerUserID,
|
||||||
|
WinningCells: winningCells,
|
||||||
|
RematchGameID: g.RematchGameID,
|
||||||
})
|
})
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gs.queries.UpdateGame(ctx, updateGameParams(g))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gs *GameStore) loadGame(id string) (*Game, error) {
|
func saveGamePlayer(queries *repository.Queries, gameID string, player *Player, slot int) error {
|
||||||
row, err := gs.queries.GetGame(context.Background(), id)
|
var userID, guestPlayerID *string
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return gameFromRow(row)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *GameStore) loadGamePlayers(id string) ([]*Player, error) {
|
|
||||||
rows, err := gs.queries.GetGamePlayers(context.Background(), id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return playersFromRows(rows), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Persistence methods on GameInstance (used during gameplay mutations).
|
|
||||||
|
|
||||||
func (gi *GameInstance) saveGame(g *Game) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
_, err := gi.queries.GetGame(ctx, g.ID)
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
_, err = gi.queries.CreateGame(ctx, repository.CreateGameParams{
|
|
||||||
ID: g.ID,
|
|
||||||
Board: g.BoardToJSON(),
|
|
||||||
CurrentTurn: int64(g.CurrentTurn),
|
|
||||||
Status: int64(g.Status),
|
|
||||||
})
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return gi.queries.UpdateGame(ctx, updateGameParams(g))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gi *GameInstance) saveGamePlayer(gameID string, player *Player, slot int) error {
|
|
||||||
var userID, guestPlayerID sql.NullString
|
|
||||||
if player.UserID != nil {
|
if player.UserID != nil {
|
||||||
userID = sql.NullString{String: *player.UserID, Valid: true}
|
userID = player.UserID
|
||||||
} else {
|
} 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,
|
GameID: gameID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
GuestPlayerID: guestPlayerID,
|
GuestPlayerID: guestPlayerID,
|
||||||
@@ -85,36 +48,25 @@ func (gi *GameInstance) saveGamePlayer(gameID string, player *Player, slot int)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared helpers for domain ↔ DB mapping.
|
func loadGame(queries *repository.Queries, id string) (*Game, error) {
|
||||||
|
row, err := queries.GetGame(context.Background(), id)
|
||||||
func updateGameParams(g *Game) repository.UpdateGameParams {
|
if err != nil {
|
||||||
var winnerUserID sql.NullString
|
return nil, err
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
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{
|
g := &Game{
|
||||||
ID: row.ID,
|
ID: row.ID,
|
||||||
CurrentTurn: int(row.CurrentTurn),
|
CurrentTurn: int(row.CurrentTurn),
|
||||||
@@ -125,18 +77,18 @@ func gameFromRow(row repository.Game) (*Game, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.WinningCells.Valid {
|
if row.WinningCells != nil {
|
||||||
_ = g.WinningCellsFromJSON(row.WinningCells.String)
|
_ = g.WinningCellsFromJSON(*row.WinningCells)
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.RematchGameID.Valid {
|
if row.RematchGameID != nil {
|
||||||
g.RematchGameID = &row.RematchGameID.String
|
g.RematchGameID = row.RematchGameID
|
||||||
}
|
}
|
||||||
|
|
||||||
return g, nil
|
return g, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func playersFromRows(rows []repository.GamePlayer) []*Player {
|
func playersFromRows(rows []*repository.GamePlayer) []*Player {
|
||||||
players := make([]*Player, 0, len(rows))
|
players := make([]*Player, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
player := &Player{
|
player := &Player{
|
||||||
@@ -144,11 +96,11 @@ func playersFromRows(rows []repository.GamePlayer) []*Player {
|
|||||||
Color: int(row.Color),
|
Color: int(row.Color),
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.UserID.Valid {
|
if row.UserID != nil {
|
||||||
player.UserID = &row.UserID.String
|
player.UserID = row.UserID
|
||||||
player.ID = PlayerID(row.UserID.String)
|
player.ID = PlayerID(*row.UserID)
|
||||||
} else if row.GuestPlayerID.Valid {
|
} else if row.GuestPlayerID != nil {
|
||||||
player.ID = PlayerID(row.GuestPlayerID.String)
|
player.ID = PlayerID(*row.GuestPlayerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
players = append(players, player)
|
players = append(players, player)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (gs *GameStore) Create() *GameInstance {
|
|||||||
gs.gamesMu.Unlock()
|
gs.gamesMu.Unlock()
|
||||||
|
|
||||||
if gs.queries != nil {
|
if gs.queries != nil {
|
||||||
gs.saveGame(gi.game) //nolint:errcheck
|
saveGame(gs.queries, gi.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
return gi
|
return gi
|
||||||
@@ -68,12 +68,12 @@ func (gs *GameStore) Get(id string) (*GameInstance, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
g, err := gs.loadGame(id)
|
g, err := loadGame(gs.queries, id)
|
||||||
if err != nil || g == nil {
|
if err != nil || g == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
players, _ := gs.loadGamePlayers(id)
|
players, _ := loadGamePlayers(gs.queries, id)
|
||||||
for _, p := range players {
|
for _, p := range players {
|
||||||
switch p.Color {
|
switch p.Color {
|
||||||
case 1:
|
case 1:
|
||||||
@@ -152,8 +152,8 @@ func (gi *GameInstance) Join(ps *PlayerSession) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gi.queries != nil {
|
if gi.queries != nil {
|
||||||
gi.saveGamePlayer(gi.game.ID, ps.Player, slot) //nolint:errcheck
|
saveGamePlayer(gi.queries, gi.game.ID, ps.Player, slot) //nolint:errcheck
|
||||||
gi.saveGame(gi.game) //nolint:errcheck
|
saveGame(gi.queries, gi.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
gi.notify()
|
gi.notify()
|
||||||
@@ -190,7 +190,7 @@ func (gi *GameInstance) CreateRematch(gs *GameStore) *GameInstance {
|
|||||||
gi.game.RematchGameID = &newID
|
gi.game.RematchGameID = &newID
|
||||||
|
|
||||||
if gi.queries != nil {
|
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
|
gs.Delete(newID) //nolint:errcheck
|
||||||
gi.game.RematchGameID = nil
|
gi.game.RematchGameID = nil
|
||||||
return nil
|
return nil
|
||||||
@@ -224,7 +224,7 @@ func (gi *GameInstance) DropPiece(col int, playerColor int) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if gi.queries != nil {
|
if gi.queries != nil {
|
||||||
gi.saveGame(gi.game) //nolint:errcheck
|
saveGame(gi.queries, gi.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
gi.notify()
|
gi.notify()
|
||||||
|
|||||||
@@ -62,16 +62,14 @@ func (si *SnakeGameInstance) countdownPhase() {
|
|||||||
si.game.Status = StatusInProgress
|
si.game.Status = StatusInProgress
|
||||||
|
|
||||||
if si.queries != nil {
|
if si.queries != nil {
|
||||||
si.saveSnakeGame(si.game) //nolint:errcheck
|
saveSnakeGame(si.queries, si.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
si.gameMu.Unlock()
|
si.gameMu.Unlock()
|
||||||
si.notify()
|
si.notify()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if si.queries != nil {
|
// No DB save during countdown ticks — state is transient
|
||||||
si.saveSnakeGame(si.game) //nolint:errcheck
|
|
||||||
}
|
|
||||||
si.gameMu.Unlock()
|
si.gameMu.Unlock()
|
||||||
si.notify()
|
si.notify()
|
||||||
}
|
}
|
||||||
@@ -98,6 +96,7 @@ func (si *SnakeGameInstance) gamePhase() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
|
|
||||||
lastInput := time.Now()
|
lastInput := time.Now()
|
||||||
|
lastSave := time.Now()
|
||||||
var moveAccum time.Duration
|
var moveAccum time.Duration
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -124,7 +123,7 @@ func (si *SnakeGameInstance) gamePhase() {
|
|||||||
if time.Since(lastInput) > inactivityLimit {
|
if time.Since(lastInput) > inactivityLimit {
|
||||||
si.game.Status = StatusFinished
|
si.game.Status = StatusFinished
|
||||||
if si.queries != nil {
|
if si.queries != nil {
|
||||||
si.saveSnakeGame(si.game) //nolint:errcheck
|
saveSnakeGame(si.queries, si.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
si.gameMu.Unlock()
|
si.gameMu.Unlock()
|
||||||
si.notify()
|
si.notify()
|
||||||
@@ -175,13 +174,10 @@ func (si *SnakeGameInstance) gamePhase() {
|
|||||||
alive := AliveCount(state)
|
alive := AliveCount(state)
|
||||||
gameOver := false
|
gameOver := false
|
||||||
if si.game.Mode == ModeSinglePlayer {
|
if si.game.Mode == ModeSinglePlayer {
|
||||||
// Single player ends when the player dies (alive == 0)
|
|
||||||
if alive == 0 {
|
if alive == 0 {
|
||||||
gameOver = true
|
gameOver = true
|
||||||
// No winner in single player - just final score
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Multiplayer ends when 1 or fewer alive
|
|
||||||
if alive <= 1 {
|
if alive <= 1 {
|
||||||
gameOver = true
|
gameOver = true
|
||||||
winnerIdx := LastAlive(state)
|
winnerIdx := LastAlive(state)
|
||||||
@@ -195,8 +191,10 @@ func (si *SnakeGameInstance) gamePhase() {
|
|||||||
si.game.Status = StatusFinished
|
si.game.Status = StatusFinished
|
||||||
}
|
}
|
||||||
|
|
||||||
if si.queries != nil {
|
// Throttle DB saves: persist on game over or every 2 seconds
|
||||||
si.saveSnakeGame(si.game) //nolint:errcheck
|
if si.queries != nil && (gameOver || time.Since(lastSave) >= 2*time.Second) {
|
||||||
|
saveSnakeGame(si.queries, si.game) //nolint:errcheck
|
||||||
|
lastSave = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
si.gameMu.Unlock()
|
si.gameMu.Unlock()
|
||||||
|
|||||||
156
snake/persist.go
156
snake/persist.go
@@ -2,30 +2,26 @@ package snake
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
|
|
||||||
"github.com/ryanhamamura/c4/db/repository"
|
"github.com/ryanhamamura/c4/db/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Persistence methods on SnakeStore (used during Get to hydrate from DB).
|
// saveSnakeGame persists the snake game state via upsert.
|
||||||
|
func saveSnakeGame(queries *repository.Queries, sg *SnakeGame) error {
|
||||||
func (ss *SnakeStore) saveSnakeGame(sg *SnakeGame) error {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
boardJSON := "{}"
|
boardJSON := "{}"
|
||||||
|
var gridWidth, gridHeight *int64
|
||||||
if sg.State != nil {
|
if sg.State != nil {
|
||||||
boardJSON = sg.State.ToJSON()
|
boardJSON = sg.State.ToJSON()
|
||||||
|
w, h := int64(sg.State.Width), int64(sg.State.Height)
|
||||||
|
gridWidth, gridHeight = &w, &h
|
||||||
}
|
}
|
||||||
|
|
||||||
var gridWidth, gridHeight sql.NullInt64
|
var winnerUserID *string
|
||||||
if sg.State != nil {
|
if sg.Winner != nil && sg.Winner.UserID != nil {
|
||||||
gridWidth = sql.NullInt64{Int64: int64(sg.State.Width), Valid: true}
|
winnerUserID = sg.Winner.UserID
|
||||||
gridHeight = sql.NullInt64{Int64: int64(sg.State.Height), Valid: true}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := ss.queries.GetSnakeGame(ctx, sg.ID)
|
return queries.UpsertSnakeGame(context.Background(), repository.UpsertSnakeGameParams{
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
_, err = ss.queries.CreateSnakeGame(ctx, repository.CreateSnakeGameParams{
|
|
||||||
ID: sg.ID,
|
ID: sg.ID,
|
||||||
Board: boardJSON,
|
Board: boardJSON,
|
||||||
Status: int64(sg.Status),
|
Status: int64(sg.Status),
|
||||||
@@ -33,77 +29,22 @@ func (ss *SnakeStore) saveSnakeGame(sg *SnakeGame) error {
|
|||||||
GridHeight: gridHeight,
|
GridHeight: gridHeight,
|
||||||
GameMode: int64(sg.Mode),
|
GameMode: int64(sg.Mode),
|
||||||
SnakeSpeed: int64(sg.Speed),
|
SnakeSpeed: int64(sg.Speed),
|
||||||
|
WinnerUserID: winnerUserID,
|
||||||
|
RematchGameID: sg.RematchGameID,
|
||||||
|
Score: int64(sg.Score),
|
||||||
})
|
})
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return ss.queries.UpdateSnakeGame(ctx, updateSnakeGameParams(sg, boardJSON))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ss *SnakeStore) loadSnakeGame(id string) (*SnakeGame, error) {
|
func saveSnakePlayer(queries *repository.Queries, gameID string, player *Player) error {
|
||||||
row, err := ss.queries.GetSnakeGame(context.Background(), id)
|
var userID, guestPlayerID *string
|
||||||
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
|
|
||||||
if player.UserID != nil {
|
if player.UserID != nil {
|
||||||
userID = sql.NullString{String: *player.UserID, Valid: true}
|
userID = player.UserID
|
||||||
} else {
|
} 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,
|
GameID: gameID,
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
GuestPlayerID: guestPlayerID,
|
GuestPlayerID: guestPlayerID,
|
||||||
@@ -113,39 +54,34 @@ func (si *SnakeGameInstance) saveSnakePlayer(gameID string, player *Player) erro
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Shared helpers for domain ↔ DB mapping.
|
func loadSnakeGame(queries *repository.Queries, id string) (*SnakeGame, error) {
|
||||||
|
row, err := queries.GetSnakeGame(context.Background(), id)
|
||||||
func updateSnakeGameParams(sg *SnakeGame, boardJSON string) repository.UpdateSnakeGameParams {
|
if err != nil {
|
||||||
var winnerUserID sql.NullString
|
return nil, err
|
||||||
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,
|
|
||||||
}
|
}
|
||||||
|
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)
|
state, err := GameStateFromJSON(row.Board)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
state = &GameState{}
|
state = &GameState{}
|
||||||
}
|
}
|
||||||
if row.GridWidth.Valid {
|
if row.GridWidth != nil {
|
||||||
state.Width = int(row.GridWidth.Int64)
|
state.Width = int(*row.GridWidth)
|
||||||
}
|
}
|
||||||
if row.GridHeight.Valid {
|
if row.GridHeight != nil {
|
||||||
state.Height = int(row.GridHeight.Int64)
|
state.Height = int(*row.GridHeight)
|
||||||
}
|
}
|
||||||
|
|
||||||
sg := &SnakeGame{
|
sg := &SnakeGame{
|
||||||
@@ -158,14 +94,14 @@ func snakeGameFromRow(row repository.Game) (*SnakeGame, error) {
|
|||||||
Speed: int(row.SnakeSpeed),
|
Speed: int(row.SnakeSpeed),
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.RematchGameID.Valid {
|
if row.RematchGameID != nil {
|
||||||
sg.RematchGameID = &row.RematchGameID.String
|
sg.RematchGameID = row.RematchGameID
|
||||||
}
|
}
|
||||||
|
|
||||||
return sg, nil
|
return sg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func snakePlayersFromRows(rows []repository.GamePlayer) []*Player {
|
func snakePlayersFromRows(rows []*repository.GamePlayer) []*Player {
|
||||||
players := make([]*Player, 0, len(rows))
|
players := make([]*Player, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
player := &Player{
|
player := &Player{
|
||||||
@@ -173,11 +109,11 @@ func snakePlayersFromRows(rows []repository.GamePlayer) []*Player {
|
|||||||
Slot: int(row.Slot),
|
Slot: int(row.Slot),
|
||||||
}
|
}
|
||||||
|
|
||||||
if row.UserID.Valid {
|
if row.UserID != nil {
|
||||||
player.UserID = &row.UserID.String
|
player.UserID = row.UserID
|
||||||
player.ID = PlayerID(row.UserID.String)
|
player.ID = PlayerID(*row.UserID)
|
||||||
} else if row.GuestPlayerID.Valid {
|
} else if row.GuestPlayerID != nil {
|
||||||
player.ID = PlayerID(row.GuestPlayerID.String)
|
player.ID = PlayerID(*row.GuestPlayerID)
|
||||||
}
|
}
|
||||||
|
|
||||||
players = append(players, player)
|
players = append(players, player)
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ package snake
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"encoding/hex"
|
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/ryanhamamura/c4/db/repository"
|
"github.com/ryanhamamura/c4/db/repository"
|
||||||
|
"github.com/ryanhamamura/c4/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SnakeStore struct {
|
type SnakeStore struct {
|
||||||
@@ -39,7 +38,7 @@ func (ss *SnakeStore) Create(width, height int, mode GameMode, speed int) *Snake
|
|||||||
if speed <= 0 {
|
if speed <= 0 {
|
||||||
speed = DefaultSpeed
|
speed = DefaultSpeed
|
||||||
}
|
}
|
||||||
id := generateID(4)
|
id := game.GenerateID(4)
|
||||||
sg := &SnakeGame{
|
sg := &SnakeGame{
|
||||||
ID: id,
|
ID: id,
|
||||||
State: &GameState{
|
State: &GameState{
|
||||||
@@ -63,7 +62,7 @@ func (ss *SnakeStore) Create(width, height int, mode GameMode, speed int) *Snake
|
|||||||
ss.gamesMu.Unlock()
|
ss.gamesMu.Unlock()
|
||||||
|
|
||||||
if ss.queries != nil {
|
if ss.queries != nil {
|
||||||
ss.saveSnakeGame(sg) //nolint:errcheck
|
saveSnakeGame(ss.queries, sg) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
return si
|
return si
|
||||||
@@ -82,12 +81,12 @@ func (ss *SnakeStore) Get(id string) (*SnakeGameInstance, bool) {
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
sg, err := ss.loadSnakeGame(id)
|
sg, err := loadSnakeGame(ss.queries, id)
|
||||||
if err != nil || sg == nil {
|
if err != nil || sg == nil {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
players, _ := ss.loadSnakePlayers(id)
|
players, _ := loadSnakePlayers(ss.queries, id)
|
||||||
if sg.Players == nil {
|
if sg.Players == nil {
|
||||||
sg.Players = make([]*Player, 8)
|
sg.Players = make([]*Player, 8)
|
||||||
}
|
}
|
||||||
@@ -207,8 +206,8 @@ func (si *SnakeGameInstance) Join(player *Player) bool {
|
|||||||
si.game.Players[slot] = player
|
si.game.Players[slot] = player
|
||||||
|
|
||||||
if si.queries != nil {
|
if si.queries != nil {
|
||||||
si.saveSnakePlayer(si.game.ID, player) //nolint:errcheck
|
saveSnakePlayer(si.queries, si.game.ID, player) //nolint:errcheck
|
||||||
si.saveSnakeGame(si.game) //nolint:errcheck
|
saveSnakeGame(si.queries, si.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
si.notify()
|
si.notify()
|
||||||
@@ -294,16 +293,10 @@ func (si *SnakeGameInstance) CreateRematch() *SnakeGameInstance {
|
|||||||
si.game.RematchGameID = &newID
|
si.game.RematchGameID = &newID
|
||||||
|
|
||||||
if si.queries != nil {
|
if si.queries != nil {
|
||||||
si.saveSnakeGame(si.game) //nolint:errcheck
|
saveSnakeGame(si.queries, si.game) //nolint:errcheck
|
||||||
}
|
}
|
||||||
si.gameMu.Unlock()
|
si.gameMu.Unlock()
|
||||||
|
|
||||||
si.notify()
|
si.notify()
|
||||||
return newSI
|
return newSI
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateID(size int) string {
|
|
||||||
b := make([]byte, size)
|
|
||||||
_, _ = rand.Read(b)
|
|
||||||
return hex.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user