This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
'use strict';
|
||||
const path = require('path');
|
||||
const http = require('http');
|
||||
const crypto = require('crypto');
|
||||
const express = require('express');
|
||||
const QRCode = require('qrcode');
|
||||
const { Server } = require('socket.io');
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const app = express();
|
||||
const server = http.createServer(app);
|
||||
const io = new Server(server);
|
||||
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
app.get('/healthz', (_req, res) => res.send('ok'));
|
||||
app.get('/qr.svg', async (req, res) => {
|
||||
const text = String(req.query.text || '').slice(0, 500);
|
||||
if (!text) return res.status(400).end();
|
||||
const svg = await QRCode.toString(text, { type: 'svg', margin: 1, errorCorrectionLevel: 'M' });
|
||||
res.type('image/svg+xml').send(svg);
|
||||
});
|
||||
|
||||
/* ---------- État des rooms (en mémoire) ---------- */
|
||||
// phases : lobby | playing | buzzed | result | ended
|
||||
const rooms = new Map();
|
||||
const ROOM_RE = /^[A-Z0-9]{4,8}$/;
|
||||
const TOKEN_RE = /^[A-Za-z0-9-]{8,40}$/;
|
||||
const CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
||||
|
||||
function newRoomId() {
|
||||
let id;
|
||||
do {
|
||||
id = Array.from({ length: 5 }, () => CHARS[crypto.randomInt(CHARS.length)]).join('');
|
||||
} while (rooms.has(id));
|
||||
return id;
|
||||
}
|
||||
function createRoom(id) {
|
||||
const room = { id, phase: 'lobby', players: new Map(), buzzer: null, lockedOut: new Set(), lastActive: Date.now() };
|
||||
rooms.set(id, room);
|
||||
return room;
|
||||
}
|
||||
function publicState(room) {
|
||||
return {
|
||||
phase: room.phase,
|
||||
buzzer: room.buzzer,
|
||||
lockedOut: [...room.lockedOut],
|
||||
players: [...room.players.values()].map(p => ({ id: p.id, name: p.name, score: p.score, connected: p.connected })),
|
||||
};
|
||||
}
|
||||
function broadcast(room) {
|
||||
room.lastActive = Date.now();
|
||||
io.to(room.id).emit('state', publicState(room));
|
||||
}
|
||||
function masterRoom(socket) {
|
||||
if (socket.data.role !== 'master') return null;
|
||||
return rooms.get(socket.data.roomId) || null;
|
||||
}
|
||||
|
||||
io.on('connection', socket => {
|
||||
/* ----- Master ----- */
|
||||
socket.on('master:create', (data, ack) => {
|
||||
ack = typeof ack === 'function' ? ack : () => {};
|
||||
let id = String((data && data.roomId) || '').toUpperCase();
|
||||
let room = ROOM_RE.test(id) ? rooms.get(id) : null;
|
||||
if (!room) room = createRoom(ROOM_RE.test(id) ? id : newRoomId());
|
||||
socket.data.role = 'master';
|
||||
socket.data.roomId = room.id;
|
||||
socket.join(room.id);
|
||||
ack({ roomId: room.id });
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('master:start', () => {
|
||||
const room = masterRoom(socket);
|
||||
if (!room || room.phase === 'ended') return;
|
||||
room.phase = 'playing';
|
||||
room.buzzer = null;
|
||||
room.lockedOut.clear();
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('master:judge', correct => {
|
||||
const room = masterRoom(socket);
|
||||
if (!room || room.phase !== 'buzzed' || !room.buzzer) return;
|
||||
const player = [...room.players.values()].find(p => p.id === room.buzzer.id);
|
||||
if (correct) {
|
||||
if (player) player.score += 1;
|
||||
room.phase = 'result'; // le buzzer reste affiché comme gagnant
|
||||
} else {
|
||||
room.lockedOut.add(room.buzzer.id);
|
||||
room.buzzer = null;
|
||||
room.phase = 'playing'; // la vidéo reprend, ce joueur est bloqué pour ce morceau
|
||||
}
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('master:next', () => {
|
||||
const room = masterRoom(socket);
|
||||
if (!room || room.phase === 'ended') return;
|
||||
room.phase = 'lobby';
|
||||
room.buzzer = null;
|
||||
room.lockedOut.clear();
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('master:end', () => {
|
||||
const room = masterRoom(socket);
|
||||
if (!room) return;
|
||||
room.phase = 'ended';
|
||||
room.buzzer = null;
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('master:newgame', () => {
|
||||
const room = masterRoom(socket);
|
||||
if (!room) return;
|
||||
room.players.forEach(p => { p.score = 0; });
|
||||
room.phase = 'lobby';
|
||||
room.buzzer = null;
|
||||
room.lockedOut.clear();
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
/* ----- Joueurs ----- */
|
||||
socket.on('player:join', (data, ack) => {
|
||||
ack = typeof ack === 'function' ? ack : () => {};
|
||||
data = data || {};
|
||||
const room = rooms.get(String(data.roomId || '').toUpperCase());
|
||||
if (!room) return ack({ error: 'no_room' });
|
||||
|
||||
const token = TOKEN_RE.test(data.token || '') ? data.token : null;
|
||||
let player = token ? room.players.get(token) : null;
|
||||
if (!player) {
|
||||
const name = String(data.name || '').trim().replace(/\s+/g, ' ').slice(0, 20);
|
||||
if (!name) return ack({ error: 'no_name' });
|
||||
const taken = [...room.players.values()].some(p => p.name.toLowerCase() === name.toLowerCase());
|
||||
if (taken) return ack({ error: 'name_taken' });
|
||||
const t = token || crypto.randomUUID();
|
||||
player = { id: crypto.randomBytes(4).toString('hex'), token: t, name, score: 0, connected: true };
|
||||
room.players.set(t, player);
|
||||
}
|
||||
player.connected = true;
|
||||
socket.data.role = 'player';
|
||||
socket.data.roomId = room.id;
|
||||
socket.data.token = player.token;
|
||||
socket.join(room.id);
|
||||
ack({ token: player.token, id: player.id, name: player.name });
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('player:buzz', () => {
|
||||
if (socket.data.role !== 'player') return;
|
||||
const room = rooms.get(socket.data.roomId);
|
||||
const player = room && room.players.get(socket.data.token);
|
||||
if (!room || !player) return;
|
||||
if (room.phase !== 'playing' || room.lockedOut.has(player.id)) return;
|
||||
room.phase = 'buzzed'; // le premier message reçu gagne
|
||||
room.buzzer = { id: player.id, name: player.name };
|
||||
broadcast(room);
|
||||
});
|
||||
|
||||
socket.on('disconnect', () => {
|
||||
if (socket.data.role !== 'player') return;
|
||||
const room = rooms.get(socket.data.roomId);
|
||||
const player = room && room.players.get(socket.data.token);
|
||||
if (player) {
|
||||
player.connected = false;
|
||||
broadcast(room);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Nettoyage des rooms inactives (12 h)
|
||||
setInterval(() => {
|
||||
const limit = Date.now() - 12 * 3600 * 1000;
|
||||
for (const [id, room] of rooms) if (room.lastActive < limit) rooms.delete(id);
|
||||
}, 30 * 60 * 1000).unref();
|
||||
|
||||
server.listen(PORT, () => console.log(`Blind test sur le port ${PORT}`));
|
||||
Reference in New Issue
Block a user