Spaces:
Running
Running
| <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> |