See the Pen simple but hard chess 😀 by sinanisler (@sinanisler) on CodePen.
<style> #game *{ user-select:none } #game { width: 480px; height: 480px; margin: 20px auto; display: flex; flex-wrap: wrap; } .square { width: 60px; height: 60px; box-sizing: border-box; position: relative; } .square.light { background-color: #f0d9b5; } .square.dark { background-color: #b58863; } .piece { font-size: 50px; text-align: center; line-height: 60px; cursor: pointer; } .piece.selected { background-color: rgba(255, 255, 0, 0.5); } /* Added CSS for the message div */ #message { width: 480px; margin: 10px auto; text-align: center; color: red; font-weight: bold; min-height: 20px; } </style> <div id="game"> <!-- Chessboard will be generated here --> </div> <!-- Added message div --> <div id="message"></div> <script> const game = document.getElementById('game'); let board = [ ["bR", "bN", "bB", "bQ", "bK", "bB", "bN", "bR"], ["bP", "bP", "bP", "bP", "bP", "bP", "bP", "bP"], ["", "", "", "", "", "", "", ""], ["", "", "", "", "", "", "", ""], ["", "", "", "", "", "", "", ""], ["", "", "", "", "", "", "", ""], ["wP", "wP", "wP", "wP", "wP", "wP", "wP", "wP"], ["wR", "wN", "wB", "wQ", "wK", "wB", "wN", "wR"] ]; const pieces = { 'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙', 'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟' }; // Assigned values to each piece type const pieceValues = { 'P': 1, 'N': 3, 'B': 3, 'R': 5, 'Q': 9, 'K': 1000 }; let selectedPiece = null; let turn = 'w'; // 'w' for white's turn, 'b' for black's turn // Set AI color to black let aiColor = 'b'; let opponentColor = aiColor === 'w' ? 'b' : 'w'; // Keep track of whether the game is over let gameOver = false; // Keep track of whether kings and rooks have moved (for castling) let kingMoved = { 'w': false, 'b': false }; let rookMoved = { 'w0': false, // white rook on a1 'w1': false, // white rook on h1 'b0': false, // black rook on a8 'b1': false // black rook on h8 }; // Keep track of en passant target square let enPassantSquare = null; // Draw the board function drawBoard() { game.innerHTML = ''; for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const square = document.createElement('div'); square.classList.add('square'); if ((row + col) % 2 == 0) { square.classList.add('light'); } else { square.classList.add('dark'); } square.dataset.row = row; square.dataset.col = col; const piece = board[row][col]; if (piece) { const pieceEl = document.createElement('div'); pieceEl.classList.add('piece'); pieceEl.textContent = pieces[piece]; pieceEl.dataset.piece = piece; pieceEl.dataset.row = row; pieceEl.dataset.col = col; square.appendChild(pieceEl); } game.appendChild(square); } } } drawBoard(); game.addEventListener('click', function (e) { if (gameOver) { return; // Do nothing if the game is over } const target = e.target; if (turn === aiColor) { return; // It's AI's turn } if (target.classList.contains('piece')) { const piece = target.dataset.piece; const color = piece[0]; if (selectedPiece) { if (color === turn && color !== aiColor) { // Change selection to the new piece selectPiece(target); // Clear any messages document.getElementById('message').innerText = ''; } else { // Try to move to this square (could be a capture) movePiece(target.parentElement); } } else { // Select the piece if it's the player's own piece if (color === turn && color !== aiColor) { selectPiece(target); // Clear any messages document.getElementById('message').innerText = ''; } } } else if (target.classList.contains('square')) { movePiece(target); } }); function selectPiece(pieceEl) { const piece = pieceEl.dataset.piece; const color = piece[0]; if (color !== turn || color === aiColor) { return; // Not this player's turn or AI's piece } if (selectedPiece) { // Deselect previous piece selectedPiece.classList.remove('selected'); } selectedPiece = pieceEl; selectedPiece.classList.add('selected'); } function movePiece(targetEl) { if (gameOver) { return; // Do nothing if the game is over } if (!selectedPiece) { return; } let toRow, toCol; if (targetEl.classList.contains('square')) { toRow = parseInt(targetEl.dataset.row); toCol = parseInt(targetEl.dataset.col); } else if (targetEl.classList.contains('piece')) { toRow = parseInt(targetEl.dataset.row); toCol = parseInt(targetEl.dataset.col); } else { return; } const fromRow = parseInt(selectedPiece.dataset.row); const fromCol = parseInt(selectedPiece.dataset.col); // Implement move validation here const piece = selectedPiece.dataset.piece; let castling = false; let enPassantCapture = false; const valid = isValidMove(piece, fromRow, fromCol, toRow, toCol, castling, enPassantCapture); if (valid) { // Move the piece board[toRow][toCol] = piece; board[fromRow][fromCol] = ""; const pieceType = piece[1]; const color = piece[0]; const dir = color === 'w' ? -1 : 1; // Handle en passant capture if (enPassantCapture) { board[fromRow][toCol] = ""; // Remove the captured pawn } // Handle castling move if (castling) { // Move the rook accordingly let rookFromCol = toCol > fromCol ? 7 : 0; let rookToCol = toCol > fromCol ? 5 : 3; let rookPiece = board[fromRow][rookFromCol]; board[fromRow][rookToCol] = rookPiece; board[fromRow][rookFromCol] = ""; // Update rookMoved rookMoved[color + (rookFromCol === 0 ? '0' : '1')] = true; } // Update moved flags if (pieceType === 'K') { kingMoved[color] = true; } if (pieceType === 'R') { let rookId = (fromCol === 0) ? '0' : (fromCol === 7) ? '1' : null; if (rookId !== null) { rookMoved[color + rookId] = true; } } // Check for pawn promotion if (piece[1] === 'P' && (toRow === 0 || toRow === 7)) { // Promote pawn to queen board[toRow][toCol] = piece[0] + 'Q'; } // Handle en passant target square if (pieceType === 'P' && Math.abs(toRow - fromRow) === 2) { enPassantSquare = { row: fromRow + dir, col: fromCol, color: color }; } else { enPassantSquare = null; } selectedPiece.classList.remove('selected'); selectedPiece = null; drawBoard(); // Clear any messages document.getElementById('message').innerText = ''; // Switch turn turn = turn === 'w' ? 'b' : 'w'; // Check for checkmate or stalemate if (isGameOver()) { let message = isKingInCheck(turn) ? 'Checkmate! ' : 'Stalemate! '; message += turn === aiColor ? 'You win!' : 'You lost!'; document.getElementById('message').innerText = message; gameOver = true; } else { // Check if it's AI's turn if (turn === aiColor) { setTimeout(makeAIMove, 500); // Slight delay for realism } } } else { // Invalid move document.getElementById('message').innerText = 'Invalid move'; } } function isValidMove(piece, fromRow, fromCol, toRow, toCol, castling, enPassantCapture) { const pieceType = piece[1]; const color = piece[0]; const dir = color === 'w' ? -1 : 1; // Direction for pawns // Check if the destination square has a friendly piece const destinationPiece = board[toRow][toCol]; if (destinationPiece && destinationPiece[0] === color) { return false; } let valid = false; switch (pieceType) { case 'P': // Pawn move if (fromCol === toCol) { // Move forward if (board[toRow][toCol] === "") { if (toRow - fromRow === dir) { valid = true; } // Initial double move else if ((color === 'w' && fromRow === 6 && toRow === 4 && board[5][fromCol] === "") || (color === 'b' && fromRow === 1 && toRow === 3 && board[2][fromCol] === "")) { valid = true; } } } else if (Math.abs(fromCol - toCol) === 1 && toRow - fromRow === dir) { // Capture if (board[toRow][toCol] && board[toRow][toCol][0] !== color) { valid = true; } // En passant else if (enPassantSquare && enPassantSquare.row === toRow && enPassantSquare.col === toCol && enPassantSquare.color !== color) { valid = true; enPassantCapture = true; // Indicate en passant capture } } break; case 'R': // Rook move if (fromRow === toRow || fromCol === toCol) { if (isPathClear(fromRow, fromCol, toRow, toCol)) { valid = true; } } break; case 'N': // Knight move if ((Math.abs(fromRow - toRow) === 2 && Math.abs(fromCol - toCol) === 1) || (Math.abs(fromRow - toRow) === 1 && Math.abs(fromCol - toCol) === 2)) { valid = true; } break; case 'B': // Bishop move if (Math.abs(fromRow - toRow) === Math.abs(fromCol - toCol)) { if (isPathClear(fromRow, fromCol, toRow, toCol)) { valid = true; } } break; case 'Q': // Queen move (rook + bishop) if ((fromRow === toRow || fromCol === toCol || Math.abs(fromRow - toRow) === Math.abs(fromCol - toCol)) && isPathClear(fromRow, fromCol, toRow, toCol)) { valid = true; } break; case 'K': // King move if (Math.abs(fromRow - toRow) <= 1 && Math.abs(fromCol - toCol) <= 1) { valid = true; } // Castling else if (!kingMoved[color] && fromRow === toRow && Math.abs(fromCol - toCol) === 2) { if (canCastle(color, fromRow, fromCol, toRow, toCol)) { valid = true; castling = true; // Indicate castling move } } break; } if (valid) { // Temporarily make the move const originalFromPiece = board[fromRow][fromCol]; const originalToPiece = board[toRow][toCol]; const capturedEnPassantPiece = enPassantCapture ? board[fromRow][toCol] : null; board[toRow][toCol] = piece; board[fromRow][fromCol] = ""; if (enPassantCapture) { board[fromRow][toCol] = ""; // Remove the captured pawn } // Check if own king is in check const inCheck = isKingInCheck(color); // Undo the move board[fromRow][fromCol] = originalFromPiece; board[toRow][toCol] = originalToPiece; if (enPassantCapture) { board[fromRow][toCol] = capturedEnPassantPiece; } if (inCheck) { return false; } else { return true; } } return false; } function canCastle(color, fromRow, fromCol, toRow, toCol) { // Determine which side the king is castling to let kingSide = toCol > fromCol; let rookCol = kingSide ? 7 : 0; let rookKey = color + (rookCol === 0 ? '0' : '1'); if (kingMoved[color] || rookMoved[rookKey]) { return false; } // Check if squares between king and rook are empty let colStep = kingSide ? 1 : -1; for (let col = fromCol + colStep; col !== rookCol; col += colStep) { if (board[fromRow][col] !== '') { return false; } } // Check that the king is not in check, and does not pass through or end in check let tempFromCol = fromCol; for (let i = 0; i <= 2; i++) { tempFromCol += colStep; if (i > 0) { // We already know the king is not in check at fromCol // Temporarily move the king let originalPiece = board[fromRow][tempFromCol]; board[fromRow][tempFromCol] = color + 'K'; board[fromRow][fromCol] = ''; if (isKingInCheck(color)) { // Undo the move board[fromRow][fromCol] = color + 'K'; board[fromRow][tempFromCol] = originalPiece; return false; } // Undo the move board[fromRow][fromCol] = color + 'K'; board[fromRow][tempFromCol] = originalPiece; } if (tempFromCol === toCol) break; } return true; } function isPathClear(fromRow, fromCol, toRow, toCol) { const rowStep = toRow - fromRow === 0 ? 0 : (toRow - fromRow) / Math.abs(toRow - fromRow); const colStep = toCol - fromCol === 0 ? 0 : (toCol - fromCol) / Math.abs(toCol - fromCol); let currentRow = fromRow + rowStep; let currentCol = fromCol + colStep; while (currentRow !== toRow || currentCol !== toCol) { if (board[currentRow][currentCol] !== "") { return false; } currentRow += rowStep; currentCol += colStep; } return true; } function isSquareAttacked(row, col, byColor) { for (let fromRow = 0; fromRow < 8; fromRow++) { for (let fromCol = 0; fromCol < 8; fromCol++) { const piece = board[fromRow][fromCol]; if (piece && piece[0] === byColor) { if (isValidMoveForAttack(piece, fromRow, fromCol, row, col)) { return true; } } } } return false; } function isValidMoveForAttack(piece, fromRow, fromCol, toRow, toCol) { const pieceType = piece[1]; const color = piece[0]; const dir = color === 'w' ? -1 : 1; // Direction for pawns switch (pieceType) { case 'P': // Pawn attack move (diagonal capture) if (Math.abs(fromCol - toCol) === 1 && toRow - fromRow === dir) { return true; } break; case 'R': // Rook move if (fromRow === toRow || fromCol === toCol) { if (isPathClear(fromRow, fromCol, toRow, toCol)) { return true; } } break; case 'N': // Knight move if ((Math.abs(fromRow - toRow) === 2 && Math.abs(fromCol - toCol) === 1) || (Math.abs(fromRow - toRow) === 1 && Math.abs(fromCol - toCol) === 2)) { return true; } break; case 'B': // Bishop move if (Math.abs(fromRow - toRow) === Math.abs(fromCol - toCol)) { if (isPathClear(fromRow, fromCol, toRow, toCol)) { return true; } } break; case 'Q': // Queen move (rook + bishop) if ((fromRow === toRow || fromCol === toCol || Math.abs(fromRow - toRow) === Math.abs(fromCol - toCol)) && isPathClear(fromRow, fromCol, toRow, toCol)) { return true; } break; case 'K': // King move if (Math.abs(fromRow - toRow) <= 1 && Math.abs(fromCol - toCol) <= 1) { return true; } break; } return false; } function isKingInCheck(color) { let kingRow, kingCol; // Find the king for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const piece = board[row][col]; if (piece === color + 'K') { kingRow = row; kingCol = col; break; } } } if (kingRow === undefined) { // King not found (should not happen in normal play) return true; } const opponentColor = color === 'w' ? 'b' : 'w'; // Check if any opponent piece can attack the king return isSquareAttacked(kingRow, kingCol, opponentColor); } // New function to evaluate the board function evaluateBoard() { let score = 0; for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const piece = board[row][col]; if (piece) { const color = piece[0]; const pieceType = piece[1]; const value = pieceValues[pieceType]; if (color === aiColor) { score += value; } else { score -= value; } } } } return score; } // Function to generate all possible moves for a given color function generateAllMoves(color) { const moves = []; for (let fromRow = 0; fromRow < 8; fromRow++) { for (let fromCol = 0; fromCol < 8; fromCol++) { const piece = board[fromRow][fromCol]; if (piece && piece[0] === color) { for (let toRow = 0; toRow < 8; toRow++) { for (let toCol = 0; toCol < 8; toCol++) { let castling = false; let enPassantCapture = false; if (isValidMove(piece, fromRow, fromCol, toRow, toCol, castling, enPassantCapture)) { moves.push({ piece: piece, fromRow: fromRow, fromCol: fromCol, toRow: toRow, toCol: toCol, castling: castling, enPassantCapture: enPassantCapture }); } } } } } } return moves; } // Function to make a temporary move function makeTemporaryMove(move) { move.capturedPiece = board[move.toRow][move.toCol]; board[move.toRow][move.toCol] = move.piece; board[move.fromRow][move.fromCol] = ""; // Handle special moves if needed (e.g., en passant, castling) } // Function to undo a temporary move function undoTemporaryMove(move) { board[move.fromRow][move.fromCol] = move.piece; board[move.toRow][move.toCol] = move.capturedPiece; // Handle special moves if needed (e.g., en passant, castling) } // Updated AI move function with minimax algorithm function makeAIMove() { // AI depth for minimax const depth = 4; const bestMove = minimaxRoot(depth, aiColor); if (!bestMove) { document.getElementById('message').innerText = 'AI has no valid moves. Game over.'; gameOver = true; return; } // Move the piece const move = bestMove; board[move.toRow][move.toCol] = move.piece; board[move.fromRow][move.fromCol] = ""; // Check for pawn promotion if (move.piece[1] === 'P' && (move.toRow === 0 || move.toRow === 7)) { // Promote pawn to queen board[move.toRow][move.toCol] = move.piece[0] + 'Q'; } drawBoard(); // Switch turn back to human turn = turn === 'w' ? 'b' : 'w'; // Check if the game should continue if (isGameOver()) { let message = isKingInCheck(turn) ? 'Checkmate! ' : 'Stalemate! '; message += turn === aiColor ? 'You win!' : 'You lost!'; document.getElementById('message').innerText = message; gameOver = true; } else { if (turn === aiColor) { setTimeout(makeAIMove, 500); // Continue AI moves if applicable } } } function minimaxRoot(depth, color) { const moves = generateAllMoves(color); let bestMove = null; let bestValue = -Infinity; for (let move of moves) { makeTemporaryMove(move); let value = minimax(depth - 1, false, -Infinity, Infinity); undoTemporaryMove(move); if (value > bestValue) { bestValue = value; bestMove = move; } } return bestMove; } function minimax(depth, isMaximizingPlayer, alpha, beta) { if (depth === 0 || isGameOver()) { return evaluateBoard(); } const color = isMaximizingPlayer ? aiColor : opponentColor; const moves = generateAllMoves(color); if (isMaximizingPlayer) { let maxEval = -Infinity; for (let move of moves) { makeTemporaryMove(move); let eval = minimax(depth - 1, false, alpha, beta); undoTemporaryMove(move); maxEval = Math.max(maxEval, eval); alpha = Math.max(alpha, eval); if (beta <= alpha) { break; } } return maxEval; } else { let minEval = Infinity; for (let move of moves) { makeTemporaryMove(move); let eval = minimax(depth - 1, true, alpha, beta); undoTemporaryMove(move); minEval = Math.min(minEval, eval); beta = Math.min(beta, eval); if (beta <= alpha) { break; } } return minEval; } } // Function to check if the game is over (checkmate or stalemate) function isGameOver() { // Check if current player has any valid moves const moves = generateAllMoves(turn); if (moves.length > 0) { return false; } return true; // No valid moves, game is over } // If AI is black and it's black's turn, make AI's move if (turn === aiColor) { setTimeout(makeAIMove, 500); } </script>