diff --git a/backend/src/api.js b/backend/src/api.js index af2d57a..7469396 100644 --- a/backend/src/api.js +++ b/backend/src/api.js @@ -41,12 +41,17 @@ const turnSchema = Joi.object({ end_points: Joi.number().integer().min(0).required() }); +const endGameSchema = Joi.object({ + winner: Joi.number().integer().positive() +}); + const schemasByPath = { "/api/users": userSchema, "/api/games": createGameSchema, "/api/games/:id/players": addPlayerSchema, "/api/games/:id/lock": lockGameSchema, - "/api/games/:id/turns": turnSchema + "/api/games/:id/turns": turnSchema, + "/api/games/:id/end": endGameSchema, }; export function generateRequestBodies() { @@ -402,6 +407,66 @@ router.patch("/games/:id/lock", asyncHandler(async (req, res) => { } })); +/** + * @swagger + * /api/games/{id}/end: + * patch: + * summary: End a game + * parameters: + * - name: id + * in: path + * required: true + * schema: + * type: integer + * responses: + * 200: + * description: Game marked as finished + * content: + * application/json: + * example: + * message: "Game finished" + * gameId: 1 + */ +router.patch("/games/:id/end", asyncHandler(async (req,res) => { + const gameId = parseInt(req.params.id, 10); + if (isNaN(gameId)) throw new ApiError(400, "Invalid game ID"); + + const { error, value } = endGameSchema.validate(req.body); + if (error) throw new ApiError(400, error.details[0].message); + + const { winner } = value; + + const conn = await pool.getConnection(); + try { + await conn.beginTransaction(); + + const [gameRows] = await conn.query("SELECT * FROM games WHERE id = ?", [gameId]); + if (!gameRows.length) throw new ApiError(404, "Game not found"); + if (gameRows[0].is_finished) throw new ApiError(400, "Game is already finished"); + + if (winner) { + const [playerRows] = await conn.query( + "SELECT 1 FROM game_players WHERE game = ? AND user = ?", + [gameId, winner] + ); + if (!playerRows.length) throw new ApiError(400, "Winner must be a player in the game"); + } + + await conn.query( + "UPDATE games SET is_finished = TRUE, winner = ? WHERE id = ?", + [winner || null, gameId] + ); + + await conn.commit(); + res.json({ message: "Game finished", gameId }); + } catch (err) { + await conn.rollback(); + throw new ApiError(500, "Failed to finish game", err); + } finally { + conn.release(); + } +})); + /** * @swagger * /api/games/{id}/players: diff --git a/backend/src/db.js b/backend/src/db.js index ae7e269..d84d918 100644 --- a/backend/src/db.js +++ b/backend/src/db.js @@ -94,6 +94,40 @@ const migrations = [ ADD CONSTRAINT uniq_game_user UNIQUE (game, user); `); } + }, + { + name: "merge_current_games_into_games", + up: async (conn) => { + await conn.query(` + ALTER TABLE game_players + DROP FOREIGN KEY game_players_ibfk_1; + `); + + await conn.query(` + ALTER TABLE turns + DROP FOREIGN KEY turns_ibfk_1; + `); + + await conn.query(`DROP TABLE IF EXISTS games;`); + await conn.query(`ALTER TABLE current_games RENAME TO games;`); + + await conn.query(` + ALTER TABLE games + ADD COLUMN is_finished BOOL DEFAULT FALSE AFTER is_local, + ADD COLUMN winner INT DEFAULT NULL AFTER is_finished, + ADD CONSTRAINT fk_games_winner FOREIGN KEY (winner) REFERENCES users(id); + `); + + await conn.query(` + ALTER TABLE game_players + ADD CONSTRAINT fk_game_players_game FOREIGN KEY (game) REFERENCES games(id); + `); + + await conn.query(` + ALTER TABLE turns + ADD CONSTRAINT fk_turns_game FOREIGN KEY (game) REFERENCES games(id); + `); + } } ];