From b5feb08e6919a7851b48f7f1296ae6abb6aa1624 Mon Sep 17 00:00:00 2001 From: gprunet Date: Sat, 17 Jan 2026 16:13:13 +0100 Subject: [PATCH] game_room system added + readme changelog --- README.md | 23 +++++- srcs/backend/db.js | 38 ++++++++++ srcs/backend/index.js | 4 +- srcs/backend/middleware/auth.js | 2 + srcs/backend/routes/game_room.js | 98 ++++++++++++++++++++++++ srcs/backend/services/game_room.js | 115 +++++++++++++++++++++++++++++ 6 files changed, 278 insertions(+), 2 deletions(-) create mode 100644 srcs/backend/routes/game_room.js create mode 100644 srcs/backend/services/game_room.js diff --git a/README.md b/README.md index 36b47ab..299b401 100644 --- a/README.md +++ b/README.md @@ -24,4 +24,25 @@ Gestion de friendship dans POSTGRESQL: Ressource: https://www.postgresql.org/docs/ https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps - https://docs.github.com/fr/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app \ No newline at end of file + https://docs.github.com/fr/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app + +///////////////////////////////////////////////////////////////////////////////////////// + +BACKEND + +17/01 Ajout du service/route pour le systeme de game_room + permet aux joueurs de creer et rejoindre des rooms + une room vide est automatiquement detruite. + Presence d'une fonction affichant toutes les rooms joignables + ainsi qu'une autre fonction pour afficher tous les joueurs de la room avec + leur scores et leur etat actuel. + Aucun moyen de changer l'etat de la room de waiting a en cours ou finished + ca attendra le systeme du jeu + +DATABASE + +17/01 Ajout des tables game_rooms, game_players, game_rounds, words + - nom, status et parametres de la game + - joueurs dans la game, leur scores et leur role actuel (dessinateur, devineur) + - historique de la game, qui a dessine quoi precedemment ainsi que les timers des rounds, sera aussi utile si on veut faire les stats de compte a l'avenir. + - contient la liste des mots utilisable par les joueurs \ No newline at end of file diff --git a/srcs/backend/db.js b/srcs/backend/db.js index 09ffbfb..1d346fa 100644 --- a/srcs/backend/db.js +++ b/srcs/backend/db.js @@ -69,6 +69,44 @@ async function createTables() created_at TIMESTAMP DEFAULT NOW(), UNIQUE(provider, client_id) ); + + CREATE TABLE IF NOT EXISTS game_rooms ( + id SERIAL PRIMARY KEY, + name VARCHAR(100) NOT NULL, + status VARCHAR(20) DEFAULT 'waiting', + max_players INT DEFAULT 8, + current_round INT DEFAULT 0, + max_rounds INT DEFAULT 3, + round_duration INT DEFAULT 90, + created_at TIMESTAMP DEFAULT NOW(), + started_at TIMESTAMP, + ended_at TIMESTAMP + ); + + CREATE TABLE IF NOT EXISTS game_players ( + id SERIAL PRIMARY KEY, + room_id INT REFERENCES game_rooms(id) ON DELETE CASCADE, + user_id INT REFERENCES users(id) ON DELETE CASCADE, + score INT DEFAULT 0, + is_drawing BOOLEAN DEFAULT FALSE, + joined_at TIMESTAMP DEFAULT NOW(), + UNIQUE(room_id, user_id) + ); + + CREATE TABLE IF NOT EXISTS words ( + id SERIAL PRIMARY KEY, + word VARCHAR(50) NOT NULL UNIQUE + ); + + CREATE TABLE IF NOT EXISTS game_rounds ( + id SERIAL PRIMARY KEY, + room_id INT REFERENCES game_rooms(id) ON DELETE CASCADE, + round_number INT NOT NULL, + word_id INT REFERENCES words(id), + drawer_id INT REFERENCES users(id), + started_at TIMESTAMP DEFAULT NOW(), + ended_at TIMESTAMP + ); `); console.log('Tables created!'); } diff --git a/srcs/backend/index.js b/srcs/backend/index.js index f97351b..790cf8d 100644 --- a/srcs/backend/index.js +++ b/srcs/backend/index.js @@ -4,13 +4,14 @@ import cors from 'cors'; import {Server} from 'socket.io'; import authRouter from './routes/auth.js'; import chatRouter from './routes/global_chat.js'; +import gameRoomRouter from './routes/game_room.js'; import {waitForDb, createTables, ensureOauthClient} from './db.js'; import setupSocketIO from './services/socket.js'; const app = express(); const server = http.createServer(app); const io = new Server(server, - { +{ cors: { origin: "*", @@ -37,6 +38,7 @@ async function startServer() app.use('/api/auth', authRouter); app.use('/api/global_chat', chatRouter); + app.use('/api/rooms', gameRoomRouter); app.get('/api', (req, res) => res.send('Backend running')); server.listen(3001, () => diff --git a/srcs/backend/middleware/auth.js b/srcs/backend/middleware/auth.js index 842c16f..c86cc0c 100644 --- a/srcs/backend/middleware/auth.js +++ b/srcs/backend/middleware/auth.js @@ -1,5 +1,7 @@ import jwt from 'jsonwebtoken'; + +//Check si le webtoken est valide export default function authMiddleware(req, res, next) { const header = req.headers.authorization; diff --git a/srcs/backend/routes/game_room.js b/srcs/backend/routes/game_room.js new file mode 100644 index 0000000..925bdc4 --- /dev/null +++ b/srcs/backend/routes/game_room.js @@ -0,0 +1,98 @@ +import express from 'express'; +import gameRoomService from '../services/game_room.js'; +import authenticateToken from '../middleware/auth.js'; +const router = express.Router(); + +router.get('/', authenticateToken, async(req, res) => +{ + try + { + const rooms = await gameRoomService.listActiveRooms(); + res.json(rooms); + } + catch (err) + { + console.error(err); + res.status(500).json({error: 'Server error'}); + } +}); + +router.get('/:roomId', authenticateToken, async(req, res) => +{ + try + { + const room = await gameRoomService.getRoomById(req.params.roomId); + if (!room) + return (res.status(404).json({error: 'Room not found'})); + res.json(room); + } + catch(err) + { + console.error(err); + res.status(500).json({error: 'Server error'}); + } +}); + +router.get('/:roomId/players', authenticateToken, async(req, res) => +{ + try + { + const players = await gameRoomService.getRoomPlayers(req.params.roomId); + res.json(players); + } + catch(err) + { + console.error(err); + res.status(500).json({error: 'Server error'}); + } +}); + +router.post('/', authenticateToken, async(req, res) => +{ + try + { + const {name} = req.body; + if (!name) + return (res.status(400).json({error: 'Room name required'})); + const room = await gameRoomService.createRoom(name); + res.status(201).json(room); + } + catch(err) + { + console.error(err); + res.status(500).json({error: 'Server error'}); + } +}); + +router.post('/:roomId/join', authenticateToken, async(req, res) => +{ + try + { + const player = await gameRoomService.joinRoom(req.params.roomId, req.user.userId); + res.json(player); + } + catch(err) + { + console.error(err); + if (err.message.includes('full') || err.message.includes('already')) + res.status(400).json({error: err.message}); + else + res.status(500).json({error: err.message}); + } +}); + +router.post('/:roomId/leave', authenticateToken, async(req, res) => +{ + try + { + await gameRoomService.leaveRoom(req.params.roomId, req.user.userId); + res.json({message: 'Left room successfully'}); + } + catch(err) + { + console.error(err); + res.status(500).json({error: 'Server error'}); + } +}); + +export default router; \ No newline at end of file diff --git a/srcs/backend/services/game_room.js b/srcs/backend/services/game_room.js new file mode 100644 index 0000000..51d40d6 --- /dev/null +++ b/srcs/backend/services/game_room.js @@ -0,0 +1,115 @@ +import {query} from '../db.js'; + +// Creer la room avec comme seul parametre le nom +// max_players, status et ses autres variables sont aux valeurs definis dans db.js +async function createRoom(name) +{ + const result = await query + ( + `INSERT INTO game_rooms (name) VALUES ($1) RETURNING *`, + [name] + ); + return (result.rows[0]); +} + +async function getRoomById(roomId) +{ + const result = await query + ( + 'SELECT * FROM game_rooms WHERE id = $1', + [roomId] + ); + return (result.rows[0]); +} + +//Liste toutes les rooms en attente +//ainsi que le nombre de joueurs dans chaque room +//utile pour montrer toutes les rooms joignables +async function listActiveRooms() +{ + const result = await query + ( + `SELECT r.*, COUNT(p.id) as player_count + FROM game_rooms r + LEFT JOIN game_players p ON r.id = p.room_id + WHERE r.status = 'waiting' + GROUP BY r.id + ORDER BY r.created_at DESC` + ); + return (result.rows); +} + +async function joinRoom(roomId, userId) +{ + const room = await getRoomById(roomId); + if (!room) + throw new Error('Room not found'); + + const playerCount = await query + ( + 'SELECT COUNT(*) FROM game_players WHERE room_id = $1', + [roomId] + ); + + if (parseInt(playerCount.rows[0].count) >= 8) + throw new Error('Room is full'); + if (room.status !== 'waiting') + throw new Error('Game already started or ended'); + const result = await query + ( + 'INSERT INTO game_players (room_id, user_id) VALUES ($1, $2) RETURNING *', + [roomId, userId] + ); + return (result.rows[0]); +} + +async function leaveRoom(roomId, userId) +{ + await query + ( + 'DELETE FROM game_players WHERE room_id = $1 AND user_id = $2', + [roomId, userId] + ); + + const playerCount = await query + ( + 'SELECT COUNT(*) FROM game_players WHERE room_id = $1', + [roomId] + ); + + if (parseInt(playerCount.rows[0].count) === 0) + { + await query + ( + 'DELETE FROM game_rooms WHERE id = $1', + [roomId] + ); + } +} + +//Renvoie la liste des joueurs trie selon leur score +//Cette liste donne egalement l'info sur qui dessine actuellement +//Utile pour le jeu en lui meme et le scoreboard de la game +async function getRoomPlayers(roomId) +{ + const result = await query + ( + `SELECT gp.*, u.username + FROM game_players gp + JOIN users u ON gp.user_id = u.id + WHERE gp.room_id = $1 + ORDER BY gp.score DESC`, + [roomId] + ); + return (result.rows); +} + +export default +{ + createRoom, + getRoomById, + listActiveRooms, + joinRoom, + leaveRoom, + getRoomPlayers +}; \ No newline at end of file