import { useState, useEffect } from 'react'; import { ArrowLeft, Phone, Play, Pause, Trophy, Users, Crown, Copy, Volume2, UserMinus } from 'lucide-react'; import Ticket from './Ticket'; import NumberBoard from './NumberBoard'; import Chat from './Chat'; import { GAME_MODES, checkTicketWin, playSound, calculatePlayerProgress } from '../utils/gameUtils'; function Game({ socket, playerData, roomData, playerTicket, onLeaveRoom, initialCalledNumbers = [], initialCurrentNumber = null, initialGameMode = null, isConnected = true, connectionHealth = 'good', reconnectAttempts = 0, onShowResults }) { const [localTicket, setLocalTicket] = useState(null); const [calledNumbers, setCalledNumbers] = useState(initialCalledNumbers); const [currentNumber, setCurrentNumber] = useState(initialCurrentNumber); const [gameMode, setGameMode] = useState(initialGameMode || roomData?.gameMode); // NEW: Multi-mode support const [activeGameModes, setActiveGameModes] = useState(roomData?.activeGameModes || null); const [winnersByMode, setWinnersByMode] = useState(roomData?.winnersByMode || {}); const [claimableWins, setClaimableWins] = useState({}); // Track which modes the player can claim const [autoCall, setAutoCall] = useState(false); const [autoCallInterval, setAutoCallInterval] = useState(10); // Default 10 seconds const [showIntervalSetting, setShowIntervalSetting] = useState(false); const [canClaimWin, setCanClaimWin] = useState(false); const [winners, setWinners] = useState([]); const [celebrationWinner, setCelebrationWinner] = useState(null); const [toastNotifications, setToastNotifications] = useState([]); const [notificationTimeouts, setNotificationTimeouts] = useState(new Map()); const [showRankingPanel, setShowRankingPanel] = useState(true); const [rankingPulse, setRankingPulse] = useState(false); const [requiredWinners, setRequiredWinners] = useState(3); const [showRoomCode, setShowRoomCode] = useState(false); const [playerProgress, setPlayerProgress] = useState(new Map()); // Store other players' progress // HOST SUPERPOWERS - New state variables const [isPaused, setIsPaused] = useState(false); const [showPlayerManagement, setShowPlayerManagement] = useState(false); const [showTransferHost, setShowTransferHost] = useState(false); const [showKickConfirm, setShowKickConfirm] = useState(false); const [selectedPlayerToKick, setSelectedPlayerToKick] = useState(null); const [selectedPlayerForHost, setSelectedPlayerForHost] = useState(null); const [hostActionLoading, setHostActionLoading] = useState(false); const isHost = roomData?.players?.find(p => p.id === playerData?.playerId)?.isHost; const currentPlayer = roomData?.players?.find(p => p.id === playerData?.playerId); const connectedPlayersCount = roomData?.players?.filter(p => p.isConnected).length || 0; const isGamePausedForPlayers = (roomData?.gameState === 'playing' && connectedPlayersCount < 2); // NEW: Check if this is a multi-mode game const isMultiMode = activeGameModes && Array.isArray(activeGameModes) && activeGameModes.length > 0; // In multi-mode games, only consider player "fully won" if all modes are claimed or game is complete // In single-mode games, hasWon means they can't compete anymore const hasWon = isMultiMode ? (currentPlayer?.hasWon && Object.keys(winnersByMode || {}).length === (activeGameModes || []).length) // All modes claimed in multi-mode : (currentPlayer?.hasWon || winners.some(w => w.player.id === playerData?.playerId)); // Traditional single-mode logic // Helper function for adding notifications with proper cleanup const addNotification = (message, type = 'info', duration = 3000) => { const toastId = Date.now(); // Clear any existing timeout for similar messages setNotificationTimeouts(prev => { const existingTimeout = prev.get(message); if (existingTimeout) { clearTimeout(existingTimeout); } return new Map(prev); }); // Remove any existing notification with same message to prevent duplicates setToastNotifications(prev => prev.filter(t => t.message !== message)); setToastNotifications(prev => [...prev, { id: toastId, message, type }]); // Auto-remove toast after specified duration const timeoutId = setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); setNotificationTimeouts(prev => { const newMap = new Map(prev); newMap.delete(message); return newMap; }); }, duration); // Store timeout for potential cleanup setNotificationTimeouts(prev => new Map(prev.set(message, timeoutId))); return { toastId, timeoutId }; }; // Send initial progress when ticket is ready useEffect(() => { if (localTicket && socket && playerData) { if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Send progress for each active game mode in multi-mode activeGameModes.forEach(mode => { const progress = calculatePlayerProgress(localTicket, mode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: mode, progress }); }); } else if (gameMode) { // Single mode progress const progress = calculatePlayerProgress(localTicket, gameMode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: gameMode, progress }); } } }, [localTicket, socket, playerData, gameMode, isMultiMode, activeGameModes, calledNumbers]); // Sync prop ticket to local state useEffect(() => { if (playerTicket && !localTicket) { setLocalTicket(playerTicket); } }, [playerTicket, localTicket]); // NEW: Sync multi-mode data from roomData useEffect(() => { if (roomData?.activeGameModes) { setActiveGameModes(roomData.activeGameModes); } if (roomData?.winnersByMode) { setWinnersByMode(roomData.winnersByMode); } }, [roomData?.activeGameModes, roomData?.winnersByMode]); // Sync initial game state for rejoining players useEffect(() => { if (initialCalledNumbers.length > 0) { setCalledNumbers(initialCalledNumbers); } if (initialCurrentNumber) { setCurrentNumber(initialCurrentNumber); } if (initialGameMode) { setGameMode(initialGameMode); } }, [initialCalledNumbers, initialCurrentNumber, initialGameMode]); // Initialize auto-call interval from room data useEffect(() => { if (roomData?.autoCallInterval) { setAutoCallInterval(roomData.autoCallInterval); } }, [roomData?.autoCallInterval]); useEffect(() => { if (!socket) return; // Connection recovery - request state sync on reconnection const handleReconnection = () => { if (playerData && roomData) { socket.emit('requestFullSync', { roomCode: roomData.code, playerId: playerData.playerId }); } }; // Listen for reconnection socket.on('connect', handleReconnection); // Handle full game state sync socket.on('fullGameSync', (syncData) => { if (syncData.calledNumbers) { setCalledNumbers(syncData.calledNumbers); } if (syncData.currentNumber) { setCurrentNumber(syncData.currentNumber); } if (syncData.autoCallStatus !== undefined) { setAutoCall(syncData.autoCallStatus); } // Acknowledge the sync if (syncData.timestamp) { socket.emit('messageAck', { messageId: `sync_${syncData.timestamp}` }); } }); socket.on('numberCalled', (data) => { setCalledNumbers(data.calledNumbers); setCurrentNumber(data.number); playSound('number'); // Acknowledge critical message if required if (data.requireAck && data.messageId) { socket.emit('messageAck', { messageId: data.messageId }); } }); socket.on('autoCallToggled', (data) => { setAutoCall(data.autoCall); }); socket.on('playerWon', (data) => { // Someone won - show immediate feedback playSound('win'); // Note: We don't show toast notification here because the 'rankingUpdated' event // that follows will handle the visual celebration to prevent duplicate notifications }); socket.on('rankingUpdated', (data) => { const prevWinnersCount = winners.length; const newWinners = data.winners; setWinners(newWinners); // Update required winners count if (data.requiredWinners) setRequiredWinners(data.requiredWinners); // Show celebration for new winner if (newWinners.length > prevWinnersCount) { const newWinner = newWinners[newWinners.length - 1]; const rank = newWinners.length; // Pulse the ranking panel to draw attention setRankingPulse(true); setTimeout(() => setRankingPulse(false), 3000); // Show full-screen celebration only for the winning player if (newWinner.player.id === playerData?.playerId) { setCelebrationWinner({ ...newWinner, rank, isYou: true }); // Auto-hide celebration after 4 seconds setTimeout(() => { setCelebrationWinner(null); }, 4000); } } playSound('win'); }); socket.on('verificationVoteReceived', (data) => { // Handle verification vote updates - keeping for future use }); socket.on('gameResumed', (data) => { setIsPaused(false); // Clear any existing resume notifications first setToastNotifications(prev => prev.filter(t => !t.message.includes('resumed') && !t.message.includes('Auto-call') )); if (data.resumedBy) { addNotification(`Game resumed by ${data.resumedBy}`, 'success', 3000); if (data.autoCallResumed) { // Add a small delay to prevent notification overlap setTimeout(() => { addNotification('Auto-call has been resumed', 'info', 2000); }, 100); } } else if (data.gameState === 'playing') { // Only show notification if multiple players rejoined (important event) if (data.message && data.message.includes('reconnected')) { addNotification(data.message, 'success', 3000); } } }); socket.on('gamePaused', (data) => { setIsPaused(true); if (data.pausedBy) { addNotification(`Game paused by ${data.pausedBy}`, 'warning', 4000); } else if (data.message && !data.message.includes('waiting for') && !data.message.includes('minimum')) { // Only show notification for important pause events (not frequent disconnects) addNotification(data.message, 'warning', 4000); } }); // HOST SUPERPOWERS - New socket listeners socket.on('playerKicked', (data) => { // Check if this player was kicked if (data.playerId === playerData?.playerId) { addNotification(`You were removed from the game by ${data.kickedBy}`, 'error', 5000); // Redirect to home after a short delay setTimeout(() => { onLeaveRoom(); }, 2000); } }); socket.on('hostTransferred', (data) => { if (data.newHost.id === playerData?.playerId) { addNotification(`You are now the host!`, 'success', 4000); } else { addNotification(`${data.newHost.nickname} is now the host`, 'info', 3000); } }); socket.on('playerProgressUpdated', (data) => { // Update the stored progress for other players if (data.playerId !== playerData?.playerId) { setPlayerProgress(prev => { const newProgress = new Map(prev); const playerProgress = newProgress.get(data.playerId) || {}; playerProgress[data.gameMode] = data.progress; newProgress.set(data.playerId, playerProgress); return newProgress; }); } }); socket.on('autoCallIntervalChanged', (data) => { setAutoCallInterval(data.interval); // Only show notification if you're the host (you changed it) if (playerData?.isHost) { const toastId = Date.now(); setToastNotifications(prev => [...prev, { id: toastId, message: `Auto-call interval set to ${data.interval} seconds`, type: 'success' }]); // Auto-remove toast after 2 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 2000); } }); // NEW: Multi-mode specific win events - PATTERN COMPLETION socket.on('playerWonMode', (data) => { // Only show notification if it's the current player who won if (data.playerId === playerData?.playerId) { // Player themselves won a pattern playSound('win'); const toastId = Date.now(); const patternName = GAME_MODES[data.gameMode]?.name || data.gameMode; const rankDisplay = data.modeRank === 1 ? '1st' : data.modeRank === 2 ? '2nd' : '3rd'; // FIXED: Pattern completion notification (not overall ranking) setToastNotifications(prev => [...prev, { id: toastId, message: `đŸŽ¯ You completed ${patternName}!`, type: 'success', patternWin: true, gameMode: data.gameMode, modeRank: data.modeRank }]); // Auto-remove toast after 4 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 4000); } // Don't show notifications for other players winning patterns to reduce spam }); socket.on('multiModeRankingUpdated', (data) => { const prevWinnersCount = winners.length; const newWinners = data.winners; setWinners(newWinners); // Update winners by mode if (data.winnersByMode) { setWinnersByMode(data.winnersByMode); } // Update required winners count if (data.room?.players) { const totalPlayers = data.room.players.length; const requiredWinners = totalPlayers === 2 ? 2 : Math.min(3, totalPlayers); setRequiredWinners(requiredWinners); } // FIXED: Show celebration ONLY for new OVERALL winner (when someone achieves final rank) if (newWinners.length > prevWinnersCount) { // Find the actually NEW winner by comparing with previous winners // BUG FIX: Don't use newWinners[length-1] because the array is sorted by rank const newWinner = newWinners.find(winner => !winners.some(prevWinner => prevWinner.player.id === winner.player.id && prevWinner.gameMode === winner.gameMode ) ); if (!newWinner) { console.log('🐛 No new winner found in multiModeRankingUpdated'); return; } // For multi-mode games, use overallRank (based on host's mode selection order) const rank = newWinner.overallRank || newWinners.length; console.log(`🎊 Multi-mode celebration: Player ${newWinner.player.nickname}, overallRank: ${newWinner.overallRank}, completionOrder: ${newWinner.completionOrder}, using rank: ${rank}`); // Pulse the ranking panel to draw attention setRankingPulse(true); setTimeout(() => setRankingPulse(false), 3000); // Show full-screen celebration only for the winning player (OVERALL final rank achievement) if (newWinner.player.id === playerData?.playerId) { setCelebrationWinner({ ...newWinner, rank, isYou: true, isPatternWin: false // This is overall ranking win }); // FIXED: Also show ranking notification const rankTitle = rank === 1 ? 'Champion 🏆' : rank === 2 ? '2nd Place đŸĨˆ' : '3rd Place đŸĨ‰'; const toastId = Date.now(); setToastNotifications(prev => [...prev, { id: toastId, message: `🎊 You achieved ${rankTitle}!`, type: 'success', rank: rank, isOverallRanking: true }]); // Auto-remove toast after 5 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 5000); // Auto-hide celebration after 4 seconds setTimeout(() => { setCelebrationWinner(null); }, 4000); } else { // For other players' overall wins, show a simple notification const championTitle = rank === 1 ? 'Champion 🏆' : rank === 2 ? '2nd Place đŸĨˆ' : '3rd Place đŸĨ‰'; addNotification( `🎊 ${newWinner.player.nickname} achieved ${championTitle}!`, 'info', 4000 ); } } playSound('win'); }); // Handle claim errors socket.on('error', (data) => { // Show error toast notification const toastId = Date.now(); setToastNotifications(prev => [...prev, { id: toastId, message: data.message, type: 'error' }]); // Auto-remove toast after 5 seconds setTimeout(() => { setToastNotifications(prev => prev.filter(t => t.id !== toastId)); }, 5000); }); return () => { socket.off('connect'); socket.off('fullGameSync'); socket.off('numberCalled'); socket.off('autoCallToggled'); socket.off('rankingUpdated'); socket.off('playerWonMode'); socket.off('multiModeRankingUpdated'); socket.off('verificationVoteReceived'); socket.off('gameResumed'); socket.off('gamePaused'); socket.off('playerProgressUpdated'); socket.off('autoCallIntervalChanged'); socket.off('error'); // HOST SUPERPOWERS - cleanup socket.off('playerKicked'); socket.off('hostTransferred'); socket.off('numberReplayed'); }; }, [socket, gameMode]); // HOST SUPERPOWERS - Click outside handler for player management dropdown useEffect(() => { const handleClickOutside = (event) => { // Only trigger if the dropdown is actually open and the click is outside if (showPlayerManagement && !event.target.closest('.player-management-dropdown') && !event.target.closest('[data-player-management-trigger]')) { setShowPlayerManagement(false); } }; // Only add listener when dropdown is open if (showPlayerManagement) { document.addEventListener('mousedown', handleClickOutside); return () => { document.removeEventListener('mousedown', handleClickOutside); }; } }, [showPlayerManagement]); // Sync local isPaused state with room's gameState and player count useEffect(() => { if (roomData?.gameState === 'paused' || isGamePausedForPlayers) { setIsPaused(true); } else if (roomData?.gameState === 'playing' && !isGamePausedForPlayers) { setIsPaused(false); } }, [roomData?.gameState, isGamePausedForPlayers]); // Check win condition whenever ticket or called numbers change useEffect(() => { // In multi-mode games, allow checking for claims even if player has won one mode // In single-mode games, don't check if player has already won if (localTicket && calledNumbers.length > 0 && (!hasWon || isMultiMode)) { if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Check which modes the player can claim wins for const newClaimableWins = {}; let hasAnyClaimableWin = false; activeGameModes.forEach(mode => { const canWin = checkTicketWin(localTicket, mode, calledNumbers); const hasAlreadyWonThisMode = winnersByMode[mode]?.some(w => w.player.id === playerData?.playerId); const modeAlreadyClaimed = winnersByMode[mode] && winnersByMode[mode].length > 0; // Check if ANY player has claimed this mode if (canWin && !hasAlreadyWonThisMode && !modeAlreadyClaimed) { newClaimableWins[mode] = true; hasAnyClaimableWin = true; } }); setClaimableWins(newClaimableWins); setCanClaimWin(hasAnyClaimableWin); } else if (gameMode) { // Single mode win checking const canWin = checkTicketWin(localTicket, gameMode, calledNumbers); setCanClaimWin(canWin); } } else { setCanClaimWin(false); setClaimableWins({}); } }, [localTicket, calledNumbers, gameMode, hasWon, isMultiMode, activeGameModes, winnersByMode, playerData?.playerId]); // Calculate initial required winners based on room data useEffect(() => { if (roomData?.players) { const totalPlayers = roomData.players.length; const initialRequiredWinners = totalPlayers === 2 ? 2 : Math.min(3, totalPlayers); setRequiredWinners(initialRequiredWinners); } }, [roomData?.players?.length]); const handleMarkNumber = (number, rowIndex, colIndex, shouldMark) => { if (!localTicket) return; // In multi-mode games, allow marking even if player has won one mode (they can still compete for others) // In single-mode games, prevent marking if player has already won if (!isMultiMode && hasWon) return; // Only allow marking, not unmarking if (!shouldMark) return; // Create a new ticket with the updated marking for immediate UI feedback const newTicket = localTicket.map((row, rIdx) => row.map((cell, cIdx) => { if (rIdx === rowIndex && cIdx === colIndex) { if (cell === null) return null; const cellNumber = typeof cell === 'object' ? cell.number : cell; if (cellNumber === number) { return { number, marked: true }; } } return cell; }) ); setLocalTicket(newTicket); // Send the manual marking to server if (socket) { socket.emit('manualMark', { number, rowIndex, colIndex, marked: true }); } // Check win condition after manual marking if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Check each active game mode const newClaimableWins = {}; let hasAnyClaimableWin = false; activeGameModes.forEach(mode => { const canWin = checkTicketWin(newTicket, mode, calledNumbers); const hasAlreadyWonThisMode = winnersByMode[mode]?.some(w => w.player.id === playerData?.playerId); const modeAlreadyClaimed = winnersByMode[mode] && winnersByMode[mode].length > 0; // Check if ANY player has claimed this mode if (canWin && !hasAlreadyWonThisMode && !modeAlreadyClaimed) { newClaimableWins[mode] = true; hasAnyClaimableWin = true; } }); setClaimableWins(newClaimableWins); setCanClaimWin(hasAnyClaimableWin); } else if (gameMode) { const canWin = checkTicketWin(newTicket, gameMode, calledNumbers); setCanClaimWin(canWin); } // Update and broadcast player progress if (socket && playerData) { if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Send progress for each active game mode activeGameModes.forEach(mode => { const progress = calculatePlayerProgress(newTicket, mode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: mode, progress }); }); } else if (gameMode) { const progress = calculatePlayerProgress(newTicket, gameMode, calledNumbers); socket.emit('updatePlayerProgress', { playerId: playerData.playerId, gameMode: gameMode, progress }); } } }; const copyRoomCode = async () => { try { await navigator.clipboard.writeText(roomData?.code || ''); setShowRoomCode(true); setTimeout(() => setShowRoomCode(false), 2000); } catch (err) { // Fallback for older browsers const textArea = document.createElement('textarea'); textArea.value = roomData?.code || ''; document.body.appendChild(textArea); textArea.select(); document.execCommand('copy'); document.body.removeChild(textArea); setShowRoomCode(true); setTimeout(() => setShowRoomCode(false), 2000); } }; const announceCurrentNumber = () => { if (currentNumber) { playSound('number'); // Try to use speech synthesis if available if ('speechSynthesis' in window) { const utterance = new SpeechSynthesisUtterance(`${currentNumber}`); utterance.rate = 0.8; utterance.volume = 0.7; window.speechSynthesis.speak(utterance); } } }; const handleCallNumber = () => { if (socket && isHost) { socket.emit('callNumber'); } }; const handleToggleAutoCall = () => { if (socket && isHost) { socket.emit('toggleAutoCall'); } }; const handleClaimWin = (specificMode = null) => { if (socket && canClaimWin) { if (isMultiMode && specificMode) { // Claim win for specific mode in multi-mode game socket.emit('claimWinForMode', { gameMode: specificMode }); playSound('win'); } else { // Traditional single-mode claim or claim all available in multi-mode socket.emit('claimWin'); playSound('win'); } } }; // NEW: Helper function to claim win for specific mode const handleClaimModeWin = (gameMode) => { if (socket && claimableWins[gameMode]) { socket.emit('claimWinForMode', { gameMode }); playSound('win'); } }; const handleSetAutoCallInterval = (seconds) => { if (socket && isHost && seconds >= 5 && seconds <= 60) { socket.emit('setAutoCallInterval', { seconds }); setShowIntervalSetting(false); } }; // HOST SUPERPOWERS - New handler functions const handlePauseGame = () => { if (socket && isHost && !hostActionLoading) { setHostActionLoading(true); socket.emit('pauseGame'); setTimeout(() => setHostActionLoading(false), 1000); } }; const handleResumeGame = () => { if (socket && isHost && !hostActionLoading) { // Check if there are enough players to resume if (connectedPlayersCount < 2) { addNotification('Cannot resume - need at least 2 players connected', 'error', 3000); return; } setHostActionLoading(true); socket.emit('resumeGame'); setTimeout(() => setHostActionLoading(false), 1000); } }; const handleKickPlayer = (playerId) => { if (socket && isHost && playerId && !hostActionLoading) { setHostActionLoading(true); socket.emit('kickPlayer', { targetPlayerId: playerId }); setShowKickConfirm(false); setSelectedPlayerToKick(null); setTimeout(() => setHostActionLoading(false), 1000); } }; const handleTransferHost = (playerId) => { if (socket && isHost && playerId && !hostActionLoading) { setHostActionLoading(true); socket.emit('transferHost', { targetPlayerId: playerId }); setShowTransferHost(false); setSelectedPlayerForHost(null); setTimeout(() => setHostActionLoading(false), 1000); } }; const openKickConfirmation = (player) => { setSelectedPlayerToKick(player); setShowKickConfirm(true); setShowPlayerManagement(false); }; const openTransferHostDialog = () => { setShowTransferHost(true); setShowPlayerManagement(false); }; const confirmKickPlayer = () => { if (!selectedPlayerToKick || !socket) return; setHostActionLoading(true); socket.emit('kickPlayer', { targetPlayerId: selectedPlayerToKick.id }); setTimeout(() => { setHostActionLoading(false); setShowKickConfirm(false); setSelectedPlayerToKick(null); }, 1000); }; const confirmTransferHost = () => { if (!selectedPlayerForHost || !socket) return; setHostActionLoading(true); socket.emit('transferHost', { targetPlayerId: selectedPlayerForHost }); setTimeout(() => { setHostActionLoading(false); setShowTransferHost(false); setSelectedPlayerForHost(null); }, 1000); }; return (
{/* Chat Component - Now modal-based, no layout impact */}
{/* Mobile-First Current Number Header - Optimized height for better toast positioning */}
Room: {roomData?.code}
{/* Connection Status Indicator */}
{!isConnected && reconnectAttempts > 0 && ( Reconnecting... ({reconnectAttempts}/5) )} {isConnected && connectionHealth !== 'good' && ( {connectionHealth} )}
{/* View Results button for ended games */} {roomData?.gameState === 'ended' && winners.length > 0 && onShowResults && ( )} {showRoomCode && ( Copied! )} {currentNumber && (
Current Number
{currentNumber}
)} {!currentNumber && (
Ready to Start
{isMultiMode ? 'Multi-Mode Game' : (GAME_MODES[gameMode]?.name || gameMode)}
)}
{/* Last few numbers for reference - Better mobile layout */} {calledNumbers.length > 0 && (
Recent:
{calledNumbers.slice(-6).map((num, index) => ( {num} ))}
)}
{/* Header with improved Leave button */}
{/* Page indicator like in the image with stars */}
⭐
{winners.length}/{requiredWinners} ⭐

{isMultiMode ? 'Multi-Mode Tambola' : (GAME_MODES[gameMode]?.name || 'Tambola Game')}

{isMultiMode ? `Playing: ${(activeGameModes || []).map(mode => GAME_MODES[mode]?.name).join(', ')}` : GAME_MODES[gameMode]?.description }

{/* Multi-mode winner count display */} {isMultiMode ? (
{(activeGameModes || []).map((mode, index) => { const modeWinners = winnersByMode[mode] || []; const rankDisplay = index === 0 ? '1st' : index === 1 ? '2nd' : '3rd'; return (
{GAME_MODES[mode]?.icon} {rankDisplay}: {modeWinners.length > 0 ? `✅ ${modeWinners[0]?.player.nickname}` : 'Available'}
); })}
) : winners.length > 0 && (
🏆 {winners.length}/{requiredWinners} Winners Found
)}
{roomData?.players?.filter(p => p.isConnected).length || 0}/ {roomData?.players?.length || 0} Players Connected {winners.length > 0 && ( â€ĸ {(() => { const connectedCount = roomData?.players?.filter(p => p.isConnected).length || 0; if (isMultiMode) { // In multi-mode: A player is only inactive when ALL patterns are complete // Check if all patterns are completed (all activeGameModes have winners) const allPatternsComplete = activeGameModes?.every(mode => winnersByMode[mode] && winnersByMode[mode].length > 0 ) || false; if (allPatternsComplete) { // Game is complete, no one is active anymore const activeCount = 0; console.log(`đŸŽ¯ Multi-mode: All patterns complete - ${activeCount} active`); return activeCount; } else { // Game is ongoing, all connected players are still active const completedPatterns = Object.keys(winnersByMode || {}).filter(mode => winnersByMode[mode]?.length > 0).length; const totalPatterns = activeGameModes?.length || 0; const activeCount = connectedCount; console.log(`đŸŽ¯ Multi-mode: Game ongoing - ${activeCount} active (${completedPatterns}/${totalPatterns} patterns complete)`); return activeCount; } } else { // In single-mode: Active = connected players minus overall winners const activeCount = Math.max(0, connectedCount - winners.length); console.log(`đŸŽ¯ Single-mode active count: ${connectedCount} connected - ${winners.length} winners = ${activeCount} active`); return activeCount; } })()} Active )}
{/* Game State Indicator - Better mobile spacing */} {(roomData?.gameState === 'paused' || isGamePausedForPlayers) && (
{roomData?.gameState === 'paused' ? 'Game Paused' : 'Waiting for Players'}
{roomData?.gameState === 'paused' ? 'Waiting for host to resume...' : connectedPlayersCount < 2 ? 'Waiting for players to reconnect or join...' : 'Waiting for players to reconnect...' }
)} {/* Host Controls */} {isHost && (
Host Controls
{/* Auto Call Interval Setting */} {showIntervalSetting && (

Set Auto-Call Interval

setAutoCallInterval(Number(e.target.value))} className="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer" />
5s 60s
)} {/* Pause/Resume Game Button - Only show during active game */} {(roomData?.gameState === 'playing' || roomData?.gameState === 'paused') && ( )} {/* Player Management Dropdown */}
{showPlayerManagement && (
Kick Players:
{roomData?.players?.filter(p => p.id !== playerData?.playerId && p.isConnected).map(player => ( ))}
)}
)} {/* Enhanced Winners Ranking Panel - Now at top for better visibility */} {winners.length > 0 && (

🏆 Live Rankings - {winners.length}/{requiredWinners} Winners Found

đŸŽ¯ {Math.max(0, requiredWinners - winners.length)} more winner{requiredWinners - winners.length !== 1 ? 's' : ''} needed
{/* Show dynamic podium positions based on required winners */} {Array.from({ length: requiredWinners }, (_, position) => { // FIXED: For multi-mode games, use ONLY overallRank (host's intended rank) // For single-mode games, use completion order fallback const targetRank = position + 1; // position 0 = rank 1, position 1 = rank 2, etc. // Debug logging for multi-mode games if (isMultiMode && winners.length > 0) { console.log(`🏆 Looking for rank ${targetRank} winner:`, winners.map(w => `${w.player.nickname}: overallRank=${w.overallRank}, completionOrder=${w.completionOrder}, gameMode=${w.gameMode}`) ); } const winner = winners.find(w => { // In multi-mode games, use only overallRank to avoid duplicates if (isMultiMode) { return w.overallRank === targetRank; } else { // In single-mode games, use completion order or fallback to array index return (w.completionOrder === targetRank) || (w.overallRank === targetRank) || (!w.completionOrder && !w.overallRank && position === winners.indexOf(w)); } }); const isOccupied = !!winner; return (
{/* Position indicator */}
{position + 1}
{/* Medal */}
{position === 0 ? 'đŸĨ‡' : position === 1 ? 'đŸĨˆ' : 'đŸĨ‰'}
{/* Winner info or placeholder */}
{isOccupied ? ( <>
{winner.player.nickname} {winner.player.id === playerData?.playerId && (
(You!) 🎉
)}
{/* Show the game mode that was won */} {winner.gameMode && (
{GAME_MODES[winner.gameMode]?.icon} {GAME_MODES[winner.gameMode]?.name}
)}
{new Date(winner.timestamp).toLocaleTimeString()}
) : (
{position === 0 ? '1st Place' : position === 1 ? '2nd Place' : '3rd Place'}
Available
)}
{/* Special effects for player's own position */} {isOccupied && winner.player.id === playerData?.playerId && (
YOU!
)}
); })}
)}
{/* Left Column - Player's Ticket */}
{localTicket && (
)} {/* Claim Win Button or Winner Status */} {hasWon ? (
🎉 Congratulations! You've Won! 🎉
Continue watching others compete for remaining positions
Your position is secured in the rankings above âŦ†ī¸
) : canClaimWin ? (
🎉 You can claim win{isMultiMode && Object.keys(claimableWins).length > 1 ? 's' : ''}! 🎉
{isMultiMode ? ( /* Multi-mode claim buttons */
{Object.entries(claimableWins).map(([mode, canClaim]) => { if (!canClaim) return null; const modeIndex = activeGameModes?.indexOf(mode) || 0; const rankDisplay = modeIndex === 0 ? '1st' : modeIndex === 1 ? '2nd' : '3rd'; return ( ); })}
) : ( /* Single-mode claim button */ )}
) : null} {/* Enhanced Players List with Rankings */}

Players Status

{roomData?.players?.map((player) => { // In multi-mode: Only count as overall winner if player won ALL active modes // In single-mode: Count as winner if in winners array let hasWonOverall = false; let rank = null; if (isMultiMode && activeGameModes && winnersByMode) { // Check if player has won ALL active game modes const hasWonAllModes = activeGameModes.every(mode => winnersByMode[mode] && winnersByMode[mode].find(w => w.player.id === player.id) ); if (hasWonAllModes) { // Find their overall rank based on when they completed all modes const overallWinners = winners.filter(w => { return activeGameModes.every(mode => winnersByMode[mode] && winnersByMode[mode].find(mw => mw.player.id === w.player.id) ); }); const winnerInfo = overallWinners.find(w => w.player.id === player.id); hasWonOverall = !!winnerInfo; rank = winnerInfo ? overallWinners.findIndex(w => w.player.id === player.id) + 1 : null; } } else { // Single mode: Use existing logic const winnerInfo = winners.find(w => w.player.id === player.id); hasWonOverall = !!winnerInfo; rank = winnerInfo ? winners.findIndex(w => w.player.id === player.id) + 1 : null; } // In multimode, check if player has won any individual patterns but not overall let hasWonAnyPattern = false; let wonPatterns = []; if (isMultiMode && winnersByMode) { Object.entries(winnersByMode).forEach(([mode, modeWinners]) => { if (modeWinners && modeWinners.find(w => w.player.id === player.id)) { hasWonAnyPattern = true; wonPatterns.push(mode); } }); } // Calculate progress for this player let allProgress = {}; if (player.id === playerData?.playerId) { // For current player, calculate from their local ticket if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Calculate progress for all active game modes activeGameModes.forEach(mode => { allProgress[mode] = calculatePlayerProgress(localTicket, mode, calledNumbers); }); } else { allProgress[gameMode] = calculatePlayerProgress(localTicket, gameMode, calledNumbers); } } else { // For other players, use stored progress data const playerProgressData = playerProgress.get(player.id) || {}; if (isMultiMode && activeGameModes && activeGameModes.length > 0) { // Get progress for all active game modes activeGameModes.forEach(mode => { allProgress[mode] = playerProgressData[mode] || { marked: 0, total: 0, percentage: 0 }; }); } else { allProgress[gameMode] = playerProgressData[gameMode] || { marked: 0, total: 0, percentage: 0 }; } } return (
{/* Player Avatar Circle */}
{player.nickname.charAt(0).toUpperCase()}
{/* Winner rank indicator */} {hasWonOverall && (
{rank}
)} {/* Show small indicators for won patterns in multimode */} {!hasWonOverall && hasWonAnyPattern && isMultiMode && (
{wonPatterns.map((pattern, idx) => (
{idx + 1}
))}
)}
{player.nickname} {player.id === playerData?.playerId && ' (You)'} {hasWonOverall && ( {rank === 1 ? 'đŸĨ‡' : rank === 2 ? 'đŸĨˆ' : 'đŸĨ‰'} )} {player.isHost && (
Host
)}
{/* Winner status */} {hasWonOverall ? ( {rank === 1 ? 'Champion' : rank === 2 ? 'Runner-up' : '3rd Place'} ) : hasWonAnyPattern && isMultiMode ? ( {wonPatterns.length} Pattern{wonPatterns.length > 1 ? 's' : ''} Won ) : ( In Progress )} {/* Connection indicator */}
{/* Progress display - Show for all non-overall-winners OR in multimode show remaining patterns */} {(!hasWonOverall || (isMultiMode && !hasWonOverall)) && ( (player.id === playerData?.playerId && localTicket) || (player.id !== playerData?.playerId && playerProgress.has(player.id)) ) && (
{isMultiMode && activeGameModes && activeGameModes.length > 0 ? ( // Multi-mode: Show progress for all patterns, highlight won ones
Pattern Progress: {activeGameModes.map((mode, index) => { const progress = allProgress[mode] || { marked: 0, total: 0, percentage: 0 }; const rankDisplay = index === 0 ? '1st' : index === 1 ? '2nd' : '3rd'; const hasWonThisPattern = wonPatterns.includes(mode); return (
{GAME_MODES[mode]?.icon} {rankDisplay}: {GAME_MODES[mode]?.name} {hasWonThisPattern && ✅} {progress.marked}/{progress.total} ({progress.percentage}%)
); })}
) : ( // Single mode: Show single progress
Progress {allProgress[gameMode]?.marked || 0}/{allProgress[gameMode]?.total || 0} ({allProgress[gameMode]?.percentage || 0}%)
)}
)}
); })}
{/* Center/Right Column - Number Board */}
{/* Enhanced Toast Notifications - Positioned to never overlap with sticky header */}
{toastNotifications.map((toast, index) => (
{toast.patternWin ? ( // PATTERN COMPLETION NOTIFICATION
đŸŽ¯
{toast.message}
Pattern Completed!
✨
) : toast.rank || toast.isOverallRanking ? ( // OVERALL RANKING NOTIFICATION
{toast.rank}
{toast.message}
{toast.rank === 1 ? 'Champion!' : toast.rank === 2 ? 'Runner-up!' : 'Third Place!'}
{toast.rank === 1 ? 'đŸĨ‡' : toast.rank === 2 ? 'đŸĨˆ' : 'đŸĨ‰'}
) : (
{toast.type === 'error' ? '❌' : toast.type === 'info' ? 'â„šī¸' : toast.type === 'warning' ? 'âš ī¸' : '✅'}
{toast.message}
)}
))}
{/* Enhanced Celebration Overlay */} {celebrationWinner && (
{/* Enhanced Confetti Effect - Different colors for pattern vs overall wins */}
{[...Array(30)].map((_, i) => (
))}
{/* Celebration Content */}
{/* Animated Winner Circle - Different for pattern vs overall wins */} {celebrationWinner.isPatternWin ? (
đŸŽ¯
) : (
{celebrationWinner.rank}
)} {/* Medal - Different for pattern vs overall wins */}
{celebrationWinner.isPatternWin ? 'đŸŽ¯' : ( celebrationWinner.rank === 1 ? 'đŸĨ‡' : celebrationWinner.rank === 2 ? 'đŸĨˆ' : 'đŸĨ‰' )}
{/* Title */}

{celebrationWinner.isPatternWin ? ( celebrationWinner.isYou ? '🎊 PATTERN COMPLETED! 🎊' : `${celebrationWinner.player.nickname} Won Pattern!` ) : ( celebrationWinner.isYou ? '🎉 CONGRATULATIONS! 🎉' : `${celebrationWinner.player.nickname} Wins!` )}

{/* Achievement */}
{celebrationWinner.isPatternWin ? ( <>

{celebrationWinner.isYou ? 'You completed' : 'Completed'}{' '} {celebrationWinner.patternName}

{celebrationWinner.isYou && (

Great job! Continue playing for more patterns! 🌟

)} ) : ( <>

{celebrationWinner.isYou ? 'You achieved' : 'Achieved'}{' '} {celebrationWinner.rank === 1 ? '1st Place' : celebrationWinner.rank === 2 ? '2nd Place' : '3rd Place'}

{celebrationWinner.isYou && (

Amazing job! Your victory is now secured! 🌟

)} )} {!celebrationWinner.isYou && winners.length < requiredWinners && (

Keep playing for remaining positions!

)}
{/* Progress indicator */}
Game Progress
{Array.from({ length: requiredWinners }, (_, pos) => (
))}
{winners.length}/{requiredWinners} positions filled
{/* Auto-close info */}
Closes automatically in a few seconds...
)} {/* HOST SUPERPOWERS - Confirmation Dialogs */} {/* Kick Player Confirmation */} {showKickConfirm && selectedPlayerToKick && (

Kick Player

Are you sure you want to kick {selectedPlayerToKick.nickname} from the game?

)} {/* Transfer Host Confirmation */} {showTransferHost && (

Transfer Host

Choose a player to transfer host role to:

{roomData?.players?.filter(p => p.id !== playerData?.playerId && p.isConnected).map(player => ( ))}
)}
); } export default Game;