经典扫雷游戏实现:从零构建HTML5扫雷游戏

一、引言

扫雷是一款经典的单人益智游戏,起源于20世纪60年代,并在90年代随着Windows操作系统的普及而风靡全球。本文将详细介绍如何使用现代网页技术(HTML、CSS和JavaScript)从零开始构建一个功能完整的扫雷游戏。我们将涵盖游戏逻辑设计、用户界面实现以及性能优化等方面。


二、游戏概述

扫雷游戏的核心规则很简单:

  1. 游戏在一个方格棋盘上进行,某些随机方格中隐藏着"地雷"
  2. 玩家需要揭开所有不含地雷的方格
  3. 揭开方格后会显示周围8个方格中的地雷数量
  4. 玩家可以标记他们认为有地雷的方格
  5. 如果揭开一个地雷,游戏立即结束

三、技术实现

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游戏逻辑

游戏的核心逻辑包括:

  1. 游戏初始化
    • 创建棋盘数据结构
    • 随机放置地雷
    • 计算每个方格周围的地雷数量
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);
            }
        }
    }
}
  1. 游戏交互处理
    • 左键点击揭开方格
    • 右键点击标记/取消标记方格
    • 递归揭开空白区域
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 = '恭喜你赢了!';
    }
}
  1. 游戏状态管理
    • 计时器
    • 胜负判断
    • 游戏统计信息
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;
}

四、功能亮点

  1. 多种难度级别‌:提供简单、中等和困难三种难度选择
  2. 游戏统计‌:记录最佳时间和胜率
  3. 响应式设计‌:适配不同屏幕尺寸
  4. 本地存储‌:使用localStorage保存最佳成绩
  5. 视觉反馈‌:不同的数字使用不同颜色,提高可读性

五、技术细节解析

地雷生成算法

游戏使用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];
}

六、性能优化

  1. 事件委托‌:使用事件委托减少事件监听器数量
  2. CSS硬件加速‌:使用transform属性实现平滑动画
  3. 最小化重绘‌:只在必要时更新DOM
  4. 内存管理‌:合理使用数据结构减少内存占用

七、扩展功能建议

  1. 添加音效和动画效果
  2. 实现多人对战模式
  3. 添加成就系统
  4. 支持自定义棋盘大小和地雷数量
  5. 添加教程和新手引导

八、总结

通过本文的介绍,我们完整实现了一个功能丰富的扫雷游戏。这个项目涵盖了现代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>

九、彩蛋

藏在星桥鹊语中的相思

相关推荐
人机888号1 小时前
instanceof 的小秘密
javascript
轻语呢喃1 小时前
时间分片思想:多数据的前端处理方法
前端·javascript·面试
Spider_Man1 小时前
假装渲染十万条,虚拟列表的障眼法你学会了吗?
前端·javascript·react.js
334554321 小时前
vue实现表格轮播
javascript·vue.js·ecmascript
诗书画唱3 小时前
【前端教程】从零开始学JavaScript交互:7个经典事件处理案例解析
前端·javascript·交互
LikM3 小时前
Reflect ES6 新增的内置对象
前端·javascript
艾小码3 小时前
还在被JavaScript数据类型搞糊涂?一篇文章帮你彻底搞懂!
前端·javascript
码力无边_OEC3 小时前
第五章:组件间的“密语”—— Messaging 通信机制
前端·javascript
子兮曰3 小时前
🌌 七夕故事之从牛郎织女到JavaScript:10个编程哲学思考
javascript
砂糖橘加盐3 小时前
vapor 性能测试和基本原理浅析
前端·javascript·vue.js