WIP: Add multiplayer Snake game
N-player (2-8) real-time Snake game alongside Connect 4. Lobby has tabs to switch between games. Players join via invite link with 10-second countdown. Game loop runs at tick-based intervals with NATS pub/sub for state sync. Keyboard input not yet working (Datastar keydown binding issue still under investigation).
This commit is contained in:
@@ -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
|
||||
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
|
||||
`
|
||||
|
||||
type CreateGameParams struct {
|
||||
@@ -41,6 +41,10 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.RematchGameID,
|
||||
&i.GameType,
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -81,7 +85,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 FROM games WHERE 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 FROM games WHERE game_type = 'connect4' AND status < 2
|
||||
`
|
||||
|
||||
func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
|
||||
@@ -103,6 +107,10 @@ func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.RematchGameID,
|
||||
&i.GameType,
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -118,7 +126,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 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 FROM games WHERE id = ?
|
||||
`
|
||||
|
||||
func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
|
||||
@@ -134,6 +142,10 @@ func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.RematchGameID,
|
||||
&i.GameType,
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -174,7 +186,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 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 FROM games g
|
||||
JOIN game_players gp ON g.id = gp.game_id
|
||||
WHERE gp.user_id = ?
|
||||
ORDER BY g.updated_at DESC
|
||||
@@ -199,6 +211,10 @@ func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) (
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.RematchGameID,
|
||||
&i.GameType,
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.MaxPlayers,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -224,7 +240,7 @@ SELECT
|
||||
FROM games g
|
||||
JOIN game_players gp_user ON g.id = gp_user.game_id AND gp_user.user_id = ?
|
||||
LEFT JOIN game_players gp_opponent ON g.id = gp_opponent.game_id AND gp_opponent.slot != gp_user.slot
|
||||
WHERE g.status < 2
|
||||
WHERE g.game_type = 'connect4' AND g.status < 2
|
||||
ORDER BY g.updated_at DESC
|
||||
`
|
||||
|
||||
|
||||
@@ -18,6 +18,10 @@ type Game struct {
|
||||
CreatedAt sql.NullTime
|
||||
UpdatedAt sql.NullTime
|
||||
RematchGameID sql.NullString
|
||||
GameType string
|
||||
GridWidth sql.NullInt64
|
||||
GridHeight sql.NullInt64
|
||||
MaxPlayers int64
|
||||
}
|
||||
|
||||
type GamePlayer struct {
|
||||
|
||||
263
db/gen/snake_games.sql.go
Normal file
263
db/gen/snake_games.sql.go
Normal file
@@ -0,0 +1,263 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// source: snake_games.sql
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
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
|
||||
`
|
||||
|
||||
type CreateSnakeGameParams struct {
|
||||
ID string
|
||||
Board string
|
||||
Status int64
|
||||
GridWidth sql.NullInt64
|
||||
GridHeight sql.NullInt64
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSnakePlayer(ctx context.Context, arg CreateSnakePlayerParams) error {
|
||||
_, err := q.db.ExecContext(ctx, createSnakePlayer,
|
||||
arg.GameID,
|
||||
arg.UserID,
|
||||
arg.GuestPlayerID,
|
||||
arg.Nickname,
|
||||
arg.Color,
|
||||
arg.Slot,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteSnakeGame = `-- name: DeleteSnakeGame :exec
|
||||
DELETE FROM games WHERE id = ? AND game_type = 'snake'
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSnakeGame(ctx context.Context, id string) error {
|
||||
_, err := q.db.ExecContext(ctx, deleteSnakeGame, id)
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
`
|
||||
|
||||
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
|
||||
for rows.Next() {
|
||||
var i Game
|
||||
if err := rows.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,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
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'
|
||||
`
|
||||
|
||||
func (q *Queries) GetSnakeGame(ctx context.Context, id string) (Game, error) {
|
||||
row := q.db.QueryRowContext(ctx, getSnakeGame, id)
|
||||
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,
|
||||
)
|
||||
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) {
|
||||
rows, err := q.db.QueryContext(ctx, getSnakePlayers, gameID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GamePlayer
|
||||
for rows.Next() {
|
||||
var i GamePlayer
|
||||
if err := rows.Scan(
|
||||
&i.GameID,
|
||||
&i.UserID,
|
||||
&i.GuestPlayerID,
|
||||
&i.Nickname,
|
||||
&i.Color,
|
||||
&i.Slot,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getUserActiveSnakeGames = `-- name: GetUserActiveSnakeGames :many
|
||||
SELECT
|
||||
g.id,
|
||||
g.status,
|
||||
g.grid_width,
|
||||
g.grid_height,
|
||||
g.updated_at
|
||||
FROM games g
|
||||
JOIN game_players gp_user ON g.id = gp_user.game_id AND gp_user.user_id = ?
|
||||
WHERE g.game_type = 'snake' AND g.status < 3
|
||||
ORDER BY g.updated_at DESC
|
||||
`
|
||||
|
||||
type GetUserActiveSnakeGamesRow struct {
|
||||
ID string
|
||||
Status int64
|
||||
GridWidth sql.NullInt64
|
||||
GridHeight sql.NullInt64
|
||||
UpdatedAt sql.NullTime
|
||||
}
|
||||
|
||||
func (q *Queries) GetUserActiveSnakeGames(ctx context.Context, userID sql.NullString) ([]GetUserActiveSnakeGamesRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getUserActiveSnakeGames, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserActiveSnakeGamesRow
|
||||
for rows.Next() {
|
||||
var i GetUserActiveSnakeGamesRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.Status,
|
||||
&i.GridWidth,
|
||||
&i.GridHeight,
|
||||
&i.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const updateSnakeGame = `-- name: UpdateSnakeGame :exec
|
||||
UPDATE games
|
||||
SET board = ?, status = ?, winner_user_id = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ? AND game_type = 'snake'
|
||||
`
|
||||
|
||||
type UpdateSnakeGameParams struct {
|
||||
Board string
|
||||
Status int64
|
||||
WinnerUserID sql.NullString
|
||||
RematchGameID sql.NullString
|
||||
ID string
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateSnakeGame(ctx context.Context, arg UpdateSnakeGameParams) error {
|
||||
_, err := q.db.ExecContext(ctx, updateSnakeGame,
|
||||
arg.Board,
|
||||
arg.Status,
|
||||
arg.WinnerUserID,
|
||||
arg.RematchGameID,
|
||||
arg.ID,
|
||||
)
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user