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