game_room system added + readme changelog

This commit is contained in:
gprunet
2026-01-17 16:13:13 +01:00
parent 18b6a6b84f
commit b5feb08e69
6 changed files with 278 additions and 2 deletions
+22 -1
View File
@@ -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
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
+38
View File
@@ -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!');
}
+3 -1
View File
@@ -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, () =>
+2
View File
@@ -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;
+98
View File
@@ -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;
+115
View File
@@ -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
};