@@ -1315,6 +1315,157 @@ const initialize = (io) => {
13151315 }
13161316 } ) ;
13171317
1318+ // Handle "Play Again" for private lobbies (host only)
1319+ // Creates a new lobby with the same settings and migrates all connected players
1320+ socket . on ( 'lobby:playAgain' , async ( data , callback ) => {
1321+ const { user : hostNetid , userId : hostUserId } = socket . userInfo ;
1322+ const { code : oldCode } = data ;
1323+
1324+ try {
1325+ console . log ( `Host ${ hostNetid } requesting play again for lobby ${ oldCode } ` ) ;
1326+ const oldRace = activeRaces . get ( oldCode ) ;
1327+ const oldPlayers = racePlayers . get ( oldCode ) ;
1328+
1329+ if ( ! oldRace || oldRace . type !== 'private' ) {
1330+ throw new Error ( 'Lobby not found or not private.' ) ;
1331+ }
1332+
1333+ if ( oldRace . hostId !== hostUserId ) {
1334+ throw new Error ( 'Only the host can start a new match.' ) ;
1335+ }
1336+
1337+ if ( oldRace . status !== 'finished' ) {
1338+ throw new Error ( 'Race has not finished yet.' ) ;
1339+ }
1340+
1341+ // Use previous lobby settings to generate a new snippet
1342+ const prevSettings = oldRace . settings || { } ;
1343+ let snippetId = null ;
1344+ let snippet = null ;
1345+
1346+ if ( prevSettings . testMode === 'timed' && prevSettings . testDuration ) {
1347+ const duration = parseInt ( prevSettings . testDuration ) || 30 ;
1348+ snippet = createTimedTestSnippet ( duration ) ;
1349+ } else {
1350+ const { difficulty, type, department } = prevSettings . snippetFilters || { } ;
1351+ const difficultyMap = { Easy : 1 , Medium : 2 , Hard : 3 } ;
1352+ const numericDifficulty = difficultyMap [ difficulty ] || null ;
1353+ const category = type && type !== 'all'
1354+ ? ( type === 'course_reviews' ? 'course-reviews' : type )
1355+ : null ;
1356+ const subject = category === 'course-reviews' && department && department !== 'all'
1357+ ? department
1358+ : null ;
1359+ const combos = [ ] ;
1360+ if ( numericDifficulty != null && category && subject ) combos . push ( { difficulty : numericDifficulty , category, subject } ) ;
1361+ if ( numericDifficulty != null && category ) combos . push ( { difficulty : numericDifficulty , category } ) ;
1362+ if ( numericDifficulty != null && subject ) combos . push ( { difficulty : numericDifficulty , subject } ) ;
1363+ if ( numericDifficulty != null ) combos . push ( { difficulty : numericDifficulty } ) ;
1364+ if ( category && subject ) combos . push ( { category, subject } ) ;
1365+ if ( category ) combos . push ( { category } ) ;
1366+ combos . push ( { } ) ;
1367+
1368+ let found = null ;
1369+ for ( const f of combos ) {
1370+ const candidate = await SnippetModel . getRandom ( f ) ;
1371+ if ( candidate ) {
1372+ found = candidate ;
1373+ break ;
1374+ }
1375+ }
1376+ if ( ! found ) throw new Error ( 'Failed to load snippet for new match.' ) ;
1377+ snippet = found ;
1378+ snippetId = snippet . id ;
1379+ }
1380+
1381+ // Create a new lobby in the database
1382+ const newLobby = await RaceModel . create ( 'private' , snippetId , hostUserId ) ;
1383+ console . log ( `Created new private lobby ${ newLobby . code } (play again from ${ oldCode } )` ) ;
1384+
1385+ // Build new race info in memory
1386+ const newRaceInfo = {
1387+ id : newLobby . id ,
1388+ code : newLobby . code ,
1389+ snippet : {
1390+ id : snippet ?. id ,
1391+ text : sanitizeSnippetText ( snippet . text ) ,
1392+ is_timed_test : snippet . is_timed_test || false ,
1393+ duration : snippet . duration || null ,
1394+ princeton_course_url : snippet . princeton_course_url || null ,
1395+ course_name : snippet . course_name || null
1396+ } ,
1397+ status : 'waiting' ,
1398+ type : 'private' ,
1399+ hostId : hostUserId ,
1400+ hostNetId : hostNetid ,
1401+ startTime : null ,
1402+ settings : { ...prevSettings }
1403+ } ;
1404+ activeRaces . set ( newLobby . code , newRaceInfo ) ;
1405+
1406+ // Migrate all connected players from the old lobby to the new one
1407+ const newPlayers = [ ] ;
1408+ const connectedOldPlayers = oldPlayers || [ ] ;
1409+
1410+ for ( const player of connectedOldPlayers ) {
1411+ const playerSocket = io . sockets . sockets . get ( player . id ) ;
1412+ if ( ! playerSocket ) continue ; // Skip disconnected players
1413+
1414+ // Leave old socket room, join new one
1415+ playerSocket . leave ( oldCode ) ;
1416+ playerSocket . join ( newLobby . code ) ;
1417+
1418+ const isHost = player . userId === hostUserId ;
1419+ const newPlayer = {
1420+ id : player . id ,
1421+ netid : player . netid ,
1422+ userId : player . userId ,
1423+ ready : isHost , // Host is implicitly ready
1424+ lobbyId : newLobby . id ,
1425+ snippetId : snippetId
1426+ } ;
1427+ newPlayers . push ( newPlayer ) ;
1428+
1429+ // Add player to the new lobby in DB
1430+ try {
1431+ await RaceModel . addPlayerToLobby ( newLobby . id , player . userId , isHost ) ;
1432+ } catch ( dbErr ) {
1433+ console . error ( `Error adding player ${ player . netid } to new lobby:` , dbErr ) ;
1434+ }
1435+ }
1436+
1437+ racePlayers . set ( newLobby . code , newPlayers ) ;
1438+
1439+ // Build client data for all players
1440+ const playersClientData = await Promise . all ( newPlayers . map ( p => getPlayerClientData ( p ) ) ) ;
1441+
1442+ const joinedData = {
1443+ code : newLobby . code ,
1444+ type : 'private' ,
1445+ lobbyId : newLobby . id ,
1446+ hostNetId : hostNetid ,
1447+ snippet : newRaceInfo . snippet ,
1448+ settings : newRaceInfo . settings ,
1449+ players : playersClientData
1450+ } ;
1451+
1452+ // Notify all players in the new room about the new lobby
1453+ io . to ( newLobby . code ) . emit ( 'lobby:playAgain' , joinedData ) ;
1454+
1455+ // Clean up old lobby from memory
1456+ activeRaces . delete ( oldCode ) ;
1457+ racePlayers . delete ( oldCode ) ;
1458+
1459+ console . log ( `Play again: migrated ${ newPlayers . length } players from ${ oldCode } to ${ newLobby . code } ` ) ;
1460+ if ( callback ) callback ( { success : true , lobby : joinedData } ) ;
1461+
1462+ } catch ( err ) {
1463+ console . error ( `Error in play again for lobby ${ oldCode } :` , err ) ;
1464+ socket . emit ( 'error' , { message : err . message || 'Failed to start new match' } ) ;
1465+ if ( callback ) callback ( { success : false , error : err . message || 'Failed to start new match' } ) ;
1466+ }
1467+ } ) ;
1468+
13181469 // --- End Private Lobby Handlers ---
13191470
13201471 // Handle player ready status
0 commit comments