前言
继俄罗斯方块之后,本文讲解扫雷游戏的开发。扫雷是 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); 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; 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; }
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 { 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) { 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个方向统计地雷数 |
| 递归扩散 |
空白格自动揭开周围格子 |
| 快速揭开 |
双击时验证周围旗帜数 |
| 胜利判断 |
已揭开格子数 = 总格子数 - 地雷数 |
| 状态反馈 |
笑脸表情变化 |
游戏已集成到博客的小游戏栏目中,可以直接访问体验。
相关链接: