DOM 版扫雷:左键翻开、右键插旗、首次点击不踩雷、洪泛展开、计时判胜。用按钮网格实现,事件处理更直观。
1. 首次安全点击与布雷
第一次点击的格子及其周围 8 格全部清空再布雷,这样永远"首点安全"。布雷后统计每格数字:
const ROWS = 9, COLS = 9, MINES = 10;
let board = [];
function buildBoard(safeR, safeC) {
board = Array.from({length: ROWS}, () => Array(COLS).fill(0));
const safe = new Set();
for (let r = safeR - 1; r <= safeR + 1; r++)
for (let c = safeC - 1; c <= safeC + 1; c++)
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) safe.add(r * COLS + c);
let placed = 0;
while (placed < MINES) {
const r = Math.floor(Math.random() * ROWS);
const c = Math.floor(Math.random() * COLS);
if (board[r][c] === -1 || safe.has(r * COLS + c)) continue;
board[r][c] = -1; placed++;
}
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (board[r][c] !== -1) {
let n = 0;
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && board[nr][nc] === -1) n++;
}
board[r][c] = n;
}
}
2. 翻开与洪泛
左键翻开:数字格只翻自己,空格用队列 BFS 展开一片。右键循环 无标记→旗→问号:
let revealed = Array.from({length: ROWS}, () => Array(COLS).fill(false));
let flags = Array.from({length: ROWS}, () => Array(COLS).fill(''));
function reveal(r, c) {
if (revealed[r][c] || flags[r][c] === 'F') return;
if (board[r][c] === -1) { revealed[r][c] = true; lost = true; return; }
const q = [[r, c]];
while (q.length) {
const [cr, cc] = q.shift();
if (revealed[cr][cc]) continue;
revealed[cr][cc] = true;
if (board[cr][cc] !== 0) continue;
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
const nr = cr + dr, nc = cc + dc;
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && !revealed[nr][nc])
q.push([nr, nc]);
}
}
}
3. 完整代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<title>扫雷</title>
<style>
body{font-family:sans-serif;text-align:center;background:#222;color:#eee}
#board{display:grid;grid-template-columns:repeat(9,44px);gap:2px;width:412px;margin:16px auto}
#board button{height:44px;font-size:16px;border:none;border-radius:4px;cursor:pointer;background:#8a8a8a}
#board button.open{background:#d5d5d5;cursor:default}
</style>
</head>
<body>
<h1>扫雷 <small>左键翻开 · 右键插旗</small></h1>
<div id="board"></div>
<div id="info">点第一格开始</div>
<script>
const ROWS = 9, COLS = 9, MINES = 10;
let board = [];
let revealed = [], flags = [];
let started = false, lost = false, won = false;
let seconds = 0, timer = null;
const bd = document.getElementById('board');
function buildBoard(safeR, safeC) {
board = Array.from({length: ROWS}, () => Array(COLS).fill(0));
const safe = new Set();
for (let r = safeR - 1; r <= safeR + 1; r++)
for (let c = safeC - 1; c <= safeC + 1; c++)
if (r >= 0 && r < ROWS && c >= 0 && c < COLS) safe.add(r * COLS + c);
let placed = 0;
while (placed < MINES) {
const r = Math.floor(Math.random() * ROWS);
const c = Math.floor(Math.random() * COLS);
if (board[r][c] === -1 || safe.has(r * COLS + c)) continue;
board[r][c] = -1; placed++;
}
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (board[r][c] !== -1) {
let n = 0;
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && board[nr][nc] === -1) n++;
}
board[r][c] = n;
}
revealed = Array.from({length: ROWS}, () => Array(COLS).fill(false));
flags = Array.from({length: ROWS}, () => Array(COLS).fill(''));
}
function reveal(r, c) {
if (revealed[r][c] || flags[r][c] === 'F') return;
if (board[r][c] === -1) { revealed[r][c] = true; lost = true; return; }
const q = [[r, c]];
while (q.length) {
const [cr, cc] = q.shift();
if (revealed[cr][cc]) continue;
revealed[cr][cc] = true;
if (board[cr][cc] !== 0) continue;
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
const nr = cr + dr, nc = cc + dc;
if (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && !revealed[nr][nc])
q.push([nr, nc]);
}
}
}
function checkWin() {
let opened = 0;
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++)
if (revealed[r][c]) opened++;
return opened === ROWS * COLS - MINES;
}
function render() {
bd.innerHTML = '';
for (let r = 0; r < ROWS; r++) for (let c = 0; c < COLS; c++) {
const btn = document.createElement('button');
btn.dataset.r = r; btn.dataset.c = c;
if (revealed[r][c]) {
btn.className = 'open';
if (board[r][c] === -1) btn.textContent = '💣';
else if (board[r][c] > 0) btn.textContent = board[r][c];
} else if (flags[r][c] === 'F') btn.textContent = '🚩';
else if (flags[r][c] === '?') btn.textContent = '?';
btn.addEventListener('click', () => {
if (lost || won) return;
if (!started) {
started = true;
buildBoard(r, c);
timer = setInterval(() => {
seconds++;
document.getElementById('info').textContent = '用时: ' + seconds + 's';
}, 1000);
}
reveal(r, c);
render();
if (lost) { clearInterval(timer); document.getElementById('info').textContent = '踩雷了!刷新重玩'; }
else if (checkWin()) { won = true; clearInterval(timer); document.getElementById('info').textContent = '胜利!用时 ' + seconds + 's'; }
});
btn.addEventListener('contextmenu', e => {
e.preventDefault();
if (!started || lost || won || revealed[r][c]) return;
flags[r][c] = flags[r][c] === 'F' ? '?' : (flags[r][c] === '?' ? '' : 'F');
render();
});
bd.appendChild(btn);
}
}
render();
</script>
</body>
</html>
4. 运行与常见问题
保存为 minesweeper.html 用浏览器打开。常见问题:①右键弹菜单——contextmenu 事件 preventDefault();②首次点雷——布雷时跳过首点周围;③计时提前开始——started 开关只在第一格翻开后置真。
💡 改进方向:①难度选择;②双击数字快速展开;③踩雷后展示全部地雷;④最快时间存 localStorage;⑤音效。