checkers / index.html
MarkTheArtist's picture
Add 2 files
4077817 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Checkers Game</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.board {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
width: min(90vw, 80vh);
height: min(90vw, 80vh);
margin: 0 auto;
border: 4px solid #5f370e;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
}
.square {
position: relative;
display: flex;
justify-content: center;
align-items: center;
}
.dark {
background-color: #5f370e;
}
.light {
background-color: #f0d9b5;
}
.piece {
width: 80%;
height: 80%;
border-radius: 50%;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 3px 5px rgba(0, 0, 0, 0.3);
z-index: 2;
}
.piece:hover {
transform: scale(1.1);
}
.red {
background: radial-gradient(circle at 30% 30%, #ff6b6b, #cc0000);
}
.black {
background: radial-gradient(circle at 30% 30%, #666, #000);
}
.king::after {
content: "♛";
color: gold;
font-size: 1.5rem;
position: absolute;
text-shadow: 0 0 3px black;
}
.selected {
box-shadow: 0 0 15px 5px rgba(255, 215, 0, 0.8);
transform: scale(1.1);
}
.possible-move {
position: absolute;
width: 30%;
height: 30%;
background-color: rgba(255, 215, 0, 0.5);
border-radius: 50%;
z-index: 1;
}
.capture-move {
position: absolute;
width: 100%;
height: 100%;
background-color: rgba(255, 0, 0, 0.2);
border-radius: 50%;
z-index: 1;
}
.turn-indicator {
transition: all 0.3s ease;
}
.active-turn {
transform: scale(1.1);
text-shadow: 0 0 10px currentColor;
}
.ai-thinking {
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.move-history {
max-height: 300px;
overflow-y: auto;
scrollbar-width: thin;
}
.move-history::-webkit-scrollbar {
width: 8px;
}
.move-history::-webkit-scrollbar-track {
background: #f0d9b5;
}
.move-history::-webkit-scrollbar-thumb {
background: #5f370e;
border-radius: 4px;
}
.difficulty-btn.active {
background-color: #5f370e;
color: white;
}
</style>
</head>
<body class="bg-amber-50 min-h-screen flex flex-col">
<header class="bg-amber-900 text-amber-50 py-4 shadow-lg">
<div class="container mx-auto px-4">
<h1 class="text-3xl font-bold text-center">Checkers Game</h1>
</div>
</header>
<main class="flex-grow container mx-auto px-4 py-8">
<div class="flex flex-col lg:flex-row gap-8">
<!-- Game board -->
<div class="flex-1">
<div class="board" id="board"></div>
</div>
<!-- Game controls and info -->
<div class="flex-1 max-w-md">
<div class="bg-amber-100 rounded-lg shadow-lg p-6 border border-amber-300">
<!-- Game mode selection -->
<div class="mb-6">
<h2 class="text-xl font-bold mb-3 text-amber-900">Game Mode</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-2">
<button id="pvp" class="bg-amber-600 hover:bg-amber-700 text-white py-2 px-4 rounded transition">
<i class="fas fa-users mr-2"></i> Human vs Human
</button>
<button id="pvc" class="bg-amber-600 hover:bg-amber-700 text-white py-2 px-4 rounded transition">
<i class="fas fa-robot mr-2"></i> Human vs AI
</button>
<button id="cvc" class="bg-amber-600 hover:bg-amber-700 text-white py-2 px-4 rounded transition">
<i class="fas fa-cogs mr-2"></i> AI vs AI
</button>
</div>
</div>
<!-- AI Difficulty -->
<div class="mb-6" id="ai-difficulty-container">
<h2 class="text-xl font-bold mb-3 text-amber-900">AI Difficulty</h2>
<div class="flex gap-2">
<button id="easy" class="difficulty-btn bg-amber-600 hover:bg-amber-700 text-white py-2 px-4 rounded transition active">
Easy
</button>
<button id="medium" class="difficulty-btn bg-amber-600 hover:bg-amber-700 text-white py-2 px-4 rounded transition">
Medium
</button>
<button id="hard" class="difficulty-btn bg-amber-600 hover:bg-amber-700 text-white py-2 px-4 rounded transition">
Hard
</button>
</div>
</div>
<!-- Game status -->
<div class="mb-6">
<h2 class="text-xl font-bold mb-3 text-amber-900">Game Status</h2>
<div class="bg-amber-200 rounded-lg p-4">
<div class="flex justify-between items-center mb-2">
<span id="red-player" class="font-bold text-red-700 turn-indicator">
<i class="fas fa-circle mr-2"></i> Red Player
</span>
<span id="red-count" class="bg-red-700 text-white px-3 py-1 rounded-full text-sm">12</span>
</div>
<div class="flex justify-between items-center">
<span id="black-player" class="font-bold text-gray-900 turn-indicator">
<i class="fas fa-circle mr-2"></i> Black Player
</span>
<span id="black-count" class="bg-black text-white px-3 py-1 rounded-full text-sm">12</span>
</div>
</div>
</div>
<!-- Move history -->
<div class="mb-6">
<h2 class="text-xl font-bold mb-3 text-amber-900">Move History</h2>
<div class="bg-amber-200 rounded-lg p-4 move-history" id="move-history">
<div class="text-center text-amber-700">Game moves will appear here</div>
</div>
</div>
<!-- Game controls -->
<div class="flex gap-2">
<button id="new-game" class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded flex-1 transition">
<i class="fas fa-plus-circle mr-2"></i> New Game
</button>
<button id="undo-move" class="bg-blue-600 hover:bg-blue-700 text-white py-2 px-4 rounded transition">
<i class="fas fa-undo mr-2"></i> Undo
</button>
</div>
</div>
</div>
</div>
</main>
<footer class="bg-amber-900 text-amber-50 py-4 text-center">
<div class="container mx-auto px-4">
<p>Checkers Game © 2023 | Created with HTML, CSS, and JavaScript</p>
</div>
</footer>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Game state
const gameState = {
board: [],
currentPlayer: 'red',
selectedPiece: null,
possibleMoves: [],
gameMode: 'pvp', // 'pvp', 'pvc', 'cvc'
gameHistory: [],
moveHistory: [],
aiThinking: false,
aiSpeed: 800, // ms delay for AI moves
aiDifficulty: 'easy', // 'easy', 'medium', 'hard'
maxDepth: 3 // Default depth for Minimax
};
// Initialize the game
initGame();
// Event listeners for buttons
document.getElementById('pvp').addEventListener('click', () => {
gameState.gameMode = 'pvp';
updateGameModeUI();
addToMoveHistory('Game mode set to Human vs Human');
});
document.getElementById('pvc').addEventListener('click', () => {
gameState.gameMode = 'pvc';
updateGameModeUI();
addToMoveHistory('Game mode set to Human vs AI');
// If it's AI's turn, make a move
if (gameState.currentPlayer === 'black' && !gameState.aiThinking) {
setTimeout(makeAIMove, gameState.aiSpeed);
}
});
document.getElementById('cvc').addEventListener('click', () => {
gameState.gameMode = 'cvc';
updateGameModeUI();
addToMoveHistory('Game mode set to AI vs AI');
// Start AI vs AI game
if (!gameState.aiThinking) {
setTimeout(makeAIMove, gameState.aiSpeed);
}
});
// AI Difficulty buttons
document.getElementById('easy').addEventListener('click', () => {
gameState.aiDifficulty = 'easy';
gameState.maxDepth = 3;
updateDifficultyUI();
addToMoveHistory('AI difficulty set to Easy');
});
document.getElementById('medium').addEventListener('click', () => {
gameState.aiDifficulty = 'medium';
gameState.maxDepth = 5;
updateDifficultyUI();
addToMoveHistory('AI difficulty set to Medium');
});
document.getElementById('hard').addEventListener('click', () => {
gameState.aiDifficulty = 'hard';
gameState.maxDepth = 7;
updateDifficultyUI();
addToMoveHistory('AI difficulty set to Hard');
});
document.getElementById('new-game').addEventListener('click', () => {
initGame();
addToMoveHistory('New game started');
});
document.getElementById('undo-move').addEventListener('click', () => {
undoLastMove();
});
function updateDifficultyUI() {
const difficulties = ['easy', 'medium', 'hard'];
difficulties.forEach(diff => {
const btn = document.getElementById(diff);
if (diff === gameState.aiDifficulty) {
btn.classList.add('bg-amber-800', 'shadow-md');
btn.classList.remove('bg-amber-600');
} else {
btn.classList.add('bg-amber-600');
btn.classList.remove('bg-amber-800', 'shadow-md');
}
});
}
function updateGameModeUI() {
const modes = ['pvp', 'pvc', 'cvc'];
modes.forEach(mode => {
const btn = document.getElementById(mode);
if (mode === gameState.gameMode) {
btn.classList.add('bg-amber-800', 'shadow-md');
btn.classList.remove('bg-amber-600');
} else {
btn.classList.add('bg-amber-600');
btn.classList.remove('bg-amber-800', 'shadow-md');
}
});
// Show/hide AI difficulty based on game mode
const aiDifficultyContainer = document.getElementById('ai-difficulty-container');
if (gameState.gameMode === 'pvp') {
aiDifficultyContainer.classList.add('hidden');
} else {
aiDifficultyContainer.classList.remove('hidden');
}
// Update player indicators
const redPlayer = document.getElementById('red-player');
const blackPlayer = document.getElementById('black-player');
if (gameState.gameMode === 'pvp') {
redPlayer.innerHTML = '<i class="fas fa-user mr-2"></i> Red Player';
blackPlayer.innerHTML = '<i class="fas fa-user mr-2"></i> Black Player';
} else if (gameState.gameMode === 'pvc') {
redPlayer.innerHTML = '<i class="fas fa-user mr-2"></i> You (Red)';
blackPlayer.innerHTML = '<i class="fas fa-robot mr-2"></i> AI (Black)';
} else {
redPlayer.innerHTML = '<i class="fas fa-robot mr-2"></i> AI (Red)';
blackPlayer.innerHTML = '<i class="fas fa-robot mr-2"></i> AI (Black)';
}
}
function initGame() {
// Clear any AI timers
gameState.aiThinking = false;
// Initialize empty board
gameState.board = Array(8).fill().map(() => Array(8).fill(null));
// Set up pieces
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
if ((row + col) % 2 === 1) {
if (row < 3) {
gameState.board[row][col] = { player: 'black', king: false };
} else if (row > 4) {
gameState.board[row][col] = { player: 'red', king: false };
}
}
}
}
gameState.currentPlayer = 'red';
gameState.selectedPiece = null;
gameState.possibleMoves = [];
gameState.gameHistory = [];
gameState.moveHistory = [];
renderBoard();
updatePlayerTurnUI();
updatePieceCounts();
document.getElementById('move-history').innerHTML = '<div class="text-center text-amber-700">Game moves will appear here</div>';
updateGameModeUI();
updateDifficultyUI();
}
function renderBoard() {
const boardElement = document.getElementById('board');
boardElement.innerHTML = '';
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const square = document.createElement('div');
square.className = `square ${(row + col) % 2 === 0 ? 'light' : 'dark'}`;
square.dataset.row = row;
square.dataset.col = col;
// Add click event to squares
square.addEventListener('click', () => handleSquareClick(row, col));
// Add pieces if they exist
const piece = gameState.board[row][col];
if (piece) {
const pieceElement = document.createElement('div');
pieceElement.className = `piece ${piece.player} ${piece.king ? 'king' : ''}`;
pieceElement.dataset.row = row;
pieceElement.dataset.col = col;
// Add click event to pieces
pieceElement.addEventListener('click', (e) => {
e.stopPropagation();
handlePieceClick(row, col);
});
square.appendChild(pieceElement);
}
boardElement.appendChild(square);
}
}
// Highlight possible moves
gameState.possibleMoves.forEach(move => {
const [targetRow, targetCol] = move.to;
const square = document.querySelector(`.square[data-row="${targetRow}"][data-col="${targetCol}"]`);
if (square) {
const highlight = document.createElement('div');
highlight.className = move.capture ? 'capture-move' : 'possible-move';
square.appendChild(highlight);
}
});
// Highlight selected piece
if (gameState.selectedPiece) {
const [row, col] = gameState.selectedPiece;
const piece = document.querySelector(`.piece[data-row="${row}"][data-col="${col}"]`);
if (piece) {
piece.classList.add('selected');
}
}
}
function handlePieceClick(row, col) {
const piece = gameState.board[row][col];
// If it's not the current player's piece, ignore
if (piece.player !== gameState.currentPlayer) return;
// If we're in AI vs AI mode, ignore human clicks
if (gameState.gameMode === 'cvc') return;
// If we're in human vs AI mode and it's AI's turn, ignore
if (gameState.gameMode === 'pvc' && gameState.currentPlayer === 'black' && !gameState.aiThinking) return;
// If the same piece is clicked again, deselect it
if (gameState.selectedPiece && gameState.selectedPiece[0] === row && gameState.selectedPiece[1] === col) {
gameState.selectedPiece = null;
gameState.possibleMoves = [];
} else {
// Select the piece and find possible moves
gameState.selectedPiece = [row, col];
gameState.possibleMoves = findPossibleMoves(row, col);
// If no possible moves, deselect
if (gameState.possibleMoves.length === 0) {
gameState.selectedPiece = null;
}
}
renderBoard();
}
function handleSquareClick(row, col) {
// If no piece is selected or it's not a dark square, ignore
if (!gameState.selectedPiece || (row + col) % 2 === 0) return;
// If we're in AI vs AI mode, ignore human clicks
if (gameState.gameMode === 'cvc') return;
// If we're in human vs AI mode and it's AI's turn, ignore
if (gameState.gameMode === 'pvc' && gameState.currentPlayer === 'black' && !gameState.aiThinking) return;
// Check if the clicked square is a valid move
const [selectedRow, selectedCol] = gameState.selectedPiece;
const move = gameState.possibleMoves.find(m =>
m.to[0] === row && m.to[1] === col
);
if (move) {
// Save current state to history before making the move
saveGameStateToHistory();
// Make the move
makeMove(selectedRow, selectedCol, row, col, move.capture);
// If the move was a capture and there are more captures available, don't switch players
if (move.capture) {
const piece = gameState.board[row][col];
const moreCaptures = findPossibleMoves(row, col).some(m => m.capture);
if (moreCaptures) {
gameState.selectedPiece = [row, col];
gameState.possibleMoves = findPossibleMoves(row, col).filter(m => m.capture);
renderBoard();
return;
}
}
// Switch players
gameState.currentPlayer = gameState.currentPlayer === 'red' ? 'black' : 'red';
gameState.selectedPiece = null;
gameState.possibleMoves = [];
renderBoard();
updatePlayerTurnUI();
updatePieceCounts();
// Check for game over
if (isGameOver()) {
const winner = gameState.currentPlayer === 'red' ? 'Black' : 'Red';
addToMoveHistory(`<span class="font-bold">Game over! ${winner} wins!</span>`);
return;
}
// If it's AI's turn in pvc or cvc mode, make AI move
if ((gameState.gameMode === 'pvc' && gameState.currentPlayer === 'black') ||
gameState.gameMode === 'cvc') {
setTimeout(makeAIMove, gameState.aiSpeed);
}
}
}
function findPossibleMoves(row, col) {
const piece = gameState.board[row][col];
if (!piece) return [];
const moves = [];
const directions = piece.king ?
[[-1, -1], [-1, 1], [1, -1], [1, 1]] :
(piece.player === 'red' ? [[-1, -1], [-1, 1]] : [[1, -1], [1, 1]]);
// Check for captures first (mandatory in checkers)
let capturesFound = false;
for (const [dr, dc] of directions) {
const jumpRow = row + dr;
const jumpCol = col + dc;
// Check if jump is within bounds
if (jumpRow < 0 || jumpRow >= 8 || jumpCol < 0 || jumpCol >= 8) continue;
const jumpPiece = gameState.board[jumpRow][jumpCol];
// If there's an opponent's piece, check if we can capture it
if (jumpPiece && jumpPiece.player !== piece.player) {
const landRow = row + 2 * dr;
const landCol = col + 2 * dc;
// Check if landing position is within bounds and empty
if (landRow >= 0 && landRow < 8 && landCol >= 0 && landCol < 8 && !gameState.board[landRow][landCol]) {
capturesFound = true;
moves.push({
from: [row, col],
to: [landRow, landCol],
capture: [jumpRow, jumpCol]
});
}
}
}
// If captures are found, return only captures (mandatory in checkers)
if (capturesFound) return moves;
// If no captures, check for regular moves
for (const [dr, dc] of directions) {
const newRow = row + dr;
const newCol = col + dc;
// Check if new position is within bounds and empty
if (newRow >= 0 && newRow < 8 && newCol >= 0 && newCol < 8 && !gameState.board[newRow][newCol]) {
moves.push({
from: [row, col],
to: [newRow, newCol],
capture: null
});
}
}
return moves;
}
function makeMove(fromRow, fromCol, toRow, toCol, capture) {
const piece = gameState.board[fromRow][fromCol];
// Move the piece
gameState.board[toRow][toCol] = {...piece};
gameState.board[fromRow][fromCol] = null;
// Remove captured piece if any
if (capture) {
gameState.board[capture[0]][capture[1]] = null;
addToMoveHistory(`${piece.player === 'red' ? 'Red' : 'Black'} captured a piece at ${String.fromCharCode(97 + capture[1])}${8 - capture[0]}`);
} else {
addToMoveHistory(`${piece.player === 'red' ? 'Red' : 'Black'} moved to ${String.fromCharCode(97 + toCol)}${8 - toRow}`);
}
// Check for promotion to king
if (!piece.king) {
if ((piece.player === 'red' && toRow === 0) || (piece.player === 'black' && toRow === 7)) {
gameState.board[toRow][toCol].king = true;
addToMoveHistory(`${piece.player === 'red' ? 'Red' : 'Black'} piece was promoted to king!`);
}
}
}
// Evaluation function for Minimax
function evaluateBoard(board, player) {
let score = 0;
let redPieces = 0;
let blackPieces = 0;
let redKings = 0;
let blackKings = 0;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece) {
if (piece.player === 'red') {
redPieces++;
if (piece.king) redKings++;
// Add position bonus for regular pieces (closer to king row is better)
if (!piece.king) {
score += (7 - row) * 0.1; // Red pieces moving up the board
} else {
score += 1.5; // Kings are more valuable
}
} else {
blackPieces++;
if (piece.king) blackKings++;
// Add position bonus for regular pieces (closer to king row is better)
if (!piece.king) {
score -= row * 0.1; // Black pieces moving down the board
} else {
score -= 1.5; // Kings are more valuable
}
}
}
}
}
// Material advantage
score += (redPieces + redKings * 2) * 1;
score -= (blackPieces + blackKings * 2) * 1;
// Additional strategic considerations
// Control of center squares
const centerSquares = [[3, 3], [3, 4], [4, 3], [4, 4]];
centerSquares.forEach(([r, c]) => {
const piece = board[r][c];
if (piece) {
if (piece.player === 'red') score += 0.3;
else score -= 0.3;
}
});
// If player is black, we need to invert the score
if (player === 'black') {
score = -score;
}
return score;
}
// Get all possible moves for a player
function getAllPossibleMoves(board, player) {
const allMoves = [];
const captures = [];
// First check for captures (mandatory in checkers)
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece && piece.player === player) {
const moves = findPossibleMovesForPiece(board, row, col);
moves.forEach(move => {
if (move.capture) {
captures.push(move);
} else {
allMoves.push(move);
}
});
}
}
}
// If there are captures, return only captures
if (captures.length > 0) {
return captures;
}
return allMoves;
}
// Find possible moves for a specific piece
function findPossibleMovesForPiece(board, row, col) {
const piece = board[row][col];
if (!piece) return [];
const moves = [];
const directions = piece.king ?
[[-1, -1], [-1, 1], [1, -1], [1, 1]] :
(piece.player === 'red' ? [[-1, -1], [-1, 1]] : [[1, -1], [1, 1]]);
// Check for captures first (mandatory in checkers)
let capturesFound = false;
for (const [dr, dc] of directions) {
const jumpRow = row + dr;
const jumpCol = col + dc;
// Check if jump is within bounds
if (jumpRow < 0 || jumpRow >= 8 || jumpCol < 0 || jumpCol >= 8) continue;
const jumpPiece = board[jumpRow][jumpCol];
// If there's an opponent's piece, check if we can capture it
if (jumpPiece && jumpPiece.player !== piece.player) {
const landRow = row + 2 * dr;
const landCol = col + 2 * dc;
// Check if landing position is within bounds and empty
if (landRow >= 0 && landRow < 8 && landCol >= 0 && landCol < 8 && !board[landRow][landCol]) {
capturesFound = true;
moves.push({
from: [row, col],
to: [landRow, landCol],
capture: [jumpRow, jumpCol]
});
}
}
}
// If captures are found, return only captures (mandatory in checkers)
if (capturesFound) return moves;
// If no captures, check for regular moves
for (const [dr, dc] of directions) {
const newRow = row + dr;
const newCol = col + dc;
// Check if new position is within bounds and empty
if (newRow >= 0 && newRow < 8 && newCol >= 0 && newCol < 8 && !board[newRow][newCol]) {
moves.push({
from: [row, col],
to: [newRow, newCol],
capture: null
});
}
}
return moves;
}
// Minimax with Alpha-Beta Pruning
function minimax(board, depth, alpha, beta, maximizingPlayer, currentPlayer) {
// Base case: if depth is 0 or game is over, return the evaluation
if (depth === 0 || isGameOverForMinimax(board, currentPlayer)) {
return { score: evaluateBoard(board, currentPlayer) };
}
const allMoves = getAllPossibleMoves(board, currentPlayer);
if (maximizingPlayer) {
let maxEval = { score: -Infinity };
let bestMove = null;
for (const move of allMoves) {
// Make a copy of the board
const newBoard = JSON.parse(JSON.stringify(board));
// Apply the move
applyMoveToBoard(newBoard, move);
// Determine if we need to continue with the same player (for multiple captures)
const piece = newBoard[move.to[0]][move.to[1]];
const moreCaptures = move.capture ?
findPossibleMovesForPiece(newBoard, move.to[0], move.to[1]).some(m => m.capture) :
false;
// Recursive call
let evalResult;
if (moreCaptures) {
// Continue with the same player if there are more captures
evalResult = minimax(newBoard, depth - 1, alpha, beta, true, currentPlayer);
} else {
// Switch to the other player
evalResult = minimax(newBoard, depth - 1, alpha, beta, false, currentPlayer === 'red' ? 'black' : 'red');
}
// Update maxEval if we found a better move
if (evalResult.score > maxEval.score) {
maxEval = { score: evalResult.score, move: move };
bestMove = move;
}
// Alpha-Beta pruning
alpha = Math.max(alpha, evalResult.score);
if (beta <= alpha) {
break;
}
}
return { score: maxEval.score, move: bestMove };
} else {
let minEval = { score: Infinity };
let bestMove = null;
for (const move of allMoves) {
// Make a copy of the board
const newBoard = JSON.parse(JSON.stringify(board));
// Apply the move
applyMoveToBoard(newBoard, move);
// Determine if we need to continue with the same player (for multiple captures)
const piece = newBoard[move.to[0]][move.to[1]];
const moreCaptures = move.capture ?
findPossibleMovesForPiece(newBoard, move.to[0], move.to[1]).some(m => m.capture) :
false;
// Recursive call
let evalResult;
if (moreCaptures) {
// Continue with the same player if there are more captures
evalResult = minimax(newBoard, depth - 1, alpha, beta, false, currentPlayer);
} else {
// Switch to the other player
evalResult = minimax(newBoard, depth - 1, alpha, beta, true, currentPlayer === 'red' ? 'black' : 'red');
}
// Update minEval if we found a better move
if (evalResult.score < minEval.score) {
minEval = { score: evalResult.score, move: move };
bestMove = move;
}
// Alpha-Beta pruning
beta = Math.min(beta, evalResult.score);
if (beta <= alpha) {
break;
}
}
return { score: minEval.score, move: bestMove };
}
}
// Apply a move to a board (used in minimax)
function applyMoveToBoard(board, move) {
const [fromRow, fromCol] = move.from;
const [toRow, toCol] = move.to;
const piece = board[fromRow][fromCol];
// Move the piece
board[toRow][toCol] = {...piece};
board[fromRow][fromCol] = null;
// Remove captured piece if any
if (move.capture) {
const [captureRow, captureCol] = move.capture;
board[captureRow][captureCol] = null;
}
// Check for promotion to king
if (!piece.king) {
if ((piece.player === 'red' && toRow === 0) || (piece.player === 'black' && toRow === 7)) {
board[toRow][toCol].king = true;
}
}
}
// Check if game is over for minimax (slightly different from regular game over check)
function isGameOverForMinimax(board, player) {
let playerPieces = 0;
let opponentPieces = 0;
let playerHasMoves = false;
let opponentHasMoves = false;
const opponent = player === 'red' ? 'black' : 'red';
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = board[row][col];
if (piece) {
if (piece.player === player) {
playerPieces++;
if (!playerHasMoves) {
const moves = findPossibleMovesForPiece(board, row, col);
if (moves.length > 0) playerHasMoves = true;
}
} else {
opponentPieces++;
if (!opponentHasMoves) {
const moves = findPossibleMovesForPiece(board, row, col);
if (moves.length > 0) opponentHasMoves = true;
}
}
}
}
}
return playerPieces === 0 || opponentPieces === 0 ||
(player === 'red' && !playerHasMoves) ||
(player === 'black' && !playerHasMoves);
}
function makeAIMove() {
if (gameState.aiThinking) return;
gameState.aiThinking = true;
const aiPlayer = gameState.currentPlayer;
// Show AI is thinking
const aiElement = document.getElementById(`${aiPlayer}-player`);
aiElement.classList.add('ai-thinking');
// Use Minimax to find the best move
const result = minimax(
gameState.board,
gameState.maxDepth,
-Infinity,
Infinity,
true,
aiPlayer
);
const bestMove = result.move;
// If no move found (shouldn't happen if game isn't over)
if (!bestMove) {
gameState.aiThinking = false;
aiElement.classList.remove('ai-thinking');
return;
}
// Save current state to history before making the move
saveGameStateToHistory();
// Make the move after a small delay for visual effect
setTimeout(() => {
const [fromRow, fromCol] = bestMove.from;
const [toRow, toCol] = bestMove.to;
makeMove(fromRow, fromCol, toRow, toCol, bestMove.capture);
// If the move was a capture and there are more captures available, don't switch players
if (bestMove.capture) {
const piece = gameState.board[toRow][toCol];
const moreCaptures = findPossibleMoves(toRow, toCol).some(m => m.capture);
if (moreCaptures) {
gameState.selectedPiece = [toRow, toCol];
gameState.possibleMoves = findPossibleMoves(toRow, toCol).filter(m => m.capture);
renderBoard();
// AI continues with the next capture
setTimeout(makeAIMove, gameState.aiSpeed);
return;
}
}
// Switch players
gameState.currentPlayer = gameState.currentPlayer === 'red' ? 'black' : 'red';
gameState.selectedPiece = null;
gameState.possibleMoves = [];
renderBoard();
updatePlayerTurnUI();
updatePieceCounts();
// Check for game over
if (isGameOver()) {
const winner = gameState.currentPlayer === 'red' ? 'Black' : 'Red';
addToMoveHistory(`<span class="font-bold">Game over! ${winner} wins!</span>`);
} else {
// If it's still AI's turn (in cvc mode), make next move
if (gameState.gameMode === 'cvc') {
setTimeout(makeAIMove, gameState.aiSpeed);
}
}
gameState.aiThinking = false;
aiElement.classList.remove('ai-thinking');
}, 500);
}
function isGameOver() {
let redPieces = 0;
let blackPieces = 0;
let redHasMoves = false;
let blackHasMoves = false;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = gameState.board[row][col];
if (piece) {
if (piece.player === 'red') {
redPieces++;
if (!redHasMoves) {
const moves = findPossibleMoves(row, col);
if (moves.length > 0) redHasMoves = true;
}
} else {
blackPieces++;
if (!blackHasMoves) {
const moves = findPossibleMoves(row, col);
if (moves.length > 0) blackHasMoves = true;
}
}
}
}
}
return redPieces === 0 || blackPieces === 0 ||
(gameState.currentPlayer === 'red' && !redHasMoves) ||
(gameState.currentPlayer === 'black' && !blackHasMoves);
}
function updatePlayerTurnUI() {
const redPlayer = document.getElementById('red-player');
const blackPlayer = document.getElementById('black-player');
if (gameState.currentPlayer === 'red') {
redPlayer.classList.add('active-turn');
blackPlayer.classList.remove('active-turn');
} else {
redPlayer.classList.remove('active-turn');
blackPlayer.classList.add('active-turn');
}
}
function updatePieceCounts() {
let redCount = 0;
let blackCount = 0;
for (let row = 0; row < 8; row++) {
for (let col = 0; col < 8; col++) {
const piece = gameState.board[row][col];
if (piece) {
if (piece.player === 'red') redCount++;
else blackCount++;
}
}
}
document.getElementById('red-count').textContent = redCount;
document.getElementById('black-count').textContent = blackCount;
}
function addToMoveHistory(text) {
const historyElement = document.getElementById('move-history');
const moveElement = document.createElement('div');
moveElement.className = 'mb-1 last:mb-0';
moveElement.innerHTML = text;
// If history is empty, replace the placeholder
if (historyElement.firstChild?.textContent === 'Game moves will appear here') {
historyElement.innerHTML = '';
}
historyElement.prepend(moveElement);
}
function saveGameStateToHistory() {
// Save a deep copy of the current state
const stateCopy = {
board: JSON.parse(JSON.stringify(gameState.board)),
currentPlayer: gameState.currentPlayer,
selectedPiece: gameState.selectedPiece ? [...gameState.selectedPiece] : null,
possibleMoves: JSON.parse(JSON.stringify(gameState.possibleMoves))
};
gameState.gameHistory.push(stateCopy);
}
function undoLastMove() {
if (gameState.gameHistory.length === 0) return;
// If AI is thinking, don't allow undo
if (gameState.aiThinking) {
addToMoveHistory("Can't undo while AI is thinking");
return;
}
const lastState = gameState.gameHistory.pop();
gameState.board = lastState.board;
gameState.currentPlayer = lastState.currentPlayer;
gameState.selectedPiece = lastState.selectedPiece;
gameState.possibleMoves = lastState.possibleMoves;
// Remove the last move from history
const historyElement = document.getElementById('move-history');
if (historyElement.firstChild) {
historyElement.removeChild(historyElement.firstChild);
}
// If no more moves, show placeholder
if (historyElement.children.length === 0) {
historyElement.innerHTML = '<div class="text-center text-amber-700">Game moves will appear here</div>';
}
renderBoard();
updatePlayerTurnUI();
updatePieceCounts();
addToMoveHistory("Undid the last move");
}
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=MarkTheArtist/checkers" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>