Hexo 博客实战:扫雷小游戏开发指南

前言

继俄罗斯方块之后,本文讲解扫雷游戏的开发。扫雷是 Windows 系统经典的益智小游戏,核心玩法是揭开格子找出所有安全区域,同时避免触雷。

相比俄罗斯方块,扫雷更侧重于逻辑推理和策略标记,需要实现数字计算、递归扩散、旗标标记等功能。

项目结构

游戏放置在 source/ai-games/minesweeper/ 目录下,文件结构如下:

1
2
3
source/ai-games/minesweeper/
├── index.md # 游戏页面(HTML + 样式)
└── minesweeper-game.js # 游戏核心逻辑(约570行)

游戏页面实现

顶部状态栏

扫雷游戏采用经典的三段式状态栏设计:

1
2
3
4
5
6
7
8
9
<div class="game-header">
<div class="game-display">
<span id="score">10</span> <!-- 剩余地雷数 -->
</div>
<button id="face-btn">😀</button> <!-- 笑脸按钮 -->
<div class="game-display">
<span id="timer">000</span> <!-- 计时器 -->
</div>
</div>
  • 剩余地雷数:初始为地雷总数,每插一面旗减1
  • 笑脸按钮:点击重新开始游戏,不同状态显示不同表情
  • 计时器:从第一次点击开始计时,秒数递增

难度选择

1
2
3
4
5
const DIFFICULTY = {
easy: { cols: 9, rows: 9, mines: 10 }, // 简单
normal: { cols: 16, rows: 16, mines: 40 }, // 普通
hard: { cols: 30, rows: 16, mines: 99 } // 困难
};

三种难度满足不同水平玩家的需求。

核心逻辑实现

游戏状态管理

1
2
3
4
5
6
7
8
9
let board = [];           // 存储地雷和数字
let revealed = []; // 记录格子是否已揭开
let flagged = []; // 记录格子是否已插旗
let minePos = []; // 存储所有地雷位置
let firstClick = true; // 是否第一次点击
let gameOver = false; // 游戏是否结束
let win = false; // 是否胜利
let mineCount = MINES; // 剩余地雷数
let revealCount = 0; // 已揭开格子数

board 中:-1 表示地雷,0-8 表示周围地雷数量。

初始化棋盘

1
2
3
4
5
6
7
8
9
10
11
12
function initBoard() {
for (let y = 0; y < ROWS; y++) {
board[y] = [];
revealed[y] = [];
flagged[y] = [];
for (let x = 0; x < COLS; x++) {
board[y][x] = 0;
revealed[y][x] = false;
flagged[y][x] = false;
}
}
}

地雷放置(首次点击安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
function placeMines(excludeX, excludeY) {
let placed = 0;
while (placed < MINES) {
const x = Math.floor(Math.random() * COLS);
const y = Math.floor(Math.random() * ROWS);

// 确保不重复放置,且避开点击位置周围3×3区域
if (board[y][x] !== -1 &&
!(Math.abs(x - excludeX) <= 1 && Math.abs(y - excludeY) <= 1)) {
board[y][x] = -1;
minePos.push({ x, y });
placed++;
}
}

// 计算每个格子的数字
for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
if (board[y][x] !== -1) {
let count = 0;
// 统计周围8个方向的地雷数
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dy === 0 && dx === 0) continue;
const ny = y + dy;
const nx = x + dx;
if (ny >= 0 && ny < ROWS && nx >= 0 &&
nx < COLS && board[ny][nx] === -1) {
count++;
}
}
}
board[y][x] = count;
}
}
}
}

首次点击安全的实现:先不放置地雷,用户点击后才生成地雷,并避开点击位置周围区域。

揭开格子(递归扩散)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function reveal(x, y) {
if (x < 0 || x >= COLS || y < 0 || y >= ROWS) return;
if (revealed[y][x] || flagged[y][x]) return;

revealed[y][x] = true;
revealCount++;

// 触雷游戏结束
if (board[y][x] === -1) {
gameOver = true;
showAllMines(x, y);
draw();
return;
}

// 如果是空白格,递归揭开周围8个格子
if (board[y][x] === 0) {
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (dy !== 0 || dx !== 0) {
reveal(x + dx, y + dy);
}
}
}
}

// 检查胜利条件
if (revealCount === COLS * ROWS - MINES) {
win = true;
gameOver = true;
flagAllMines();
}

draw();
}

这是扫雷的核心算法:揭开空白格时自动扩散揭开周围格子,形成”开图”效果。

快速揭开(双击)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function quickReveal(x, y) {
if (!revealed[y][x] || board[y][x] <= 0) return;

// 统计周围已插旗数
let flagCount = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const ny = y + dy;
const nx = x + dx;
if (ny >= 0 && ny < ROWS && nx >= 0 &&
nx < COLS && flagged[ny][nx]) {
flagCount++;
}
}
}

// 如果旗帜数等于数字,揭开周围未标记的格子
if (flagCount === board[y][x]) {
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
const ny = y + dy;
const nx = x + dx;
if (ny >= 0 && ny < ROWS && nx >= 0 &&
nx < COLS && !revealed[ny][nx] && !flagged[ny][nx]) {
reveal(nx, ny);
}
}
}
}
}

这是高级技巧:当确定周围旗帜正确时,双击数字可一键揭开剩余格子。

插旗标记

1
2
3
4
5
6
7
function toggleFlag(x, y) {
if (gameOver || revealed[y][x]) return;
flagged[y][x] = !flagged[y][x];
mineCount += flagged[y][x] ? -1 : 1;
draw();
updateScore();
}

右键或长按可以插旗标记地雷,同时更新剩余地雷计数。

绘制系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function draw() {
ctx.fillStyle = '#1a1a1a';
ctx.fillRect(0, 0, canvas.width, canvas.height);

for (let y = 0; y < ROWS; y++) {
for (let x = 0; x < COLS; x++) {
const px = x * CELL_SIZE;
const py = y * CELL_SIZE;

if (revealed[y][x]) {
// 已揭开格子:凹陷效果
ctx.fillStyle = '#2d2d2d';
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);

if (board[y][x] === -1) {
ctx.fillText('💣', ...); // 显示地雷
} else if (board[y][x] > 0) {
ctx.fillStyle = COLORS[board[y][x]];
ctx.fillText(board[y][x], ...); // 显示数字
}
} else {
// 未揭开格子:凸起3D效果
ctx.fillStyle = '#4a4a4a';
ctx.fillRect(px, py, CELL_SIZE, CELL_SIZE);
// 模拟光照的亮边和暗边
ctx.fillStyle = '#666';
ctx.fillRect(px, py, CELL_SIZE, 2);
ctx.fillRect(px, py, 2, CELL_SIZE);
ctx.fillStyle = '#333';
ctx.fillRect(px + CELL_SIZE - 2, py, 2, CELL_SIZE);
ctx.fillRect(px, py + CELL_SIZE - 2, CELL_SIZE, 2);

if (flagged[y][x]) {
ctx.fillText('🚩', ...); // 显示旗帜
}
}
}
}
}

数字颜色按经典扫雷配色:1=蓝、2=绿、3=红、4=深蓝、5=棕、6=青、7=黑、8=灰。

控制方式

鼠标操作

操作 功能
左键点击 揭开格子
右键点击 插旗/取消旗帜
双击数字 快速揭开周围格子
鼠标移动 高亮当前格子
鼠标按下 笑脸变为😮

触摸操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function handleTouchStart(e) {
// 记录触摸开始位置和时间
touchStartTime = Date.now();
touchStartPos = { x, y };
selectedCell = { x, y };
draw();
}

function handleTouchEnd(e) {
const touchDuration = Date.now() - touchStartTime;

if (touchDuration > 500) {
// 长按(>0.5秒) = 插旗
toggleFlag(x, y);
} else {
// 短按 = 揭开
if (firstClick) {
placeMines(x, y);
firstClick = false;
startTimer();
}
reveal(x, y);
}
}

// 支持滑动切换选中格子
function handleTouchMove(e) {
// 更新选中格子位置
selectedCell = { x, y };
draw();
}

手机端操作:

  • 点击揭开格子
  • 滑动切换选中格子
  • 长按插旗/取消旗帜
  • 快速双击实现快速揭开

状态管理

1
2
3
4
5
6
function updateFace(state) {
if (state === 'win') faceBtn.textContent = '😊';
else if (state === 'dead') faceBtn.textContent = '😵';
else if (state === 'press') faceBtn.textContent = '😮';
else faceBtn.textContent = '😀';
}

笑脸按钮根据游戏状态显示不同表情,提供直观的反馈。

总结

扫雷游戏的核心要点总结:

功能 实现方式
地雷生成 首次点击后随机生成,确保第一次安全
数字计算 遍历周围8个方向统计地雷数
递归扩散 空白格自动揭开周围格子
快速揭开 双击时验证周围旗帜数
胜利判断 已揭开格子数 = 总格子数 - 地雷数
状态反馈 笑脸表情变化

游戏已集成到博客的小游戏栏目中,可以直接访问体验。


相关链接: