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
All checks were successful
Build and Push Docker Image / docker (push) Successful in 9s
This commit is contained in:
@@ -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) }); } });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user