一、引言
扫雷是一款经典的单人益智游戏,起源于20世纪60年代,并在90年代随着Windows操作系统的普及而风靡全球。本文将详细介绍如何使用现代网页技术(HTML、CSS和JavaScript)从零开始构建一个功能完整的扫雷游戏。我们将涵盖游戏逻辑设计、用户界面实现以及性能优化等方面。
二、游戏概述
扫雷游戏的核心规则很简单:
- 游戏在一个方格棋盘上进行,某些随机方格中隐藏着"地雷"
- 玩家需要揭开所有不含地雷的方格
- 揭开方格后会显示周围8个方格中的地雷数量
- 玩家可以标记他们认为有地雷的方格
- 如果揭开一个地雷,游戏立即结束

三、技术实现
HTML结构
游戏的基本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游戏逻辑
游戏的核心逻辑包括:
- 游戏初始化
- 创建棋盘数据结构
- 随机放置地雷
- 计算每个方格周围的地雷数量
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);
}
}
}
}
- 游戏交互处理
- 左键点击揭开方格
- 右键点击标记/取消标记方格
- 递归揭开空白区域
javascript
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 = '恭喜你赢了!';
}
}
- 游戏状态管理
- 计时器
- 胜负判断
- 游戏统计信息
javascript
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布局实现响应式棋盘:
css
.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洗牌算法的简化版本来随机放置地雷:
javascript
let minesPlaced = 0;
while (minesPlaced < mineCount) {
const randomIndex = Math.floor(Math.random() * boardSize * boardSize);
if (!boardData[randomIndex].isMine) {
boardData[randomIndex].isMine = true;
minesPlaced++;
}
}
递归揭开空白区域
当玩家点击一个周围没有地雷的方格时,游戏会自动递归揭开所有相邻的空白方格:
javascript
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);
}
}
}
}
数字颜色编码
为了提升游戏体验,不同数字使用不同颜色表示:
javascript
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和前端开发的优秀示例项目。读者可以在此基础上进一步扩展功能,或者优化现有实现。
完整代码:
html
<!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);
}
}
}
// 创建游戏板UI
for (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>
九、彩蛋
藏在星桥鹊语中的相思
