const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const path = require('path'); const riddles = require('./riddles'); const app = express(); const server = http.createServer(app); const io = new Server(server); app.use(express.static(path.join(__dirname, 'public'))); const FINISH_LINE = 10; const COLORS = ['#e74c3c', '#3498db', '#2ecc71', '#f39c12', '#9b59b6', '#1abc9c', '#e67e22', '#e91e63', '#ff6b6b', '#54a0ff', '#5f27cd', '#01CBC6', '#ffd32a', '#0be881', '#f8b739']; const rooms = {}; function genRoomId() { const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'; let id = ''; for (let i = 0; i < 6; i++) id += chars[Math.floor(Math.random() * chars.length)]; return id; } function shuffle(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function getOrdinal(n) { const s = ['th', 'st', 'nd', 'rd']; const v = n % 100; return n + (s[(v - 20) % 10] || s[v] || s[0]); } function normalizeAnswer(ans) { return ans.toLowerCase().trim().replace(/^(a |an |the )/, '').replace(/[^a-z0-9\s]/g, '').replace(/\s+/g, ' '); } function checkAnswer(playerAns, riddleIndex) { const riddle = riddles[riddleIndex]; const norm = normalizeAnswer(playerAns); return riddle.answers.some(a => normalizeAnswer(a) === norm); } function publicPlayers(room) { return Object.entries(room.players).map(([id, p]) => ({ id, name: p.name, position: p.position, color: p.color, finished: p.finished, finishPosition: p.finishPosition })); } io.on('connection', (socket) => { let roomId = null; socket.on('createRoom', ({ name }) => { if (!name || name.trim().length === 0) return; roomId = genRoomId(); // Make sure room ID is unique while (rooms[roomId]) roomId = genRoomId(); const color = COLORS[0]; rooms[roomId] = { host: socket.id, state: 'lobby', finishCount: 0, players: { [socket.id]: { name: name.trim().slice(0, 20), color, position: 0, queue: shuffle(Array.from({ length: riddles.length }, (_, i) => i)), queuePos: 0, finished: false, finishPosition: null } } }; socket.join(roomId); socket.emit('roomCreated', { roomId }); socket.emit('lobbyState', { players: publicPlayers(rooms[roomId]), isHost: true, roomId }); }); socket.on('joinRoom', ({ name, code }) => { if (!name || name.trim().length === 0) return socket.emit('joinError', 'Please enter your name.'); const upperCode = (code || '').trim().toUpperCase(); const room = rooms[upperCode]; if (!room) return socket.emit('joinError', 'Room not found. Check the code and try again.'); if (room.state !== 'lobby') return socket.emit('joinError', 'That race already started. Wait for the next one!'); if (Object.keys(room.players).length >= 15) return socket.emit('joinError', 'Room is full (max 15 racers).'); const usedColors = new Set(Object.values(room.players).map(p => p.color)); const color = COLORS.find(c => !usedColors.has(c)) || COLORS[Object.keys(room.players).length % COLORS.length]; room.players[socket.id] = { name: name.trim().slice(0, 20), color, position: 0, queue: shuffle(Array.from({ length: riddles.length }, (_, i) => i)), queuePos: 0, finished: false, finishPosition: null }; roomId = upperCode; socket.join(upperCode); socket.emit('joinedRoom', { roomId: upperCode }); socket.emit('lobbyState', { players: publicPlayers(room), isHost: false, roomId: upperCode }); // Tell everyone else a new racer joined socket.to(upperCode).emit('playerUpdate', { players: publicPlayers(room), message: `${room.players[socket.id].name} joined the race!` }); }); socket.on('startGame', () => { if (!roomId) return; const room = rooms[roomId]; if (!room || room.host !== socket.id || room.state !== 'lobby') return; if (Object.keys(room.players).length < 1) return; room.state = 'playing'; // Send each player their first riddle individually Object.entries(room.players).forEach(([pid, player]) => { const riddleIdx = player.queue[player.queuePos]; io.to(pid).emit('gameStarted', { players: publicPlayers(room), riddle: { question: riddles[riddleIdx].question, hint: riddles[riddleIdx].hint }, riddleNum: 1, total: FINISH_LINE }); }); }); socket.on('submitAnswer', ({ answer }) => { if (!roomId) return; const room = rooms[roomId]; if (!room || room.state !== 'playing') return; const player = room.players[socket.id]; if (!player || player.finished) return; if (!answer || answer.trim().length === 0) return; const riddleIdx = player.queue[player.queuePos]; if (!checkAnswer(answer, riddleIdx)) { return socket.emit('answerResult', { correct: false }); } // Correct answer! player.position++; player.queuePos++; if (player.position >= FINISH_LINE) { room.finishCount++; player.finished = true; player.finishPosition = room.finishCount; // Update track for all io.to(roomId).emit('positionUpdate', { players: publicPlayers(room) }); // Announce finish to all io.to(roomId).emit('playerFinished', { name: player.name, place: player.finishPosition }); // Tell this player they're done socket.emit('answerResult', { correct: true, finished: true, finishPosition: player.finishPosition }); // Check if everyone is done if (Object.values(room.players).every(p => p.finished)) { room.state = 'finished'; const results = Object.values(room.players) .sort((a, b) => a.finishPosition - b.finishPosition) .map(p => ({ name: p.name, color: p.color, place: p.finishPosition })); setTimeout(() => io.to(roomId).emit('gameOver', { results }), 1500); } } else { const nextRiddleIdx = player.queue[player.queuePos]; const nextRiddle = riddles[nextRiddleIdx]; // Update track for all io.to(roomId).emit('positionUpdate', { players: publicPlayers(room) }); // Send next riddle to this player socket.emit('answerResult', { correct: true, finished: false, riddle: { question: nextRiddle.question, hint: nextRiddle.hint }, riddleNum: player.position + 1, total: FINISH_LINE }); } }); socket.on('getHint', () => { if (!roomId) return; const room = rooms[roomId]; if (!room || room.state !== 'playing') return; const player = room.players[socket.id]; if (!player || player.finished) return; const riddle = riddles[player.queue[player.queuePos]]; socket.emit('hint', { hint: riddle.hint }); }); socket.on('playAgain', () => { if (!roomId) return; const room = rooms[roomId]; if (!room || room.host !== socket.id) return; room.state = 'lobby'; room.finishCount = 0; Object.values(room.players).forEach(p => { p.position = 0; p.queue = shuffle(Array.from({ length: riddles.length }, (_, i) => i)); p.queuePos = 0; p.finished = false; p.finishPosition = null; }); io.to(roomId).emit('returnToLobby', { players: publicPlayers(room) }); }); socket.on('disconnect', () => { if (!roomId || !rooms[roomId]) return; const room = rooms[roomId]; const player = room.players[socket.id]; if (!player) return; const name = player.name; const wasHost = room.host === socket.id; delete room.players[socket.id]; if (Object.keys(room.players).length === 0) { delete rooms[roomId]; return; } if (wasHost) { room.host = Object.keys(room.players)[0]; } io.to(roomId).emit('playerUpdate', { players: publicPlayers(room), message: `${name} left the race.`, newHost: wasHost ? room.host : null }); // If game was in progress and all remaining players finished, end it if (room.state === 'playing') { const allDone = Object.values(room.players).every(p => p.finished); if (allDone && Object.keys(room.players).length > 0) { room.state = 'finished'; const results = Object.values(room.players) .sort((a, b) => a.finishPosition - b.finishPosition) .map(p => ({ name: p.name, color: p.color, place: p.finishPosition })); io.to(roomId).emit('gameOver', { results }); } } }); }); const PORT = process.env.PORT || 3000; server.listen(PORT, () => { console.log(`\n🏎️ Riddle Racer is running!`); console.log(` Open http://localhost:${PORT} in your browser\n`); });