// Game configuration const difficulties = { easy: { rows: 10, cols: 10, mines: 10, bestTimeKey: 'minesweeperBestTimeEasy' }, medium: { rows: 16, cols: 16, mines: 40, bestTimeKey: 'minesweeperBestTimeMedium' }, hard: { rows: 20, cols: 24, mines: 99, bestTimeKey: 'minesweeperBestTimeHard' } }; // Game state let gameBoard = []; let boardSize = { rows: 0, cols: 0 }; let mineCount = 0; let flagCount = 0; let revealedCount = 0; let isGameOver = false; let isVictory = false; let timerInterval = null; let gameTime = 0; let firstClick = true; let currentDifficulty = 'easy'; // DOM elements const gameBoardElement = document.getElementById('gameBoard'); const minesValueElement = document.getElementById('minesValue'); const flagsValueElement = document.getElementById('flagsValue'); const timeValueElement = document.getElementById('timeValue'); const bestTimeValueElement = document.getElementById('bestTimeValue'); const restartButton = document.getElementById('restartButton'); const startButton = document.getElementById('startButton'); const playAgainButton = document.getElementById('playAgainButton'); const newGameButton = document.getElementById('newGameButton'); const startScreen = document.getElementById('startScreen'); const gameOverScreen = document.getElementById('gameOverScreen'); const victoryScreen = document.getElementById('victoryScreen'); const finalTimeElement = document.getElementById('finalTime'); const difficultyButtons = document.querySelectorAll('.difficulty-btn'); // Event listeners startButton.addEventListener('click', () => { startScreen.style.display = 'none'; initializeGame(); }); restartButton.addEventListener('click', initializeGame); playAgainButton.addEventListener('click', () => { gameOverScreen.style.display = 'none'; initializeGame(); }); newGameButton.addEventListener('click', () => { victoryScreen.style.display = 'none'; initializeGame(); }); difficultyButtons.forEach(button => { button.addEventListener('click', (e) => { // Remove active class from all buttons difficultyButtons.forEach(btn => btn.classList.remove('active')); // Add active class to clicked button e.target.classList.add('active'); // Set current difficulty currentDifficulty = e.target.getAttribute('data-difficulty'); // Update best time display updateBestTimeDisplay(); // Restart game if already playing if (!startScreen.style.display || startScreen.style.display === 'none') { initializeGame(); } }); }); // Initialize the game function initializeGame() { // Get current difficulty settings const config = difficulties[currentDifficulty]; boardSize.rows = config.rows; boardSize.cols = config.cols; mineCount = config.mines; // Reset game state gameBoard = []; flagCount = 0; revealedCount = 0; isGameOver = false; isVictory = false; firstClick = true; gameTime = 0; // Update UI minesValueElement.textContent = mineCount; flagsValueElement.textContent = flagCount; timeValueElement.textContent = 0; updateBestTimeDisplay(); // Clear timer if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } // Create empty board createEmptyBoard(); // Render the board renderBoard(); } // Create empty board function createEmptyBoard() { gameBoard = []; // Create 2D array for the board for (let row = 0; row < boardSize.rows; row++) { const rowData = []; for (let col = 0; col < boardSize.cols; col++) { rowData.push({ row, col, isMine: false, isRevealed: false, isFlagged: false, neighborMines: 0 }); } gameBoard.push(rowData); } // Configure board size in CSS grid gameBoardElement.style.gridTemplateRows = `repeat(${boardSize.rows}, 30px)`; gameBoardElement.style.gridTemplateColumns = `repeat(${boardSize.cols}, 30px)`; } // Generate mines (called after first click) function generateMines(safeRow, safeCol) { let minesPlaced = 0; // Safe area around first click (3x3 grid) const safeArea = []; for (let r = Math.max(0, safeRow - 1); r <= Math.min(boardSize.rows - 1, safeRow + 1); r++) { for (let c = Math.max(0, safeCol - 1); c <= Math.min(boardSize.cols - 1, safeCol + 1); c++) { safeArea.push(`${r},${c}`); } } // Place mines randomly while (minesPlaced < mineCount) { const row = Math.floor(Math.random() * boardSize.rows); const col = Math.floor(Math.random() * boardSize.cols); // Skip if this is a safe area or already has a mine if (safeArea.includes(`${row},${col}`) || gameBoard[row][col].isMine) { continue; } // Place mine gameBoard[row][col].isMine = true; minesPlaced++; } // Calculate neighbor mine counts calculateNeighborMines(); } // Calculate number of adjacent mines for each cell function calculateNeighborMines() { for (let row = 0; row < boardSize.rows; row++) { for (let col = 0; col < boardSize.cols; col++) { if (gameBoard[row][col].isMine) continue; let count = 0; // Check all 8 adjacent cells for (let r = Math.max(0, row - 1); r <= Math.min(boardSize.rows - 1, row + 1); r++) { for (let c = Math.max(0, col - 1); c <= Math.min(boardSize.cols - 1, col + 1); c++) { if (r === row && c === col) continue; if (gameBoard[r][c].isMine) count++; } } gameBoard[row][col].neighborMines = count; } } } // Render the game board function renderBoard() { // Clear existing board gameBoardElement.innerHTML = ''; // Create and append cells for (let row = 0; row < boardSize.rows; row++) { for (let col = 0; col < boardSize.cols; col++) { const cell = gameBoard[row][col]; const cellElement = document.createElement('div'); cellElement.className = 'cell cell-covered'; cellElement.setAttribute('data-row', row); cellElement.setAttribute('data-col', col); // Add event listeners cellElement.addEventListener('click', handleCellClick); cellElement.addEventListener('contextmenu', handleCellRightClick); cellElement.addEventListener('dblclick', handleCellDoubleClick); // Update cell appearance based on state updateCellAppearance(cell, cellElement); // Append to board gameBoardElement.appendChild(cellElement); } } } // Update single cell appearance function updateCellAppearance(cell, cellElement) { if (!cellElement) { cellElement = gameBoardElement.querySelector(`[data-row="${cell.row}"][data-col="${cell.col}"]`); } if (!cellElement) return; // Reset classes cellElement.className = 'cell'; cellElement.textContent = ''; if (cell.isRevealed) { cellElement.classList.add('cell-revealed'); if (cell.isMine) { cellElement.classList.add('cell-mine'); } else if (cell.neighborMines > 0) { cellElement.textContent = cell.neighborMines; cellElement.classList.add(`number-${cell.neighborMines}`); } } else { cellElement.classList.add('cell-covered'); if (cell.isFlagged) { cellElement.classList.add('cell-flagged'); } } } // Handle left click on cell function handleCellClick(e) { if (isGameOver) return; const row = parseInt(e.target.getAttribute('data-row')); const col = parseInt(e.target.getAttribute('data-col')); const cell = gameBoard[row][col]; // If first click, generate mines after ensuring this cell is safe if (firstClick) { firstClick = false; generateMines(row, col); startTimer(); } // Don't reveal flagged cells if (cell.isFlagged) return; // Reveal the cell revealCell(row, col); // Check for game over or victory checkGameStatus(); } // Handle right click on cell (flag) function handleCellRightClick(e) { e.preventDefault(); if (isGameOver || firstClick) return; const row = parseInt(e.target.getAttribute('data-row')); const col = parseInt(e.target.getAttribute('data-col')); const cell = gameBoard[row][col]; // Can't flag revealed cells if (cell.isRevealed) return; // Toggle flag toggleFlag(row, col); // Check for victory checkGameStatus(); } // Handle double click (quick reveal) function handleCellDoubleClick(e) { if (isGameOver || firstClick) return; const row = parseInt(e.target.getAttribute('data-row')); const col = parseInt(e.target.getAttribute('data-col')); const cell = gameBoard[row][col]; // Only work on revealed cells with numbers if (!cell.isRevealed || cell.neighborMines === 0) return; // Count flags around cell let flagsAround = 0; for (let r = Math.max(0, row - 1); r <= Math.min(boardSize.rows - 1, row + 1); r++) { for (let c = Math.max(0, col - 1); c <= Math.min(boardSize.cols - 1, col + 1); c++) { if (r === row && c === col) continue; if (gameBoard[r][c].isFlagged) flagsAround++; } } // Quick reveal only works if flags count matches the number if (flagsAround === cell.neighborMines) { // Reveal all non-flagged cells around for (let r = Math.max(0, row - 1); r <= Math.min(boardSize.rows - 1, row + 1); r++) { for (let c = Math.max(0, col - 1); c <= Math.min(boardSize.cols - 1, col + 1); c++) { if (r === row && c === col) continue; if (!gameBoard[r][c].isFlagged && !gameBoard[r][c].isRevealed) { revealCell(r, c); } } } // Check for game over or victory checkGameStatus(); } } // Toggle flag on cell function toggleFlag(row, col) { const cell = gameBoard[row][col]; // Toggle flag state cell.isFlagged = !cell.isFlagged; // Update flag count if (cell.isFlagged) { flagCount++; } else { flagCount--; } // Update UI flagsValueElement.textContent = flagCount; updateCellAppearance(cell); } // Reveal cell and its neighbors if it's a zero function revealCell(row, col) { const cell = gameBoard[row][col]; // Skip if already revealed or flagged if (cell.isRevealed || cell.isFlagged) return; // Reveal the cell cell.isRevealed = true; revealedCount++; // Update appearance updateCellAppearance(cell); // If it's a mine, game over if (cell.isMine) { gameOver(false); return; } // If it's a zero, cascade reveal neighbors if (cell.neighborMines === 0) { for (let r = Math.max(0, row - 1); r <= Math.min(boardSize.rows - 1, row + 1); r++) { for (let c = Math.max(0, col - 1); c <= Math.min(boardSize.cols - 1, col + 1); c++) { if (r === row && c === col) continue; revealCell(r, c); } } } } // Check if the game is over (victory or defeat) function checkGameStatus() { // Calculate total non-mine cells const totalNonMineCells = (boardSize.rows * boardSize.cols) - mineCount; // Victory if all non-mine cells are revealed if (revealedCount === totalNonMineCells) { gameOver(true); } } // Handle game over function gameOver(victory) { isGameOver = true; isVictory = victory; // Stop timer if (timerInterval) { clearInterval(timerInterval); timerInterval = null; } // Reveal all mines for (let row = 0; row < boardSize.rows; row++) { for (let col = 0; col < boardSize.cols; col++) { const cell = gameBoard[row][col]; if (cell.isMine && !cell.isFlagged) { cell.isRevealed = true; updateCellAppearance(cell); } // Highlight incorrectly flagged cells if (!cell.isMine && cell.isFlagged) { cell.isRevealed = true; updateCellAppearance(cell); const cellElement = gameBoardElement.querySelector(`[data-row="${row}"][data-col="${col}"]`); if (cellElement) { cellElement.style.backgroundColor = '#ff9800'; } } } } // Check if this is a new best time for the current difficulty if (victory) { updateBestTime(); // Show victory screen finalTimeElement.textContent = `Your time: ${gameTime} seconds`; victoryScreen.style.display = 'flex'; } else { // Show game over screen gameOverScreen.style.display = 'flex'; } } // Start the timer function startTimer() { gameTime = 0; timeValueElement.textContent = gameTime; timerInterval = setInterval(() => { gameTime++; timeValueElement.textContent = gameTime; }, 1000); } // Update best time if current time is better function updateBestTime() { const config = difficulties[currentDifficulty]; const bestTime = parseInt(localStorage.getItem(config.bestTimeKey)) || Infinity; if (gameTime < bestTime) { localStorage.setItem(config.bestTimeKey, gameTime); updateBestTimeDisplay(); } } // Update best time display function updateBestTimeDisplay() { const config = difficulties[currentDifficulty]; const bestTime = localStorage.getItem(config.bestTimeKey); if (bestTime) { bestTimeValueElement.textContent = bestTime; } else { bestTimeValueElement.textContent = '-'; } } // Prevent context menu on right-click gameBoardElement.addEventListener('contextmenu', (e) => { e.preventDefault(); }); // Initialize the game when the page loads document.addEventListener('DOMContentLoaded', () => { initializeGame(); });