chess / static /js /game.js
smodusermc's picture
Create js/game.js
11683d6 verified
Raw History Blame Contribute Delete
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();
}
})();