Spaces:
Running
Running
| /* --- 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 += `<div class="flex gap-2 text-xs"> | |
| <span class="text-gray-500">${moveNum}.</span> | |
| <span class="text-gray-200">${history[i]}</span> | |
| ${history[i+1] ? `<span class="text-gray-200">${history[i+1]}</span>` : '<span class="opacity-20">...</span>'} | |
| </div>`; | |
| } | |
| historyEl.innerHTML = html; | |
| historyEl.scrollTop = historyEl.scrollHeight; | |
| } | |
| function resetGame() { | |
| game.reset(); | |
| gameOver = false; | |
| selectedSquare = null; | |
| updateStatus(); | |
| renderBoard(); | |
| document.getElementById('move-history').innerHTML = '<p class="text-center italic opacity-50">Game started...</p>'; | |
| 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); | |
| } | |
| } |