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
+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
};