/* --- Chess Game Logic & AI --- */ // Game State let game = new Chess(); let boardEl = document.getElementById('board'); let selectedSquare = null; let aiDepth = 2; // Default Medium let playerColor = 'w'; // Player is white let gameOver = false; // Piece Unicode Map const pieces = { 'p': '♟', 'n': '♞', 'b': '♝', 'r': '♜', 'q': '♛', 'k': '♚', 'P': '♟', 'N': '♞', 'B': '♝', 'R': '♜', 'Q': '♛', 'K': '♚' }; // Piece Values for AI const pieceValues = { p: 10, n: 30, b: 30, r: 50, q: 90, k: 900, P: -10, N: -30, B: -30, R: -50, Q: -90, K: -900 // From Black perspective }; // Initial Render document.addEventListener('DOMContentLoaded', () => { renderBoard(); updateStatus(); }); // --- Board Rendering --- function renderBoard() { boardEl.innerHTML = ''; const boardState = game.board(); // Last move highlight const history = game.history({ verbose: true }); const lastMove = history.length ? history[history.length - 1] : null; for (let i = 0; i < 8; i++) { for (let j = 0; j < 8; j++) { const squareDiv = document.createElement('div'); const isLight = (i + j) % 2 === 0; const squareName = String.fromCharCode(97 + j) + (8 - i); const piece = boardState[i][j]; squareDiv.className = `square ${isLight ? 'light' : 'dark'}`; squareDiv.dataset.square = squareName; squareDiv.onclick = () => handleSquareClick(squareName); // Highlight selected if (selectedSquare === squareName) { squareDiv.classList.add('selected'); } // Highlight last move if (lastMove && (lastMove.from === squareName || lastMove.to === squareName)) { squareDiv.classList.add('last-move'); } // Highlight King in Check if (piece && piece.type === 'k' && piece.color === game.turn() && game.in_check()) { squareDiv.classList.add('check'); } // Highlight valid moves if a piece is selected if (selectedSquare) { const moves = game.moves({ square: selectedSquare, verbose: true }); const move = moves.find(m => m.to === squareName); if (move) { if (move.flags.includes('c') || move.flags.includes('e')) { squareDiv.classList.add('valid-capture'); } else { squareDiv.classList.add('valid-move'); } } } // Render Piece if (piece) { const pieceSpan = document.createElement('span'); pieceSpan.className = `piece ${piece.color === 'w' ? 'white' : 'black'}`; // Using standard chess unicode pieceSpan.textContent = pieces[piece.type]; // Note: Standard unicode usually comes in outline/filled variants. // For simplicity in this constrained env, we use text color to differentiate. if(piece.color === 'w') pieceSpan.textContent = pieces[piece.type.toUpperCase()]; else pieceSpan.textContent = pieces[piece.type]; squareDiv.appendChild(pieceSpan); } boardEl.appendChild(squareDiv); } } } // --- Interaction --- function handleSquareClick(square) { if (gameOver || game.turn() !== playerColor) return; const piece = game.get(square); // Select a piece if (piece && piece.color === playerColor) { selectedSquare = square; playSound('move'); renderBoard(); return; } // Move piece if (selectedSquare) { const moves = game.moves({ square: selectedSquare, verbose: true }); const move = moves.find(m => m.to === square); if (move) { game.move(move.san); selectedSquare = null; renderBoard(); updateStatus(); updateHistory(); // Trigger AI after short delay if (!game.game_over()) { setTimeout(makeAIMove, 250); } } else { // Deselect if clicking invalid empty square if(!piece) { selectedSquare = null; renderBoard(); } } } } // --- AI Engine (Minimax) --- function makeAIMove() { if (game.game_over()) return; updateStatus("AI is thinking..."); // Use timeout to allow UI update before heavy calculation setTimeout(() => { const bestMove = calculateBestMove(game, aiDepth); game.move(bestMove); renderBoard(); updateStatus(); updateHistory(); playSound('move'); }, 100); } function calculateBestMove(gameInstance, depth) { const possibleMoves = gameInstance.moves(); if (possibleMoves.length === 0) return null; // Alpha-Beta Pruning let bestMove = -Infinity; let bestMoveFound = null; // Shuffle moves to add randomness to equal positions possibleMoves.sort(() => Math.random() - 0.5); for (const move of possibleMoves) { gameInstance.move(move); const value = minimax(gameInstance, depth - 1, -10000, 10000, false); gameInstance.undo(); if (value >= bestMove) { bestMove = value; bestMoveFound = move; } } return bestMoveFound || possibleMoves[Math.floor(Math.random() * possibleMoves.length)]; } function minimax(gameInstance, depth, alpha, beta, isMaximizingPlayer) { if (depth === 0) { return -evaluateBoard(gameInstance.board()); } const possibleMoves = gameInstance.moves(); if (possibleMoves.length === 0) { if (gameInstance.in_checkmate()) return isMaximizingPlayer ? -10000 : 10000; // Depth * value return 0; // Stalemate } if (isMaximizingPlayer) { let bestMove = -9999; for (const move of possibleMoves) { gameInstance.move(move); bestMove = Math.max(bestMove, minimax(gameInstance, depth - 1, alpha, beta, !isMaximizingPlayer)); gameInstance.undo(); alpha = Math.max(alpha, bestMove); if (beta <= alpha) return bestMove; } return bestMove; } else { let bestMove = 9999; for (const move of possibleMoves) { gameInstance.move(move); bestMove = Math.min(bestMove, minimax(gameInstance, depth - 1, alpha, beta, !isMaximizingPlayer)); gameInstance.undo(); beta = Math.min(beta, bestMove); if (beta <= alpha) return bestMove; } return bestMove; } } function evaluateBoard(board) { let totalEvaluation = 0; for (let i = 0; i < 8; i++) { for (let j = 0; j < 8; j++) { totalEvaluation += getPieceValue(board[i][j]); } } return totalEvaluation; } function getPieceValue(piece) { if (piece === null) return 0; // Simple logic: AI is Black, wants to minimize positive score (which favors white) // We want AI (Black) to maximize its own value. // Let's standardise: Positive = Good for Black, Negative = Good for White // Re-map for standard Minimax // If I am playing as Black (AI), I want positive score. let value = 0; if (piece.color === 'b') { value = pieceValues[piece.type]; } else { value = pieceValues[piece.type.toUpperCase()]; // Negative values } // Add position bonuses (Simplified: center control) const centerBonus = (i, j) => { if (i >= 3 && i <= 4 && j >= 3 && j <= 4) return 1; return 0; } // Adjust based on simple position if(piece.color === 'b') return value + (piece.type === 'n' ? centerBonus(i,j) : 0); return value; } // --- UI Helpers --- function updateStatus(msg = null) { const statusEl = document.getElementById('game-status'); if (msg) { statusEl.textContent = msg; return; } let status = ''; const turn = game.turn() === 'w' ? 'White' : 'Black'; if (game.in_checkmate()) { status = `Game Over: ${turn === 'White' ? 'Black' : 'White'} wins by checkmate!`; gameOver = true; playSound('capture'); } else if (game.in_draw()) { status = 'Game Over: Draw!'; gameOver = true; } else { status = `${turn} to move`; if (game.in_check()) status += ' (Check!)'; } statusEl.textContent = status; } function updateHistory() { const historyEl = document.getElementById('move-history'); const history = game.history(); let html = ''; for (let i = 0; i < history.length; i += 2) { const moveNum = Math.floor(i / 2) + 1; html += `
${moveNum}. ${history[i]} ${history[i+1] ? `${history[i+1]}` : '...'}
`; } historyEl.innerHTML = html; historyEl.scrollTop = historyEl.scrollHeight; } function resetGame() { game.reset(); gameOver = false; selectedSquare = null; updateStatus(); renderBoard(); document.getElementById('move-history').innerHTML = '

Game started...

'; playSound('move'); } function setDifficulty(level) { aiDepth = level; document.querySelectorAll('.diff-btn').forEach(btn => { btn.classList.remove('bg-primary', 'text-white', 'active'); btn.classList.add('text-gray-400'); }); // Simple active state logic based on click text const buttons = document.querySelectorAll('.diff-btn'); const index = level - 1; buttons[index].classList.add('bg-primary', 'text-white', 'active'); buttons[index].classList.remove('text-gray-400'); resetGame(); } // Simple Sound Synth (Beeps) const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); function playSound(type) { if (audioCtx.state === 'suspended') audioCtx.resume(); const osc = audioCtx.createOscillator(); const gainNode = audioCtx.createGain(); osc.connect(gainNode); gainNode.connect(audioCtx.destination); if (type === 'move') { osc.type = 'sine'; osc.frequency.setValueAtTime(300, audioCtx.currentTime); osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.1); gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1); osc.start(); osc.stop(audioCtx.currentTime + 0.1); } else if (type === 'capture') { osc.type = 'triangle'; osc.frequency.setValueAtTime(150, audioCtx.currentTime); gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime); gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2); osc.start(); osc.stop(audioCtx.currentTime + 0.2); } }