Spaces:
Paused
Paused
Download static/js/game.js from smodusermc/chess: direct link, hf CLI and curl.
- Browser
- Download file 58.8 kB
-
https://huggingface.co/spaces/smodusermc/chess/resolve/main/static/js/game.js
- Command line
-
hf download hf://spaces/smodusermc/chess/static/js/game.js
-
curl -L -o game.js https://huggingface.co/spaces/smodusermc/chess/resolve/main/static/js/game.js
58.8 kB
| // ======================================== | |
| // 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 = ` | |
| <i class="fas ${icons[type]}"></i> | |
| <span>${message}</span> | |
| `; | |
| 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 = '<i class="fas fa-handshake"></i>'; | |
| } else if (isWinner) { | |
| elements.gameOverIcon.innerHTML = '<i class="fas fa-trophy"></i>'; | |
| } else { | |
| elements.gameOverIcon.classList.add('loss'); | |
| elements.gameOverIcon.innerHTML = '<i class="fas fa-times"></i>'; | |
| } | |
| } | |
| 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 = '<i class="fas fa-trophy"></i>'; | |
| } else { | |
| elements.gameOverIcon.classList.add('loss'); | |
| elements.gameOverIcon.innerHTML = '<i class="fas fa-times"></i>'; | |
| } | |
| } | |
| 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 = '<i class="fas fa-handshake"></i>'; | |
| } | |
| 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 = ` | |
| <span class="move-number">${moveNum}.</span> | |
| <span class="move-white">${whiteMove}</span> | |
| <span class="move-black">${blackMove}</span> | |
| `; | |
| 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 += `<span class="piece-${color === 'w' ? 'white' : 'black'}">${PIECES[pieceChar]}</span>`; | |
| } | |
| } | |
| } | |
| 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 = ` | |
| <div class="sender">${escapeHtml(username)}</div> | |
| <div class="text">${escapeHtml(message)}</div> | |
| <div class="time">${time}</div> | |
| `; | |
| elements.chatMessages.appendChild(messageEl); | |
| elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; | |
| } | |
| function updateChatBadge() { | |
| if (!elements.chatBadge) return; | |
| if (unreadMessages > 0) { | |
| elements.chatBadge.textContent = unreadMessages > 9 ? '9+' : unreadMessages; | |
| elements.chatBadge.classList.remove('hidden'); | |
| } else { | |
| elements.chatBadge.classList.add('hidden'); | |
| } | |
| } | |
| function sendChatMessage() { | |
| if (!elements.chatInput) return; | |
| const message = elements.chatInput.value.trim(); | |
| if (message && socket) { | |
| socket.emit('chat_message', { message: message }); | |
| elements.chatInput.value = ''; | |
| } | |
| } | |
| // ======================================== | |
| // Rooms List | |
| // ======================================== | |
| function renderRoomsList(rooms) { | |
| if (!elements.roomsList) return; | |
| if (!rooms || rooms.length === 0) { | |
| elements.roomsList.innerHTML = '<p class="no-rooms">No rooms available. Create one!</p>'; | |
| return; | |
| } | |
| elements.roomsList.innerHTML = rooms.map(room => ` | |
| <div class="room-item" data-room-id="${room.room_id}"> | |
| <div class="room-info"> | |
| <div class="room-host">${escapeHtml(room.host)}'s Room</div> | |
| <div class="room-time">${room.time_control ? room.time_control + ' min' : 'No Limit'}</div> | |
| </div> | |
| <button class="btn btn-secondary join-room-btn" data-room-id="${room.room_id}"> | |
| Join | |
| </button> | |
| </div> | |
| `).join(''); | |
| // Add click handlers | |
| elements.roomsList.querySelectorAll('.join-room-btn').forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| const roomId = btn.dataset.roomId; | |
| joinRoom(roomId); | |
| }); | |
| }); | |
| } | |
| function joinRoom(roomId) { | |
| const username = elements.usernameInput ? elements.usernameInput.value.trim() : 'Anonymous'; | |
| gameState.username = username || 'Anonymous'; | |
| if (socket) { | |
| socket.emit('join_room', { | |
| username: gameState.username, | |
| room_id: roomId | |
| }); | |
| } | |
| } | |
| // ======================================== | |
| // Reset Game | |
| // ======================================== | |
| function resetGame() { | |
| gameState.roomId = null; | |
| gameState.playerColor = null; | |
| gameState.opponent = null; | |
| gameState.timeControl = null; | |
| gameState.isMyTurn = false; | |
| gameState.gameStarted = false; | |
| gameState.gameOver = false; | |
| gameState.selectedSquare = null; | |
| gameState.legalMoves = []; | |
| gameState.lastMove = null; | |
| gameState.isFlipped = false; | |
| gameState.pendingPromotion = null; | |
| stopTimer(); | |
| timers.white = null; | |
| timers.black = null; | |
| if (gameState.disconnectInterval) { | |
| clearInterval(gameState.disconnectInterval); | |
| } | |
| chess = null; | |
| } | |
| // ======================================== | |
| // Event Listeners | |
| // ======================================== | |
| function initEventListeners() { | |
| // Time control buttons | |
| if (elements.timeBtns) { | |
| elements.timeBtns.forEach(btn => { | |
| btn.addEventListener('click', () => { | |
| elements.timeBtns.forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| }); | |
| }); | |
| } | |
| // Create game | |
| if (elements.createGameBtn) { | |
| elements.createGameBtn.addEventListener('click', () => { | |
| const username = elements.usernameInput ? elements.usernameInput.value.trim() : ''; | |
| const activeTimeBtn = document.querySelector('.time-btn.active'); | |
| const timeControl = activeTimeBtn && activeTimeBtn.dataset.time !== 'null' | |
| ? parseInt(activeTimeBtn.dataset.time) | |
| : null; | |
| gameState.username = username || 'Anonymous'; | |
| if (socket) { | |
| socket.emit('create_room', { | |
| username: gameState.username, | |
| time_control: timeControl | |
| }); | |
| } | |
| }); | |
| } | |
| // Join game | |
| if (elements.joinGameBtn) { | |
| elements.joinGameBtn.addEventListener('click', () => { | |
| const roomCode = elements.roomCodeInput ? elements.roomCodeInput.value.trim().toUpperCase() : ''; | |
| if (!roomCode) { | |
| showToast('Please enter a room code', 'warning'); | |
| return; | |
| } | |
| joinRoom(roomCode); | |
| }); | |
| } | |
| // Refresh rooms | |
| if (elements.refreshRoomsBtn) { | |
| elements.refreshRoomsBtn.addEventListener('click', () => { | |
| if (socket) { | |
| socket.emit('get_rooms'); | |
| } | |
| }); | |
| } | |
| // Copy room code | |
| if (elements.copyCodeBtn) { | |
| elements.copyCodeBtn.addEventListener('click', () => { | |
| if (gameState.roomId) { | |
| navigator.clipboard.writeText(gameState.roomId).then(() => { | |
| showToast('Room code copied!', 'success'); | |
| }).catch(() => { | |
| showToast('Failed to copy', 'error'); | |
| }); | |
| } | |
| }); | |
| } | |
| // Cancel waiting | |
| if (elements.cancelWaitingBtn) { | |
| elements.cancelWaitingBtn.addEventListener('click', () => { | |
| showScreen('lobby-screen'); | |
| if (socket) { | |
| socket.emit('leave_room'); | |
| } | |
| resetGame(); | |
| }); | |
| } | |
| // Resign button | |
| if (elements.resignBtn) { | |
| elements.resignBtn.addEventListener('click', () => { | |
| if (!gameState.gameOver) { | |
| showModal('resign-modal'); | |
| } | |
| }); | |
| } | |
| // Confirm resign | |
| if (elements.confirmResignBtn) { | |
| elements.confirmResignBtn.addEventListener('click', () => { | |
| hideModal('resign-modal'); | |
| if (socket) { | |
| socket.emit('resign'); | |
| } | |
| }); | |
| } | |
| // Cancel resign | |
| if (elements.cancelResignBtn) { | |
| elements.cancelResignBtn.addEventListener('click', () => { | |
| hideModal('resign-modal'); | |
| }); | |
| } | |
| // Draw button | |
| if (elements.drawBtn) { | |
| elements.drawBtn.addEventListener('click', () => { | |
| if (!gameState.gameOver && socket) { | |
| socket.emit('offer_draw'); | |
| showToast('Draw offer sent', 'info'); | |
| } | |
| }); | |
| } | |
| // Accept draw | |
| if (elements.acceptDrawBtn) { | |
| elements.acceptDrawBtn.addEventListener('click', () => { | |
| hideModal('draw-offer-modal'); | |
| if (socket) { | |
| socket.emit('accept_draw'); | |
| } | |
| }); | |
| } | |
| // Decline draw | |
| if (elements.declineDrawBtn) { | |
| elements.declineDrawBtn.addEventListener('click', () => { | |
| hideModal('draw-offer-modal'); | |
| if (socket) { | |
| socket.emit('decline_draw'); | |
| } | |
| }); | |
| } | |
| // Flip board | |
| if (elements.flipBoardBtn) { | |
| elements.flipBoardBtn.addEventListener('click', () => { | |
| gameState.isFlipped = !gameState.isFlipped; | |
| renderBoard(); | |
| }); | |
| } | |
| // Chat toggle | |
| if (elements.chatToggleBtn && elements.chatPanel) { | |
| elements.chatToggleBtn.addEventListener('click', () => { | |
| elements.chatPanel.classList.toggle('hidden'); | |
| if (!elements.chatPanel.classList.contains('hidden')) { | |
| unreadMessages = 0; | |
| updateChatBadge(); | |
| } | |
| }); | |
| } | |
| // Send chat | |
| if (elements.sendChatBtn) { | |
| elements.sendChatBtn.addEventListener('click', sendChatMessage); | |
| } | |
| if (elements.chatInput) { | |
| elements.chatInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter') { | |
| sendChatMessage(); | |
| } | |
| }); | |
| } | |
| // Theme toggle | |
| if (elements.themeToggle && elements.themeDropdown) { | |
| elements.themeToggle.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| elements.themeDropdown.classList.toggle('hidden'); | |
| }); | |
| } | |
| // Theme selection | |
| document.querySelectorAll('.theme-option').forEach(option => { | |
| option.addEventListener('click', () => { | |
| setTheme(option.dataset.theme); | |
| if (elements.themeDropdown) { | |
| elements.themeDropdown.classList.add('hidden'); | |
| } | |
| }); | |
| }); | |
| // Close dropdown when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (elements.themeToggle && elements.themeDropdown) { | |
| if (!elements.themeToggle.contains(e.target) && !elements.themeDropdown.contains(e.target)) { | |
| elements.themeDropdown.classList.add('hidden'); | |
| } | |
| } | |
| }); | |
| // Sound toggle | |
| if (elements.soundToggle) { | |
| elements.soundToggle.addEventListener('click', () => { | |
| gameState.soundEnabled = !gameState.soundEnabled; | |
| elements.soundToggle.innerHTML = gameState.soundEnabled | |
| ? '<i class="fas fa-volume-up"></i>' | |
| : '<i class="fas fa-volume-mute"></i>'; | |
| showToast(gameState.soundEnabled ? 'Sound enabled' : 'Sound disabled', 'info'); | |
| }); | |
| } | |
| // Promotion pieces | |
| if (elements.promotionPieces) { | |
| elements.promotionPieces.querySelectorAll('.promotion-piece').forEach(piece => { | |
| piece.addEventListener('click', () => { | |
| const promotionPiece = piece.dataset.piece; | |
| hideModal('promotion-modal'); | |
| if (gameState.pendingPromotion) { | |
| makeMove(gameState.pendingPromotion.from, gameState.pendingPromotion.to, promotionPiece); | |
| gameState.pendingPromotion = null; | |
| } | |
| }); | |
| }); | |
| } | |
| // Play again | |
| if (elements.playAgainBtn) { | |
| elements.playAgainBtn.addEventListener('click', () => { | |
| hideModal('game-over-modal'); | |
| resetGame(); | |
| showScreen('lobby-screen'); | |
| }); | |
| } | |
| // Back to lobby | |
| if (elements.backToLobbyBtn) { | |
| elements.backToLobbyBtn.addEventListener('click', () => { | |
| hideModal('game-over-modal'); | |
| resetGame(); | |
| showScreen('lobby-screen'); | |
| }); | |
| } | |
| // Enter key for room code | |
| if (elements.roomCodeInput) { | |
| elements.roomCodeInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && elements.joinGameBtn) { | |
| elements.joinGameBtn.click(); | |
| } | |
| }); | |
| } | |
| // Enter key for username | |
| if (elements.usernameInput) { | |
| elements.usernameInput.addEventListener('keypress', (e) => { | |
| if (e.key === 'Enter' && elements.createGameBtn) { | |
| elements.createGameBtn.click(); | |
| } | |
| }); | |
| } | |
| } | |
| // ======================================== | |
| // Initialization | |
| // ======================================== | |
| function init() { | |
| console.log('Initializing Chess Arena...'); | |
| // Cache DOM elements | |
| cacheElements(); | |
| // Initialize theme | |
| initTheme(); | |
| // Initialize sounds | |
| initSounds(); | |
| // Initialize socket | |
| initSocket(); | |
| // Initialize event listeners | |
| initEventListeners(); | |
| // Request initial rooms list | |
| setTimeout(() => { | |
| if (socket) { | |
| socket.emit('get_rooms'); | |
| } | |
| }, 500); | |
| // Hide loading screen | |
| setTimeout(() => { | |
| if (elements.loadingScreen) { | |
| elements.loadingScreen.classList.add('hidden'); | |
| } | |
| if (elements.app) { | |
| elements.app.classList.remove('hidden'); | |
| } | |
| console.log('Chess Arena initialized successfully'); | |
| }, 1500); | |
| } | |
| // Start the app when DOM is ready | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { | |
| init(); | |
| } | |
| })(); |