'use strict'; // Referencias DOM const boardDiv = document.getElementById('board'); const statusDiv = document.getElementById('status'); const restartBtn = document.getElementById('restart-btn'); const playerSideSelect = document.getElementById('player-side'); const scorePlayerSpan = document.getElementById('score-player'); const scoreAiSpan = document.getElementById('score-ai'); const scoreDrawSpan = document.getElementById('score-d'); // Constantes const WIN_COMBOS = [ [0, 1, 2], [3, 4, 5], [6, 7, 8], // Filas [0, 3, 6], [1, 4, 7], [2, 5, 8], // Columnas [0, 4, 8], [2, 4, 6] // Diagonales ]; let board = Array(9).fill(''); let gameOver = false; let playerSide = 'X'; let aiSide = 'O'; let scores = { player: 0, ai: 0, D: 0 }; function loadScores() { try { const raw = localStorage.getItem('ttt_ai_scores'); if (raw) { const parsed = JSON.parse(raw); if (parsed && typeof parsed === 'object') { scores.player = Number(parsed.player) || 0; scores.ai = Number(parsed.ai) || 0; scores.D = Number(parsed.D) || 0; } } } catch (_) { // Ignorar errores } } function saveScores() { try { localStorage.setItem('ttt_ai_scores', JSON.stringify(scores)); } catch (_) { // Ignorar errores } } function updateScoreUI() { if (scorePlayerSpan) scorePlayerSpan.textContent = String(scores.player); if (scoreAiSpan) scoreAiSpan.textContent = String(scores.ai); if (scoreDrawSpan) scoreDrawSpan.textContent = String(scores.D); } function initBoardDom() { // Si ya existen 9 hijos, reutilizarlos (mejor rendimiento) if (boardDiv.children.length === 9) { Array.from(boardDiv.children).forEach(c => { c.classList.remove('win'); c.textContent = ''; c.setAttribute('aria-label', 'Casilla'); c.removeEventListener('click', onCellClick); c.addEventListener('click', onCellClick); c.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') onCellClick(e); }); }); boardDiv.setAttribute('aria-disabled', 'false'); return; } boardDiv.textContent = ''; for (let idx = 0; idx < 9; idx++) { const cell = document.createElement('button'); cell.type = 'button'; cell.className = 'cell'; cell.setAttribute('data-idx', String(idx)); cell.setAttribute('role', 'gridcell'); cell.setAttribute('aria-label', `Casilla ${idx + 1}`); cell.addEventListener('click', onCellClick); cell.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') onCellClick(e); }); boardDiv.appendChild(cell); } boardDiv.setAttribute('aria-disabled', 'false'); } function render() { for (let i = 0; i < 9; i++) { const el = boardDiv.children[i]; if (!el) continue; el.textContent = board[i]; } } function setStatus(msg) { statusDiv.textContent = msg; } function getWinCombo(b, player) { for (const combo of WIN_COMBOS) { const [a, m, c] = combo; if (b[a] === player && b[m] === player && b[c] === player) { return combo; } } return null; } function isDraw(b) { return b.every(cell => cell !== '') && !getWinCombo(b, 'X') && !getWinCombo(b, 'O'); } function highlightCombo(combo) { if (!combo) return; combo.forEach(i => { const el = boardDiv.children[i]; if (el) el.classList.add('win'); }); } function endGame(result, combo) { gameOver = true; boardDiv.setAttribute('aria-disabled', 'true'); if (result === playerSide) { setStatus('¡Tú ganas! 🎉'); scores.player += 1; highlightCombo(combo); } else if (result === aiSide) { setStatus('¡La máquina gana! 🤖'); scores.ai += 1; highlightCombo(combo); } else { setStatus('¡Empate!'); scores.D += 1; } updateScoreUI(); saveScores(); } function onCellClick(e) { const target = e.currentTarget; const idx = Number(target.getAttribute('data-idx')); if (gameOver || !Number.isInteger(idx)) return; // Solo permitir movimiento del jugador cuando sea su turno const turn = nextTurn(board); if (turn !== playerSide) return; if (board[idx] !== '') return; // Movimiento jugador board[idx] = playerSide; render(); // Comprobar fin const combo = getWinCombo(board, playerSide); if (combo) { endGame(playerSide, combo); return; } if (isDraw(board)) { endGame('D', null); return; } // Turno de la máquina setStatus(`Turno de la máquina (${aiSide})`); setTimeout(machineMove, 350); } function nextTurn(b) { const xCount = b.filter(v => v === 'X').length; const oCount = b.filter(v => v === 'O').length; // En Tic-Tac-Toe empieza 'X'; si hay igual cantidad, le toca a 'X', si X > O, le toca 'O' return xCount === oCount ? 'X' : 'O'; } // Minimax con poda alfa-beta function minimax(b, depth, isMaximizing, alpha, beta) { const aiWin = getWinCombo(b, aiSide); const playerWin = getWinCombo(b, playerSide); if (aiWin) return 10 - depth; if (playerWin) return depth - 10; if (isDraw(b)) return 0; if (isMaximizing) { let best = -Infinity; for (let i = 0; i < 9; i++) { if (b[i] === '') { b[i] = aiSide; const score = minimax(b, depth + 1, false, alpha, beta); b[i] = ''; best = Math.max(best, score); alpha = Math.max(alpha, best); if (beta <= alpha) break; } } return best; } else { let best = Infinity; for (let i = 0; i < 9; i++) { if (b[i] === '') { b[i] = playerSide; const score = minimax(b, depth + 1, true, alpha, beta); b[i] = ''; best = Math.min(best, score); beta = Math.min(beta, best); if (beta <= alpha) break; } } return best; } } function machineMove() { if (gameOver) return; // Si no es turno de la máquina, salir const turn = nextTurn(board); if (turn !== aiSide) return; // Elegir mejor jugada con Minimax let bestScore = -Infinity; let move = null; // Pequeña heurística: si está libre, prioriza centro (4) en primera jugada if (board.every(c => c === '')) { move = 4; // centro } else { for (let i = 0; i < 9; i++) { if (board[i] === '') { board[i] = aiSide; const score = minimax(board, 0, false, -Infinity, Infinity); board[i] = ''; if (score > bestScore) { bestScore = score; move = i; } } } // Si no encontró nada (no debería), elige primera libre if (move === null) move = board.findIndex(c => c === ''); } if (move !== null) { board[move] = aiSide; } render(); // Comprobar fin const combo = getWinCombo(board, aiSide); if (combo) { endGame(aiSide, combo); return; } if (isDraw(board)) { endGame('D', null); return; } setStatus(`Tu turno (${playerSide})`); } function startGame() { // Lado del jugador según selector if (playerSideSelect && (playerSideSelect.value === 'X' || playerSideSelect.value === 'O')) { playerSide = playerSideSelect.value; } else { playerSide = 'X'; } aiSide = playerSide === 'X' ? 'O' : 'X'; // Reset board = Array(9).fill(''); gameOver = false; initBoardDom(); Array.from(boardDiv.children).forEach(c => c.classList.remove('win')); render(); // Si empieza la IA (jugador eligió 'O'), IA mueve primero const turn = nextTurn(board); if (turn === aiSide) { setStatus(`Turno de la máquina (${aiSide})`); setTimeout(machineMove, 300); } else { setStatus(`Tu turno (${playerSide})`); } } // Listeners restartBtn.addEventListener('click', startGame); if (playerSideSelect) { playerSideSelect.addEventListener('change', () => { // Reiniciar al cambiar de lado startGame(); }); } // Init loadScores(); updateScoreUI(); startGame();