refactor: enhance session management and room joining logic in WebSocket handling
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s

This commit is contained in:
2025-09-04 18:22:30 +02:00
parent a63c5858f7
commit 33aa410c09
8 changed files with 199 additions and 18 deletions

View File

@@ -20,14 +20,51 @@ function drawNextTrack(room) {
export function setupWebSocket(server) {
const wss = new WebSocketServer({ server });
wss.on('connection', (ws) => {
const id = uuidv4();
const player = { id, name: `Player-${id.slice(0, 4)}`, ws, connected: true, roomId: null };
const id = uuidv4();
const sessionId = uuidv4();
let player = { id, sessionId, name: `Player-${id.slice(0, 4)}`, ws, connected: true, roomId: null };
const send = (type, payload) => { try { ws.send(JSON.stringify({ type, ...payload })); } catch {} };
send('connected', { playerId: id });
send('connected', { playerId: id, sessionId });
function isParticipant(room, pid) {
if (!room) return false;
if (room.state.turnOrder?.includes(pid)) return true;
if (room.state.timeline && Object.prototype.hasOwnProperty.call(room.state.timeline, pid)) return true;
if (room.state.tokens && Object.prototype.hasOwnProperty.call(room.state.tokens, pid)) return true;
return false;
}
ws.on('message', async (raw) => {
let msg; try { msg = JSON.parse(raw.toString()); } catch { return; }
// Allow client to resume by session token
if (msg.type === 'resume') {
const reqSession = String(msg.sessionId || '');
if (!reqSession) { send('resume_result', { ok: false, reason: 'no_session' }); return; }
let found = null; let foundRoom = null;
for (const room of rooms.values()) {
for (const p of room.players.values()) {
if (p.sessionId === reqSession) { found = p; foundRoom = room; break; }
}
if (found) break;
}
if (!found) { send('resume_result', { ok: false, reason: 'not_found' }); return; }
// Rebind socket and mark connected
try { if (found.ws && found.ws !== ws) { try { found.ws.terminate(); } catch {} } } catch {}
found.ws = ws; found.connected = true; player = found; // switch our local reference to the existing player
// If they were a participant, ensure they are not marked spectator
if (foundRoom) {
if (isParticipant(foundRoom, found.id)) {
if (foundRoom.state.spectators) delete foundRoom.state.spectators[found.id];
found.spectator = false;
}
// Notify room
broadcast(foundRoom, 'room_update', { room: roomSummary(foundRoom) });
}
send('resume_result', { ok: true, playerId: found.id, roomId: foundRoom?.id });
return;
}
// Automatic answer check (anyone can try during guess phase)
if (msg.type === 'submit_answer') {
const room = rooms.get(player.roomId);
@@ -72,8 +109,13 @@ export function setupWebSocket(server) {
if (msg.type === 'create_room') { const room = createRoom(msg.name, player); player.roomId = room.id; broadcast(room, 'room_update', { room: roomSummary(room) }); return; }
if (msg.type === 'join_room') {
const code = String(msg.code || '').toUpperCase(); const room = rooms.get(code); if (!room) return send('error', { message: 'Room not found' });
room.players.set(player.id, player); player.roomId = room.id; room.state.ready[player.id] = false;
if (room.state.status === 'playing' || room.state.status === 'ended') { room.state.spectators[player.id] = true; player.spectator = true; } else { delete room.state.spectators[player.id]; player.spectator = false; }
room.players.set(player.id, player); player.roomId = room.id; if (!room.state.ready) room.state.ready = {}; if (room.state.ready[player.id] == null) room.state.ready[player.id] = false;
const inProgress = room.state.status === 'playing' || room.state.status === 'ended';
const wasParticipant = isParticipant(room, player.id);
if (inProgress) {
if (wasParticipant) { if (room.state.spectators) delete room.state.spectators[player.id]; player.spectator = false; }
else { room.state.spectators[player.id] = true; player.spectator = true; }
} else { delete room.state.spectators[player.id]; player.spectator = false; }
broadcast(room, 'room_update', { room: roomSummary(room) }); return; }
if (msg.type === 'leave_room') {
if (!player.roomId) return; const room = rooms.get(player.roomId); if (!room) return; room.players.delete(player.id); player.roomId = null; if (room.state.ready) delete room.state.ready[player.id]; if (room.state.spectators) delete room.state.spectators[player.id]; if (room.players.size === 0) rooms.delete(room.id); else broadcast(room, 'room_update', { room: roomSummary(room) }); return; }
@@ -118,6 +160,19 @@ export function setupWebSocket(server) {
room.state.currentGuesser = nextPlayer(room.state.turnOrder, room.state.currentGuesser); room.state.phase = 'guess'; broadcast(room, 'room_update', { room: roomSummary(room) }); drawNextTrack(room); return; }
});
ws.on('close', () => {
// Mark player disconnected but keep them in the room for resume
try {
if (player) {
player.connected = false;
if (player.roomId && rooms.has(player.roomId)) {
const room = rooms.get(player.roomId);
broadcast(room, 'room_update', { room: roomSummary(room) });
}
}
} catch {}
});
ws.on('close', () => { player.connected = false; if (player.roomId && rooms.has(player.roomId)) { const room = rooms.get(player.roomId); broadcast(room, 'room_update', { room: roomSummary(room) }); } });
});
}

View File

@@ -28,6 +28,23 @@ function cleanTitleNoise(raw) {
function normalizeTitle(s) { return normalizeCommon(cleanTitleNoise(s)); }
function normalizeArtist(s) { return normalizeCommon(s).replace(/\bthe\b/g, ' ').replace(/\s+/g, ' ').trim(); }
// Produce a variant with anything inside parentheses (...) or double quotes "..." removed.
function stripOptionalSegments(raw) {
let s = String(raw);
// Remove double-quoted segments first
s = s.replace(/"[^"]*"/g, ' ');
// Remove parenthetical segments (non-nested)
s = s.replace(/\([^)]*\)/g, ' ');
// Remove square-bracket segments [ ... ] (non-nested)
s = s.replace(/\[[^\]]*\]/g, ' ');
return s;
}
function normalizeTitleBaseOptional(s) {
// Clean general noise, then drop optional quoted/parenthetical parts, then normalize
return normalizeCommon(stripOptionalSegments(cleanTitleNoise(s)));
}
function tokenize(s) { return s ? String(s).split(' ').filter(Boolean) : []; }
function tokenSet(s) { return new Set(tokenize(s)); }
function jaccard(a, b) {
@@ -79,12 +96,29 @@ const TITLE_JACCARD_THRESHOLD = 0.8;
const ARTIST_SIM_THRESHOLD = 0.82;
export function scoreTitle(guessRaw, truthRaw) {
const g = normalizeTitle(guessRaw);
const t = normalizeTitle(truthRaw);
const sim = simRatio(g, t);
const jac = jaccard(g, t);
const pass = sim >= TITLE_SIM_THRESHOLD || jac >= TITLE_JACCARD_THRESHOLD;
return { pass, sim, jac, g, t };
// Full normalized (keeps parentheses/quotes content after punctuation cleanup)
const gFull = normalizeTitle(guessRaw);
const tFull = normalizeTitle(truthRaw);
// Base normalized (treat anything in () or "" as optional and remove it)
const gBase = normalizeTitleBaseOptional(guessRaw);
const tBase = normalizeTitleBaseOptional(truthRaw);
const pairs = [
[gFull, tFull],
[gFull, tBase],
[gBase, tFull],
[gBase, tBase],
];
let bestSim = 0; let bestJac = 0; let pass = false; let bestPair = pairs[0];
for (const [g, t] of pairs) {
const sim = simRatio(g, t);
const jac = jaccard(g, t);
if (sim >= TITLE_SIM_THRESHOLD || jac >= TITLE_JACCARD_THRESHOLD) pass = true;
if (sim > bestSim || (sim === bestSim && jac > bestJac)) { bestSim = sim; bestJac = jac; bestPair = [g, t]; }
}
return { pass, sim: bestSim, jac: bestJac, g: bestPair[0], t: bestPair[1] };
}
export function scoreArtist(guessRaw, truthArtistsRaw, primaryCount) {