From d1a7442ffab92098d37c7b2a547a3e566acf392c Mon Sep 17 00:00:00 2001 From: fermdez Date: Fri, 3 Oct 2025 00:05:08 +0200 Subject: [PATCH] Mejoras y optimizaciones en general. --- 3-en-raya-computer/index.html | 21 +- 3-en-raya-computer/script.js | 371 +++++++++++++++++------ 3-en-raya-computer/styles.css | 52 ++++ 3-en-raya/index.html | 21 +- 3-en-raya/script.js | 212 ++++++++++--- 3-en-raya/styles.css | 52 ++++ 4-en-raya/index.html | 22 +- 4-en-raya/script.js | 529 ++++++++++++++++++++++++++------- 4-en-raya/styles.css | 52 ++++ adivina/index.html | 37 ++- adivina/script.js | 143 +++++++-- adivina/styles.css | 183 ++++++++++-- banderas/index.html | 22 +- banderas/script.js | 186 ++++++++---- banderas/styles.css | 64 ++++ buscaminas/index.html | 25 +- buscaminas/script.js | 309 ++++++++++++++----- buscaminas/styles.css | 75 ++++- index.html | 65 ++-- ladrillos/index.html | 19 +- ladrillos/script.js | 372 ++++++++++++++++++----- ladrillos/styles.css | 39 +++ memoria-v2/index.html | 24 +- memoria-v2/script.js | 263 +++++++++++----- memoria/index.html | 22 +- memoria/script.js | 213 ++++++++++--- memoria/styles.css | 83 +++++- piedra-papel-tijera/index.html | 30 +- piedra-papel-tijera/script.js | 159 +++++++--- pong/index.html | 18 +- pong/script.js | 387 ++++++++++++++++++------ pong/styles.css | 49 +++ 32 files changed, 3336 insertions(+), 783 deletions(-) diff --git a/3-en-raya-computer/index.html b/3-en-raya-computer/index.html index a9f40b8..ddea3cf 100644 --- a/3-en-raya-computer/index.html +++ b/3-en-raya-computer/index.html @@ -3,12 +3,27 @@ Tres en Raya vs Máquina + -

Tres en Raya vs Máquina

-
-
+

Tres en Raya vs Máquina

+ +
+ + +
+ +
+
+ +
+ Marcador — Tú: 0 · Máquina: 0 · Empates: 0 +
+ diff --git a/3-en-raya-computer/script.js b/3-en-raya-computer/script.js index 4184c15..111e411 100644 --- a/3-en-raya-computer/script.js +++ b/3-en-raya-computer/script.js @@ -1,122 +1,307 @@ +'use strict'; + +// Referencias DOM const boardDiv = document.getElementById('board'); const statusDiv = document.getElementById('status'); const restartBtn = document.getElementById('restart-btn'); -let board, gameOver; +const playerSideSelect = document.getElementById('player-side'); +const scorePlayerSpan = document.getElementById('score-player'); +const scoreAiSpan = document.getElementById('score-ai'); +const scoreDrawSpan = document.getElementById('score-d'); -function initGame() { - board = Array(9).fill(''); - gameOver = false; - statusDiv.textContent = "Tu turno (X)"; - renderBoard(); +// 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 renderBoard() { - boardDiv.innerHTML = ''; - board.forEach((cell, idx) => { - const cellDiv = document.createElement('div'); - cellDiv.className = 'cell'; - cellDiv.textContent = cell; - cellDiv.onclick = () => handlePlayerMove(idx); - boardDiv.appendChild(cellDiv); +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 handlePlayerMove(idx) { - if (gameOver || board[idx] !== '') return; - board[idx] = 'X'; - renderBoard(); - if (checkWinner('X')) { - statusDiv.textContent = `¡Tú ganas! 🎉`; - gameOver = true; +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()) { - statusDiv.textContent = "¡Empate!"; - gameOver = true; + if (isDraw(board)) { + endGame('D', null); return; } - statusDiv.textContent = "Turno de la máquina (O)"; - setTimeout(machineMove, 400); + + // 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; - for (let i = 0; i < 9; i++) { - if (board[i] === '') { - board[i] = 'O'; - let score = minimax(board, 0, false); - board[i] = ''; - if (score > bestScore) { - bestScore = score; - move = i; - } - } - } - if (move !== null) board[move] = 'O'; - renderBoard(); - if (checkWinner('O')) { - statusDiv.textContent = `¡La máquina gana! 🤖`; - gameOver = true; - return; - } - if (isDraw()) { - statusDiv.textContent = "¡Empate!"; - gameOver = true; - return; - } - statusDiv.textContent = "Tu turno (X)"; -} -// Algoritmo Minimax -function minimax(newBoard, depth, isMaximizing) { - if (checkWinnerOnBoard(newBoard, 'O')) return 10 - depth; - if (checkWinnerOnBoard(newBoard, 'X')) return depth - 10; - if (newBoard.every(cell => cell !== '')) return 0; - - if (isMaximizing) { - let bestScore = -Infinity; - for (let i = 0; i < 9; i++) { - if (newBoard[i] === '') { - newBoard[i] = 'O'; - let score = minimax(newBoard, depth + 1, false); - newBoard[i] = ''; - bestScore = Math.max(score, bestScore); - } - } - return bestScore; + // Pequeña heurística: si está libre, prioriza centro (4) en primera jugada + if (board.every(c => c === '')) { + move = 4; // centro } else { - let bestScore = Infinity; for (let i = 0; i < 9; i++) { - if (newBoard[i] === '') { - newBoard[i] = 'X'; - let score = minimax(newBoard, depth + 1, true); - newBoard[i] = ''; - bestScore = Math.min(score, bestScore); + if (board[i] === '') { + board[i] = aiSide; + const score = minimax(board, 0, false, -Infinity, Infinity); + board[i] = ''; + if (score > bestScore) { + bestScore = score; + move = i; + } } } - return bestScore; + // 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})`); } } -function checkWinner(player) { - return checkWinnerOnBoard(board, player); +// Listeners +restartBtn.addEventListener('click', startGame); +if (playerSideSelect) { + playerSideSelect.addEventListener('change', () => { + // Reiniciar al cambiar de lado + startGame(); + }); } -function checkWinnerOnBoard(b, player) { - const wins = [ - [0,1,2],[3,4,5],[6,7,8], - [0,3,6],[1,4,7],[2,5,8], - [0,4,8],[2,4,6] - ]; - return wins.some(combo => combo.every(i => b[i] === player)); -} - -function isDraw() { - return board.every(cell => cell !== '') && !checkWinner('X') && !checkWinner('O'); -} - -restartBtn.onclick = initGame; - -initGame(); \ No newline at end of file +// Init +loadScores(); +updateScoreUI(); +startGame(); \ No newline at end of file diff --git a/3-en-raya-computer/styles.css b/3-en-raya-computer/styles.css index 9c18416..f985193 100644 --- a/3-en-raya-computer/styles.css +++ b/3-en-raya-computer/styles.css @@ -119,4 +119,56 @@ h1 { .cell { font-size: 3em; } +} +/* Controles y marcador */ +#controls { + margin: 1rem auto 0.5rem auto; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} +#controls select#player-side { + 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(52, 152, 219, 0.35); +} + +/* Resaltado de combinación ganadora */ +.win { + background: #eaf7ff; + 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#player-side { + 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); + } } \ No newline at end of file diff --git a/3-en-raya/index.html b/3-en-raya/index.html index 1ff83dc..2ce9989 100644 --- a/3-en-raya/index.html +++ b/3-en-raya/index.html @@ -3,12 +3,27 @@ Tres en Raya + -

Tres en Raya (Tic Tac Toe)

-
-
+

Tres en Raya (Tic Tac Toe)

+ +
+ + +
+ +
+
+ +
+ Marcador — X: 0 · O: 0 · Empates: 0 +
+ diff --git a/3-en-raya/script.js b/3-en-raya/script.js index 5e038d4..ebd6ccc 100644 --- a/3-en-raya/script.js +++ b/3-en-raya/script.js @@ -1,52 +1,184 @@ +'use strict'; + const boardDiv = document.getElementById('board'); const statusDiv = document.getElementById('status'); const restartBtn = document.getElementById('restart-btn'); -let board, currentPlayer, gameOver; +const firstPlayerSelect = document.getElementById('first-player'); +const scoreXSpan = document.getElementById('score-x'); +const scoreOSpan = document.getElementById('score-o'); +const scoreDrawSpan = document.getElementById('score-d'); -function initGame() { - board = Array(9).fill(''); - currentPlayer = 'X'; - gameOver = false; - statusDiv.textContent = "Turno de " + currentPlayer; - renderBoard(); -} +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 +]; -function renderBoard() { - boardDiv.innerHTML = ''; - board.forEach((cell, idx) => { - const cellDiv = document.createElement('div'); - cellDiv.className = 'cell'; - cellDiv.textContent = cell; - cellDiv.onclick = () => handleCellClick(idx); - boardDiv.appendChild(cellDiv); - }); -} +let board = Array(9).fill(''); +let currentPlayer = 'X'; +let gameOver = false; +let scores = { X: 0, O: 0, D: 0 }; -function handleCellClick(idx) { - if (gameOver || board[idx] !== '') return; - board[idx] = currentPlayer; - renderBoard(); - if (checkWinner(currentPlayer)) { - statusDiv.textContent = `¡${currentPlayer} gana! 🎉`; - gameOver = true; - } else if (board.every(cell => cell !== '')) { - statusDiv.textContent = "¡Empate!"; - gameOver = true; - } else { - currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; - statusDiv.textContent = "Turno de " + currentPlayer; +function loadScores() { + try { + const raw = localStorage.getItem('ttt_scores'); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + scores.X = Number(parsed.X) || 0; + scores.O = Number(parsed.O) || 0; + scores.D = Number(parsed.D) || 0; + } + } + } catch (_) { + // Ignorar errores de parsing o acceso } } -function checkWinner(player) { - const winCases = [ - [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 - ]; - return winCases.some(combo => combo.every(i => board[i] === player)); +function saveScores() { + try { + localStorage.setItem('ttt_scores', JSON.stringify(scores)); + } catch (_) { + // Ignorar errores de almacenamiento + } } -restartBtn.onclick = initGame; +function updateScoreUI() { + if (scoreXSpan) scoreXSpan.textContent = String(scores.X); + if (scoreOSpan) scoreOSpan.textContent = String(scores.O); + if (scoreDrawSpan) scoreDrawSpan.textContent = String(scores.D); +} -initGame(); \ No newline at end of file +function initBoardDom() { + // Crear la cuadrícula de 3x3 solo si no existe + if (boardDiv.children.length === 9) { + // limpiar estado de clases win previas + Array.from(boardDiv.children).forEach(c => c.classList.remove('win')); + 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('aria-label', `Casilla ${idx + 1}`); + cell.setAttribute('role', 'gridcell'); + 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 cellEl = boardDiv.children[i]; + if (!cellEl) continue; + cellEl.textContent = board[i]; + } +} + +function setStatus(msg) { + statusDiv.textContent = msg; +} + +function checkWinner(player) { + for (const combo of WIN_COMBOS) { + const [a, b, c] = combo; + if (board[a] === player && board[b] === player && board[c] === player) { + return { win: true, combo }; + } + } + return { win: false, combo: null }; +} + +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 === 'X' || result === 'O') { + setStatus(`¡${result} gana! 🎉`); + scores[result] += 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; + if (board[idx] !== '') return; + + board[idx] = currentPlayer; + render(); + + const { win, combo } = checkWinner(currentPlayer); + if (win) { + endGame(currentPlayer, combo); + return; + } + + // Comprueba empate + if (board.every(cell => cell !== '')) { + endGame('D', null); + return; + } + + // Cambiar turno + currentPlayer = currentPlayer === 'X' ? 'O' : 'X'; + setStatus(`Turno de ${currentPlayer}`); +} + +function startGame() { + board = Array(9).fill(''); + gameOver = false; + + // Determinar primer jugador desde el selector si existe + if (firstPlayerSelect && (firstPlayerSelect.value === 'X' || firstPlayerSelect.value === 'O')) { + currentPlayer = firstPlayerSelect.value; + } else { + currentPlayer = 'X'; + } + + boardDiv.setAttribute('aria-disabled', 'false'); + initBoardDom(); + // Remover cualquier resaltado previo + Array.from(boardDiv.children).forEach(c => c.classList.remove('win')); + render(); + setStatus(`Turno de ${currentPlayer}`); +} + +// Listeners +restartBtn.addEventListener('click', startGame); +if (firstPlayerSelect) { + firstPlayerSelect.addEventListener('change', () => { + // Solo afecta si la partida no ha empezado o si no ha terminado aún + if (!gameOver && board.every(v => v === '')) { + currentPlayer = firstPlayerSelect.value; + setStatus(`Turno de ${currentPlayer}`); + } + }); +} + +// Init +loadScores(); +updateScoreUI(); +startGame(); \ No newline at end of file diff --git a/3-en-raya/styles.css b/3-en-raya/styles.css index c0b6953..57ba1aa 100644 --- a/3-en-raya/styles.css +++ b/3-en-raya/styles.css @@ -118,4 +118,56 @@ h1 { .cell { font-size: 3em; } +} +/* Controles de partida y marcador */ +#controls { + margin: 1rem auto 0.5rem auto; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} +#controls select#first-player { + 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; +} + +/* Ajustes para botones-celda (accesibilidad) */ +.cell { + border: none; + outline: none; +} +.cell:focus-visible { + box-shadow: 0 0 0 4px rgba(52, 152, 219, 0.35); +} + +/* Resaltado de combinación ganadora */ +.win { + background: #eaf7ff; + 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#first-player { + 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); + } } \ No newline at end of file diff --git a/4-en-raya/index.html b/4-en-raya/index.html index 9da395d..edb8cb4 100644 --- a/4-en-raya/index.html +++ b/4-en-raya/index.html @@ -3,12 +3,28 @@ 4 en Raya vs Máquina + -

4 en Raya vs Máquina

-
-
+

4 en Raya vs Máquina

+ +
+ + +
+ +
+
+ +
+ Marcador — Tú: 0 · Máquina: 0 · Empates: 0 +
+ diff --git a/4-en-raya/script.js b/4-en-raya/script.js index 81fc7ba..d323a52 100644 --- a/4-en-raya/script.js +++ b/4-en-raya/script.js @@ -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(); \ No newline at end of file +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(); \ No newline at end of file diff --git a/4-en-raya/styles.css b/4-en-raya/styles.css index 871b737..f4da0f2 100644 --- a/4-en-raya/styles.css +++ b/4-en-raya/styles.css @@ -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); + } } \ No newline at end of file diff --git a/adivina/index.html b/adivina/index.html index d4e1c99..dc92133 100644 --- a/adivina/index.html +++ b/adivina/index.html @@ -3,18 +3,39 @@ Adivina el Número + -

¡Adivina el número!

-
-

Estoy pensando en un número entre 1 y 100.

-

¿Puedes adivinarlo en 7 intentos?

- - - -
+

¡Adivina el número!

+
+

Estoy pensando en un número entre 1 y 100.

+

¿Puedes adivinarlo en 7 intentos?

+ + + + +
+ + + + +
+ +
+
Intentos restantes: 7
+
Mejor marca:
+ +
+

Historial

+
    +
    diff --git a/adivina/script.js b/adivina/script.js index 35b91e7..e685809 100644 --- a/adivina/script.js +++ b/adivina/script.js @@ -1,55 +1,156 @@ -let randomNumber; -let attemptsLeft = 7; +'use strict'; +const DIFFICULTIES = { + normal: { max: 100, attempts: 7 }, + facil: { max: 50, attempts: 10 }, + dificil: { max: 200, attempts: 7 }, + extremo: { max: 1000, attempts: 10 } +}; + +let randomNumber = 0; +let attemptsLeft = 0; +let attemptsTotal = 0; +let maxNumber = 100; +let history = []; +let currentDifficulty = 'normal'; + +// DOM refs const guessInput = document.getElementById('guess-input'); const guessBtn = document.getElementById('guess-btn'); const restartBtn = document.getElementById('restart-btn'); const infoDiv = document.getElementById('info'); const attemptsSpan = document.getElementById('attempts-left'); +const difficultySelect = document.getElementById('difficulty'); +const errorDiv = document.getElementById('error'); +const rangeMaxSpan = document.getElementById('range-max'); +const attemptsTotalSpan = document.getElementById('attempts-total'); +const bestScoreSpan = document.getElementById('best-score'); +const historyList = document.getElementById('history-list'); + +function getBest(difficulty) { + const key = `adivina_best_${difficulty}`; + const v = localStorage.getItem(key); + return v ? parseInt(v, 10) : null; +} + +function setBest(difficulty, attemptsUsed) { + const prev = getBest(difficulty); + if (prev === null || attemptsUsed < prev) { + localStorage.setItem(`adivina_best_${difficulty}`, String(attemptsUsed)); + bestScoreSpan.textContent = String(attemptsUsed); + } +} + +function updateBestDisplay() { + const best = getBest(currentDifficulty); + bestScoreSpan.textContent = best !== null ? String(best) : '—'; +} + +function renderHistory() { + // Safe clear + while (historyList.firstChild) historyList.removeChild(historyList.firstChild); + history.forEach(item => { + const li = document.createElement('li'); + li.textContent = `${item.guess} → ${item.hint}`; + historyList.appendChild(li); + }); +} function startGame() { - randomNumber = Math.floor(Math.random() * 100) + 1; - attemptsLeft = 7; - attemptsSpan.textContent = attemptsLeft; + currentDifficulty = (difficultySelect && difficultySelect.value) || 'normal'; + const conf = DIFFICULTIES[currentDifficulty] || DIFFICULTIES.normal; + + maxNumber = conf.max; + attemptsTotal = conf.attempts; + attemptsLeft = attemptsTotal; + randomNumber = Math.floor(Math.random() * maxNumber) + 1; + history = []; + + // Update UI + if (rangeMaxSpan) rangeMaxSpan.textContent = String(maxNumber); + if (attemptsTotalSpan) attemptsTotalSpan.textContent = String(attemptsTotal); + attemptsSpan.textContent = String(attemptsLeft); + errorDiv.textContent = ''; infoDiv.textContent = ''; + renderHistory(); + updateBestDisplay(); + + // Reset input/button state guessInput.value = ''; guessInput.disabled = false; + guessInput.setAttribute('min', '1'); + guessInput.setAttribute('max', String(maxNumber)); guessBtn.disabled = false; restartBtn.classList.add('hidden'); + guessInput.focus(); } function checkGuess() { - const guess = Number(guessInput.value); + errorDiv.textContent = ''; - if (guess < 1 || guess > 100 || isNaN(guess)) { - infoDiv.textContent = "Por favor ingresa un número válido entre 1 y 100."; + const raw = guessInput.value.trim(); + if (raw === '') { + errorDiv.textContent = 'Introduce un número.'; + return; + } + + const guess = Number(raw); + if (!Number.isFinite(guess) || !Number.isInteger(guess)) { + errorDiv.textContent = 'El valor debe ser un número entero.'; + return; + } + if (guess < 1 || guess > maxNumber) { + errorDiv.textContent = `El número debe estar entre 1 y ${maxNumber}.`; return; } attemptsLeft--; - attemptsSpan.textContent = attemptsLeft; + attemptsSpan.textContent = String(attemptsLeft); if (guess === randomNumber) { - infoDiv.textContent = "¡Correcto! 🎉 Has adivinado el número."; - endGame(true); - } else if (attemptsLeft === 0) { + infoDiv.textContent = '¡Correcto! 🎉 Has adivinado el número.'; + const usedAttempts = attemptsTotal - attemptsLeft; + history.push({ guess, hint: '✅ Correcto' }); + renderHistory(); + endGame(true, usedAttempts); + return; + } + + const hint = guess < randomNumber ? '⬇️ Bajo' : '⬆️ Alto'; + infoDiv.textContent = guess < randomNumber + ? 'Demasiado bajo. Intenta nuevamente.' + : 'Demasiado alto. Intenta nuevamente.'; + history.push({ guess, hint }); + renderHistory(); + + if (attemptsLeft === 0) { infoDiv.textContent = `¡Oh no! Te quedaste sin intentos. El número era ${randomNumber}.`; - endGame(false); - } else if (guess < randomNumber) { - infoDiv.textContent = "Demasiado bajo. Intenta nuevamente."; - } else { - infoDiv.textContent = "Demasiado alto. Intenta nuevamente."; + endGame(false, attemptsTotal); } } -function endGame(won) { +function endGame(won, usedAttempts) { guessInput.disabled = true; guessBtn.disabled = true; restartBtn.classList.remove('hidden'); + + if (won) { + setBest(currentDifficulty, usedAttempts); + } } -guessBtn.onclick = checkGuess; -restartBtn.onclick = startGame; -guessInput.onkeydown = (e) => { if (e.key === "Enter") checkGuess(); }; +// Listeners (avoid inline handlers) +guessBtn.addEventListener('click', checkGuess); +restartBtn.addEventListener('click', startGame); +guessInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') checkGuess(); +}); +guessInput.addEventListener('input', () => { + errorDiv.textContent = ''; +}); +if (difficultySelect) { + difficultySelect.addEventListener('change', startGame); +} +// Init startGame(); \ No newline at end of file diff --git a/adivina/styles.css b/adivina/styles.css index 885cce5..d6ae7e1 100644 --- a/adivina/styles.css +++ b/adivina/styles.css @@ -1,43 +1,106 @@ +:root { + --bg: #f7f7f7; + --text: #222; + --muted: #888; + --card-bg: #ffffff; + --shadow: 0 0 8px #bbb; + --accent: #2196f3; + --accent-hover: #1769aa; + --accent-contrast: #ffffff; + --error: #c62828; +} + body { - background: #f7f7f7; - font-family: Arial, sans-serif; + background: var(--bg); + color: var(--text); + font-family: 'Montserrat', Arial, sans-serif; text-align: center; + margin: 0; + padding: 20px; } h1 { - margin-top: 40px; - color: #222; + margin-top: 20px; + color: var(--text); + font-weight: 700; } #game-box { - background: white; + background: var(--card-bg); width: 350px; - margin: 50px auto; + max-width: 94vw; + margin: 30px auto; padding: 30px 20px; border-radius: 12px; - box-shadow: 0 0 8px #bbb; + box-shadow: var(--shadow); +} + +.input-row { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 10px; } input[type="number"] { - width: 100px; + width: 120px; font-size: 1.1em; - padding: 6px; - margin-right: 12px; + padding: 8px 10px; margin-bottom: 10px; + border: 1px solid #ddd; + border-radius: 8px; + outline: none; +} + +input[type="number"]:focus-visible { + border-color: var(--accent); + box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.25); +} + +select#difficulty { + font-size: 1em; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid #ddd; + margin: 8px 0 14px 0; } button { font-size: 1em; - padding: 7px 20px; + padding: 9px 22px; border: none; - border-radius: 5px; - background: #2196f3; - color: white; + border-radius: 44px; + background: var(--accent); + color: var(--accent-contrast); cursor: pointer; margin-bottom: 10px; + transition: transform .12s, box-shadow .16s, background .16s; } + button:hover { - background: #1769aa; + background: var(--accent-hover); + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(33,150,243,0.3); +} + +button:focus-visible { + outline: none; + box-shadow: 0 0 0 3px rgba(33,150,243,0.35); +} + +button:disabled { + opacity: 0.6; + cursor: not-allowed; + transform: none; + box-shadow: none; +} + +#error { + color: var(--error); + font-size: 0.95em; + min-height: 1.6em; } #info { @@ -48,21 +111,93 @@ button:hover { #attempts { margin-top: 8px; - color: #888; + color: var(--muted); +} + +#best { + margin-top: 6px; + color: var(--muted); +} + +#history { + margin-top: 16px; + text-align: left; +} + +#history h2 { + font-size: 1.05em; + margin: 8px 0; + color: var(--text); +} + +#history-list { + list-style: none; + padding: 0; + margin: 0; + max-height: 160px; + overflow-y: auto; + border-top: 1px dashed #ddd; + padding-top: 8px; +} + +#history-list li { + padding: 6px 0; + border-bottom: 1px dashed #eee; + color: var(--text); } .hidden { display: none; } +.sr-only { + position: absolute !important; + height: 1px; width: 1px; + overflow: hidden; + clip: rect(1px, 1px, 1px, 1px); + white-space: nowrap; +} + +@media (max-width: 480px) { + #game-box { + padding: 24px 14px; + } + .input-row { + flex-direction: column; + gap: 8px; + } +} + @media (min-width: 728px) { - body, - #game-box, - input[type="number"], - button, - #info, - #attempts { - width: 95%; - font-size: 130%; + #game-box { width: 450px; } + body { font-size: 110%; } +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f1222; + --text: #eaeaf0; + --muted: #a0a3b0; + --card-bg: #171a2e; + --shadow: 0 0 0 rgba(0,0,0,0); + --accent: #3d5afe; + --accent-hover: #0a2459; + --accent-contrast: #ffffff; + --error: #ef5350; + } + #game-box { + box-shadow: 0 6px 24px rgba(61,90,254,0.12); + border: 1px solid rgba(255,255,255,0.06); + } + input[type="number"], select#difficulty { + background: #20233a; + color: var(--text); + border-color: #2c3252; + } + #history-list { + border-top-color: #2c3252; + } + #history-list li { + border-bottom-color: #2c3252; } } \ No newline at end of file diff --git a/banderas/index.html b/banderas/index.html index 6e146fc..891ef2c 100644 --- a/banderas/index.html +++ b/banderas/index.html @@ -3,18 +3,30 @@ Empareja la Bandera + -

    Empareja la Bandera

    +

    Empareja la Bandera

    Haz clic en una bandera y después en el país correspondiente. ¿Puedes emparejar todas?

    + +
    + + +
    +
    Aciertos: /
    -
    -
    -
    +
    +
    +
    -
    +
    \ No newline at end of file diff --git a/banderas/script.js b/banderas/script.js index 0a30390..b4360d7 100644 --- a/banderas/script.js +++ b/banderas/script.js @@ -1,3 +1,5 @@ +'use strict'; + // Lista completa con país y siglas según ISO 3166-1 alpha-2 const countryList = [ { name: "España", code: "ES" }, { name: "Francia", code: "FR" }, { name: "Alemania", code: "DE" }, { name: "Italia", code: "IT" }, @@ -13,88 +15,119 @@ const countryList = [ function getFlagEmoji(code) { // Las banderas se generan a partir de letras usando unicode, no por siglas textuales // Solo funcionan para países con código alpha-2 y script latino - if (!code || code.length !== 2) return ""; + if (!code || code.length !== 2) return ''; return String.fromCodePoint( - ...code.toUpperCase().split("").map(c => 0x1F1E6 + c.charCodeAt(0) - 65) + ...code.toUpperCase().split('').map(c => 0x1F1E6 + c.charCodeAt(0) - 65) ); } -// Para elegir N países aleatorios distintos con bandera +// Elegir N países aleatorios distintos con bandera, sin mutar la lista original function pickRandomCountries(list, n) { + const pool = list.slice(); // copia de seguridad const chosen = []; - const used = {}; - while (chosen.length < n && list.length > 0) { - let idx = Math.floor(Math.random() * list.length); - let c = list[idx]; - let flag = getFlagEmoji(c.code); - if (flag && !used[c.code]) { - chosen.push({name: c.name, code: c.code, flag}); - used[c.code] = true; + const used = new Set(); + while (chosen.length < n && pool.length > 0) { + const idx = Math.floor(Math.random() * pool.length); + const c = pool[idx]; + const flag = getFlagEmoji(c.code); + if (flag && !used.has(c.code)) { + chosen.push({ name: c.name, code: c.code, flag }); + used.add(c.code); } - list.splice(idx,1); + // Eliminar del pool para evitar repetir + pool.splice(idx, 1); } return chosen; } -let flagsDiv = document.getElementById('flags'); -let countriesDiv = document.getElementById('countries'); -let statusDiv = document.getElementById('status'); -let scoreSpan = document.getElementById('score-value'); -let scoreTotal = document.getElementById('score-total'); -let restartBtn = document.getElementById('restart-btn'); +// Referencias DOM +const flagsDiv = document.getElementById('flags'); +const countriesDiv = document.getElementById('countries'); +const statusDiv = document.getElementById('status'); +const scoreSpan = document.getElementById('score-value'); +const scoreTotal = document.getElementById('score-total'); +const restartBtn = document.getElementById('restart-btn'); +const pairsSelect = document.getElementById('pairs-count'); -let pairs = [], flags = [], countries = [], selectedFlag = null, selectedCountry = null, score = 0, totalPairs = 12; +// Estado +let pairs = []; +let flags = []; +let countries = []; +let selectedFlag = null; +let selectedCountry = null; +let score = 0; +let totalPairs = 12; +// Utilidades function shuffle(arr) { - for (let i = arr.length-1; i>0; i--) { - const j = Math.floor(Math.random() * (i+1)); + for (let i = arr.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); [arr[i], arr[j]] = [arr[j], arr[i]]; } } - -function startGame() { - let fullList = countryList.slice(); // copia - pairs = pickRandomCountries(fullList, totalPairs); - flags = pairs.map(o=>o); - countries = pairs.map(o=>o); - shuffle(flags); shuffle(countries); - score = 0; - scoreSpan.textContent = score; - scoreTotal.textContent = pairs.length; - selectedFlag = selectedCountry = null; - renderFlags(); - renderCountries(); - statusDiv.textContent = 'Empareja todas las banderas con su país'; +function setStatus(msg) { + statusDiv.textContent = msg; } +// Renderizado function renderFlags() { - flagsDiv.innerHTML = ''; + // Limpieza segura + flagsDiv.textContent = ''; flags.forEach((p, i) => { - const d = document.createElement('div'); + const d = document.createElement('button'); + d.type = 'button'; d.className = 'flag'; - d.textContent = p.flag; - d.setAttribute('tabindex', 0); - d.onclick = () => selectFlag(i); + d.textContent = p.flag; // seguro (caracter unicode) + d.setAttribute('tabindex', '0'); + d.setAttribute('role', 'listitem'); + d.setAttribute('aria-label', `Bandera de ${p.name}`); + d.setAttribute('aria-disabled', p.matched ? 'true' : 'false'); + d.setAttribute('aria-selected', selectedFlag === i ? 'true' : 'false'); + if (p.matched) d.classList.add('matched'); if (selectedFlag === i) d.classList.add('selected'); + + d.addEventListener('click', () => selectFlag(i)); + d.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + selectFlag(i); + } + }); + flagsDiv.appendChild(d); }); } function renderCountries() { - countriesDiv.innerHTML = ''; + countriesDiv.textContent = ''; countries.forEach((p, i) => { - const d = document.createElement('div'); + const d = document.createElement('button'); + d.type = 'button'; d.className = 'country'; - d.textContent = p.name; - d.setAttribute('tabindex', 0); - d.onclick = () => selectCountry(i); + d.textContent = p.name; // seguro + d.setAttribute('tabindex', '0'); + d.setAttribute('role', 'listitem'); + d.setAttribute('aria-label', `País ${p.name}`); + d.setAttribute('aria-disabled', p.matched ? 'true' : 'false'); + d.setAttribute('aria-selected', selectedCountry === i ? 'true' : 'false'); + if (p.matched) d.classList.add('matched'); if (selectedCountry === i) d.classList.add('selected'); + + d.addEventListener('click', () => selectCountry(i)); + d.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + selectCountry(i); + } + }); + countriesDiv.appendChild(d); }); } +// Lógica selección function selectFlag(i) { if (flags[i].matched) return; selectedFlag = i; @@ -110,32 +143,81 @@ function selectCountry(i) { } function checkMatch() { + if (selectedFlag === null || selectedCountry === null) return; + const flagObj = flags[selectedFlag]; const countryObj = countries[selectedCountry]; + + if (!flagObj || !countryObj) return; + if (flagObj.code === countryObj.code) { flags[selectedFlag].matched = true; countries[selectedCountry].matched = true; score++; - scoreSpan.textContent = score; - statusDiv.textContent = '¡Correcto!'; + scoreSpan.textContent = String(score); + setStatus('¡Correcto!'); renderFlags(); renderCountries(); if (score === pairs.length) { - statusDiv.textContent = '¡Has emparejado todas las banderas! 🎉'; + setStatus('¡Has emparejado todas las banderas! 🎉'); } } else { - statusDiv.textContent = 'No es correcto, intenta otra vez.'; + setStatus('No es correcto, intenta otra vez.'); + // Reset de selección tras breve pausa setTimeout(() => { - statusDiv.textContent = ''; - selectedFlag = selectedCountry = null; + setStatus(''); + selectedFlag = null; + selectedCountry = null; renderFlags(); renderCountries(); }, 850); return; } + + // Reset selección tras acierto selectedFlag = null; selectedCountry = null; } -restartBtn.onclick = startGame; +// Inicio/reinicio +function startGame() { + // Leer número de parejas desde selector, con límites + const desired = pairsSelect && Number(pairsSelect.value) ? Number(pairsSelect.value) : 12; + totalPairs = Math.max(4, Math.min(desired, countryList.length)); + + const fullList = countryList.slice(); // copia + pairs = pickRandomCountries(fullList, totalPairs); + // Si no se pudieron escoger las deseadas (por restricciones), ajustar total + if (pairs.length < totalPairs) { + totalPairs = pairs.length; + } + + flags = pairs.map(o => ({ ...o })); + countries = pairs.map(o => ({ ...o })); + + shuffle(flags); + shuffle(countries); + + score = 0; + scoreSpan.textContent = String(score); + scoreTotal.textContent = String(pairs.length); + selectedFlag = null; + selectedCountry = null; + + renderFlags(); + renderCountries(); + setStatus('Empareja todas las banderas con su país'); + + // Enfocar primera bandera para accesibilidad + const firstFlag = flagsDiv.querySelector('.flag'); + if (firstFlag) firstFlag.focus(); +} + +// Listeners (evitar handlers inline) +restartBtn.addEventListener('click', startGame); +if (pairsSelect) { + pairsSelect.addEventListener('change', startGame); +} + +// Init startGame(); \ No newline at end of file diff --git a/banderas/styles.css b/banderas/styles.css index 20d95da..1b68e9a 100644 --- a/banderas/styles.css +++ b/banderas/styles.css @@ -73,4 +73,68 @@ h1 { } @media (max-width: 520px) { .flags, .countries { grid-template-columns: 1fr;} +} +/* Controles y accesibilidad */ +#controls { + margin: 1rem auto 0.5rem auto; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} +#controls select#pairs-count { + font-size: 1em; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid #ddd; +} + +.flag, .country { + outline: none; +} +.flag:focus-visible, .country:focus-visible { + box-shadow: 0 0 0 3px rgba(35, 105, 143, 0.35); +} +.flag[aria-disabled="true"], .country[aria-disabled="true"] { + cursor: default; + opacity: 0.85; +} + +/* Modo oscuro básico */ +@media (prefers-color-scheme: dark) { + body { + background: #0f1222; + color: #eaeaf0; + } + #score, #status { + color: #a0a3b0; + } + .flag, .country { + background: #20233a; + color: #eaeaf0; + border-color: #2c3252; + box-shadow: 0 2px 10px rgba(61, 90, 254, 0.12); + } + .flag.selected, .country.selected { + background: #1e3a8a; + border-color: #3d5afe; + box-shadow: 0 0 0 2px #3d5afe; + } + .flag.matched, .country.matched { + background: #1b5e20; + color: #d7ffd9; + border-color: #66bb6a; + box-shadow: 0 0 0 2px #66bb6a; + } + #restart-btn { + background: #3d5afe; + } + #restart-btn:hover { + background: #0a2459; + } + #controls select#pairs-count { + background: #20233a; + color: #eaeaf0; + border-color: #2c3252; + } } \ No newline at end of file diff --git a/buscaminas/index.html b/buscaminas/index.html index 478cb98..4b9b7ec 100644 --- a/buscaminas/index.html +++ b/buscaminas/index.html @@ -3,12 +3,31 @@ Buscaminas + -

    Buscaminas

    -
    -
    +

    Buscaminas

    + +
    + + + + +
    + +
    +
    🚩 Restantes: 0
    +
    diff --git a/buscaminas/script.js b/buscaminas/script.js index 0878572..f0a165a 100644 --- a/buscaminas/script.js +++ b/buscaminas/script.js @@ -1,43 +1,181 @@ -const SIZE = 10; -const MINES = 15; +'use strict'; + +// Configuración por defecto (se puede cambiar desde los selectores del UI) +const DEFAULT_SIZE = 10; +const DEFAULT_MINES = 15; + +// Referencias DOM const boardDiv = document.getElementById('board'); const statusDiv = document.getElementById('status'); const restartBtn = document.getElementById('restart-btn'); -let board, revealed, flagged, gameOver, cellsRevealed; +const sizeSelect = document.getElementById('size'); +const minesSelect = document.getElementById('mines'); +const flagsLeftSpan = document.getElementById('flags-left'); -function initGame() { - board = Array.from({length: SIZE}, () => Array(SIZE).fill(0)); - revealed = Array.from({length: SIZE}, () => Array(SIZE).fill(false)); - flagged = Array.from({length: SIZE}, () => Array(SIZE).fill(false)); - gameOver = false; - cellsRevealed = 0; - statusDiv.textContent = 'Haz clic izquierdo para revelar, clic derecho para marcar.'; - placeMines(); - calculateNumbers(); - renderBoard(); +// Estado +let SIZE = DEFAULT_SIZE; +let MINES = DEFAULT_MINES; +let board = []; // matriz de valores: 'M' para mina o 0..8 número de minas adyacentes +let revealed = []; // matriz booleana: celdas reveladas +let flagged = []; // matriz booleana: celdas marcadas +let gameOver = false; +let cellsRevealed = 0; +let flagsLeft = 0; +let firstClickMade = false; + +// Utilidades +function setStatus(msg) { + statusDiv.textContent = msg; +} +function updateFlagsLeft() { + let count = 0; + for (let r = 0; r < SIZE; r++) { + for (let c = 0; c < SIZE; c++) { + if (flagged[r][c]) count++; + } + } + flagsLeft = Math.max(0, MINES - count); + if (flagsLeftSpan) flagsLeftSpan.textContent = String(flagsLeft); +} +function inBounds(r, c) { + return r >= 0 && r < SIZE && c >= 0 && c < SIZE; } -function placeMines() { - let placed = 0; - while (placed < MINES) { - const r = Math.floor(Math.random() * SIZE); - const c = Math.floor(Math.random() * SIZE); - if (board[r][c] !== 'M') { - board[r][c] = 'M'; - placed++; +// Inicializa estructuras y UI base +function startGame() { + // Leer valores desde selectores + const parsedSize = sizeSelect ? Number(sizeSelect.value) : DEFAULT_SIZE; + const parsedMines = minesSelect ? Number(minesSelect.value) : DEFAULT_MINES; + SIZE = Number.isInteger(parsedSize) ? Math.max(6, Math.min(parsedSize, 18)) : DEFAULT_SIZE; + MINES = Number.isInteger(parsedMines) ? Math.max(5, Math.min(parsedMines, SIZE * SIZE - 1)) : DEFAULT_MINES; + + // Crear matrices + board = Array.from({ length: SIZE }, () => Array(SIZE).fill(0)); + revealed = Array.from({ length: SIZE }, () => Array(SIZE).fill(false)); + flagged = Array.from({ length: SIZE }, () => Array(SIZE).fill(false)); + gameOver = false; + firstClickMade = false; + cellsRevealed = 0; + + // Contador de banderas y estado + updateFlagsLeft(); + setStatus('Haz clic izquierdo o Enter para revelar, clic derecho, F o Espacio para marcar banderas.'); + + // Ajustar grid dinámico + boardDiv.style.gridTemplateColumns = `repeat(${SIZE}, 1fr)`; + boardDiv.setAttribute('aria-disabled', 'false'); + + // Construir o reutilizar DOM del tablero + initBoardDom(); + render(); +} + +// Crear/reutilizar celdas del tablero con listeners accesibles +function initBoardDom() { + const totalCells = SIZE * SIZE; + // Si el número de hijos no coincide, reconstruimos + if (boardDiv.children.length !== totalCells) { + boardDiv.textContent = ''; + for (let r = 0; r < SIZE; r++) { + for (let c = 0; c < SIZE; 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}`); + // Listeners accesibles + cell.addEventListener('click', onLeftClick); + cell.addEventListener('contextmenu', onRightClick); + cell.addEventListener('keydown', (e) => { + // Enter o NumpadEnter para revelar + if (e.key === 'Enter') { + e.preventDefault(); + onLeftClick(e); + } + // Espacio o F para marcar + if (e.key === ' ' || e.key === 'Spacebar' || e.key.toLowerCase() === 'f') { + e.preventDefault(); + onRightClick(e); + } + }); + boardDiv.appendChild(cell); + } + } + } else { + // Reutilizar: limpiar clases y re-enganchar listeners + Array.from(boardDiv.children).forEach((cell) => { + cell.classList.remove('revealed', 'mine', 'flag'); + cell.textContent = ''; + cell.removeEventListener('click', onLeftClick); + cell.removeEventListener('contextmenu', onRightClick); + cell.addEventListener('click', onLeftClick); + cell.addEventListener('contextmenu', onRightClick); + }); + } +} + +// Renderiza las celdas según estado interno sin reconstruir DOM +function render() { + for (let r = 0; r < SIZE; r++) { + for (let c = 0; c < SIZE; c++) { + const idx = r * SIZE + c; + const cell = boardDiv.children[idx]; + if (!cell) continue; + + // Reset estilos base + cell.classList.remove('revealed', 'mine', 'flag'); + + if (revealed[r][c]) { + cell.classList.add('revealed'); + // contenido según valor + if (board[r][c] === 'M') { + cell.classList.add('mine'); + cell.textContent = '💣'; + } else if (board[r][c] > 0) { + cell.textContent = String(board[r][c]); + } else { + cell.textContent = ''; + } + cell.setAttribute('aria-disabled', 'true'); + } else if (flagged[r][c]) { + cell.classList.add('flag'); + cell.textContent = '🚩'; + cell.setAttribute('aria-disabled', 'false'); + } else { + cell.textContent = ''; + cell.setAttribute('aria-disabled', 'false'); + } } } } +// Colocar minas, evitando la celda del primer clic (primera jugada segura) +function placeMines(excludeR, excludeC) { + let placed = 0; + const used = new Set(); + while (placed < MINES) { + const r = Math.floor(Math.random() * SIZE); + const c = Math.floor(Math.random() * SIZE); + const key = r * SIZE + c; + if ((r === excludeR && c === excludeC) || used.has(key) || board[r][c] === 'M') continue; + board[r][c] = 'M'; + used.add(key); + placed++; + } +} + +// Calcula números adyacentes para cada celda function calculateNumbers() { for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { if (board[r][c] === 'M') continue; let count = 0; - for (let i = -1; i <= 1; i++) { - for (let j = -1; j <= 1; j++) { - let nr = r+i, nc = c+j; - if (nr >= 0 && nr < SIZE && nc >=0 && nc < SIZE && board[nr][nc] === 'M') count++; + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + const nr = r + dr, nc = c + dc; + if (inBounds(nr, nc) && board[nr][nc] === 'M') count++; } } board[r][c] = count; @@ -45,90 +183,111 @@ function calculateNumbers() { } } -function renderBoard() { - boardDiv.innerHTML = ''; - for (let r = 0; r < SIZE; r++) { - for (let c = 0; c < SIZE; c++) { - const cell = document.createElement('div'); - cell.classList.add('cell'); - if (revealed[r][c]) { - cell.classList.add('revealed'); - if (board[r][c] === 'M') { - cell.classList.add('mine'); - cell.textContent = '💣'; - } else if (board[r][c] > 0) { - cell.textContent = board[r][c]; - } else { - cell.textContent = ''; - } - } else if (flagged[r][c]) { - cell.classList.add('flag'); - cell.textContent = '🚩'; - } else { - cell.textContent = ''; - } - // Click izquierdo - cell.onmousedown = (e) => { - if (gameOver) return; - if (e.button === 0) revealCell(r, c); - else if (e.button === 2) toggleFlag(r, c); - }; - cell.oncontextmenu = (e) => e.preventDefault(); - boardDiv.appendChild(cell); - } - } -} - +// Revela una celda y expande si es 0 mediante BFS function revealCell(r, c) { + if (!inBounds(r, c)) return; if (revealed[r][c] || flagged[r][c]) return; + revealed[r][c] = true; cellsRevealed++; + if (board[r][c] === 'M') { endGame(false); - renderBoard(); + render(); return; - } else if (board[r][c] === 0) { - // Revela recursivo - for (let i = -1; i <= 1; i++) { - for (let j = -1; j <= 1; j++) { - let nr = r+i, nc = c+j; - if (nr >= 0 && nr < SIZE && nc >= 0 && nc < SIZE) { - if (!revealed[nr][nc]) revealCell(nr, nc); + } + + if (board[r][c] === 0) { + const queue = [[r, c]]; + while (queue.length) { + const [cr, cc] = queue.shift(); + for (let dr = -1; dr <= 1; dr++) { + for (let dc = -1; dc <= 1; dc++) { + const nr = cr + dr, nc = cc + dc; + if (inBounds(nr, nc) && !revealed[nr][nc] && !flagged[nr][nc]) { + revealed[nr][nc] = true; + cellsRevealed++; + if (board[nr][nc] === 0) queue.push([nr, nc]); + } } } } } + checkWin(); - renderBoard(); } +// Alterna bandera en una celda (si no está revelada) function toggleFlag(r, c) { + if (!inBounds(r, c)) return; if (revealed[r][c]) return; flagged[r][c] = !flagged[r][c]; - renderBoard(); + updateFlagsLeft(); } +// Comprueba victoria (todas las celdas no-mina están reveladas) function checkWin() { - if (cellsRevealed === SIZE*SIZE - MINES) { + if (cellsRevealed === SIZE * SIZE - MINES) { endGame(true); } } +// Finaliza el juego (muestra minas si pierdes) function endGame(won) { gameOver = true; - // Revela todas las minas si perdiste + boardDiv.setAttribute('aria-disabled', 'true'); + if (!won) { for (let r = 0; r < SIZE; r++) { for (let c = 0; c < SIZE; c++) { if (board[r][c] === 'M') revealed[r][c] = true; } } - statusDiv.textContent = "¡BOOM! Has perdido 💣"; + setStatus('¡BOOM! Has perdido 💣'); } else { - statusDiv.textContent = "¡Felicidades! Has ganado 🎉"; + setStatus('¡Felicidades! Has ganado 🎉'); } } -restartBtn.onclick = initGame; +// Listeners por celda +function onLeftClick(e) { + if (gameOver) return; + const target = e.currentTarget; + const r = Number(target.getAttribute('data-row')); + const c = Number(target.getAttribute('data-col')); + if (!Number.isInteger(r) || !Number.isInteger(c)) return; -initGame(); \ No newline at end of file + // Primera jugada segura: colocamos minas después del primer clic + if (!firstClickMade) { + placeMines(r, c); + calculateNumbers(); + firstClickMade = true; + } + + revealCell(r, c); + render(); +} + +function onRightClick(e) { + e.preventDefault(); + if (gameOver) return; + const target = e.currentTarget; + const r = Number(target.getAttribute('data-row')); + const c = Number(target.getAttribute('data-col')); + if (!Number.isInteger(r) || !Number.isInteger(c)) return; + + toggleFlag(r, c); + render(); +} + +// Eventos globales +restartBtn.addEventListener('click', startGame); +if (sizeSelect) { + sizeSelect.addEventListener('change', startGame); +} +if (minesSelect) { + minesSelect.addEventListener('change', startGame); +} + +// Init +startGame(); \ No newline at end of file diff --git a/buscaminas/styles.css b/buscaminas/styles.css index 61e98f3..e9585a6 100644 --- a/buscaminas/styles.css +++ b/buscaminas/styles.css @@ -146,4 +146,77 @@ h1 { } } -/* :::::::::::::::::::::::::::::: */ \ No newline at end of file +/* :::::::::::::::::::::::::::::: */ +/* Controles (tamaño/minas) y contador de banderas */ +#controls { + margin: 0.8rem auto 0.4rem auto; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; +} +#controls select { + font-size: 1em; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid #ddd; +} +#counters { + font-size: 1rem; + color: #333; + margin-bottom: 0.4rem; +} + +/* Accesibilidad: foco visible en celdas */ +.cell { + border: none; + outline: none; +} +.cell:focus-visible { + box-shadow: 0 0 0 4px rgba(215, 38, 61, 0.35); +} + +/* Estado deshabilitado para botón reinicio */ +#restart-btn:disabled { + opacity: 0.6; + cursor: not-allowed; + box-shadow: none; +} + +/* Modo oscuro mejorado */ +@media (prefers-color-scheme: dark) { + body { + background: #0f1222; + color: #eaeaf0; + } + #board { + background: #182a46; + box-shadow: 0 6px 24px rgba(61, 90, 254, 0.12); + border: 1px solid rgba(255,255,255,0.06); + } + .cell { + background: #20233a; + color: #eaeaf0; + box-shadow: 0 2px 10px rgba(61, 90, 254, 0.12); + } + .cell.revealed { background: #264b74; } + .cell.mine { background: #7c1f2a; color: #fff; } + .cell.flag { background: #8a6d28; color: #ffd54f; } + #status, #counters { + color: #a0a3b0; + } + #restart-btn { + background: #3d5afe; + } + #restart-btn:hover { + background: #0a2459; + } + #controls select { + background: #20233a; + color: #eaeaf0; + border-color: #2c3252; + } + .cell:focus-visible { + box-shadow: 0 0 0 4px rgba(61, 90, 254, 0.35); + } +} \ No newline at end of file diff --git a/index.html b/index.html index e0711e4..d4e28f7 100644 --- a/index.html +++ b/index.html @@ -6,14 +6,20 @@ FerMdez - Games - + + + + + + +