diff --git a/client/src/components/Results.jsx b/client/src/components/Results.jsx index b90a2e7..701f4df 100644 --- a/client/src/components/Results.jsx +++ b/client/src/components/Results.jsx @@ -12,7 +12,7 @@ import ProfileModal from './ProfileModal.jsx'; function Results({ onShowLeaderboard }) { const navigate = useNavigate(); - const { raceState, typingState, resetRace, joinPublicRace, playAgain } = useRace(); + const { raceState, typingState, resetRace, joinPublicRace } = useRace(); const { isRunning, endTutorial } = useTutorial(); const { user } = useAuth(); // State for profile modal @@ -353,20 +353,13 @@ function Results({ onShowLeaderboard }) { {raceState.type === 'practice' ? renderPracticeResults() : renderRaceResults()} - {/* Play Again button for private match host */} - {raceState.type === 'private' && raceState.completed && user?.netid === raceState.hostNetId && ( - - )} - {/* Queue Next Race button for quick matches */} {raceState.type === 'public' && ( )} - + diff --git a/client/src/context/RaceContext.jsx b/client/src/context/RaceContext.jsx index b1c6189..4d70d16 100644 --- a/client/src/context/RaceContext.jsx +++ b/client/src/context/RaceContext.jsx @@ -567,36 +567,6 @@ export const RaceProvider = ({ children }) => { resetAnticheatState(); }; - // Handle play again – host created a new lobby and all players are migrated - const handleLobbyPlayAgain = (data) => { - console.log('Play again – joining new lobby:', data.code); - resetAnticheatState(); - setTypingState({ - input: '', position: 0, correctChars: 0, errors: 0, - completed: false, wpm: 0, accuracy: 0, lockedPosition: 0 - }); - setRaceState({ - code: data.code, - type: data.type || 'private', - lobbyId: data.lobbyId, - hostNetId: data.hostNetId, - snippet: data.snippet ? { ...data.snippet, text: sanitizeSnippetText(data.snippet.text) } : null, - players: data.players || [], - startTime: null, - inProgress: false, - completed: false, - results: [], - manuallyStarted: false, - timedTest: { - enabled: data.settings?.testMode === 'timed', - duration: data.settings?.testDuration || 15 - }, - snippetFilters: data.settings?.snippetFilters || { difficulty: 'all', type: 'all', department: 'all' }, - settings: data.settings || { testMode: 'snippet', testDuration: 15 }, - countdown: null - }); - }; - // Register event listeners socket.on('race:joined', handleRaceJoined); socket.on('race:playersUpdate', handlePlayersUpdate); @@ -618,7 +588,6 @@ export const RaceProvider = ({ children }) => { socket.on('race:playerLeft', handlePlayerLeft); socket.on('anticheat:lock', handleAnticheatLock); socket.on('anticheat:reset', handleAnticheatReset); - socket.on('lobby:playAgain', handleLobbyPlayAgain); // Clean up on unmount return () => { @@ -642,7 +611,6 @@ export const RaceProvider = ({ children }) => { socket.off('race:playerLeft', handlePlayerLeft); socket.off('anticheat:lock', handleAnticheatLock); socket.off('anticheat:reset', handleAnticheatReset); - socket.off('lobby:playAgain', handleLobbyPlayAgain); socket.off('snippetNotFound', handleSnippetNotFound); // Cleanup snippet not found listener }; // Add raceState.snippet?.id to dependency array to reset typing state on snippet change @@ -1112,16 +1080,6 @@ export const RaceProvider = ({ children }) => { }); }; - const playAgain = () => { - if (!socket || !connected || !raceState.code || raceState.type !== 'private') return; - socket.emit('lobby:playAgain', { code: raceState.code }, (response) => { - if (!response.success) { - console.error('Failed to play again:', response.error); - } - // State update handled by lobby:playAgain listener - }); - }; - // joinPrivateLobby is declared earlier with useCallback to avoid TDZ const kickPlayer = (targetNetId) => { @@ -1234,7 +1192,6 @@ export const RaceProvider = ({ children }) => { kickPlayer, updateLobbySettings, startPrivateRace, - playAgain, setPlayerReady, handleInput, updateProgress, diff --git a/client/src/pages/Race.jsx b/client/src/pages/Race.jsx index bd4ebcf..512dad5 100644 --- a/client/src/pages/Race.jsx +++ b/client/src/pages/Race.jsx @@ -70,19 +70,6 @@ function Race() { navigate('/home', { replace: true }); } }, [raceState.code, navigate]); - - // After play again, navigate back to the new private lobby - useEffect(() => { - if ( - raceState.type === 'private' && - raceState.code && - !raceState.inProgress && - !raceState.completed && - raceState.countdown === null - ) { - navigate(`/lobby/${raceState.code}`, { replace: true }); - } - }, [raceState.code, raceState.type, raceState.inProgress, raceState.completed, raceState.countdown, navigate]); // Handle back button const handleBack = () => { diff --git a/server/controllers/socket-handlers.js b/server/controllers/socket-handlers.js index 9c9d27c..1ddd77d 100644 --- a/server/controllers/socket-handlers.js +++ b/server/controllers/socket-handlers.js @@ -1315,157 +1315,6 @@ const initialize = (io) => { } }); - // Handle "Play Again" for private lobbies (host only) - // Creates a new lobby with the same settings and migrates all connected players - socket.on('lobby:playAgain', async (data, callback) => { - const { user: hostNetid, userId: hostUserId } = socket.userInfo; - const { code: oldCode } = data; - - try { - console.log(`Host ${hostNetid} requesting play again for lobby ${oldCode}`); - const oldRace = activeRaces.get(oldCode); - const oldPlayers = racePlayers.get(oldCode); - - if (!oldRace || oldRace.type !== 'private') { - throw new Error('Lobby not found or not private.'); - } - - if (oldRace.hostId !== hostUserId) { - throw new Error('Only the host can start a new match.'); - } - - if (oldRace.status !== 'finished') { - throw new Error('Race has not finished yet.'); - } - - // Use previous lobby settings to generate a new snippet - const prevSettings = oldRace.settings || {}; - let snippetId = null; - let snippet = null; - - if (prevSettings.testMode === 'timed' && prevSettings.testDuration) { - const duration = parseInt(prevSettings.testDuration) || 30; - snippet = createTimedTestSnippet(duration); - } else { - const { difficulty, type, department } = prevSettings.snippetFilters || {}; - const difficultyMap = { Easy: 1, Medium: 2, Hard: 3 }; - const numericDifficulty = difficultyMap[difficulty] || null; - const category = type && type !== 'all' - ? (type === 'course_reviews' ? 'course-reviews' : type) - : null; - const subject = category === 'course-reviews' && department && department !== 'all' - ? department - : null; - const combos = []; - if (numericDifficulty != null && category && subject) combos.push({ difficulty: numericDifficulty, category, subject }); - if (numericDifficulty != null && category) combos.push({ difficulty: numericDifficulty, category }); - if (numericDifficulty != null && subject) combos.push({ difficulty: numericDifficulty, subject }); - if (numericDifficulty != null) combos.push({ difficulty: numericDifficulty }); - if (category && subject) combos.push({ category, subject }); - if (category) combos.push({ category }); - combos.push({}); - - let found = null; - for (const f of combos) { - const candidate = await SnippetModel.getRandom(f); - if (candidate) { - found = candidate; - break; - } - } - if (!found) throw new Error('Failed to load snippet for new match.'); - snippet = found; - snippetId = snippet.id; - } - - // Create a new lobby in the database - const newLobby = await RaceModel.create('private', snippetId, hostUserId); - console.log(`Created new private lobby ${newLobby.code} (play again from ${oldCode})`); - - // Build new race info in memory - const newRaceInfo = { - id: newLobby.id, - code: newLobby.code, - snippet: { - id: snippet?.id, - text: sanitizeSnippetText(snippet.text), - is_timed_test: snippet.is_timed_test || false, - duration: snippet.duration || null, - princeton_course_url: snippet.princeton_course_url || null, - course_name: snippet.course_name || null - }, - status: 'waiting', - type: 'private', - hostId: hostUserId, - hostNetId: hostNetid, - startTime: null, - settings: { ...prevSettings } - }; - activeRaces.set(newLobby.code, newRaceInfo); - - // Migrate all connected players from the old lobby to the new one - const newPlayers = []; - const connectedOldPlayers = oldPlayers || []; - - for (const player of connectedOldPlayers) { - const playerSocket = io.sockets.sockets.get(player.id); - if (!playerSocket) continue; // Skip disconnected players - - // Leave old socket room, join new one - playerSocket.leave(oldCode); - playerSocket.join(newLobby.code); - - const isHost = player.userId === hostUserId; - const newPlayer = { - id: player.id, - netid: player.netid, - userId: player.userId, - ready: isHost, // Host is implicitly ready - lobbyId: newLobby.id, - snippetId: snippetId - }; - newPlayers.push(newPlayer); - - // Add player to the new lobby in DB - try { - await RaceModel.addPlayerToLobby(newLobby.id, player.userId, isHost); - } catch (dbErr) { - console.error(`Error adding player ${player.netid} to new lobby:`, dbErr); - } - } - - racePlayers.set(newLobby.code, newPlayers); - - // Build client data for all players - const playersClientData = await Promise.all(newPlayers.map(p => getPlayerClientData(p))); - - const joinedData = { - code: newLobby.code, - type: 'private', - lobbyId: newLobby.id, - hostNetId: hostNetid, - snippet: newRaceInfo.snippet, - settings: newRaceInfo.settings, - players: playersClientData - }; - - // Notify all players in the new room about the new lobby - io.to(newLobby.code).emit('lobby:playAgain', joinedData); - - // Clean up old lobby from memory - activeRaces.delete(oldCode); - racePlayers.delete(oldCode); - - console.log(`Play again: migrated ${newPlayers.length} players from ${oldCode} to ${newLobby.code}`); - if (callback) callback({ success: true, lobby: joinedData }); - - } catch (err) { - console.error(`Error in play again for lobby ${oldCode}:`, err); - socket.emit('error', { message: err.message || 'Failed to start new match' }); - if (callback) callback({ success: false, error: err.message || 'Failed to start new match' }); - } - }); - // --- End Private Lobby Handlers --- // Handle player ready status