feat: add single player snake mode
Add solo mode where players survive as long as possible while tracking score (food eaten). Single player games start with a shorter 3-second countdown vs 10 seconds for multiplayer, maintain exactly 1 food item for classic snake feel, and end when the player dies rather than when one player remains. - Add GameMode type (ModeMultiplayer/ModeSinglePlayer) and Score field - Filter single player games from "Join a Game" lobby list - Show "Ready?" and "Score: X" UI for single player mode - Hide invite link for single player games - Preserve game mode on rematch
This commit is contained in:
@@ -13,7 +13,7 @@ import (
|
||||
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
|
||||
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
|
||||
`
|
||||
|
||||
type CreateGameParams struct {
|
||||
@@ -45,6 +45,8 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -85,7 +87,7 @@ func (q *Queries) DeleteGame(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
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 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 FROM games WHERE game_type = 'connect4' AND status < 2
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
|
||||
@@ -111,6 +113,8 @@ func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -126,7 +130,7 @@ func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
|
||||
}
|
||||
|
||||
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 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 FROM games WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
|
||||
@@ -146,6 +150,8 @@ func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -186,7 +192,7 @@ func (q *Queries) GetGamePlayers(ctx context.Context, gameID string) ([]GamePlay
|
||||
}
|
||||
|
||||
const getGamesByUserID = `-- name: GetGamesByUserID :many
|
||||
SELECT g.id, g.board, g.current_turn, g.status, g.winner_user_id, g.winning_cells, g.created_at, g.updated_at, g.rematch_game_id, g.game_type, g.grid_width, g.grid_height, g.max_players FROM games g
|
||||
SELECT g.id, g.board, g.current_turn, g.status, g.winner_user_id, g.winning_cells, g.created_at, g.updated_at, g.rematch_game_id, g.game_type, g.grid_width, g.grid_height, g.max_players, g.game_mode, g.score FROM games g
|
||||
JOIN game_players gp ON g.id = gp.game_id
|
||||
WHERE gp.user_id = ?
|
||||
ORDER BY g.updated_at DESC
|
||||
@@ -215,6 +221,8 @@ func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) (
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ type Game struct {
|
||||
GridWidth sql.NullInt64
|
||||
GridHeight sql.NullInt64
|
||||
MaxPlayers int64
|
||||
GameMode int64
|
||||
Score int64
|
||||
}
|
||||
|
||||
type GamePlayer struct {
|
||||
|
||||
@@ -11,9 +11,9 @@ import (
|
||||
)
|
||||
|
||||
const createSnakeGame = `-- name: CreateSnakeGame :one
|
||||
INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players)
|
||||
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
|
||||
INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode)
|
||||
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
|
||||
`
|
||||
|
||||
type CreateSnakeGameParams struct {
|
||||
@@ -22,6 +22,7 @@ type CreateSnakeGameParams struct {
|
||||
Status int64
|
||||
GridWidth sql.NullInt64
|
||||
GridHeight sql.NullInt64
|
||||
GameMode int64
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSnakeGame(ctx context.Context, arg CreateSnakeGameParams) (Game, error) {
|
||||
@@ -31,6 +32,7 @@ func (q *Queries) CreateSnakeGame(ctx context.Context, arg CreateSnakeGameParams
|
||||
arg.Status,
|
||||
arg.GridWidth,
|
||||
arg.GridHeight,
|
||||
arg.GameMode,
|
||||
)
|
||||
var i Game
|
||||
err := row.Scan(
|
||||
@@ -47,6 +49,8 @@ func (q *Queries) CreateSnakeGame(ctx context.Context, arg CreateSnakeGameParams
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -87,7 +91,7 @@ func (q *Queries) DeleteSnakeGame(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
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 FROM games WHERE game_type = 'snake' 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 FROM games WHERE game_type = 'snake' AND status < 2 AND game_mode = 0
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]Game, error) {
|
||||
@@ -113,6 +117,8 @@ func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]Game, error) {
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -128,7 +134,7 @@ func (q *Queries) GetActiveSnakeGames(ctx context.Context) ([]Game, error) {
|
||||
}
|
||||
|
||||
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 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 FROM games WHERE id = ? AND game_type = 'snake'
|
||||
`
|
||||
|
||||
func (q *Queries) GetSnakeGame(ctx context.Context, id string) (Game, error) {
|
||||
@@ -148,6 +154,8 @@ func (q *Queries) GetSnakeGame(ctx context.Context, id string) (Game, error) {
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
&i.GameMode,
|
||||
&i.Score,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -239,7 +247,7 @@ func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullSt
|
||||
|
||||
const updateSnakeGame = `-- name: UpdateSnakeGame :exec
|
||||
UPDATE games
|
||||
SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, score = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND game_type = 'snake'
|
||||
`
|
||||
|
||||
@@ -248,6 +256,7 @@ type UpdateSnakeGameParams struct {
|
||||
Status int64
|
||||
WinnerUserID sql.NullString
|
||||
RematchGameID sql.NullString
|
||||
Score int64
|
||||
ID string
|
||||
}
|
||||
|
||||
@@ -257,6 +266,7 @@ func (q *Queries) UpdateSnakeGame(ctx context.Context, arg UpdateSnakeGameParams
|
||||
arg.Status,
|
||||
arg.WinnerUserID,
|
||||
arg.RematchGameID,
|
||||
arg.Score,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
|
||||
7
db/migrations/004_add_snake_mode.sql
Normal file
7
db/migrations/004_add_snake_mode.sql
Normal file
@@ -0,0 +1,7 @@
|
||||
-- +goose Up
|
||||
ALTER TABLE games ADD COLUMN game_mode INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE games ADD COLUMN score INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
-- +goose Down
|
||||
ALTER TABLE games DROP COLUMN score;
|
||||
ALTER TABLE games DROP COLUMN game_mode;
|
||||
@@ -171,6 +171,7 @@ func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
Status: int64(sg.Status),
|
||||
GridWidth: gridWidth,
|
||||
GridHeight: gridHeight,
|
||||
GameMode: int64(sg.Mode),
|
||||
})
|
||||
return err
|
||||
}
|
||||
@@ -193,6 +194,7 @@ func (p *SnakePersister) SaveSnakeGame(sg *snake.SnakeGame) error {
|
||||
Status: int64(sg.Status),
|
||||
WinnerUserID: winnerUserID,
|
||||
RematchGameID: rematchGameID,
|
||||
Score: int64(sg.Score),
|
||||
ID: sg.ID,
|
||||
})
|
||||
}
|
||||
@@ -220,6 +222,8 @@ func (p *SnakePersister) LoadSnakeGame(id string) (*snake.SnakeGame, error) {
|
||||
State: state,
|
||||
Players: make([]*snake.Player, 8),
|
||||
Status: snake.Status(row.Status),
|
||||
Mode: snake.GameMode(row.GameMode),
|
||||
Score: int(row.Score),
|
||||
}
|
||||
|
||||
if row.RematchGameID.Valid {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
-- name: CreateSnakeGame :one
|
||||
INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players)
|
||||
VALUES (?, ?, 0, ?, 'snake', ?, ?, 8)
|
||||
INSERT INTO games (id, board, current_turn, status, game_type, grid_width, grid_height, max_players, game_mode)
|
||||
VALUES (?, ?, 0, ?, 'snake', ?, ?, 8, ?)
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSnakeGame :one
|
||||
@@ -8,14 +8,14 @@ SELECT * FROM games WHERE id = ? AND game_type = 'snake';
|
||||
|
||||
-- name: UpdateSnakeGame :exec
|
||||
UPDATE games
|
||||
SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
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';
|
||||
|
||||
-- name: GetActiveSnakeGames :many
|
||||
SELECT * FROM games WHERE game_type = 'snake' AND status < 2;
|
||||
SELECT * FROM games WHERE game_type = 'snake' AND status < 2 AND game_mode = 0;
|
||||
|
||||
-- name: CreateSnakePlayer :exec
|
||||
INSERT INTO game_players (game_id, user_id, guest_player_id, nickname, color, slot)
|
||||
|
||||
Reference in New Issue
Block a user