使用HTML实现扫雷游戏及矩阵点击逻辑详解
一、扫雷游戏的基本结构与核心逻辑
扫雷是经典的益智游戏,核心玩法围绕二维矩阵展开,玩家通过点击格子排查地雷,标记可疑位置,最终揭开所有非雷格子即可获胜。使用HTML、CSS和JavaScript实现扫雷时,核心依赖以下部分:
二维矩阵:存储每个格子的状态(是否地雷、周围地雷数、是否揭开、是否标记)
DOM渲染:根据矩阵状态生成对应的游戏界面
点击逻辑:区分左键点击(揭开格子)和右键点击(标记/取消标记格子)
递归展开:点击空白格子时,自动展开周围无雷的相邻格子
二、基础HTML结构搭建
首先搭建游戏的基础页面结构,包含游戏容器、难度选择、状态显示区域和雷区矩阵容器:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HTML扫雷游戏</title>
<style>
.game-container {
width: fit-content;
margin: 20px auto;
text-align: center;
}
.control-panel {
margin-bottom: 15px;
}
.mine-grid {
display: grid;
gap: 1px;
background-color: #ccc;
border: 2px solid #999;
}
.cell {
width: 30px;
height: 30px;
background-color: #ddd;
border: 1px solid #999;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
user-select: none;
font-weight: bold;
}
.cell.revealed {
background-color: #eee;
cursor: default;
}
.cell.mine {
background-color: #f44;
}
.cell.flagged::after {
content: "?";
}
.status {
margin-top: 10px;
font-size: 16px;
}
</style>
</head>
<body>
<div class="game-container">
<div class="control-panel">
<button data-difficulty="easy">简单(9×9,10雷)</button>
<button data-difficulty="medium">中等(16×16,40雷)</button>
<button data-difficulty="hard">困难(16×30,99雷)</button>
</div>
<div id="mineGrid" class="mine-grid"></div>
<div class="status" id="gameStatus">点击格子开始游戏</div>
</div>
<script src="game.js"></script>
</body>
</html>三、核心JavaScript逻辑实现
1. 游戏初始化与矩阵生成
首先需要定义不同难度的配置,初始化时生成对应的二维矩阵,随机布置地雷,再计算每个格子周围的地雷数量:
// 游戏配置
const DIFFICULTY_CONFIG = {
easy: { rows: 9, cols: 9, mines: 10 },
medium: { rows: 16, cols: 16, mines: 40 },
hard: { rows: 16, cols: 30, mines: 99 }
};
// 游戏状态变量
let gameMatrix = []; // 二维矩阵存储格子状态
let gameConfig = DIFFICULTY_CONFIG.easy; // 当前难度配置
let isGameOver = false;
let revealedCount = 0; // 已揭开的格子数
let totalSafeCells = 0; // 总安全格子数
// 初始化游戏
function initGame(difficulty) {
gameConfig = DIFFICULTY_CONFIG[difficulty] || DIFFICULTY_CONFIG.easy;
isGameOver = false;
revealedCount = 0;
totalSafeCells = gameConfig.rows * gameConfig.cols - gameConfig.mines;
document.getElementById('gameStatus').textContent = '点击格子开始游戏';
// 初始化矩阵:每个格子默认状态
gameMatrix = [];
for (let i = 0; i < gameConfig.rows; i++) {
gameMatrix[i] = [];
for (let j = 0; j < gameConfig.cols; j++) {
gameMatrix[i][j] = {
isM false,
isRevealed: false,
isFlagged: false,
neighborMines: 0 // 周围地雷数量
};
}
}
// 随机布置地雷
let placedMines = 0;
while (placedMines < gameConfig.mines) {
const row = Math.floor(Math.random() * gameConfig.rows);
const col = Math.floor(Math.random() * gameConfig.cols);
if (!gameMatrix[row][col].isMine) {
gameMatrix[row][col].isMine = true;
placedMines++;
}
}
// 计算每个格子周围的地雷数
for (let i = 0; i < gameConfig.rows; i++) {
for (let j = 0; j < gameConfig.cols; j++) {
if (!gameMatrix[i][j].isMine) {
gameMatrix[i][j].neighborMines = countNeighborMines(i, j);
}
}
}
// 渲染雷区
renderGrid();
}
// 统计格子周围的地雷数量
function countNeighborMines(row, col) {
let count = 0;
// 遍历周围8个方向
for (let r = row - 1; r <= row + 1; r++) {
for (let c = col - 1; c <= col + 1; c++) {
// 排除自身,且坐标在矩阵范围内
if (r === row && c === col) continue;
if (r >= r < gameConfig.rows && c >= 0 && c < gameConfig.cols) {
if (gameMatrix[r][c].isMine) count++;
}
}
}
return count;
}2. 雷区渲染逻辑
根据矩阵状态生成对应的DOM元素,为每个格子绑定点击事件,同时设置网格的列数适配矩阵宽度:
// 渲染雷区网格
function renderGrid() {
const gridContainer = document.getElementById('mineGrid');
.innerHTML = '';
// 设置网格列数
gridContainer.style.gridTemplateColumns = `repeat(${gameConfig.cols}, 30px)`;
for (let i = 0; i < gameConfig.rows; i++) {
for (let j = 0; j < gameConfig.cols; 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();
handleCellRightClick(i, j);
});
// 根据状态更新格子显示
updateCellDisplay(cell, i, j);
gridContainer.appendChild(cell);
}
}
}
// 更新单个格子的显示状态
function updateCellDisplay(cell, row, col) {
const cellData = gameMatrix[row][col];
// 重置类名
cell.className = 'cell';
cell.textContent = '';
if (cellData.isRevealed) {
cell.classList.add('revealed');
if (cellData.isMine) {
cell.classList.add('mine');
cell.textContent = '?';
} else if (cellData.neighborMines > 0) {
// 不同数字显示不同颜色
cell.textContent = cellData.neighborMines;
const colorMap = ['', '', 'blue', 'green', 'red', 'darkblue', 'darkred', 'cyan', 'black', 'gray'];
cell.style.color = colorMap[cellData.neighborMines];
}
} else if (cellData.isFlagged) {
cell.classList.add('flagged');
}
}3. 矩阵点击核心逻辑实现
点击逻辑分为左键揭开和右键标记两部分,其中左键点击空白格子时需要实现递归展开相邻的无雷区域:
// 左键点击格子处理
function handleCellClick(row, col) {
if (isGameOver) return;
const cellData = gameMatrix[row][col];
if (cellData.isRevealed || cellData.isFlagged) return;
// 揭开格子
revealCell(row, col);
}
// 递归揭开格子(核心展开逻辑)
function revealCell(row, col) {
// 边界检查、已揭开或标记的检查
if (row < 0 || row >= gameConfig.rows || col < 0 || col >= gameConfig.cols) return;
const cellData = gameMatrix[row][col];
if (cellData.isRevealed || cellData.isFlagged) return;
// 标记为已揭开
cellData.isRevealed = true;
revealedCount++;
// 如果是地雷,游戏结束
if (cellData.isMine) {
isGameOver = true;
document.getElementById('gameStatus').textContent = '游戏结束!你踩到地雷了';
// 揭开所有地雷
for (let i = 0; i < gameConfig.rows; i++) {
for (let j = 0; j < gameConfig.cols; j++) {
if (gameMatrix[i][j].isMine) {
gameMatrix[i][j].isRevealed = true;
}
}
}
renderGrid();
return;
}
// 检查是否获胜:已揭开的安全格子数等于总安全格子数
if (revealedCount === totalSafeCells) {
isGameOver = true;
document.getElementById('gameStatus').textContent = '恭喜你!扫雷成功';
renderGrid();
return;
}
// 如果当前格子周围地雷数为0,递归展开周围格子
if (cellData.neighborMines === 0) {
// 遍历周围8个方向
for (let r = row - 1; r <= row + 1; r++) {
for (let c = col - 1; c <= col + 1; c++) {
if (r === row && c === col) continue;
revealCell(r, c);
}
}
}
// 更新当前格子的显示
const gridContainer = document.getElementById('mineGrid');
const index = row * gameConfig.cols + col;
const cellElement = gridContainer.children[index];
updateCellDisplay(cellElement, row, col);
}
// 右键点击格子处理(标记/取消标记)
function handleCellRightClick(row, col) {
if (isGameOver) return;
const cellData = gameMatrix[row][col];
if (cellData.isRevealed) return;
// 切换标记状态
cellData.isFlagged = !cellData.isFlagged;
// 更新显示
const gridContainer = document.getElementById('mineGrid');
const index = row * gameConfig.cols + col;
const cellElement = gridContainer.children[index];
updateCellDisplay(cellElement, row, col);
}4. 难度切换绑定
最后为难度选择按钮绑定点击事件,实现切换难度重新初始化游戏:
// 绑定难度按钮事件
document.querySelectorAll('.control-panel button').forEach(btn => {
btn.addEventListener('click', () const difficulty = btn.dataset.difficulty;
initGame(difficulty);
});
});
// 页面加载时初始化简单难度游戏
window.addEventListener('DOMContentLoaded', () => {
initGame('easy');
});四、逻辑关键点说明
矩阵点击逻辑的核心在于revealCell函数的递归设计:当点击的格子周围地雷数为0时,会自动向周围8个方向递归调用揭开函数,直到遇到周围有地雷的格子为止,这正是扫雷中点击空白区域自动展开的效果实现原理。
同时需要注意边界判断,避免递归时出现数组越界的问题,另外游戏状态的判断要放在揭开逻辑的最前端,防止游戏结束后还能继续操作。
如果需要优化性能,可以将DOM操作尽量合并,避免频繁更新单个格子导致的重绘重排,也可以在递归时加入已访问标记,避免重复处理同一个格子。