Mejoras y optimizaciones en general.
This commit is contained in:
@@ -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();
|
||||
// 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();
|
Reference in New Issue
Block a user