经典扫雷游戏实现:从零构建HTML5扫雷游戏
一、引言
扫雷是一款经典的单人益智游戏,起源于20世纪60年代,并在90年代随着Windows操作系统的普及而风靡全球。本文将详细介绍如何使用现代网页技术(HTML、CSS和JavaScript)从零开始构建一个功能完整的扫雷游戏。我们将涵盖游戏逻辑设计、用户界面实现以及性能优化等方面。
二、游戏概述
扫雷游戏的核心规则很简单:
- 游戏在一个方格棋盘上进行,某些随机方格中隐藏着"地雷"
- 玩家需要揭开所有不含地雷的方格
- 揭开方格后会显示周围8个方格中的地雷数量
- 玩家可以标记他们认为有地雷的方格
- 如果揭开一个地雷,游戏立即结束
三、技术实现
HTML结构
游戏的基本HTML结构包括:
- 游戏标题和难度选择按钮
- 游戏信息显示区域(剩余地雷数、计时器、重置按钮)
- 游戏棋盘
- 游戏状态和统计信息显示
<div class="game-container"><header><h1><i class="fas fa-bomb"></i> 经典扫雷</h1><div class="game-controls"><div class="difficulty-selector"><button class="active" data-level="easy">简单</button><button data-level="medium">中等</button><button data-level="hard">困难</button></div><div class="game-info"><span class="flags"><i class="fas fa-flag"></i> <span id="flag-count">10</span></span><button id="reset-btn"><i class="fas fa-redo"></i></button><span class="timer"><i class="fas fa-clock"></i> <span id="time">0</span></span></div></div></header><div class="game-board" id="game-board"></div><div class="game-status"><div id="message"></div><div class="game-stats"><div>最佳时间: <span id="best-time">-</span>秒</div><div>当前胜率: <span id="win-rate">0%</span></div></div></div>
</div>
JavaScript游戏逻辑
游戏的核心逻辑包括:
- 游戏初始化
- 创建棋盘数据结构
- 随机放置地雷
- 计算每个方格周围的地雷数量
function initGame() {// 初始化游戏状态boardData = [];// 创建游戏板数据for (let i = 0; i < boardSize * boardSize; i++) {boardData.push({isMine: false,isRevealed: false,isFlagged: false,neighborMines: 0});}// 放置地雷let minesPlaced = 0;while (minesPlaced < mineCount) {const randomIndex = Math.floor(Math.random() * boardSize * boardSize);if (!boardData[randomIndex].isMine) {boardData[randomIndex].isMine = true;minesPlaced++;}}// 计算每个格子周围的地雷数for (let i = 0; i < boardSize; i++) {for (let j = 0; j < boardSize; j++) {const index = i * boardSize + j;if (!boardData[index].isMine) {boardData[index].neighborMines = countAdjacentMines(i, j);}}}
}
- 游戏交互处理
- 左键点击揭开方格
- 右键点击标记/取消标记方格
- 递归揭开空白区域
function handleCellClick(row, col) {if (gameOver) return;const index = row * boardSize + col;const cell = board.children[index];const cellData = boardData[index];if (cellData.isRevealed || cellData.isFlagged) return;if (cellData.isMine) {// 点到地雷,游戏结束gameOver = true;revealAllMines();messageElement.textContent = '游戏结束!';return;}revealCell(row, col);// 检查是否获胜if (revealedCount === boardSize * boardSize - mineCount) {gameOver = true;messageElement.textContent = '恭喜你赢了!';}
}
- 游戏状态管理
- 计时器
- 胜负判断
- 游戏统计信息
function startTimer() {clearInterval(timerInterval);timerInterval = setInterval(() => {timer++;timerElement.textContent = timer;}, 1000);
}function updateStats() {bestTimeElement.textContent = bestTime === Infinity ? '-' : bestTime;winRateElement.textContent = gamesPlayed > 0 ? `${Math.round((gamesWon / gamesPlayed) * 100)}%` : '0%';
}
CSS样式设计
游戏的视觉设计采用现代、简洁的风格,使用CSS Grid布局实现响应式棋盘:
.game-board {display: grid;grid-template-columns: repeat(10, 1fr);gap: 3px;background: #bdc3c7;padding: 5px;border-radius: 5px;
}.cell {aspect-ratio: 1/1;display: flex;justify-content: center;align-items: center;background: #ecf0f1;border-radius: 3px;cursor: pointer;
}.cell.revealed {background: #d5dbdb;
}.cell.flagged {background: #f9e79f;
}.cell.mine {background: #e74c3c;color: white;
}
四、功能亮点
- 多种难度级别:提供简单、中等和困难三种难度选择
- 游戏统计:记录最佳时间和胜率
- 响应式设计:适配不同屏幕尺寸
- 本地存储:使用localStorage保存最佳成绩
- 视觉反馈:不同的数字使用不同颜色,提高可读性
五、技术细节解析
地雷生成算法
游戏使用Fisher-Yates洗牌算法的简化版本来随机放置地雷:
let minesPlaced = 0;
while (minesPlaced < mineCount) {const randomIndex = Math.floor(Math.random() * boardSize * boardSize);if (!boardData[randomIndex].isMine) {boardData[randomIndex].isMine = true;minesPlaced++;}
}
递归揭开空白区域
当玩家点击一个周围没有地雷的方格时,游戏会自动递归揭开所有相邻的空白方格:
function revealCell(row, col) {// ...if (cellData.neighborMines > 0) {cell.textContent = cellData.neighborMines;cell.style.color = getNumberColor(cellData.neighborMines);} else {// 递归揭示周围的格子for (let i = Math.max(0, row - 1); i <= Math.min(boardSize - 1, row + 1); i++) {for (let j = Math.max(0, col - 1); j <= Math.min(boardSize - 1, col + 1); j++) {if (i === row && j === col) continue;revealCell(i, j);}}}
}
数字颜色编码
为了提升游戏体验,不同数字使用不同颜色表示:
function getNumberColor(num) {const colors = ['', // 0'#1976D2', // 1'#388E3C', // 2'#D32F2F', // 3'#7B1FA2', // 4'#FF8F00', // 5'#0097A7', // 6'#5D4037', // 7'#616161' // 8];return colors[num];
}
六、性能优化
- 事件委托:使用事件委托减少事件监听器数量
- CSS硬件加速:使用transform属性实现平滑动画
- 最小化重绘:只在必要时更新DOM
- 内存管理:合理使用数据结构减少内存占用
七、扩展功能建议
- 添加音效和动画效果
- 实现多人对战模式
- 添加成就系统
- 支持自定义棋盘大小和地雷数量
- 添加教程和新手引导
八、总结
通过本文的介绍,我们完整实现了一个功能丰富的扫雷游戏。这个项目涵盖了现代Web开发的多个重要方面,包括:
- DOM操作和事件处理
- 游戏状态管理
- 递归算法应用
- 响应式设计
- 本地存储使用
这个扫雷游戏不仅具有娱乐性,同时也是学习JavaScript和前端开发的优秀示例项目。读者可以在此基础上进一步扩展功能,或者优化现有实现。
完整代码:
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>经典扫雷</title><link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
</head><body><div class="game-container"><header><h1><i class="fas fa-bomb"></i> 经典扫雷</h1><div class="game-controls"><div class="difficulty-selector"><button class="active" data-level="easy">简单</button><button data-level="medium">中等</button><button data-level="hard">困难</button></div><div class="game-info"><span class="flags"><i class="fas fa-flag"></i> <span id="flag-count">10</span></span><button id="reset-btn"><i class="fas fa-redo"></i></button><span class="timer"><i class="fas fa-clock"></i> <span id="time">0</span></span></div></div></header><div class="game-board" id="game-board"></div><div class="game-status"><div id="message"></div><div class="game-stats"><div>最佳时间: <span id="best-time">-</span>秒</div><div>当前胜率: <span id="win-rate">0%</span></div></div></div></div><script>document.addEventListener('DOMContentLoaded', () => {const board = document.getElementById('game-board');const flagCountElement = document.getElementById('flag-count');const timerElement = document.getElementById('time');const messageElement = document.getElementById('message');const resetButton = document.getElementById('reset-btn');const difficultyButtons = document.querySelectorAll('.difficulty-selector button');const bestTimeElement = document.getElementById('best-time');const winRateElement = document.getElementById('win-rate');let boardSize = 10;let mineCount = 10;let boardData = [];let revealedCount = 0;let flagCount = 0;let gameOver = false;let timer = 0;let timerInterval = null;let gamesPlayed = 0;let gamesWon = 0;let bestTime = localStorage.getItem('minesweeperBestTime') || Infinity;// 初始化游戏function initGame() {clearInterval(timerInterval);timer = 0;timerElement.textContent = timer;revealedCount = 0;flagCount = 0;flagCountElement.textContent = mineCount;gameOver = false;messageElement.textContent = '';board.innerHTML = '';boardData = [];// 创建游戏板数据for (let i = 0; i < boardSize * boardSize; i++) {boardData.push({isMine: false,isRevealed: false,isFlagged: false,neighborMines: 0});}// 放置地雷let minesPlaced = 0;while (minesPlaced < mineCount) {const randomIndex = Math.floor(Math.random() * boardSize * boardSize);if (!boardData[randomIndex].isMine) {boardData[randomIndex].isMine = true;minesPlaced++;}}// 计算每个格子周围的地雷数for (let i = 0; i < boardSize; i++) {for (let j = 0; j < boardSize; j++) {const index = i * boardSize + j;if (!boardData[index].isMine) {boardData[index].neighborMines = countAdjacentMines(i, j);}}}// 创建游戏板UIfor (let i = 0; i < boardSize; i++) {for (let j = 0; j < boardSize; j++) {const index = i * boardSize + j;const cell = document.createElement('div');cell.className = 'cell';cell.dataset.row = i;cell.dataset.col = j;cell.addEventListener('click', () => handleCellClick(i, j));cell.addEventListener('contextmenu', (e) => {e.preventDefault();handleRightClick(i, j);});board.appendChild(cell);}}// 调整游戏板大小board.style.gridTemplateColumns = `repeat(${boardSize}, 1fr)`;}// 计算相邻地雷数function countAdjacentMines(row, col) {let count = 0;for (let i = Math.max(0, row - 1); i <= Math.min(boardSize - 1, row + 1); i++) {for (let j = Math.max(0, col - 1); j <= Math.min(boardSize - 1, col + 1); j++) {if (i === row && j === col) continue;const index = i * boardSize + j;if (boardData[index].isMine) count++;}}return count;}// 处理格子点击function handleCellClick(row, col) {if (gameOver) return;const index = row * boardSize + col;const cell = board.children[index];const cellData = boardData[index];// 开始游戏时启动计时器if (revealedCount === 0 && !cellData.isFlagged) {startTimer();}if (cellData.isRevealed || cellData.isFlagged) return;if (cellData.isMine) {// 点到地雷,游戏结束gameOver = true;revealAllMines();cell.classList.add('mine');messageElement.textContent = '游戏结束!';clearInterval(timerInterval);gamesPlayed++;updateStats();return;}revealCell(row, col);// 检查是否获胜if (revealedCount === boardSize * boardSize - mineCount) {gameOver = true;messageElement.textContent = '恭喜你赢了!';clearInterval(timerInterval);gamesWon++;if (timer < bestTime) {bestTime = timer;localStorage.setItem('minesweeperBestTime', bestTime);bestTimeElement.textContent = bestTime;}updateStats();}}// 处理右键点击(插旗)function handleRightClick(row, col) {if (gameOver) return;const index = row * boardSize + col;const cell = board.children[index];const cellData = boardData[index];if (cellData.isRevealed) return;if (cellData.isFlagged) {// 取消旗子cellData.isFlagged = false;cell.classList.remove('flagged');flagCount--;} else {// 插旗cellData.isFlagged = true;cell.classList.add('flagged');flagCount++;}flagCountElement.textContent = mineCount - flagCount;}// 揭示格子function revealCell(row, col) {const index = row * boardSize + col;const cell = board.children[index];const cellData = boardData[index];if (cellData.isRevealed || cellData.isFlagged) return;cellData.isRevealed = true;cell.classList.add('revealed');revealedCount++;if (cellData.neighborMines > 0) {cell.textContent = cellData.neighborMines;cell.style.color = getNumberColor(cellData.neighborMines);} else {// 如果是空白格子,递归揭示周围的格子for (let i = Math.max(0, row - 1); i <= Math.min(boardSize - 1, row + 1); i++) {for (let j = Math.max(0, col - 1); j <= Math.min(boardSize - 1, col + 1); j++) {if (i === row && j === col) continue;revealCell(i, j);}}}}// 获取数字颜色function getNumberColor(num) {const colors = ['', // 0'#1976D2', // 1'#388E3C', // 2'#D32F2F', // 3'#7B1FA2', // 4'#FF8F00', // 5'#0097A7', // 6'#5D4037', // 7'#616161' // 8];return colors[num];}// 揭示所有地雷function revealAllMines() {for (let i = 0; i < boardSize; i++) {for (let j = 0; j < boardSize; j++) {const index = i * boardSize + j;if (boardData[index].isMine) {const cell = board.children[index];cell.classList.add('mine');cell.innerHTML = '<i class="fas fa-bomb"></i>';}}}}// 开始计时器function startTimer() {clearInterval(timerInterval);timerInterval = setInterval(() => {timer++;timerElement.textContent = timer;}, 1000);}// 更新统计信息function updateStats() {bestTimeElement.textContent = bestTime === Infinity ? '-' : bestTime;winRateElement.textContent = gamesPlayed > 0 ? `${Math.round((gamesWon / gamesPlayed) * 100)}%` : '0%';}// 设置难度级别function setDifficulty(level) {difficultyButtons.forEach(btn => btn.classList.remove('active'));event.target.classList.add('active');switch (level) {case 'easy':boardSize = 10;mineCount = 10;break;case 'medium':boardSize = 16;mineCount = 40;break;case 'hard':boardSize = 20;mineCount = 80;break;}initGame();}// 事件监听resetButton.addEventListener('click', initGame);difficultyButtons.forEach(btn => {btn.addEventListener('click', () => setDifficulty(btn.dataset.level));});// 初始化游戏initGame();updateStats();});</script>
</body>
<style>body {font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);margin: 0;padding: 20px;min-height: 100vh;display: flex;justify-content: center;align-items: center;}.game-container {background: white;border-radius: 15px;box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);padding: 25px;width: 100%;max-width: 800px;}header h1 {color: #2c3e50;text-align: center;margin-bottom: 20px;}.game-controls {display: flex;justify-content: space-between;align-items: center;margin-bottom: 20px;flex-wrap: wrap;gap: 15px;}.difficulty-selector button {padding: 8px 15px;border: none;border-radius: 5px;background: #ecf0f1;cursor: pointer;transition: all 0.3s;}.difficulty-selector button.active {background: #3498db;color: white;}.game-info {display: flex;align-items: center;gap: 15px;}#reset-btn {background: #e74c3c;color: white;border: none;width: 40px;height: 40px;border-radius: 50%;cursor: pointer;transition: all 0.3s;}#reset-btn:hover {transform: rotate(360deg);}.flags,.timer {font-weight: bold;color: #2c3e50;}.game-board {display: grid;grid-template-columns: repeat(10, 1fr);gap: 3px;margin: 0 auto;background: #bdc3c7;padding: 5px;border-radius: 5px;}.cell {aspect-ratio: 1/1;display: flex;justify-content: center;align-items: center;background: #ecf0f1;border-radius: 3px;cursor: pointer;font-weight: bold;user-select: none;transition: all 0.2s;}.cell:hover {background: #d6eaf8;}.cell.revealed {background: #d5dbdb;}.cell.flagged {background: #f9e79f;}.cell.mine {background: #e74c3c;color: white;}.game-status {margin-top: 20px;text-align: center;}#message {font-size: 1.2em;font-weight: bold;min-height: 24px;margin-bottom: 10px;}.game-stats {display: flex;justify-content: center;gap: 20px;color: #7f8c8d;}@media (max-width: 600px) {.game-board {grid-template-columns: repeat(8, 1fr);}}
</style></html>