// ======================================== // Chess Arena - Main Game JavaScript // ======================================== (function() { 'use strict'; // ======================================== // Configuration & Constants // ======================================== const PIECES = { 'K': '♔', 'Q': '♕', 'R': '♖', 'B': '♗', 'N': '♘', 'P': '♙', 'k': '♚', 'q': '♛', 'r': '♜', 'b': '♝', 'n': '♞', 'p': '♟' }; const FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; const RANKS = ['8', '7', '6', '5', '4', '3', '2', '1']; // ======================================== // Game State // ======================================== let socket = null; let chess = null; let gameState = { roomId: null, playerColor: null, username: null, opponent: null, timeControl: null, isMyTurn: false, gameStarted: false, gameOver: false, selectedSquare: null, legalMoves: [], lastMove: null, isFlipped: false, soundEnabled: true, pendingPromotion: null }; let timers = { white: null, black: null, interval: null }; let unreadMessages = 0; // ======================================== // Chess.js Helper Functions (Version Compatible) // ======================================== function chessInCheck() { if (typeof chess.inCheck === 'function') { return chess.inCheck(); } else if (typeof chess.in_check === 'function') { return chess.in_check(); } else if (typeof chess.isCheck === 'function') { return chess.isCheck(); } // Manual check detection return isKingInCheck(); } function chessIsCheckmate() { if (typeof chess.isCheckmate === 'function') { return chess.isCheckmate(); } else if (typeof chess.in_checkmate === 'function') { return chess.in_checkmate(); } return false; } function chessIsDraw() { if (typeof chess.isDraw === 'function') { return chess.isDraw(); } else if (typeof chess.in_draw === 'function') { return chess.in_draw(); } return false; } function chessIsStalemate() { if (typeof chess.isStalemate === 'function') { return chess.isStalemate(); } else if (typeof chess.in_stalemate === 'function') { return chess.in_stalemate(); } return false; } function chessIsGameOver() { if (typeof chess.isGameOver === 'function') { return chess.isGameOver(); } else if (typeof chess.game_over === 'function') { return chess.game_over(); } return chessIsCheckmate() || chessIsDraw() || chessIsStalemate(); } function chessTurn() { if (typeof chess.turn === 'function') { return chess.turn(); } return chess.turn; } function chessFen() { if (typeof chess.fen === 'function') { return chess.fen(); } return chess.fen; } function chessPgn() { if (typeof chess.pgn === 'function') { return chess.pgn(); } return chess.pgn || ''; } function chessHistory() { if (typeof chess.history === 'function') { return chess.history(); } return chess.history || []; } function chessGet(square) { if (typeof chess.get === 'function') { return chess.get(square); } return null; } function chessMoves(options) { if (typeof chess.moves === 'function') { return chess.moves(options); } return []; } function chessMove(moveObj) { if (typeof chess.move === 'function') { try { return chess.move(moveObj); } catch (e) { console.error('Move error:', e); return null; } } return null; } function isKingInCheck() { // Manual check detection as fallback const turn = chessTurn(); const kingSquare = findKingSquare(turn); if (!kingSquare) return false; // Check if any opponent piece can capture the king const opponentColor = turn === 'w' ? 'b' : 'w'; for (let file of FILES) { for (let rank of RANKS) { const square = file + rank; const piece = chessGet(square); if (piece && piece.color === opponentColor) { const moves = chessMoves({ square: square, verbose: true }); for (let move of moves) { if (move.to === kingSquare) { return true; } } } } } return false; } // ======================================== // Audio // ======================================== const sounds = { move: null, capture: null, check: null, castle: null, promote: null, gameStart: null, gameEnd: null, notify: null }; function initSounds() { const AudioContext = window.AudioContext || window.webkitAudioContext; if (!AudioContext) return; const audioCtx = new AudioContext(); function createSound(frequency, duration, type = 'sine') { return function() { if (!gameState.soundEnabled) return; try { const oscillator = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); oscillator.connect(gainNode); gainNode.connect(audioCtx.destination); oscillator.frequency.value = frequency; oscillator.type = type; gainNode.gain.setValueAtTime(0.3, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + duration); oscillator.start(audioCtx.currentTime); oscillator.stop(audioCtx.currentTime + duration); } catch (e) { console.log('Audio error:', e); } }; } sounds.move = createSound(400, 0.1); sounds.capture = createSound(300, 0.15); sounds.check = createSound(600, 0.2); sounds.castle = createSound(350, 0.15); sounds.promote = createSound(800, 0.2); sounds.gameStart = createSound(500, 0.3); sounds.gameEnd = createSound(200, 0.5); sounds.notify = createSound(700, 0.1); } // ======================================== // DOM Elements // ======================================== const elements = {}; function cacheElements() { elements.loadingScreen = document.getElementById('loading-screen'); elements.app = document.getElementById('app'); elements.lobbyScreen = document.getElementById('lobby-screen'); elements.waitingScreen = document.getElementById('waiting-screen'); elements.gameScreen = document.getElementById('game-screen'); elements.usernameInput = document.getElementById('username-input'); elements.roomCodeInput = document.getElementById('room-code-input'); elements.createGameBtn = document.getElementById('create-game-btn'); elements.joinGameBtn = document.getElementById('join-game-btn'); elements.timeBtns = document.querySelectorAll('.time-btn'); elements.roomsList = document.getElementById('rooms-list'); elements.refreshRoomsBtn = document.getElementById('refresh-rooms'); elements.displayRoomCode = document.getElementById('display-room-code'); elements.waitingTimeControl = document.getElementById('waiting-time-control'); elements.copyCodeBtn = document.getElementById('copy-code-btn'); elements.cancelWaitingBtn = document.getElementById('cancel-waiting'); elements.chessboard = document.getElementById('chessboard'); elements.opponentName = document.getElementById('opponent-name'); elements.opponentColor = document.getElementById('opponent-color'); elements.opponentTime = document.getElementById('opponent-time'); elements.opponentTimer = document.getElementById('opponent-timer'); elements.playerName = document.getElementById('player-name'); elements.playerColor = document.getElementById('player-color'); elements.playerTime = document.getElementById('player-time'); elements.playerTimer = document.getElementById('player-timer'); elements.opponentCaptured = document.getElementById('opponent-captured'); elements.playerCaptured = document.getElementById('player-captured'); elements.moveHistory = document.getElementById('move-history'); elements.resignBtn = document.getElementById('resign-btn'); elements.drawBtn = document.getElementById('draw-btn'); elements.flipBoardBtn = document.getElementById('flip-board-btn'); elements.chatToggleBtn = document.getElementById('chat-toggle-btn'); elements.chatBadge = document.getElementById('chat-badge'); elements.chatPanel = document.getElementById('chat-panel'); elements.chatMessages = document.getElementById('chat-messages'); elements.chatInput = document.getElementById('chat-input'); elements.sendChatBtn = document.getElementById('send-chat-btn'); elements.themeToggle = document.getElementById('theme-toggle'); elements.themeDropdown = document.getElementById('theme-dropdown'); elements.soundToggle = document.getElementById('sound-toggle'); elements.gameOverModal = document.getElementById('game-over-modal'); elements.gameOverIcon = document.getElementById('game-over-icon'); elements.gameOverTitle = document.getElementById('game-over-title'); elements.gameOverMessage = document.getElementById('game-over-message'); elements.playAgainBtn = document.getElementById('play-again-btn'); elements.backToLobbyBtn = document.getElementById('back-to-lobby-btn'); elements.drawOfferModal = document.getElementById('draw-offer-modal'); elements.drawOfferMessage = document.getElementById('draw-offer-message'); elements.acceptDrawBtn = document.getElementById('accept-draw-btn'); elements.declineDrawBtn = document.getElementById('decline-draw-btn'); elements.resignModal = document.getElementById('resign-modal'); elements.confirmResignBtn = document.getElementById('confirm-resign-btn'); elements.cancelResignBtn = document.getElementById('cancel-resign-btn'); elements.promotionModal = document.getElementById('promotion-modal'); elements.promotionPieces = document.getElementById('promotion-pieces'); elements.disconnectModal = document.getElementById('disconnect-modal'); elements.disconnectMessage = document.getElementById('disconnect-message'); elements.disconnectCountdown = document.getElementById('disconnect-countdown'); elements.toastContainer = document.getElementById('toast-container'); } // ======================================== // Utilities // ======================================== function showToast(message, type = 'info') { const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: 'fa-check-circle', error: 'fa-exclamation-circle', info: 'fa-info-circle', warning: 'fa-exclamation-triangle' }; toast.innerHTML = ` ${message} `; elements.toastContainer.appendChild(toast); setTimeout(() => { if (toast.parentNode) { toast.remove(); } }, 3000); } function formatTime(seconds) { if (seconds === null || seconds === undefined) return '--:--'; const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins}:${secs.toString().padStart(2, '0')}`; } function showScreen(screenId) { document.querySelectorAll('.screen').forEach(screen => { screen.classList.add('hidden'); }); const screen = document.getElementById(screenId); if (screen) { screen.classList.remove('hidden'); } } function showModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.remove('hidden'); } } function hideModal(modalId) { const modal = document.getElementById(modalId); if (modal) { modal.classList.add('hidden'); } } function hideAllModals() { document.querySelectorAll('.modal').forEach(modal => { modal.classList.add('hidden'); }); } function capitalizeFirst(str) { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // ======================================== // Theme Management // ======================================== function initTheme() { const savedTheme = localStorage.getItem('chess-theme') || 'dark'; setTheme(savedTheme); } function setTheme(themeName) { document.body.className = ''; document.body.classList.add(`theme-${themeName}`); localStorage.setItem('chess-theme', themeName); document.querySelectorAll('.theme-option').forEach(option => { option.classList.remove('active'); if (option.dataset.theme === themeName) { option.classList.add('active'); } }); } // ======================================== // Socket Connection // ======================================== function initSocket() { try { socket = io({ transports: ['websocket', 'polling'], upgrade: true, reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000 }); socket.on('connect', () => { console.log('Connected to server'); showToast('Connected to server', 'success'); }); socket.on('disconnect', () => { console.log('Disconnected from server'); showToast('Disconnected from server', 'error'); }); socket.on('connect_error', (error) => { console.error('Connection error:', error); showToast('Connection error', 'error'); }); socket.on('error', (data) => { showToast(data.message || 'An error occurred', 'error'); }); socket.on('room_created', handleRoomCreated); socket.on('room_joined', handleRoomJoined); socket.on('game_restored', handleGameRestored); socket.on('opponent_joined', handleOpponentJoined); socket.on('opponent_reconnected', handleOpponentReconnected); socket.on('game_start', handleGameStart); socket.on('move_made', handleMoveMade); socket.on('game_over', handleGameOver); socket.on('opponent_disconnected', handleOpponentDisconnected); socket.on('draw_offered', handleDrawOffered); socket.on('draw_declined', handleDrawDeclined); socket.on('chat_message', handleChatMessage); socket.on('rooms_list', handleRoomsList); } catch (e) { console.error('Socket initialization error:', e); showToast('Failed to connect to server', 'error'); } } // ======================================== // Socket Event Handlers // ======================================== function handleRoomCreated(data) { gameState.roomId = data.room_id; gameState.playerColor = data.color; gameState.timeControl = data.time_control; if (elements.displayRoomCode) { elements.displayRoomCode.textContent = data.room_id; } if (elements.waitingTimeControl) { elements.waitingTimeControl.textContent = data.time_control ? `${data.time_control} minutes` : 'No Limit'; } showScreen('waiting-screen'); showToast('Room created! Share the code with your friend.', 'success'); } function handleRoomJoined(data) { gameState.roomId = data.room_id; gameState.playerColor = data.color; gameState.timeControl = data.time_control; gameState.opponent = data.opponent; showToast(`Joined room! Playing against ${data.opponent}`, 'success'); } function handleGameRestored(data) { gameState.roomId = data.room_id; gameState.playerColor = data.color; gameState.opponent = data.opponent; chess = new Chess(data.game_state.fen); timers.white = data.white_time; timers.black = data.black_time; initGameScreen(); renderBoard(); updateTimers(); showToast('Game restored!', 'success'); } function handleOpponentJoined(data) { gameState.opponent = data.username; showToast(`${data.username} joined the game!`, 'success'); } function handleOpponentReconnected(data) { hideModal('disconnect-modal'); gameState.opponent = data.username; chess = new Chess(data.game_state.fen); timers.white = data.white_time; timers.black = data.black_time; renderBoard(); updateTimers(); startTimer(); showToast(`${data.username} reconnected!`, 'success'); } function handleGameStart(data) { console.log('Game starting:', data); gameState.gameStarted = true; gameState.opponent = gameState.playerColor === 'white' ? data.black : data.white; gameState.timeControl = data.time_control; // Initialize chess.js if (data.game_state && data.game_state.fen) { chess = new Chess(data.game_state.fen); } else { chess = new Chess(); } // Initialize timers if (gameState.timeControl) { timers.white = gameState.timeControl * 60; timers.black = gameState.timeControl * 60; } initGameScreen(); renderBoard(); updateTimers(); startTimer(); if (sounds.gameStart) sounds.gameStart(); showToast('Game started! Good luck!', 'success'); } function handleMoveMade(data) { console.log('Move received:', data); // Update chess.js state chess = new Chess(data.fen); // Store last move for highlighting gameState.lastMove = data.move; // Update timers if (gameState.timeControl) { timers.white = data.white_time; timers.black = data.black_time; } // Clear selection gameState.selectedSquare = null; gameState.legalMoves = []; // Play appropriate sound if (data.is_checkmate && sounds.gameEnd) { sounds.gameEnd(); } else if (data.is_check && sounds.check) { sounds.check(); } else if (data.move && data.move.includes('x') && sounds.capture) { sounds.capture(); } else if (data.move && (data.move === 'O-O' || data.move === 'O-O-O') && sounds.castle) { sounds.castle(); } else if (sounds.move) { sounds.move(); } // Update display renderBoard(); updateTimers(); updateMoveHistory(); updateCapturedPieces(); // Check game end conditions if (data.is_checkmate || data.is_draw || data.is_stalemate) { handleGameEnd(data); } } function handleGameOver(data) { gameState.gameOver = true; stopTimer(); const isWinner = data.winner === gameState.username; const isDraw = !data.winner; if (elements.gameOverIcon) { elements.gameOverIcon.className = 'game-over-icon'; if (isDraw) { elements.gameOverIcon.classList.add('draw'); elements.gameOverIcon.innerHTML = ''; } else if (isWinner) { elements.gameOverIcon.innerHTML = ''; } else { elements.gameOverIcon.classList.add('loss'); elements.gameOverIcon.innerHTML = ''; } } if (elements.gameOverTitle) { if (isDraw) { elements.gameOverTitle.textContent = 'Draw!'; } else if (isWinner) { elements.gameOverTitle.textContent = 'Victory!'; } else { elements.gameOverTitle.textContent = 'Defeat'; } } if (elements.gameOverMessage) { if (isDraw) { elements.gameOverMessage.textContent = getDrawMessage(data.reason); } else if (isWinner) { elements.gameOverMessage.textContent = getWinMessage(data.reason); } else { elements.gameOverMessage.textContent = getLossMessage(data.reason, data); } } if (sounds.gameEnd) sounds.gameEnd(); showModal('game-over-modal'); } function getDrawMessage(reason) { switch (reason) { case 'stalemate': return 'Stalemate - No legal moves available.'; case 'insufficient': return 'Insufficient material to checkmate.'; case 'threefold': return 'Threefold repetition.'; case 'fifty_moves': return 'Fifty-move rule.'; case 'draw_agreement': return 'Draw by agreement.'; case 'draw': return 'Draw by agreement.'; default: return 'The game ended in a draw.'; } } function getWinMessage(reason) { switch (reason) { case 'checkmate': return 'Checkmate! Excellent play!'; case 'resignation': return 'Your opponent resigned.'; case 'timeout': return 'Your opponent ran out of time.'; case 'abandonment': return 'Your opponent abandoned the game.'; default: return 'You won the game!'; } } function getLossMessage(reason, data) { switch (reason) { case 'checkmate': return 'Checkmate! Better luck next time.'; case 'resignation': return 'You resigned the game.'; case 'timeout': return 'You ran out of time.'; case 'abandonment': return 'Game forfeited due to disconnection.'; default: return 'You lost the game.'; } } function handleGameEnd(data) { gameState.gameOver = true; stopTimer(); const turn = chessTurn(); if (data.is_checkmate) { const winner = turn === 'w' ? 'Black' : 'White'; const isWinner = (winner === 'White' && gameState.playerColor === 'white') || (winner === 'Black' && gameState.playerColor === 'black'); if (elements.gameOverIcon) { elements.gameOverIcon.className = 'game-over-icon'; if (isWinner) { elements.gameOverIcon.innerHTML = ''; } else { elements.gameOverIcon.classList.add('loss'); elements.gameOverIcon.innerHTML = ''; } } if (elements.gameOverTitle) { elements.gameOverTitle.textContent = isWinner ? 'Victory!' : 'Defeat'; } if (elements.gameOverMessage) { elements.gameOverMessage.textContent = isWinner ? 'Checkmate! Excellent play!' : 'Checkmate! Better luck next time.'; } } else if (data.is_stalemate || data.is_draw) { if (elements.gameOverIcon) { elements.gameOverIcon.className = 'game-over-icon draw'; elements.gameOverIcon.innerHTML = ''; } if (elements.gameOverTitle) { elements.gameOverTitle.textContent = 'Draw!'; } if (elements.gameOverMessage) { elements.gameOverMessage.textContent = data.is_stalemate ? 'Stalemate - No legal moves available.' : 'The game ended in a draw.'; } } showModal('game-over-modal'); } function handleOpponentDisconnected(data) { showModal('disconnect-modal'); if (elements.disconnectMessage) { elements.disconnectMessage.textContent = `${data.username} disconnected. Waiting for reconnection...`; } let countdown = data.timeout; stopTimer(); const disconnectInterval = setInterval(() => { countdown--; if (elements.disconnectCountdown) { elements.disconnectCountdown.textContent = formatTime(countdown); } if (countdown <= 0) { clearInterval(disconnectInterval); } }, 1000); gameState.disconnectInterval = disconnectInterval; } function handleDrawOffered(data) { if (elements.drawOfferMessage) { elements.drawOfferMessage.textContent = `${data.from} offers a draw`; } showModal('draw-offer-modal'); if (sounds.notify) sounds.notify(); } function handleDrawDeclined(data) { showToast(`${data.by} declined the draw offer`, 'info'); } function handleChatMessage(data) { addChatMessage(data.username, data.message, data.timestamp); if (data.username !== gameState.username) { unreadMessages++; updateChatBadge(); if (sounds.notify) sounds.notify(); } } function handleRoomsList(data) { renderRoomsList(data.rooms); } // ======================================== // Game Screen Initialization // ======================================== function initGameScreen() { showScreen('game-screen'); if (elements.playerName) { elements.playerName.textContent = gameState.username || 'You'; } if (elements.playerColor) { elements.playerColor.textContent = capitalizeFirst(gameState.playerColor); } if (elements.opponentName) { elements.opponentName.textContent = gameState.opponent || 'Opponent'; } if (elements.opponentColor) { elements.opponentColor.textContent = gameState.playerColor === 'white' ? 'Black' : 'White'; } // Hide timers if no time control if (!gameState.timeControl) { if (elements.playerTimer) elements.playerTimer.style.display = 'none'; if (elements.opponentTimer) elements.opponentTimer.style.display = 'none'; } else { if (elements.playerTimer) elements.playerTimer.style.display = 'flex'; if (elements.opponentTimer) elements.opponentTimer.style.display = 'flex'; } // Flip board if playing black if (gameState.playerColor === 'black') { gameState.isFlipped = true; } // Reset game state gameState.gameOver = false; gameState.selectedSquare = null; gameState.legalMoves = []; gameState.lastMove = null; // Clear move history if (elements.moveHistory) { elements.moveHistory.innerHTML = ''; } // Clear captured pieces if (elements.opponentCaptured) elements.opponentCaptured.innerHTML = ''; if (elements.playerCaptured) elements.playerCaptured.innerHTML = ''; // Clear chat if (elements.chatMessages) elements.chatMessages.innerHTML = ''; unreadMessages = 0; updateChatBadge(); } // ======================================== // Board Rendering // ======================================== function renderBoard() { if (!elements.chessboard || !chess) { console.error('Chessboard element or chess instance not found'); return; } elements.chessboard.innerHTML = ''; const files = gameState.isFlipped ? [...FILES].reverse() : FILES; const ranks = gameState.isFlipped ? [...RANKS].reverse() : RANKS; ranks.forEach((rank, rankIdx) => { files.forEach((file, fileIdx) => { const square = file + rank; const isLight = (rankIdx + fileIdx) % 2 === 0; const squareEl = document.createElement('div'); squareEl.className = `board-square ${isLight ? 'light' : 'dark'}`; squareEl.dataset.square = square; // Add coordinates on edges if (fileIdx === 7) { const rankLabel = document.createElement('span'); rankLabel.className = 'coord-rank'; rankLabel.textContent = rank; squareEl.appendChild(rankLabel); } if (rankIdx === 7) { const fileLabel = document.createElement('span'); fileLabel.className = 'coord-file'; fileLabel.textContent = file; squareEl.appendChild(fileLabel); } // Add piece const piece = chessGet(square); if (piece) { const pieceEl = document.createElement('span'); pieceEl.className = `piece piece-${piece.color === 'w' ? 'white' : 'black'}`; const pieceChar = piece.color === 'w' ? piece.type.toUpperCase() : piece.type; pieceEl.textContent = PIECES[pieceChar]; squareEl.appendChild(pieceEl); } // Highlight selected square if (gameState.selectedSquare === square) { squareEl.classList.add('selected'); } // Highlight last move if (gameState.lastMove) { const lastMoveFrom = typeof gameState.lastMove === 'object' ? gameState.lastMove.from : null; const lastMoveTo = typeof gameState.lastMove === 'object' ? gameState.lastMove.to : extractSquareFromSan(gameState.lastMove); if (square === lastMoveFrom || square === lastMoveTo) { squareEl.classList.add('last-move'); } } // Highlight legal moves if (gameState.legalMoves.includes(square)) { const targetPiece = chessGet(square); if (targetPiece) { squareEl.classList.add('legal-capture'); } else { squareEl.classList.add('legal-move'); } } // Add check indicator if (chessInCheck()) { const turn = chessTurn(); const kingSquare = findKingSquare(turn); if (square === kingSquare) { const checkRing = document.createElement('div'); checkRing.className = 'check-ring'; squareEl.appendChild(checkRing); } } // Add click handler squareEl.addEventListener('click', () => handleSquareClick(square)); elements.chessboard.appendChild(squareEl); }); }); // Update turn indicator updateTurnIndicator(); } function extractSquareFromSan(san) { if (!san || typeof san !== 'string') return null; // Extract destination square from SAN notation const match = san.match(/[a-h][1-8]/g); if (match && match.length > 0) { return match[match.length - 1]; } return null; } function findKingSquare(color) { for (let file of FILES) { for (let rank of RANKS) { const square = file + rank; const piece = chessGet(square); if (piece && piece.type === 'k' && piece.color === color) { return square; } } } return null; } function handleSquareClick(square) { if (gameState.gameOver || !chess) return; const turn = chessTurn(); const myTurn = (turn === 'w' && gameState.playerColor === 'white') || (turn === 'b' && gameState.playerColor === 'black'); if (!myTurn) { showToast("It's not your turn", 'warning'); return; } const piece = chessGet(square); // If clicking on own piece, select it if (piece && ((piece.color === 'w' && gameState.playerColor === 'white') || (piece.color === 'b' && gameState.playerColor === 'black'))) { selectSquare(square); return; } // If a piece is selected and clicking on a legal move, make the move if (gameState.selectedSquare && gameState.legalMoves.includes(square)) { attemptMove(gameState.selectedSquare, square); return; } // Clear selection gameState.selectedSquare = null; gameState.legalMoves = []; renderBoard(); } function selectSquare(square) { gameState.selectedSquare = square; // Get legal moves for this piece const moves = chessMoves({ square: square, verbose: true }); gameState.legalMoves = moves.map(m => m.to); renderBoard(); } function attemptMove(from, to) { const piece = chessGet(from); // Check for pawn promotion if (piece && piece.type === 'p') { const targetRank = to.charAt(1); if ((piece.color === 'w' && targetRank === '8') || (piece.color === 'b' && targetRank === '1')) { gameState.pendingPromotion = { from, to }; showPromotionModal(piece.color); return; } } makeMove(from, to); } function makeMove(from, to, promotion = null) { const moveObj = { from: from, to: to }; if (promotion) { moveObj.promotion = promotion; } const move = chessMove(moveObj); if (move) { gameState.lastMove = { from, to }; gameState.selectedSquare = null; gameState.legalMoves = []; // Send move to server socket.emit('make_move', { move: move.san, fen: chessFen(), pgn: chessPgn(), turn: chessTurn(), is_check: chessInCheck(), is_checkmate: chessIsCheckmate(), is_draw: chessIsDraw(), is_stalemate: chessIsStalemate() }); // Play promotion sound if (promotion && sounds.promote) { sounds.promote(); } // Render immediately for responsiveness renderBoard(); } else { showToast('Invalid move', 'error'); gameState.selectedSquare = null; gameState.legalMoves = []; renderBoard(); } } function showPromotionModal(color) { if (elements.promotionPieces) { const pieces = elements.promotionPieces.querySelectorAll('.promotion-piece'); pieces.forEach(piece => { const pieceType = piece.dataset.piece; const pieceChar = color === 'w' ? pieceType.toUpperCase() : pieceType; piece.textContent = PIECES[pieceChar]; piece.className = `promotion-piece piece-${color === 'w' ? 'white' : 'black'}`; }); } showModal('promotion-modal'); } // ======================================== // Timer Management // ======================================== function startTimer() { if (!gameState.timeControl) return; stopTimer(); timers.interval = setInterval(() => { if (gameState.gameOver || !chess) { stopTimer(); return; } const currentTurn = chessTurn(); if (currentTurn === 'w') { timers.white = Math.max(0, (timers.white || 0) - 0.1); } else { timers.black = Math.max(0, (timers.black || 0) - 0.1); } updateTimers(); // Check for timeout if (timers.white <= 0 || timers.black <= 0) { stopTimer(); } }, 100); } function stopTimer() { if (timers.interval) { clearInterval(timers.interval); timers.interval = null; } } function updateTimers() { if (!gameState.timeControl) return; const playerIsWhite = gameState.playerColor === 'white'; const playerTime = playerIsWhite ? timers.white : timers.black; const opponentTime = playerIsWhite ? timers.black : timers.white; if (elements.playerTime) { elements.playerTime.textContent = formatTime(playerTime); } if (elements.opponentTime) { elements.opponentTime.textContent = formatTime(opponentTime); } // Add low time warning if (elements.playerTimer) { if (playerTime !== null && playerTime < 30) { elements.playerTimer.classList.add('low-time'); } else { elements.playerTimer.classList.remove('low-time'); } } if (elements.opponentTimer) { if (opponentTime !== null && opponentTime < 30) { elements.opponentTimer.classList.add('low-time'); } else { elements.opponentTimer.classList.remove('low-time'); } } } function updateTurnIndicator() { if (!chess) return; const currentTurn = chessTurn(); const isPlayerTurn = (currentTurn === 'w' && gameState.playerColor === 'white') || (currentTurn === 'b' && gameState.playerColor === 'black'); const playerInfo = document.querySelector('.player-self-info'); const opponentInfo = document.querySelector('.opponent-info'); if (playerInfo && opponentInfo) { if (isPlayerTurn) { playerInfo.classList.add('active-turn'); opponentInfo.classList.remove('active-turn'); } else { playerInfo.classList.remove('active-turn'); opponentInfo.classList.add('active-turn'); } } } // ======================================== // Move History // ======================================== function updateMoveHistory() { if (!elements.moveHistory || !chess) return; const history = chessHistory(); elements.moveHistory.innerHTML = ''; for (let i = 0; i < history.length; i += 2) { const moveNum = Math.floor(i / 2) + 1; const whiteMove = history[i] || ''; const blackMove = history[i + 1] || ''; const row = document.createElement('div'); row.className = 'move-row'; row.innerHTML = ` ${moveNum}. ${whiteMove} ${blackMove} `; elements.moveHistory.appendChild(row); } // Scroll to bottom elements.moveHistory.scrollTop = elements.moveHistory.scrollHeight; } // ======================================== // Captured Pieces // ======================================== function updateCapturedPieces() { if (!chess) return; const startingPieces = { 'w': { p: 8, n: 2, b: 2, r: 2, q: 1 }, 'b': { p: 8, n: 2, b: 2, r: 2, q: 1 } }; const currentPieces = { 'w': { p: 0, n: 0, b: 0, r: 0, q: 0 }, 'b': { p: 0, n: 0, b: 0, r: 0, q: 0 } }; // Count current pieces for (let file of FILES) { for (let rank of RANKS) { const piece = chessGet(file + rank); if (piece && piece.type !== 'k') { currentPieces[piece.color][piece.type]++; } } } // Calculate captured pieces const captured = { 'w': {}, 'b': {} }; for (let color of ['w', 'b']) { for (let type of ['q', 'r', 'b', 'n', 'p']) { const diff = startingPieces[color][type] - currentPieces[color][type]; if (diff > 0) { captured[color][type] = diff; } } } // Render captured pieces const playerCapturedColor = gameState.playerColor === 'white' ? 'b' : 'w'; const opponentCapturedColor = gameState.playerColor === 'white' ? 'w' : 'b'; if (elements.playerCaptured) { elements.playerCaptured.innerHTML = renderCapturedPieces(captured[playerCapturedColor], playerCapturedColor); } if (elements.opponentCaptured) { elements.opponentCaptured.innerHTML = renderCapturedPieces(captured[opponentCapturedColor], opponentCapturedColor); } } function renderCapturedPieces(captured, color) { let html = ''; for (let type of ['q', 'r', 'b', 'n', 'p']) { if (captured[type]) { const pieceChar = color === 'w' ? type.toUpperCase() : type; for (let i = 0; i < captured[type]; i++) { html += `${PIECES[pieceChar]}`; } } } return html; } // ======================================== // Chat // ======================================== function addChatMessage(username, message, timestamp) { if (!elements.chatMessages) return; const time = timestamp ? new Date(timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; const messageEl = document.createElement('div'); messageEl.className = 'chat-message'; messageEl.innerHTML = `
No rooms available. Create one!
'; return; } elements.roomsList.innerHTML = rooms.map(room => `