checkmate-nexus / script.js
diribes's picture
crea un sitio web para jugar al ajedrez contra la ia
8085056 verified
/* --- Chess Game Logic & AI --- */
// Game State
let game = new Chess();
let boardEl = document.getElementById('board');
let selectedSquare = null;
let aiDepth = 2; // Default Medium
let playerColor = 'w'; // Player is white
let gameOver = false;
// Piece Unicode Map
const pieces = {
'p': 'β™Ÿ', 'n': 'β™ž', 'b': '♝', 'r': 'β™œ', 'q': 'β™›', 'k': 'β™š',
'P': 'β™Ÿ', 'N': 'β™ž', 'B': '♝', 'R': 'β™œ', 'Q': 'β™›', 'K': 'β™š'
};
// Piece Values for AI
const pieceValues = {
p: 10, n: 30, b: 30, r: 50, q: 90, k: 900,
P: -10, N: -30, B: -30, R: -50, Q: -90, K: -900 // From Black perspective
};
// Initial Render
document.addEventListener('DOMContentLoaded', () => {
renderBoard();
updateStatus();
});
// --- Board Rendering ---
function renderBoard() {
boardEl.innerHTML = '';
const boardState = game.board();
// Last move highlight
const history = game.history({ verbose: true });
const lastMove = history.length ? history[history.length - 1] : null;
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
const squareDiv = document.createElement('div');
const isLight = (i + j) % 2 === 0;
const squareName = String.fromCharCode(97 + j) + (8 - i);
const piece = boardState[i][j];
squareDiv.className = `square ${isLight ? 'light' : 'dark'}`;
squareDiv.dataset.square = squareName;
squareDiv.onclick = () => handleSquareClick(squareName);
// Highlight selected
if (selectedSquare === squareName) {
squareDiv.classList.add('selected');
}
// Highlight last move
if (lastMove && (lastMove.from === squareName || lastMove.to === squareName)) {
squareDiv.classList.add('last-move');
}
// Highlight King in Check
if (piece && piece.type === 'k' && piece.color === game.turn() && game.in_check()) {
squareDiv.classList.add('check');
}
// Highlight valid moves if a piece is selected
if (selectedSquare) {
const moves = game.moves({ square: selectedSquare, verbose: true });
const move = moves.find(m => m.to === squareName);
if (move) {
if (move.flags.includes('c') || move.flags.includes('e')) {
squareDiv.classList.add('valid-capture');
} else {
squareDiv.classList.add('valid-move');
}
}
}
// Render Piece
if (piece) {
const pieceSpan = document.createElement('span');
pieceSpan.className = `piece ${piece.color === 'w' ? 'white' : 'black'}`;
// Using standard chess unicode
pieceSpan.textContent = pieces[piece.type];
// Note: Standard unicode usually comes in outline/filled variants.
// For simplicity in this constrained env, we use text color to differentiate.
if(piece.color === 'w') pieceSpan.textContent = pieces[piece.type.toUpperCase()];
else pieceSpan.textContent = pieces[piece.type];
squareDiv.appendChild(pieceSpan);
}
boardEl.appendChild(squareDiv);
}
}
}
// --- Interaction ---
function handleSquareClick(square) {
if (gameOver || game.turn() !== playerColor) return;
const piece = game.get(square);
// Select a piece
if (piece && piece.color === playerColor) {
selectedSquare = square;
playSound('move');
renderBoard();
return;
}
// Move piece
if (selectedSquare) {
const moves = game.moves({ square: selectedSquare, verbose: true });
const move = moves.find(m => m.to === square);
if (move) {
game.move(move.san);
selectedSquare = null;
renderBoard();
updateStatus();
updateHistory();
// Trigger AI after short delay
if (!game.game_over()) {
setTimeout(makeAIMove, 250);
}
} else {
// Deselect if clicking invalid empty square
if(!piece) {
selectedSquare = null;
renderBoard();
}
}
}
}
// --- AI Engine (Minimax) ---
function makeAIMove() {
if (game.game_over()) return;
updateStatus("AI is thinking...");
// Use timeout to allow UI update before heavy calculation
setTimeout(() => {
const bestMove = calculateBestMove(game, aiDepth);
game.move(bestMove);
renderBoard();
updateStatus();
updateHistory();
playSound('move');
}, 100);
}
function calculateBestMove(gameInstance, depth) {
const possibleMoves = gameInstance.moves();
if (possibleMoves.length === 0) return null;
// Alpha-Beta Pruning
let bestMove = -Infinity;
let bestMoveFound = null;
// Shuffle moves to add randomness to equal positions
possibleMoves.sort(() => Math.random() - 0.5);
for (const move of possibleMoves) {
gameInstance.move(move);
const value = minimax(gameInstance, depth - 1, -10000, 10000, false);
gameInstance.undo();
if (value >= bestMove) {
bestMove = value;
bestMoveFound = move;
}
}
return bestMoveFound || possibleMoves[Math.floor(Math.random() * possibleMoves.length)];
}
function minimax(gameInstance, depth, alpha, beta, isMaximizingPlayer) {
if (depth === 0) {
return -evaluateBoard(gameInstance.board());
}
const possibleMoves = gameInstance.moves();
if (possibleMoves.length === 0) {
if (gameInstance.in_checkmate()) return isMaximizingPlayer ? -10000 : 10000; // Depth * value
return 0; // Stalemate
}
if (isMaximizingPlayer) {
let bestMove = -9999;
for (const move of possibleMoves) {
gameInstance.move(move);
bestMove = Math.max(bestMove, minimax(gameInstance, depth - 1, alpha, beta, !isMaximizingPlayer));
gameInstance.undo();
alpha = Math.max(alpha, bestMove);
if (beta <= alpha) return bestMove;
}
return bestMove;
} else {
let bestMove = 9999;
for (const move of possibleMoves) {
gameInstance.move(move);
bestMove = Math.min(bestMove, minimax(gameInstance, depth - 1, alpha, beta, !isMaximizingPlayer));
gameInstance.undo();
beta = Math.min(beta, bestMove);
if (beta <= alpha) return bestMove;
}
return bestMove;
}
}
function evaluateBoard(board) {
let totalEvaluation = 0;
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
totalEvaluation += getPieceValue(board[i][j]);
}
}
return totalEvaluation;
}
function getPieceValue(piece) {
if (piece === null) return 0;
// Simple logic: AI is Black, wants to minimize positive score (which favors white)
// We want AI (Black) to maximize its own value.
// Let's standardise: Positive = Good for Black, Negative = Good for White
// Re-map for standard Minimax
// If I am playing as Black (AI), I want positive score.
let value = 0;
if (piece.color === 'b') {
value = pieceValues[piece.type];
} else {
value = pieceValues[piece.type.toUpperCase()]; // Negative values
}
// Add position bonuses (Simplified: center control)
const centerBonus = (i, j) => {
if (i >= 3 && i <= 4 && j >= 3 && j <= 4) return 1;
return 0;
}
// Adjust based on simple position
if(piece.color === 'b') return value + (piece.type === 'n' ? centerBonus(i,j) : 0);
return value;
}
// --- UI Helpers ---
function updateStatus(msg = null) {
const statusEl = document.getElementById('game-status');
if (msg) {
statusEl.textContent = msg;
return;
}
let status = '';
const turn = game.turn() === 'w' ? 'White' : 'Black';
if (game.in_checkmate()) {
status = `Game Over: ${turn === 'White' ? 'Black' : 'White'} wins by checkmate!`;
gameOver = true;
playSound('capture');
} else if (game.in_draw()) {
status = 'Game Over: Draw!';
gameOver = true;
} else {
status = `${turn} to move`;
if (game.in_check()) status += ' (Check!)';
}
statusEl.textContent = status;
}
function updateHistory() {
const historyEl = document.getElementById('move-history');
const history = game.history();
let html = '';
for (let i = 0; i < history.length; i += 2) {
const moveNum = Math.floor(i / 2) + 1;
html += `<div class="flex gap-2 text-xs">
<span class="text-gray-500">${moveNum}.</span>
<span class="text-gray-200">${history[i]}</span>
${history[i+1] ? `<span class="text-gray-200">${history[i+1]}</span>` : '<span class="opacity-20">...</span>'}
</div>`;
}
historyEl.innerHTML = html;
historyEl.scrollTop = historyEl.scrollHeight;
}
function resetGame() {
game.reset();
gameOver = false;
selectedSquare = null;
updateStatus();
renderBoard();
document.getElementById('move-history').innerHTML = '<p class="text-center italic opacity-50">Game started...</p>';
playSound('move');
}
function setDifficulty(level) {
aiDepth = level;
document.querySelectorAll('.diff-btn').forEach(btn => {
btn.classList.remove('bg-primary', 'text-white', 'active');
btn.classList.add('text-gray-400');
});
// Simple active state logic based on click text
const buttons = document.querySelectorAll('.diff-btn');
const index = level - 1;
buttons[index].classList.add('bg-primary', 'text-white', 'active');
buttons[index].classList.remove('text-gray-400');
resetGame();
}
// Simple Sound Synth (Beeps)
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function playSound(type) {
if (audioCtx.state === 'suspended') audioCtx.resume();
const osc = audioCtx.createOscillator();
const gainNode = audioCtx.createGain();
osc.connect(gainNode);
gainNode.connect(audioCtx.destination);
if (type === 'move') {
osc.type = 'sine';
osc.frequency.setValueAtTime(300, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(100, audioCtx.currentTime + 0.1);
gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.1);
osc.start();
osc.stop(audioCtx.currentTime + 0.1);
} else if (type === 'capture') {
osc.type = 'triangle';
osc.frequency.setValueAtTime(150, audioCtx.currentTime);
gainNode.gain.setValueAtTime(0.2, audioCtx.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioCtx.currentTime + 0.2);
osc.start();
osc.stop(audioCtx.currentTime + 0.2);
}
}