'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'); const sizeSelect = document.getElementById('size'); const minesSelect = document.getElementById('mines'); const flagsLeftSpan = document.getElementById('flags-left'); // 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; } // 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 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; } } } // 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); render(); return; } 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(); } // 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]; updateFlagsLeft(); } // Comprueba victoria (todas las celdas no-mina están reveladas) function checkWin() { if (cellsRevealed === SIZE * SIZE - MINES) { endGame(true); } } // Finaliza el juego (muestra minas si pierdes) function endGame(won) { gameOver = true; 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; } } setStatus('¡BOOM! Has perdido 💣'); } else { setStatus('¡Felicidades! Has ganado 🎉'); } } // 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; // 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();