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>