从零构建经典:使用 HTML、CSS 和 JavaScript 打造高级扫雷游戏

扫雷不仅仅是一款经典的益智游戏,它更是磨练逻辑推理和算法思维的绝佳训练场。作为一名前端开发者,你一定思考过:如何将这种基于网格、状态复杂的逻辑游戏移植到浏览器中?在这篇文章中,我们将深入探讨如何仅使用原生的 HTML、CSS 和 JavaScript,从零开始构建一个功能完备、视觉精致的扫雷游戏。

我们将通过实际开发过程,一起探索游戏状态管理、DOM 操作优化、递归算法应用以及用户交互设计的奥秘。无论你是刚入门的新手,还是希望巩固基础知识的资深开发者,这个项目都将为你提供宝贵的实战经验。

最终效果预览

在深入代码细节之前,让我们先通过截图来看看最终成品的模样。这将帮助我们对目标有一个清晰的视觉认知。

!Screenshot-2023-09-25-173603

准备工作:工欲善其事

在开始敲代码之前,我们需要准备好“武器”。请确保你的开发环境中包含以下要素:

  • 基础知识储备:你需要对 HTML(结构)、CSS(样式)和 JavaScript(交互逻辑)有基本的了解。如果你熟悉 ES6+ 的语法(如 INLINECODEffd0b7e0, INLINECODEa100bc55, 箭头函数),那将会更加得心应手。
  • 代码编辑器:选择一款你顺手编辑器,比如 Visual Studio Code、Sublime Text 或者 WebStorm。
  • 现代浏览器:我们将使用最新的 Web 标准,因此请确保使用 Chrome、Firefox、Edge 或 Safari 的最新版本进行测试。

项目结构与实现思路

为了保持代码的清晰和可维护性,我们将采用模块化的思维来构建这个游戏。以下是我们的实现路径:

  • HTML 结构搭建:构建游戏的骨架,包括头部信息(如剩余雷数、计时器)和游戏主网格区域。
  • 视觉样式设计 (CSS):定义网格布局,美化单元格(未点开、已点开、插旗状态),并设置配色方案以区分不同的数字和状态。
  • 核心逻辑

* 数据结构:创建二维数组来映射游戏网格,存储每个格子的状态(是否有雷、周围雷数、是否已揭开)。

* 游戏初始化:随机布雷算法,以及计算每个非雷格子周围的雷数。

* 交互处理:编写递归函数来处理“泛洪算法”,即当点击空白格时自动展开周围的安全区域。

* 胜负判定:实时监控游戏状态,判断踩雷(失败)或找出所有地雷(胜利)。

第一步:构建 HTML 骨架

首先,我们需要一个容器来承载游戏。我们将使用语义化的标签来组织结构。





    
    
    经典扫雷 Web 版
    


    

JavaScript 扫雷挑战

剩余地雷: 10

解析:这里我们使用了一个主要的 INLINECODE5237b26e (INLINECODE3a7a55ba) 来动态生成格子。我们预留了显示“剩余地雷”的区域和一个重置按钮,这对于提升用户体验至关重要。

第二步:打造视觉样式

接下来,我们用 CSS 让它看起来像一个真正的游戏。我们将使用 CSS Grid 布局,因为它非常适合处理二维网格。

/* style.css */
body {
    font-family: ‘Segoe UI‘, Tahoma, Geneva, Verdana, sans-serif;
    background-color: #f0f2f5;
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    margin: 0;
}

.container {
    text-align: center;
    background: white;
    padding: 20px;
    border-radius: 10px;
    box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

h1 {
    color: #333;
}

.game-info {
    margin-bottom: 15px;
    display: flex;
    justify-content: center;
    gap: 20px;
    align-items: center;
}

#gameBoard {
    display: grid;
    /* 动态设置列数,将在 JS 中辅助控制 */
    gap: 2px;
    margin: 0 auto;
    background-color: #bbb;
    border: 4px solid #999;
}

.cell {
    width: 30px;
    height: 30px;
    background-color: #ccc;
    border: 2px outset #fff;
    display: flex;
    justify-content: center;
    align-items: center;
    font-weight: bold;
    cursor: pointer;
    user-select: none;
}

.cell:active {
    border: 1px solid #999;
}

/* 不同的数字颜色,提高可读性 */
.cell[data-num="1"] { color: blue; }
.cell[data-num="2"] { color: green; }
.cell[data-num="3"] { color: red; }
.cell[data-num="4"] { color: darkblue; }

/* 踩雷后的样式 */
.cell.mine {
    background-color: #ff4d4d;
    border: 1px solid #999;
}

/* 已揭开的安全格子 */
.cell.revealed {
    background-color: #eee;
    border: 1px solid #999;
}

第三步:核心逻辑实现

这是最激动人心的部分。让我们一步步编写 JavaScript,赋予游戏生命。我们将把代码分为几个主要部分:配置、初始化、逻辑计算和渲染。

#### 1. 初始化设置

我们需要定义游戏的基本参数,并编写一个函数来“重置”或“开始”游戏。

// script.js

// 游戏配置
const numRows = 8;
const numCols = 8;
const numMines = 10; // 总地雷数

const gameBoard = document.getElementById(‘gameBoard‘);
const mineCountDisplay = document.getElementById(‘mine-count‘);

let board = []; // 存储游戏逻辑数据的二维数组
let gameOver = false;

// 初始化游戏
function initGame() {
    // 重置状态
    gameOver = false;
    board = [];
    mineCountDisplay.textContent = numMines;
    gameBoard.innerHTML = ‘‘;
    
    // 设置 CSS Grid 的列数
    gameBoard.style.gridTemplateColumns = `repeat(${numCols}, 30px)`;

    createBoard();
    placeMines();
    calculateNumbers();
    renderBoard();
}

function createBoard() {
    for (let i = 0; i < numRows; i++) {
        board[i] = [];
        for (let j = 0; j < numCols; j++) {
            board[i][j] = {
                isMine: false,    // 是否是雷
                revealed: false,  // 是否已揭开
                count: 0,         // 周围雷数
                element: null     // 对应的 DOM 元素
            };
        }
    }
}

#### 2. 随机布雷算法

为了保证每次游戏的随机性,我们需要一个算法来放置地雷。这里使用 while 循环来确保我们放置了确切数量的地雷,且不会重复放置在同一个位置。

function placeMines() {
    let minesPlaced = 0;
    while (minesPlaced < numMines) {
        // 生成随机坐标
        const row = Math.floor(Math.random() * numRows);
        const col = Math.floor(Math.random() * numCols);

        // 只有当该位置没有雷时才放置
        if (!board[row][col].isMine) {
            board[row][col].isMine = true;
            minesPlaced++;
        }
    }
}

#### 3. 计算周围雷数

这是扫雷的核心逻辑之一。对于每一个非雷格子,我们需要检查它周围的 8 个邻居有多少个地雷。这涉及到边界检查,防止数组越界。

function calculateNumbers() {
    // 遍历整个网格
    for (let i = 0; i < numRows; i++) {
        for (let j = 0; j < numCols; j++) {
            if (!board[i][j].isMine) {
                let count = 0;
                // 检查周围 3x3 的区域
                for (let dx = -1; dx <= 1; dx++) {
                    for (let dy = -1; dy = 0 && ni = 0 && nj < numCols) {
                            if (board[ni][nj].isMine) {
                                count++;
                            }
                        }
                    }
                }
                board[i][j].count = count;
            }
        }
    }
}

#### 4. 渲染游戏盘面

现在,我们将逻辑数据转换为可视化的 DOM 元素。

function renderBoard() {
    gameBoard.innerHTML = ‘‘; // 清空现有内容

    for (let i = 0; i < numRows; i++) {
        for (let j = 0; j  handleCellClick(i, j));
            // 可选:添加右键插旗功能
            cell.addEventListener(‘contextmenu‘, (e) => {
                e.preventDefault();
                handleRightClick(i, j);
            });

            // 保存 DOM 引用,方便后续更新而不重绘整个网格
            board[i][j].element = cell;
            gameBoard.appendChild(cell);
        }
    }
}

#### 5. 处理交互与递归展开

这是体验最关键的部分。当我们点击一个格子时:

  • 如果是雷,游戏结束。
  • 如果数字大于 0,显示数字。
  • 关键点:如果数字是 0(空白),我们需要递归地打开周围所有的格子,直到遇到有数字的边界。这个过程被称为“泛洪填充”。
function handleCellClick(row, col) {
    if (gameOver || board[row][col].revealed) return;

    const cellData = board[row][col];
    const cellElement = cellData.element;

    cellData.revealed = true;
    cellElement.classList.add(‘revealed‘);

    if (cellData.isMine) {
        // 踩雷逻辑
        cellElement.classList.add(‘mine‘);
        cellElement.textContent = ‘💣‘;
        revealAllMines();
        setTimeout(() => alert(‘游戏结束!你踩到了地雷。‘), 100);
        gameOver = true;
    } else {
        if (cellData.count > 0) {
            // 显示周围雷数
            cellElement.textContent = cellData.count;
            cellElement.setAttribute(‘data-num‘, cellData.count);
        } else {
            // 如果周围没有雷,递归揭开周围格子
            // 注意:这里可以加入性能优化,避免过深的递归栈
            for (let dx = -1; dx <= 1; dx++) {
                for (let dy = -1; dy = 0 && ni = 0 && nj  {
        row.forEach(cell => {
            if (cell.isMine) {
                cell.element.classList.add(‘revealed‘, ‘mine‘);
                cell.element.textContent = ‘💣‘;
            }
        });
    });
}

// 辅助函数:检查胜利条件
function checkWin() {
    let revealedCount = 0;
    board.forEach(row => {
        row.forEach(cell => {
            if (cell.revealed && !cell.isMine) {
                revealedCount++;
            }
        });
    });

    // 如果所有非雷格子都被揭开,则胜利
    if (revealedCount === (numRows * numCols) - numMines) {
        gameOver = true;
        setTimeout(() => alert(‘恭喜你!你赢了!‘), 100);
    }
}

// 启动游戏
initGame();

进阶思考与优化建议

现在我们已经拥有了一个可运行的游戏。但作为一名追求卓越的开发者,我们还可以做些什么来提升它呢?

  • 首发防雷(FPP – First Click Protection)

在当前的逻辑中,玩家第一次点击就有可能踩雷。这不太公平。我们可以修改逻辑,将布雷动作推迟到玩家第一次点击之后。这样我们可以确保玩家点击的第一个位置及其周围一定是安全的。只需将 INLINECODEda909865 的调用移出 INLINECODEc730fe31,并在 handleCellClick 中判断如果是第一次点击,则先布雷(并排除当前坐标)。

  • 性能优化

对于大型网格(例如 30×16),频繁的 DOM 操作可能会导致卡顿。在 INLINECODE1ceee61d 中,我们使用了 INLINECODEfd588b74。对于更极端的情况,可以使用 DocumentFragment 来批量插入 DOM,减少重排和重绘的次数。此外,对于递归展开,可以使用循环代替递归以防止调用栈溢出,或者使用尾递归优化(尽管 JS 引擎支持各异)。

  • 移动端适配

目前的点击事件是基于 INLINECODE7de3e587 的。在移动设备上,我们可能需要处理 INLINECODEa7b2af91 事件来消除 300ms 的点击延迟,或者设计一个专门的“插旗模式”切换按钮,因为手机上很难进行右键操作。

  • 代码结构优化

在大型项目中,我们可以使用面向对象编程(OOP)或 ES6 Class 来封装游戏逻辑。例如,创建一个 INLINECODEf9a03c50 类,将 INLINECODE58ee0a01、INLINECODE024efe64、INLINECODE3eeb3f77 作为方法和属性,这样可以避免全局变量污染,使代码更加模块化。

结语

在这篇文章中,我们不仅构建了一个游戏,更重要的是实践了 Web 开发的核心技能:从设计 DOM 结构,到编写高效的 CSS Grid 布局,再到实现复杂的递归逻辑和状态管理。通过亲手实现扫雷,你现在对浏览器如何处理用户交互以及如何利用 JavaScript 操作数据有了更深的理解。

编程的乐趣就在于将抽象的逻辑转化为可视化的、可交互的产物。希望这个项目能激发你的灵感,去尝试创建更复杂的游戏或应用。为什么不现在就试着给你的扫雷游戏添加一个计时器,或者设计一个“困难模式”呢?祝编码愉快!

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。如需转载,请注明文章出处豆丁博客和来源网址。https://shluqu.cn/20202.html
点赞
0.00 平均评分 (0% 分数) - 0