Mejoras y optimizaciones en general.
This commit is contained in:
@@ -3,12 +3,28 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>4 en Raya vs Máquina</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="styles.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>4 en Raya vs Máquina</h1>
|
||||
<div id="board"></div>
|
||||
<div id="status"></div>
|
||||
<h1 id="title">4 en Raya vs Máquina</h1>
|
||||
|
||||
<div id="controls">
|
||||
<label for="difficulty">Dificultad IA:</label>
|
||||
<select id="difficulty">
|
||||
<option value="facil">Fácil (profundidad 1)</option>
|
||||
<option value="normal" selected>Normal (profundidad 3)</option>
|
||||
<option value="dificil">Difícil (profundidad 4)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="board" role="grid" aria-labelledby="title"></div>
|
||||
<div id="status" aria-live="polite"></div>
|
||||
|
||||
<div id="score">
|
||||
Marcador — Tú: <span id="score-human">0</span> · Máquina: <span id="score-ai">0</span> · Empates: <span id="score-d">0</span>
|
||||
</div>
|
||||
|
||||
<button id="restart-btn">Reiniciar</button>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
|
@@ -1,151 +1,468 @@
|
||||
'use strict';
|
||||
|
||||
// Configuración del tablero y fichas
|
||||
const ROWS = 6;
|
||||
const COLS = 7;
|
||||
const HUMAN = '🟡';
|
||||
const AI = '❌';
|
||||
|
||||
// Referencias DOM
|
||||
const boardDiv = document.getElementById('board');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const restartBtn = document.getElementById('restart-btn');
|
||||
let board, gameOver, currentPlayer;
|
||||
const difficultySelect = document.getElementById('difficulty');
|
||||
const scoreHumanSpan = document.getElementById('score-human');
|
||||
const scoreAiSpan = document.getElementById('score-ai');
|
||||
const scoreDrawSpan = document.getElementById('score-d');
|
||||
|
||||
function initGame() {
|
||||
board = Array.from({length: ROWS}, () => Array(COLS).fill(''));
|
||||
gameOver = false;
|
||||
currentPlayer = HUMAN;
|
||||
statusDiv.textContent = "Tu turno (🟡)";
|
||||
renderBoard();
|
||||
// Estado del juego
|
||||
let board = Array.from({ length: ROWS }, () => Array(COLS).fill(''));
|
||||
let gameOver = false;
|
||||
let currentPlayer = HUMAN;
|
||||
let aiDepth = 3; // por defecto "normal"
|
||||
let scores = { human: 0, ai: 0, D: 0 };
|
||||
|
||||
// Utilidades de almacenamiento
|
||||
function loadScores() {
|
||||
try {
|
||||
const raw = localStorage.getItem('connect4_scores');
|
||||
if (raw) {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
scores.human = Number(parsed.human) || 0;
|
||||
scores.ai = Number(parsed.ai) || 0;
|
||||
scores.D = Number(parsed.D) || 0;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
// Ignorar errores de almacenamiento/parsing
|
||||
}
|
||||
}
|
||||
function saveScores() {
|
||||
try {
|
||||
localStorage.setItem('connect4_scores', JSON.stringify(scores));
|
||||
} catch (_) {
|
||||
// Ignorar errores de almacenamiento
|
||||
}
|
||||
}
|
||||
function updateScoreUI() {
|
||||
if (scoreHumanSpan) scoreHumanSpan.textContent = String(scores.human);
|
||||
if (scoreAiSpan) scoreAiSpan.textContent = String(scores.ai);
|
||||
if (scoreDrawSpan) scoreDrawSpan.textContent = String(scores.D);
|
||||
}
|
||||
|
||||
function renderBoard() {
|
||||
boardDiv.innerHTML = '';
|
||||
// Creamos todas las celdas primero
|
||||
for (let row = 0; row < ROWS; row++) {
|
||||
for (let col = 0; col < COLS; col++) {
|
||||
const cellDiv = document.createElement('div');
|
||||
cellDiv.className = 'cell';
|
||||
cellDiv.textContent = board[row][col];
|
||||
// Si es turno humano y no está terminado el juego
|
||||
if (!gameOver && currentPlayer === HUMAN && board[row][col] === '') {
|
||||
let lowestRow = getLowestEmptyRow(col);
|
||||
if (lowestRow === row) {
|
||||
cellDiv.style.cursor = "pointer";
|
||||
}
|
||||
// Inicialización del DOM del tablero (reutiliza nodos para rendimiento)
|
||||
function initBoardDom() {
|
||||
const totalCells = ROWS * COLS;
|
||||
if (boardDiv.children.length === totalCells) {
|
||||
// Reutilizar botones ya creados
|
||||
Array.from(boardDiv.children).forEach((c) => {
|
||||
c.classList.remove('win');
|
||||
c.textContent = '';
|
||||
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 r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const cell = document.createElement('button');
|
||||
cell.type = 'button';
|
||||
cell.className = 'cell';
|
||||
cell.setAttribute('data-row', String(r));
|
||||
cell.setAttribute('data-col', String(c));
|
||||
cell.setAttribute('role', 'gridcell');
|
||||
cell.setAttribute('aria-label', `Fila ${r + 1}, Columna ${c + 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');
|
||||
}
|
||||
|
||||
// Render del tablero (sin reconstruir DOM)
|
||||
function render() {
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
const idx = r * COLS + c;
|
||||
const el = boardDiv.children[idx];
|
||||
if (!el) continue;
|
||||
el.textContent = board[r][c];
|
||||
|
||||
// Indicador de clic solo en la fila más baja disponible de cada columna durante turno humano
|
||||
if (!gameOver && currentPlayer === HUMAN && board[r][c] === '') {
|
||||
const lowestRow = getLowestEmptyRow(c);
|
||||
el.style.cursor = lowestRow === r ? 'pointer' : 'default';
|
||||
} else {
|
||||
el.style.cursor = 'default';
|
||||
}
|
||||
// El evento click siempre llama para la columna
|
||||
cellDiv.onclick = () => {
|
||||
if (!gameOver && currentPlayer === HUMAN) {
|
||||
playerMove(col);
|
||||
}
|
||||
};
|
||||
boardDiv.appendChild(cellDiv);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(msg) {
|
||||
statusDiv.textContent = msg;
|
||||
}
|
||||
|
||||
// Lógica de movimiento
|
||||
function getLowestEmptyRow(col) {
|
||||
for (let r = ROWS - 1; r >= 0; r--) {
|
||||
if (board[r][col] === '') return r;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function playerMove(col) {
|
||||
if (gameOver || currentPlayer !== HUMAN) return;
|
||||
let row = getLowestEmptyRow(col);
|
||||
if (row === -1) return; // columna llena
|
||||
board[row][col] = HUMAN;
|
||||
renderBoard();
|
||||
if (checkWinner(HUMAN)) {
|
||||
statusDiv.textContent = "¡Has ganado! 🎉";
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
if (isFull()) {
|
||||
statusDiv.textContent = "¡Empate!";
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
currentPlayer = AI;
|
||||
statusDiv.textContent = "Turno de la máquina (❌)";
|
||||
setTimeout(machineMove, 450);
|
||||
function isDraw() {
|
||||
// Si la fila superior está llena, no hay movimientos posibles
|
||||
return board[0].every((cell) => cell !== '');
|
||||
}
|
||||
|
||||
function machineMove() {
|
||||
if (gameOver || currentPlayer !== AI) return;
|
||||
// IA sencilla: elige columna aleatoria libre
|
||||
let validCols = [];
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (board[0][c] === '') validCols.push(c);
|
||||
}
|
||||
if (validCols.length === 0) return;
|
||||
let col = validCols[Math.floor(Math.random() * validCols.length)];
|
||||
let row = getLowestEmptyRow(col);
|
||||
board[row][col] = AI;
|
||||
renderBoard();
|
||||
if (checkWinner(AI)) {
|
||||
statusDiv.textContent = "¡La máquina gana! 🤖";
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
if (isFull()) {
|
||||
statusDiv.textContent = "¡Empate!";
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
currentPlayer = HUMAN;
|
||||
statusDiv.textContent = "Tu turno (🟡)";
|
||||
}
|
||||
|
||||
function isFull() {
|
||||
return board[0].every(cell => cell !== '');
|
||||
}
|
||||
|
||||
function checkWinner(player) {
|
||||
// Comprobación de ganador; devuelve posiciones ganadoras para destacar
|
||||
function checkWinnerPositions(b, player) {
|
||||
// Horizontal
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS - 3; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r][c + 1] === player &&
|
||||
board[r][c + 2] === player &&
|
||||
board[r][c + 3] === player
|
||||
) return true;
|
||||
if (b[r][c] === player && b[r][c + 1] === player && b[r][c + 2] === player && b[r][c + 3] === player) {
|
||||
return [[r, c], [r, c + 1], [r, c + 2], [r, c + 3]];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Vertical
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
for (let r = 0; r < ROWS - 3; r++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r + 1][c] === player &&
|
||||
board[r + 2][c] === player &&
|
||||
board[r + 3][c] === player
|
||||
) return true;
|
||||
if (b[r][c] === player && b[r + 1][c] === player && b[r + 2][c] === player && b[r + 3][c] === player) {
|
||||
return [[r, c], [r + 1, c], [r + 2, c], [r + 3, c]];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Diagonal descendente \
|
||||
for (let r = 0; r < ROWS - 3; r++) {
|
||||
for (let c = 0; c < COLS - 3; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r + 1][c + 1] === player &&
|
||||
board[r + 2][c + 2] === player &&
|
||||
board[r + 3][c + 3] === player
|
||||
) return true;
|
||||
if (b[r][c] === player && b[r + 1][c + 1] === player && b[r + 2][c + 2] === player && b[r + 3][c + 3] === player) {
|
||||
return [[r, c], [r + 1, c + 1], [r + 2, c + 2], [r + 3, c + 3]];
|
||||
}
|
||||
}
|
||||
}
|
||||
// Diagonal ascendente /
|
||||
for (let r = 3; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS - 3; c++) {
|
||||
if (
|
||||
board[r][c] === player &&
|
||||
board[r - 1][c + 1] === player &&
|
||||
board[r - 2][c + 2] === player &&
|
||||
board[r - 3][c + 3] === player
|
||||
) return true;
|
||||
if (b[r][c] === player && b[r - 1][c + 1] === player && b[r - 2][c + 2] === player && b[r - 3][c + 3] === player) {
|
||||
return [[r, c], [r - 1, c + 1], [r - 2, c + 2], [r - 3, c + 3]];
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
function checkWinner(player) {
|
||||
return !!checkWinnerPositions(board, player);
|
||||
}
|
||||
|
||||
restartBtn.onclick = initGame;
|
||||
initGame();
|
||||
function highlightWin(positions) {
|
||||
if (!positions) return;
|
||||
positions.forEach(([r, c]) => {
|
||||
const idx = r * COLS + c;
|
||||
const el = boardDiv.children[idx];
|
||||
if (el) el.classList.add('win');
|
||||
});
|
||||
}
|
||||
|
||||
function endGame(result, winPositions) {
|
||||
gameOver = true;
|
||||
boardDiv.setAttribute('aria-disabled', 'true');
|
||||
|
||||
if (result === HUMAN) {
|
||||
setStatus('¡Has ganado! 🎉');
|
||||
scores.human += 1;
|
||||
highlightWin(winPositions);
|
||||
} else if (result === AI) {
|
||||
setStatus('¡La máquina gana! 🤖');
|
||||
scores.ai += 1;
|
||||
highlightWin(winPositions);
|
||||
} else {
|
||||
setStatus('¡Empate!');
|
||||
scores.D += 1;
|
||||
}
|
||||
|
||||
updateScoreUI();
|
||||
saveScores();
|
||||
}
|
||||
|
||||
// Interacción usuario
|
||||
function onCellClick(e) {
|
||||
const target = e.currentTarget;
|
||||
const row = Number(target.getAttribute('data-row'));
|
||||
const col = Number(target.getAttribute('data-col'));
|
||||
if (!Number.isInteger(row) || !Number.isInteger(col)) return;
|
||||
|
||||
if (gameOver || currentPlayer !== HUMAN) return;
|
||||
const r = getLowestEmptyRow(col);
|
||||
if (r === -1) return; // columna llena
|
||||
|
||||
board[r][col] = HUMAN;
|
||||
render();
|
||||
|
||||
const winPos = checkWinnerPositions(board, HUMAN);
|
||||
if (winPos) {
|
||||
endGame(HUMAN, winPos);
|
||||
return;
|
||||
}
|
||||
if (isDraw()) {
|
||||
endGame('D', null);
|
||||
return;
|
||||
}
|
||||
|
||||
currentPlayer = AI;
|
||||
setStatus('Turno de la máquina (❌)');
|
||||
setTimeout(machineMove, 220);
|
||||
}
|
||||
|
||||
// IA Minimax con evaluación heurística y profundidad limitada
|
||||
function validMoves(b) {
|
||||
const cols = [];
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
if (b[0][c] === '') cols.push(c);
|
||||
}
|
||||
return cols;
|
||||
}
|
||||
function applyMove(b, col, player) {
|
||||
for (let r = ROWS - 1; r >= 0; r--) {
|
||||
if (b[r][col] === '') {
|
||||
b[r][col] = player;
|
||||
return r;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
function undoMove(b, col) {
|
||||
// Quita la ficha más alta en la columna
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
if (b[r][col] !== '') {
|
||||
b[r][col] = '';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function evaluateWindow(cells) {
|
||||
let aiCount = 0, humanCount = 0, empty = 0;
|
||||
for (const v of cells) {
|
||||
if (v === AI) aiCount++;
|
||||
else if (v === HUMAN) humanCount++;
|
||||
else empty++;
|
||||
}
|
||||
// Bloques con ambas fichas no suman
|
||||
if (aiCount > 0 && humanCount > 0) return 0;
|
||||
if (aiCount > 0) {
|
||||
if (aiCount === 1) return 2;
|
||||
if (aiCount === 2) return 10;
|
||||
if (aiCount === 3) return 50;
|
||||
if (aiCount === 4) return 100000;
|
||||
}
|
||||
if (humanCount > 0) {
|
||||
if (humanCount === 1) return -2;
|
||||
if (humanCount === 2) return -10;
|
||||
if (humanCount === 3) return -55; // penaliza más
|
||||
if (humanCount === 4) return -100000;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function evaluateBoard(b) {
|
||||
let score = 0;
|
||||
// Centrismo: favorece columnas centrales (mejor conectividad)
|
||||
const centerCol = Math.floor(COLS / 2);
|
||||
let centerCount = 0;
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
if (b[r][centerCol] === AI) centerCount++;
|
||||
}
|
||||
score += centerCount * 3;
|
||||
|
||||
// Horizontal
|
||||
for (let r = 0; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS - 3; c++) {
|
||||
const window = [b[r][c], b[r][c + 1], b[r][c + 2], b[r][c + 3]];
|
||||
score += evaluateWindow(window);
|
||||
}
|
||||
}
|
||||
// Vertical
|
||||
for (let c = 0; c < COLS; c++) {
|
||||
for (let r = 0; r < ROWS - 3; r++) {
|
||||
const window = [b[r][c], b[r + 1][c], b[r + 2][c], b[r + 3][c]];
|
||||
score += evaluateWindow(window);
|
||||
}
|
||||
}
|
||||
// Diagonal descendente \
|
||||
for (let r = 0; r < ROWS - 3; r++) {
|
||||
for (let c = 0; c < COLS - 3; c++) {
|
||||
const window = [b[r][c], b[r + 1][c + 1], b[r + 2][c + 2], b[r + 3][c + 3]];
|
||||
score += evaluateWindow(window);
|
||||
}
|
||||
}
|
||||
// Diagonal ascendente /
|
||||
for (let r = 3; r < ROWS; r++) {
|
||||
for (let c = 0; c < COLS - 3; c++) {
|
||||
const window = [b[r][c], b[r - 1][c + 1], b[r - 2][c + 2], b[r - 3][c + 3]];
|
||||
score += evaluateWindow(window);
|
||||
}
|
||||
}
|
||||
return score;
|
||||
}
|
||||
|
||||
function minimax(b, depth, maximizing, alpha, beta) {
|
||||
// Terminal: victoria/derrota/empate
|
||||
const aiWin = checkWinnerPositions(b, AI);
|
||||
const humanWin = checkWinnerPositions(b, HUMAN);
|
||||
if (aiWin) return 1000000 + depth; // ganar rápido
|
||||
if (humanWin) return -1000000 - depth; // perder tarde
|
||||
if (validMoves(b).length === 0 || depth === 0) {
|
||||
return evaluateBoard(b);
|
||||
}
|
||||
|
||||
if (maximizing) {
|
||||
let best = -Infinity;
|
||||
// Ordenar movimientos por proximidad al centro
|
||||
const moves = validMoves(b).sort((a, d) => Math.abs(a - Math.floor(COLS / 2)) - Math.abs(d - Math.floor(COLS / 2)));
|
||||
for (const col of moves) {
|
||||
applyMove(b, col, AI);
|
||||
const val = minimax(b, depth - 1, false, alpha, beta);
|
||||
undoMove(b, col);
|
||||
best = Math.max(best, val);
|
||||
alpha = Math.max(alpha, best);
|
||||
if (beta <= alpha) break; // poda
|
||||
}
|
||||
return best;
|
||||
} else {
|
||||
let best = Infinity;
|
||||
const moves = validMoves(b).sort((a, d) => Math.abs(a - Math.floor(COLS / 2)) - Math.abs(d - Math.floor(COLS / 2)));
|
||||
for (const col of moves) {
|
||||
applyMove(b, col, HUMAN);
|
||||
const val = minimax(b, depth - 1, true, alpha, beta);
|
||||
undoMove(b, col);
|
||||
best = Math.min(best, val);
|
||||
beta = Math.min(beta, best);
|
||||
if (beta <= alpha) break; // poda
|
||||
}
|
||||
return best;
|
||||
}
|
||||
}
|
||||
|
||||
function getBestMove(b, depth) {
|
||||
let bestScore = -Infinity;
|
||||
let bestCol = null;
|
||||
|
||||
// Heurística inicial: priorizar centro si vacío
|
||||
const center = Math.floor(COLS / 2);
|
||||
if (b[0][center] === '') {
|
||||
bestCol = center;
|
||||
}
|
||||
|
||||
const moves = validMoves(b);
|
||||
for (const col of moves) {
|
||||
applyMove(b, col, AI);
|
||||
const score = minimax(b, depth - 1, false, -Infinity, Infinity);
|
||||
undoMove(b, col);
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestCol = col;
|
||||
}
|
||||
}
|
||||
// safety: si no hay mejor, primera válida
|
||||
if (bestCol === null && moves.length > 0) bestCol = moves[0];
|
||||
return bestCol;
|
||||
}
|
||||
|
||||
function machineMove() {
|
||||
if (gameOver || currentPlayer !== AI) return;
|
||||
|
||||
const col = getBestMove(board, aiDepth);
|
||||
if (col === null) {
|
||||
// Sin movimientos válidos: empate
|
||||
endGame('D', null);
|
||||
return;
|
||||
}
|
||||
const row = getLowestEmptyRow(col);
|
||||
if (row === -1) {
|
||||
// Columna llena por carrera, elegir siguiente válida
|
||||
const altMoves = validMoves(board).filter((c) => c !== col);
|
||||
if (!altMoves.length) {
|
||||
endGame('D', null);
|
||||
return;
|
||||
}
|
||||
const alt = altMoves[0];
|
||||
const altRow = getLowestEmptyRow(alt);
|
||||
if (altRow === -1) {
|
||||
endGame('D', null);
|
||||
return;
|
||||
}
|
||||
board[altRow][alt] = AI;
|
||||
} else {
|
||||
board[row][col] = AI;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
const winPos = checkWinnerPositions(board, AI);
|
||||
if (winPos) {
|
||||
endGame(AI, winPos);
|
||||
return;
|
||||
}
|
||||
if (isDraw()) {
|
||||
endGame('D', null);
|
||||
return;
|
||||
}
|
||||
|
||||
currentPlayer = HUMAN;
|
||||
setStatus('Tu turno (🟡)');
|
||||
}
|
||||
|
||||
function difficultyToDepth(val) {
|
||||
switch (val) {
|
||||
case 'facil': return 1;
|
||||
case 'dificil': return 4;
|
||||
case 'normal':
|
||||
default: return 3;
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
// Configurar dificultad
|
||||
if (difficultySelect) {
|
||||
aiDepth = difficultyToDepth(difficultySelect.value);
|
||||
} else {
|
||||
aiDepth = 3;
|
||||
}
|
||||
|
||||
// Reset estado
|
||||
board = Array.from({ length: ROWS }, () => Array(COLS).fill(''));
|
||||
gameOver = false;
|
||||
currentPlayer = HUMAN;
|
||||
|
||||
initBoardDom();
|
||||
// Limpiar resaltados previos
|
||||
Array.from(boardDiv.children).forEach((c) => c.classList.remove('win'));
|
||||
render();
|
||||
|
||||
setStatus('Tu turno (🟡)');
|
||||
boardDiv.setAttribute('aria-disabled', 'false');
|
||||
}
|
||||
|
||||
// Listeners
|
||||
restartBtn.addEventListener('click', startGame);
|
||||
if (difficultySelect) {
|
||||
difficultySelect.addEventListener('change', () => {
|
||||
startGame();
|
||||
});
|
||||
}
|
||||
|
||||
// Init
|
||||
loadScores();
|
||||
updateScoreUI();
|
||||
startGame();
|
@@ -129,4 +129,56 @@ h1 {
|
||||
.cell {
|
||||
font-size: 2em;
|
||||
}
|
||||
}
|
||||
/* Controles de IA y marcador */
|
||||
#controls {
|
||||
margin: 1rem auto 0.5rem auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
#controls select#difficulty {
|
||||
font-size: 1em;
|
||||
padding: 6px 10px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
#score {
|
||||
margin: 0.6rem auto 0.4rem auto;
|
||||
font-size: 1em;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
/* Botones-celda accesibles */
|
||||
.cell {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
.cell:focus-visible {
|
||||
box-shadow: 0 0 0 4px rgba(21, 101, 192, 0.35);
|
||||
}
|
||||
|
||||
/* Resaltado de combinación ganadora */
|
||||
.win {
|
||||
background: #c8e6ff;
|
||||
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.25), 0 2px 10px rgba(61, 90, 254, 0.16);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#controls select#difficulty {
|
||||
background: #20233a;
|
||||
color: #eaeaf0;
|
||||
border-color: #2c3252;
|
||||
}
|
||||
#score {
|
||||
color: #a0a3b0;
|
||||
}
|
||||
.cell:focus-visible {
|
||||
box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35);
|
||||
}
|
||||
.win {
|
||||
background: #172441;
|
||||
box-shadow: 0 0 0 2px rgba(61, 90, 254, 0.35), 0 2px 12px rgba(61, 90, 254, 0.22);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user