经典扫雷游戏实现:从零构建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>

九、彩蛋

藏在星桥鹊语中的相思

相关推荐
十一吖i1 小时前
vue3表格显示隐藏列全屏拖动功能
前端·javascript·vue.js
冰暮流星2 小时前
css之线性渐变
前端·css
徐同保2 小时前
tailwindcss暗色主题切换
开发语言·前端·javascript
生莫甲鲁浪戴3 小时前
Android Studio新手开发第二十七天
前端·javascript·android studio
细节控菜鸡5 小时前
【2025最新】ArcGIS for JS 实现随着时间变化而变化的热力图
开发语言·javascript·arcgis
拉不动的猪6 小时前
h5后台切换检测利用visibilitychange的缺点分析
前端·javascript·面试
桃子不吃李子6 小时前
nextTick的使用
前端·javascript·vue.js
冰暮流星7 小时前
css3新增背景图片样式
前端·css·css3
Devil枫8 小时前
HarmonyOS鸿蒙应用:仓颉语言与JavaScript核心差异深度解析
开发语言·javascript·ecmascript
惺忪97988 小时前
回调函数的概念
开发语言·前端·javascript