使用HTML、CSS和JavaScript实现的经典扫雷游戏

扫雷游戏

一个使用纯HTML、CSS和JavaScript实现的经典扫雷游戏。

游戏特点

  • 🎮 经典玩法:与Windows经典扫雷游戏相同的规则和体验
  • 🎚️ 多级难度:提供初级(12x12)、中级(16x16)和高级(19x19)三种难度选择
  • 🎯 第一次点击安全:首次点击永远不会触雷
  • 🚩 标记功能:右键点击可标记可疑的地雷位置
  • 🔢 数字提示:显示周围地雷数量的彩色数字
  • 📱 响应式设计:适配不同屏幕尺寸,包括移动设备

在线体验

🎮 点击这里在线体验游戏

游戏截图

扫雷游戏截图

安装与运行

喜欢的话,可以去我的github上Fork一下
扫雷github仓库

使用git复制下面代码即可下载到本地

git clone https://github.com/Forminio/minesweeper-game.git

技术实现详解

1. 游戏核心数据结构

游戏使用两个二维数组作为核心数据结构:

  • mineBoard :记录每个格子是地雷(‘M’)还是数字(0-8)
  • showBoard :记录每个格子的显示状态(‘hidden’、‘revealed’、‘flag’、‘mine’)

2. 地雷生成算法

关键实现点:

  • 使用随机数生成地雷位置
  • 确保首次点击位置及其周围8个格子不会有地雷
  • 地雷放置完成后,计算每个非地雷格子周围的地雷数量

3. 自动展开算法(递归实现)

关键实现点:

  • 使用递归算法自动展开空白区域
  • 当点击到数字0(周围无雷)的格子时,自动展开周围的8个格子
  • 递归继续展开,直到遇到数字格子为止
  • 边界检查确保不会越界

4. 胜利检测算法

关键实现点:

  • 计算已揭示的格子数量
  • 当已揭示格子数量等于总格子数减去地雷数时,判定为胜利
  • 胜利时自动标记所有未标记的地雷

5. 渲染与DOM操作

关键实现点:

  • 使用CSS Grid布局实现游戏网格
  • 动态调整网格大小以适应不同难度
  • 为每个单元格添加点击和右键点击事件
  • 根据单元格状态设置不同的样式和内容

6. 响应式设计

关键实现点:

  • 使用媒体查询适配不同屏幕尺寸
  • 在小屏幕设备上调整按钮布局和单元格大小
  • 确保游戏在移动设备上也能良好运行

7. 健壮性与错误处理

关键实现点:

  • 添加多种事件监听确保游戏在各种情况下都能正常运行
  • 定期检查游戏状态,自动修复可能的问题
  • 处理页面可见性变化、缓存加载等特殊情况

设计亮点

  • 经典Windows风格UI :使用灰色背景、凸起和凹陷的边框,重现经典Windows扫雷游戏的视觉风格
  • 彩色数字提示 :不同数字使用不同颜色,提高游戏可读性
  • 游戏状态反馈 :通过模态窗口提供游戏结束反馈,显示游戏结果和用时
  • 自适应布局 :游戏界面能够适应不同屏幕尺寸,在桌面和移动设备上都有良好体验

以下是完整实现代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript扫雷</title>
<style>
    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }
    
    /* 创建隐形盒子撑开页面 */
    .page-container {
        width: 100%;
        min-height: 100vh;
        display: flex;
        justify-content: center;
        align-items: center;
        padding: 20px;
        background-color: transparent;
    }
    
    body {
        font-family: 'Microsoft YaHei', Arial, sans-serif;
        background-color: transparent;
        color: #333;
        line-height: 1.6;
        margin: 0;
        padding: 0;
        width: 100%;
        min-height: 100vh;
    }
    
    .game-container {
        width: 100%;
        max-width: 1200px; /* 与编译器容器宽度一致 */
        margin: 0 auto;
        background: #c0c0c0;
        border: 3px outset #fff;
        box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
        padding: 20px;
        border-radius: 8px;
    }

    .status-bar {
        display: flex;
        justify-content: space-between;
        align-items: center;
        margin-bottom: 15px;
        padding: 10px;
        background: #c0c0c0;
        border: 2px inset #fff;
        border-radius: 4px;
        color: #333;  /* 添加文字颜色 */
    }
    
    .mine-count {
        font-weight: bold;
        font-size: 18px;
    }
    
    .difficulty-controls {
        display: flex;
        flex-wrap: wrap;
        gap: 10px;
        margin-bottom: 15px;
        justify-content: center;
    }
    
    .difficulty-btn, .new-game-btn {
        padding: 8px 15px;
        background: #f0f0f0;
        border: 2px outset #fff;
        cursor: pointer;
        font-family: 'Microsoft YaHei', Arial, sans-serif;
        border-radius: 4px;
        transition: all 0.3s;
        color: #333;  /* 添加默认文字颜色 */
        font-weight: bold;  /* 加粗文字 */
    }
    
    .difficulty-btn:active, .new-game-btn:active {
        border: 2px inset #fff;
    }
    
    .difficulty-btn.active {
        background: #4568dc;  /* 更改为蓝色背景 */
        border: 2px inset #fff;
        color: white;  /* 白色文字 */
    }
    
    .new-game-btn {
        font-weight: bold;
        background: #4568dc;
        color: white;
    }
    
    .new-game-btn:hover {
        background: #3a5bc9;
    }

    .grid-container {
        width: 100%;
        overflow: auto;
        margin: 0 auto;
        display: flex;
        justify-content: center;
        padding: 10px;
        background: #a0a0a0;
        border: 2px inset #fff;
        border-radius: 4px;
        min-height: 400px;
        height: auto; /* 允许高度自适应 */
    }

    .grid {
        display: grid;
        gap: 1px;
        background: #808080;
        border: 2px inset #fff;
        padding: 2px;
        margin: 0 auto;
        /* 确保网格在初始加载时有固定尺寸 */
        min-width: 300px;
        min-height: 300px;
    }

    .cell {
        width: 25px;
        height: 25px;
        background: #c0c0c0;
        border: 2px outset #fff;
        display: flex;
        align-items: center;
        justify-content: center;
        cursor: pointer;
        font-weight: bold;
        font-size: 16px;  /* 增大字体大小 */
    }

    .revealed {
        border: 1px solid #808080;
        background: #c0c0c0;
    }

    .flagged {
        color: red;
    }
    
    .game-over-overlay {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.5);
        display: flex;
        justify-content: center;
        align-items: center;
        z-index: 100;
    }
    
    .game-over-modal {
        background: #c0c0c0;
        border: 3px outset #fff;
        padding: 20px;
        text-align: center;
        width: 300px;
        border-radius: 8px;
    }
    
    .game-over-modal h2 {
        margin-top: 0;
        margin-bottom: 15px;
    }
    
    .game-over-modal p {
        margin-bottom: 20px;
    }
    
    .hidden {
        display: none;
    }
    
    /* 数字颜色 */
    .num-1 { color: #0066ff; font-weight: bold; }  /* 更亮的蓝色 */
    .num-2 { color: #00aa00; font-weight: bold; }  /* 更亮的绿色 */
    .num-3 { color: #ff0000; font-weight: bold; }  /* 鲜红色 */
    .num-4 { color: #000099; font-weight: bold; }  /* 深蓝色 */
    .num-5 { color: #aa0000; font-weight: bold; }  /* 深红色 */
    .num-6 { color: #008080; font-weight: bold; }  /* 青色 */
    .num-7 { color: #000000; font-weight: bold; }  /* 黑色 */
    .num-8 { color: #666666; font-weight: bold; }  /* 灰色 */
    
    @media (max-width: 768px) {
        .difficulty-controls {
            flex-direction: column;
            align-items: center;
        }
        
        .difficulty-btn, .new-game-btn {
            width: 100%;
            max-width: 200px;
        }
        
        .cell {
            width: 20px;
            height: 20px;
            font-size: 12px;
        }
    }
</style>
</head>
<body>
<div class="page-container">
    <div class="game-container">
        <div class="status-bar">
            <div class="mine-count">剩余雷数: <span id="mine-count">20</span></div>
            <button class="new-game-btn" onclick="initGame()">新游戏</button>
        </div>
        
        <div class="difficulty-controls">
            <button class="difficulty-btn active" onclick="setDifficulty('beginner')">初级 (12x12)</button>
            <button class="difficulty-btn" onclick="setDifficulty('intermediate')">中级 (16x16)</button>
            <button class="difficulty-btn" onclick="setDifficulty('expert')">高级 (19x19)</button>
        </div>
        
        <div class="grid-container">
            <div id="grid" class="grid"></div>
        </div>
        
        <div id="game-over" class="game-over-overlay hidden">
            <div class="game-over-modal">
                <h2 id="game-result">游戏结束</h2>
                <p id="game-message"></p>
                <button class="new-game-btn" onclick="closeModal(); initGame();">再来一局</button>
            </div>
        </div>
    </div>

    <script>
        // 游戏配置
        const DIFFICULTY = {
            beginner: { rows: 12, cols: 12, mines: 20 },
            intermediate: { rows: 16, cols: 16, mines: 40 },
            expert: { rows: 19, cols: 19, mines: 99 }
        };
        
        let currentDifficulty = 'beginner';
        let ROWS = DIFFICULTY[currentDifficulty].rows;
        let COLS = DIFFICULTY[currentDifficulty].cols;
        let MINE_COUNT = DIFFICULTY[currentDifficulty].mines;
        
        let mineBoard = [];
        let showBoard = [];
        let gameOver = false;
        let flaggedCount = 0;
        let startTime = 0;
        let firstClick = true;
        let isGridRendered = false;  // 添加标志来跟踪网格是否已渲染

        function setDifficulty(difficulty) {
            // 更新当前难度按钮样式
            document.querySelectorAll('.difficulty-btn').forEach(btn => {
                btn.classList.remove('active');
            });
            event.target.classList.add('active');
            
            currentDifficulty = difficulty;
            ROWS = DIFFICULTY[difficulty].rows;
            COLS = DIFFICULTY[difficulty].cols;
            MINE_COUNT = DIFFICULTY[difficulty].mines;
            
            initGame();
        }

        function initGame() {
            gameOver = false;
            flaggedCount = 0;
            firstClick = true;
            document.getElementById('mine-count').textContent = MINE_COUNT;
            
            // 隐藏游戏结束弹窗
            document.getElementById('game-over').classList.add('hidden');
            
            // 初始化棋盘
            mineBoard = Array(ROWS).fill().map(() => Array(COLS).fill(0));
            showBoard = Array(ROWS).fill().map(() => Array(COLS).fill('hidden'));
            
            renderGrid();
            isGridRendered = true;  // 设置网格已渲染标志
        }
        
        function placeMines(firstX, firstY) {
            // 布置雷(确保第一次点击不是雷)
            let minesPlaced = 0;
            while (minesPlaced < MINE_COUNT) {
                const x = Math.floor(Math.random() * ROWS);
                const y = Math.floor(Math.random() * COLS);
                
                // 确保第一次点击的位置及其周围没有雷
                if (mineBoard[x][y] !== 'M' && 
                    (Math.abs(x - firstX) > 1 || Math.abs(y - firstY) > 1)) {
                    mineBoard[x][y] = 'M';
                    minesPlaced++;
                }
            }
            
            // 计算数字
            for (let i = 0; i < ROWS; i++) {
                for (let j = 0; j < COLS; j++) {
                    if (mineBoard[i][j] !== 'M') {
                        mineBoard[i][j] = countAdjacentMines(i, j);
                    }
                }
            }
        }

        function countAdjacentMines(x, y) {
            let count = 0;
            for (let i = -1; i <= 1; i++) {
                for (let j = -1; j <= 1; j++) {
                    const newX = x + i;
                    const newY = y + j;
                    if (newX >= 0 && newX < ROWS && 
                        newY >= 0 && newY < COLS &&
                        mineBoard[newX][newY] === 'M') {
                        count++;
                    }
                }
            }
            return count;
        }

        function reveal(x, y) {
            if (gameOver || showBoard[x][y] === 'revealed' || showBoard[x][y] === 'flag') return;
            
            // 第一次点击时布置雷
            if (firstClick) {
                placeMines(x, y);
                firstClick = false;
                startTime = Date.now();
            }

            if (mineBoard[x][y] === 'M') {
                gameOver = true;
                revealAllMines();
                showGameOver(false);
                return;
            }

            showBoard[x][y] = 'revealed';
            
            // 自动展开空白区域
            if (mineBoard[x][y] === 0) {
                for (let i = -1; i <= 1; i++) {
                    for (let j = -1; j <= 1; j++) {
                        const newX = x + i;
                        const newY = y + j;
                        if (newX >= 0 && newX < ROWS && 
                            newY >= 0 && newY < COLS &&
                            showBoard[newX][newY] !== 'revealed') {
                            reveal(newX, newY);
                        }
                    }
                }
            }

            checkWin();
            renderGrid();
        }

        function toggleFlag(x, y, event) {
            event.preventDefault();
            if (gameOver || showBoard[x][y] === 'revealed') return;

            if (showBoard[x][y] === 'flag') {
                showBoard[x][y] = 'hidden';
                flaggedCount--;
            } else {
                showBoard[x][y] = 'flag';
                flaggedCount++;
            }

            document.getElementById('mine-count').textContent = MINE_COUNT - flaggedCount;
            renderGrid();
            checkWin();
        }

        function checkWin() {
            if (gameOver) return;
            
            // 检查是否所有非雷格子都已揭开
            let revealedCount = 0;
            for (let i = 0; i < ROWS; i++) {
                for (let j = 0; j < COLS; j++) {
                    if (showBoard[i][j] === 'revealed') revealedCount++;
                }
            }
            
            if (revealedCount === ROWS * COLS - MINE_COUNT) {
                gameOver = true;
                // 自动标记所有未标记的雷
                for (let i = 0; i < ROWS; i++) {
                    for (let j = 0; j < COLS; j++) {
                        if (mineBoard[i][j] === 'M' && showBoard[i][j] !== 'flag') {
                            showBoard[i][j] = 'flag';
                        }
                    }
                }
                renderGrid();
                showGameOver(true);
            }
        }
        
        function showGameOver(isWin) {
            const modal = document.getElementById('game-over');
            const result = document.getElementById('game-result');
            const message = document.getElementById('game-message');
            
            modal.classList.remove('hidden');
            
            if (isWin) {
                result.textContent = '恭喜你赢了!';
                const timeTaken = Math.floor((Date.now() - startTime) / 1000);
                message.textContent = `你用了${timeTaken}秒完成了游戏!`;
            } else {
                result.textContent = '游戏结束';
                message.textContent = '很遗憾,你触雷了!';
            }
        }
        
        function closeModal() {
            document.getElementById('game-over').classList.add('hidden');
        }

        function revealAllMines() {
            for (let i = 0; i < ROWS; i++) {
                for (let j = 0; j < COLS; j++) {
                    if (mineBoard[i][j] === 'M') {
                        showBoard[i][j] = 'mine';
                    }
                }
            }
            renderGrid();
        }

        function renderGrid() {
            const grid = document.getElementById('grid');
            if (!grid) return;  // 添加安全检查
            
            grid.style.gridTemplateColumns = `repeat(${COLS}, 25px)`;
            grid.innerHTML = '';
            
            // 确保网格有固定尺寸
            const gridWidth = COLS * 25 + 4;
            const gridHeight = ROWS * 25 + 4;
            
            grid.style.width = `${gridWidth}px`;
            grid.style.height = `${gridHeight}px`;
            
            // 设置网格容器的高度,确保能完全显示网格,增加10px以消除滚动条
            const gridContainer = document.querySelector('.grid-container');
            if (gridContainer) {
                gridContainer.style.height = `${gridHeight + 35}px`;  // 从+20px改为+30px
            }
            
            // 调整游戏容器宽度以匹配父容器
            const gameContainer = document.querySelector('.game-container');
            if (gameContainer) {
                gameContainer.style.width = '100%';
            }
            
            // 创建初始网格
            for (let i = 0; i < ROWS; i++) {
                for (let j = 0; j < COLS; j++) {
                    const cell = document.createElement('div');
                    cell.className = 'cell';
                    
                    if (showBoard[i][j] === 'revealed') {
                        cell.classList.add('revealed');
                        if (mineBoard[i][j] !== 0) {
                            cell.textContent = mineBoard[i][j];
                            cell.classList.add(`num-${mineBoard[i][j]}`);
                        }
                    } else if (showBoard[i][j] === 'flag') {
                        cell.textContent = '🚩';
                        cell.classList.add('flagged');
                    }
                    
                    if (showBoard[i][j] === 'mine') {
                        cell.textContent = '💣';
                        cell.style.backgroundColor = '#ff6666';
                    }
                    
                    cell.addEventListener('click', () => reveal(i, j));
                    cell.addEventListener('contextmenu', (e) => toggleFlag(i, j, e));
                    
                    grid.appendChild(cell);
                }
            }
        }

        // 检查网格是否为空,如果为空则重新初始化
        function checkAndReinitializeGrid() {
            const grid = document.getElementById('grid');
            if (grid && (!grid.children.length || grid.children.length === 0)) {
                console.log('检测到空网格,重新初始化游戏');
                // 重置为初级难度
                resetToBeginner();
                initGame();
            }
        }
        
        // 新增:重置为初级难度的函数
        function resetToBeginner() {
            currentDifficulty = 'beginner';
            ROWS = DIFFICULTY[currentDifficulty].rows;
            COLS = DIFFICULTY[currentDifficulty].cols;
            MINE_COUNT = DIFFICULTY[currentDifficulty].mines;
            
            // 更新难度按钮样式
            document.querySelectorAll('.difficulty-btn').forEach(btn => {
                btn.classList.remove('active');
                if (btn.textContent.includes('初级')) {
                    btn.classList.add('active');
                }
            });
            
            // 更新剩余雷数显示
            document.getElementById('mine-count').textContent = MINE_COUNT;
        }

        // 初始化游戏
        // 确保DOM完全加载后立即初始化游戏
        document.addEventListener('DOMContentLoaded', function() {
            initGame();
            // 强制立即渲染网格,确保游戏加载时显示画面
            renderGrid();
        });

        // 添加备份初始化方法,以防DOMContentLoaded事件已经触发
        if (document.readyState === 'complete' || document.readyState === 'interactive') {
            setTimeout(function() {
                initGame();
                renderGrid();
            }, 1);
        }

        // 添加页面可见性变化监听
        document.addEventListener('visibilitychange', function() {
            if (document.visibilityState === 'visible') {
                // 当页面变为可见时,检查网格是否需要重新初始化
                setTimeout(checkAndReinitializeGrid, 100);
            }
        });

        // 添加自定义事件监听,用于处理导航栏刷新
        window.addEventListener('pageshow', function(event) {
            // pageshow事件在每次页面显示时触发,包括从缓存加载
            if (event.persisted) {
                // 页面从缓存加载时,重置为初级难度
                resetToBeginner();
                setTimeout(checkAndReinitializeGrid, 100);
            }
        });

        // 修改:添加导航栏刷新检测
        window.addEventListener('load', function() {
            // 检查是否是通过导航栏刷新加载的页面
            if (performance && performance.navigation) {
                // navigation.type: 0=直接访问, 1=刷新, 2=前进/后退
                if (performance.navigation.type === 1) {
                    console.log('检测到页面刷新,重置为初级难度');
                    resetToBeginner();
                    initGame();
                }
            }
        });

        // 添加额外的安全措施,定期检查网格是否为空
        setInterval(function() {
            const grid = document.getElementById('grid');
            if (grid && (!grid.children.length || grid.children.length === 0) && isGridRendered) {
                console.log('定期检查:检测到空网格,重新初始化游戏');
                initGame();
            }
        }, 1000); // 每秒检查一次
    </script>
</div>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值