Files
race-game/server.js
2026-02-18 13:55:52 +00:00

301 lines
8.7 KiB
JavaScript

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'];
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 >= 8) return socket.emit('joinError', 'Room is full (max 8 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`);
});