Add play again button for rematch after game ends

When a game finishes (win or draw), players see a "Play again" button.
Clicking it creates a new game and the opponent sees a "Join Rematch"
link to join the same game.
This commit is contained in:
Ryan Hamamura
2026-01-14 18:02:26 -10:00
parent 5f452914f8
commit 2cd5b1d289
9 changed files with 138 additions and 37 deletions

View File

@@ -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
RETURNING id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id
`
type CreateGameParams struct {
@@ -40,6 +40,7 @@ func (q *Queries) CreateGame(ctx context.Context, arg CreateGameParams) (Game, e
&i.WinningCells,
&i.CreatedAt,
&i.UpdatedAt,
&i.RematchGameID,
)
return i, err
}
@@ -80,7 +81,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 FROM games WHERE status < 2
SELECT id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id FROM games WHERE status < 2
`
func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
@@ -101,6 +102,7 @@ func (q *Queries) GetActiveGames(ctx context.Context) ([]Game, error) {
&i.WinningCells,
&i.CreatedAt,
&i.UpdatedAt,
&i.RematchGameID,
); err != nil {
return nil, err
}
@@ -116,7 +118,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 FROM games WHERE id = ?
SELECT id, board, current_turn, status, winner_user_id, winning_cells, created_at, updated_at, rematch_game_id FROM games WHERE id = ?
`
func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
@@ -131,6 +133,7 @@ func (q *Queries) GetGame(ctx context.Context, id string) (Game, error) {
&i.WinningCells,
&i.CreatedAt,
&i.UpdatedAt,
&i.RematchGameID,
)
return i, err
}
@@ -171,7 +174,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 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 FROM games g
JOIN game_players gp ON g.id = gp.game_id
WHERE gp.user_id = ?
ORDER BY g.updated_at DESC
@@ -195,6 +198,7 @@ func (q *Queries) GetGamesByUserID(ctx context.Context, userID sql.NullString) (
&i.WinningCells,
&i.CreatedAt,
&i.UpdatedAt,
&i.RematchGameID,
); err != nil {
return nil, err
}
@@ -265,17 +269,18 @@ func (q *Queries) GetUserActiveGames(ctx context.Context, userID sql.NullString)
const updateGame = `-- name: UpdateGame :exec
UPDATE games
SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, updated_at = CURRENT_TIMESTAMP
SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?
`
type UpdateGameParams struct {
Board string
CurrentTurn int64
Status int64
WinnerUserID sql.NullString
WinningCells sql.NullString
ID string
Board string
CurrentTurn int64
Status int64
WinnerUserID sql.NullString
WinningCells sql.NullString
RematchGameID sql.NullString
ID string
}
func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) error {
@@ -285,6 +290,7 @@ func (q *Queries) UpdateGame(ctx context.Context, arg UpdateGameParams) error {
arg.Status,
arg.WinnerUserID,
arg.WinningCells,
arg.RematchGameID,
arg.ID,
)
return err

View File

@@ -9,14 +9,15 @@ import (
)
type Game struct {
ID string
Board string
CurrentTurn int64
Status int64
WinnerUserID sql.NullString
WinningCells sql.NullString
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
ID string
Board string
CurrentTurn int64
Status int64
WinnerUserID sql.NullString
WinningCells sql.NullString
CreatedAt sql.NullTime
UpdatedAt sql.NullTime
RematchGameID sql.NullString
}
type GamePlayer struct {

View File

@@ -0,0 +1,5 @@
-- +goose Up
ALTER TABLE games ADD COLUMN rematch_game_id TEXT;
-- +goose Down
ALTER TABLE games DROP COLUMN rematch_game_id;

View File

@@ -43,13 +43,19 @@ func (p *GamePersister) SaveGame(g *game.Game) error {
winningCells = sql.NullString{String: wc, Valid: true}
}
rematchGameID := sql.NullString{}
if g.RematchGameID != nil {
rematchGameID = sql.NullString{String: *g.RematchGameID, Valid: true}
}
return p.queries.UpdateGame(ctx, gen.UpdateGameParams{
Board: g.BoardToJSON(),
CurrentTurn: int64(g.CurrentTurn),
Status: int64(g.Status),
WinnerUserID: winnerUserID,
WinningCells: winningCells,
ID: g.ID,
Board: g.BoardToJSON(),
CurrentTurn: int64(g.CurrentTurn),
Status: int64(g.Status),
WinnerUserID: winnerUserID,
WinningCells: winningCells,
RematchGameID: rematchGameID,
ID: g.ID,
})
}
@@ -74,6 +80,10 @@ func (p *GamePersister) LoadGame(id string) (*game.Game, error) {
g.WinningCellsFromJSON(row.WinningCells.String)
}
if row.RematchGameID.Valid {
g.RematchGameID = &row.RematchGameID.String
}
return g, nil
}

View File

@@ -8,7 +8,7 @@ SELECT * FROM games WHERE id = ?;
-- name: UpdateGame :exec
UPDATE games
SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, updated_at = CURRENT_TIMESTAMP
SET board = ?, current_turn = ?, status = ?, winner_user_id = ?, winning_cells = ?, rematch_game_id = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?;
-- name: DeleteGame :exec