完整源码在本文结尾处
一、游戏概述
扫雷是一款经典的益智游戏,玩家需要在不触发地雷的情况下揭开所有安全格子。本教程将带你从零开始开发一个具有精美界面和动画效果的扫雷游戏,包含难度选择、棋盘大小调整等高级功能。
二、游戏核心功能
- 三种难度级别:初级(9×9)、中级(16×16)、高级(16×30)
- 棋盘大小调整:小、中、大三种尺寸
- 计时系统:记录游戏用时
- 地雷计数器:显示剩余地雷数量
- 标记系统:右键标记可疑地雷位置
- 动画效果:格子揭示、地雷爆炸、胜利庆祝等动画
三、开发步骤详解
- 游戏数据结构设计
// 游戏配置
const DIFFICULTY = {beginner: { rows: 9, cols: 9, mines: 10 },intermediate: { rows: 16, cols: 16, mines: 40 },expert: { rows: 16, cols: 30, mines: 99 }
};// 尺寸因子
const SIZE_FACTOR = {small: 0.8,medium: 1.0,large: 1.2
};// 游戏状态
let gameConfig = { ...DIFFICULTY.beginner, sizeFactor: SIZE_FACTOR.small };
let board = []; // 存储每个格子的值(-1表示地雷,0-8表示周围地雷数)
let revealed = []; // 记录格子是否被揭开
let flagged = []; // 记录格子是否被标记
let mines = []; // 存储所有地雷位置
- 初始化游戏
function initGame() {// 重置游戏状态clearInterval(timerInterval);timer = 0;gameOver = false;gameWon = false;firstClick = true;// 创建棋盘数据结构createBoard();// 渲染棋盘到DOMrenderBoard();
}function createBoard() {// 初始化二维数组for (let i = 0; i < gameConfig.rows; i++) {board[i] = [];revealed[i] = [];flagged[i] = [];for (let j = 0; j < gameConfig.cols; j++) {board[i][j] = 0;revealed[i][j] = false;flagged[i][j] = false;}}
}
- 渲染棋盘
function renderBoard() {gridElement.innerHTML = '';gridElement.style.gridTemplateColumns = `repeat(${gameConfig.cols}, 1fr)`;// 应用尺寸因子const cellSize = Math.floor(30 * gameConfig.sizeFactor);document.documentElement.style.setProperty('--cell-size', `${cellSize}px`);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();handleRightClick(i, j);});gridElement.appendChild(cell);}}
}
- 放置地雷(确保第一次点击安全)
function placeMines(firstRow, firstCol) {mines = [];let minesPlaced = 0;// 确保第一次点击周围没有地雷const safeCells = [];for (let i = -1; i <= 1; i++) {for (let j = -1; j <= 1; j++) {const newRow = firstRow + i;const newCol = firstCol + j;if (newRow >= 0 && newRow < gameConfig.rows && newCol >= 0 && newCol < gameConfig.cols) {safeCells.push(`${newRow},${newCol}`);}}}// 随机放置地雷while (minesPlaced < gameConfig.mines) {const row = Math.floor(Math.random() * gameConfig.rows);const col = Math.floor(Math.random() * gameConfig.cols);// 跳过第一次点击及其周围的格子if (row === firstRow && col === firstCol) continue;if (safeCells.includes(`${row},${col}`)) continue;if (board[row][col] === -1) continue;board[row][col] = -1;mines.push({ row, col });minesPlaced++;// 更新周围格子的数字for (let i = -1; i <= 1; i++) {for (let j = -1; j <= 1; j++) {const newRow = row + i;const newCol = col + j;if (newRow >= 0 && newRow < gameConfig.rows && newCol >= 0 && newCol < gameConfig.cols && board[newRow][newCol] !== -1) {board[newRow][newCol]++;}}}}
}
- 处理格子点击
function handleCellClick(row, col) {if (gameOver || gameWon || flagged[row][col]) return;// 第一次点击放置地雷if (firstClick) {placeMines(row, col);firstClick = false;// 开始计时timerInterval = setInterval(() => {timer++;timerElement.textContent = timer;}, 1000);}// 如果点击到地雷if (board[row][col] === -1) {revealMines();gameOver = true;statusElement.textContent = "游戏结束!你踩到地雷了 💥";statusElement.classList.add("game-over");clearInterval(timerInterval);return;}// 揭示格子revealCell(row, col);// 检查胜利条件checkWin();
}function revealCell(row, col) {if (revealed[row][col] || flagged[row][col]) return;revealed[row][col] = true;const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);cell.classList.add('revealed');if (board[row][col] > 0) {cell.textContent = board[row][col];cell.classList.add(`num-${board[row][col]}`);} else if (board[row][col] === 0) {// 递归揭示周围格子for (let i = -1; i <= 1; i++) {for (let j = -1; j <= 1; j++) {const newRow = row + i;const newCol = col + j;if (newRow >= 0 && newRow < gameConfig.rows && newCol >= 0 && newCol < gameConfig.cols) {revealCell(newRow, newCol);}}}}
}
- 右键标记功能
function handleRightClick(row, col) {if (gameOver || gameWon || revealed[row][col]) return;// 切换旗帜状态flagged[row][col] = !flagged[row][col];flagCount += flagged[row][col] ? 1 : -1;// 更新UIconst cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);if (flagged[row][col]) {cell.classList.add('flagged');cell.textContent = '🚩';} else {cell.classList.remove('flagged');cell.textContent = '';}// 更新剩余地雷计数remainingMines = gameConfig.mines - flagCount;mineCountElement.textContent = remainingMines;// 检查胜利条件checkWin();
}
- 胜利条件检查
function checkWin() {// 检查是否所有非地雷格子都被揭示let allRevealed = true;for (let i = 0; i < gameConfig.rows; i++) {for (let j = 0; j < gameConfig.cols; j++) {if (board[i][j] !== -1 && !revealed[i][j]) {allRevealed = false;break;}}if (!allRevealed) break;}// 检查是否所有地雷都被正确标记let allMinesFlagged = true;mines.forEach(mine => {if (!flagged[mine.row][mine.col]) {allMinesFlagged = false;}});if (allRevealed || allMinesFlagged) {gameWon = true;clearInterval(timerInterval);statusElement.textContent = `恭喜获胜!用时 ${timer} 秒 🎉`;statusElement.classList.add("win");revealMines();}
}
四、关键CSS样式解析
- 格子基础样式
.cell {width: var(--cell-size, 30px);height: var(--cell-size, 30px);background: linear-gradient(145deg, #95a5a6, #7f8c8d);border: 2px outset #ecf0f1;display: flex;align-items: center;justify-content: center;cursor: pointer;transition: all 0.2s ease;border-radius: 4px;
}.cell:hover:not(.revealed) {background: linear-gradient(145deg, #bdc3c7, #95a5a6);transform: scale(0.95);
}
- 动画效果
/* 揭示动画 */
.revealed {animation: reveal 0.3s ease;
}@keyframes reveal {0% { transform: scale(1); opacity: 0.8; }50% { transform: scale(0.95); opacity: 0.9; }100% { transform: scale(0.97); opacity: 1; }
}/* 地雷爆炸动画 */
.mine {animation: mineExplode 0.5s ease;
}@keyframes mineExplode {0% { transform: scale(1); }50% { transform: scale(1.2); }100% { transform: scale(1); }
}/* 旗帜标记动画 */
.flagged {animation: flagWave 0.5s ease;
}@keyframes flagWave {0% { transform: rotate(-10deg); }50% { transform: rotate(10deg); }100% { transform: rotate(0); }
}
五、如何扩展游戏
- 添加关卡系统:设计不同主题的关卡(沙漠、雪地、太空等)
- 实现存档功能:使用localStorage保存游戏进度
- 添加音效:为点击、爆炸、胜利等事件添加音效
- 设计成就系统:根据游戏表现解锁成就
- 添加多人模式:实现玩家间的竞争或合作模式
六、开发小贴士
- 测试驱动开发:先编写测试用例再实现功能
- 模块化设计:将游戏拆分为独立模块(渲染、逻辑、事件处理)
- 性能优化:对于大型棋盘使用虚拟滚动技术
- 响应式设计:确保在不同设备上都有良好的体验
- 代码注释:为复杂逻辑添加详细注释
七、总结
通过本教程,你已经学会了如何开发一个功能完整的扫雷游戏。记住,游戏开发就像扫雷一样——需要耐心、策略和一点冒险精神!当你遇到"地雷"(bug)时,不要灰心,标记它、分析它,然后优雅地解决它。
现在,快去创建你自己的扫雷游戏吧!如果你在开发过程中遇到任何问题,记得程序员休闲群QQ:708877645里有众多扫雷高手可以帮你排雷!💣➡️🚩
完整源代码
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>精美扫雷游戏</title><style>/* 所有CSS样式保持不变 */* {margin: 0;padding: 0;box-sizing: border-box;font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;}body {display: flex;justify-content: center;align-items: center;min-height: 100vh;background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);padding: 20px;}.game-container {background: rgba(255, 255, 255, 0.9);border-radius: 20px;box-shadow: 0 15px 30px rgba(0, 0, 0, 0.3);padding: 30px;width: 100%;max-width: 800px;text-align: center;transform: translateY(0);transition: transform 0.3s ease;}.game-container:hover {transform: translateY(-5px);}.game-title {font-size: 2.5rem;color: #2c3e50;margin-bottom: 20px;text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);animation: pulse 2s infinite;}@keyframes pulse {0% { transform: scale(1); }50% { transform: scale(1.03); }100% { transform: scale(1); }}.controls {display: flex;justify-content: center;gap: 15px;margin-bottom: 25px;flex-wrap: wrap;}.control-group {background: #f8f9fa;border-radius: 15px;padding: 15px;box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);}.control-title {font-size: 1.2rem;margin-bottom: 10px;color: #3498db;}.difficulty-btn, .size-btn, .new-game-btn {padding: 12px 20px;border: none;border-radius: 50px;cursor: pointer;font-weight: bold;font-size: 1rem;transition: all 0.3s ease;box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);margin: 5px;}.difficulty-btn {background: linear-gradient(135deg, #3498db, #2c3e50);color: white;}.size-btn {background: linear-gradient(135deg, #2ecc71, #27ae60);color: white;}.new-game-btn {background: linear-gradient(135deg, #e74c3c, #c0392b);color: white;font-size: 1.1rem;padding: 15px 25px;}.difficulty-btn:hover, .size-btn:hover, .new-game-btn:hover {transform: translateY(-3px);box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);}.difficulty-btn.active, .size-btn.active {transform: scale(1.05);box-shadow: 0 0 15px rgba(52, 152, 219, 0.6);animation: activeGlow 1.5s infinite alternate;}@keyframes activeGlow {0% { box-shadow: 0 0 10px rgba(52, 152, 219, 0.6); }100% { box-shadow: 0 0 20px rgba(52, 152, 219, 0.8); }}.status-bar {display: flex;justify-content: space-between;margin-bottom: 20px;background: linear-gradient(to right, #3498db, #2c3e50);border-radius: 50px;padding: 15px 25px;color: white;box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);}.status-item {font-size: 1.2rem;font-weight: bold;}.grid-container {overflow: auto;margin: 0 auto;padding: 10px;background: #2c3e50;border-radius: 15px;box-shadow: inset 0 0 20px rgba(0, 0, 0, 0.3);}.grid {display: grid;gap: 2px;margin: 0 auto;background: #34495e;border-radius: 10px;padding: 5px;}.cell {width: var(--cell-size, 30px);height: var(--cell-size, 30px);background: linear-gradient(145deg, #95a5a6, #7f8c8d);border: 2px outset #ecf0f1;display: flex;align-items: center;justify-content: center;cursor: pointer;font-weight: bold;font-size: 1.1rem;transition: all 0.2s ease;user-select: none;border-radius: 4px;}.cell:hover:not(.revealed) {background: linear-gradient(145deg, #bdc3c7, #95a5a6);transform: scale(0.95);}.revealed {background: #ecf0f1;border: 1px solid #bdc3c7;transform: scale(0.97);animation: reveal 0.3s ease;}@keyframes reveal {0% { transform: scale(1); opacity: 0.8; }50% { transform: scale(0.95); opacity: 0.9; }100% { transform: scale(0.97); opacity: 1; }}.mine {background: #e74c3c !important;animation: mineExplode 0.5s ease;}@keyframes mineExplode {0% { transform: scale(1); }50% { transform: scale(1.2); }100% { transform: scale(1); }}.flagged {background: linear-gradient(145deg, #f1c40f, #f39c12);animation: flagWave 0.5s ease;}@keyframes flagWave {0% { transform: rotate(-10deg); }50% { transform: rotate(10deg); }100% { transform: rotate(0); }}/* 数字颜色 */.num-1 { color: #3498db; }.num-2 { color: #2ecc71; }.num-3 { color: #e74c3c; }.num-4 { color: #9b59b6; }.num-5 { color: #e67e22; }.num-6 { color: #1abc9c; }.num-7 { color: #34495e; }.num-8 { color: #7f8c8d; }.status-message {margin-top: 20px;padding: 15px;border-radius: 10px;font-size: 1.3rem;font-weight: bold;min-height: 50px;display: flex;align-items: center;justify-content: center;transition: all 0.5s ease;background: rgba(236, 240, 241, 0.9);box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);}.game-over {color: #e74c3c;animation: shake 0.5s ease-in-out;}@keyframes shake {0%, 100% { transform: translateX(0); }25% { transform: translateX(-10px); }75% { transform: translateX(10px); }}.win {color: #2ecc71;animation: bounce 0.5s ease-in-out;}@keyframes bounce {0%, 100% { transform: translateY(0); }50% { transform: translateY(-10px); }}.instructions {margin-top: 25px;background: rgba(52, 73, 94, 0.1);padding: 15px;border-radius: 10px;text-align: left;}.instructions h3 {color: #2c3e50;margin-bottom: 10px;}.instructions p {margin-bottom: 8px;color: #34495e;}/* 响应式设计 */@media (max-width: 768px) {.controls {flex-direction: column;align-items: center;}.difficulty-btn, .size-btn {width: 100%;max-width: 250px;}.cell {width: 25px;height: 25px;font-size: 0.9rem;}}@media (max-width: 480px) {.cell {width: 20px;height: 20px;font-size: 0.8rem;}.game-title {font-size: 2rem;}}</style>
</head>
<body><div class="game-container"><h1 class="game-title">程序员休闲群QQ:708877645扫雷</h1><div class="controls"><div class="control-group"><div class="control-title">难度选择</div><button class="difficulty-btn active" data-difficulty="beginner">初级</button><button class="difficulty-btn" data-difficulty="intermediate">中级</button><button class="difficulty-btn" data-difficulty="expert">高级</button></div><div class="control-group"><div class="control-title">棋盘大小</div><button class="size-btn active" data-size="small">小</button><button class="size-btn" data-size="medium">中</button><button class="size-btn" data-size="large">大</button></div><button class="new-game-btn">🔄 新游戏</button></div><div class="status-bar"><div class="status-item">⏱️ 时间: <span id="timer">0</span>秒</div><div class="status-item">💣 剩余: <span id="mine-count">10</span></div><div class="status-item">🚩 标记: <span id="flag-count">0</span></div></div><div class="grid-container"><div id="grid" class="grid"></div></div><div id="status" class="status-message">欢迎来到扫雷游戏!选择难度后开始游戏</div><div class="instructions"><h3>游戏说明:</h3><p>• 左键点击:揭开格子</p><p>• 右键点击:标记/取消标记地雷</p><p>• 胜利条件:标记所有地雷并揭开所有安全格子</p><p>• 失败条件:点击到地雷格子</p></div></div><script>// 游戏配置const DIFFICULTY = {beginner: { rows: 9, cols: 9, mines: 10 },intermediate: { rows: 16, cols: 16, mines: 40 },expert: { rows: 16, cols: 30, mines: 99 }};// 修复:正确定义 SIZE_FACTOR 常量const SIZE_FACTOR = {small: 0.8,medium: 1.0,large: 1.2};// 游戏状态let gameConfig = { ...DIFFICULTY.beginner, sizeFactor: SIZE_FACTOR.small };let board = [];let revealed = [];let flagged = [];let mines = [];let gameOver = false;let gameWon = false;let firstClick = true;let timer = 0;let timerInterval = null;let remainingMines = gameConfig.mines;let flagCount = 0;// DOM元素const gridElement = document.getElementById('grid');const statusElement = document.getElementById('status');const timerElement = document.getElementById('timer');const mineCountElement = document.getElementById('mine-count');const flagCountElement = document.getElementById('flag-count');const newGameBtn = document.querySelector('.new-game-btn');// 初始化游戏function initGame() {// 重置游戏状态clearInterval(timerInterval);timer = 0;timerElement.textContent = timer;gameOver = false;gameWon = false;firstClick = true;remainingMines = gameConfig.mines;flagCount = 0;mineCountElement.textContent = remainingMines;flagCountElement.textContent = flagCount;statusElement.textContent = "游戏开始!点击任意格子";statusElement.className = "status-message";// 初始化数组board = [];revealed = [];flagged = [];mines = [];// 创建棋盘createBoard();renderBoard();}// 创建棋盘数据结构function createBoard() {// 初始化二维数组for (let i = 0; i < gameConfig.rows; i++) {board[i] = [];revealed[i] = [];flagged[i] = [];for (let j = 0; j < gameConfig.cols; j++) {board[i][j] = 0;revealed[i][j] = false;flagged[i][j] = false;}}}// 渲染棋盘到DOMfunction renderBoard() {gridElement.innerHTML = '';gridElement.style.gridTemplateColumns = `repeat(${gameConfig.cols}, 1fr)`;// 应用尺寸因子const cellSize = Math.floor(30 * gameConfig.sizeFactor);document.documentElement.style.setProperty('--cell-size', `${cellSize}px`);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();handleRightClick(i, j);});gridElement.appendChild(cell);}}}// 放置地雷(确保第一次点击安全)function placeMines(firstRow, firstCol) {mines = [];let minesPlaced = 0;// 确保第一次点击周围没有地雷const safeCells = [];for (let i = -1; i <= 1; i++) {for (let j = -1; j <= 1; j++) {const newRow = firstRow + i;const newCol = firstCol + j;if (newRow >= 0 && newRow < gameConfig.rows && newCol >= 0 && newCol < gameConfig.cols) {safeCells.push(`${newRow},${newCol}`);}}}// 随机放置地雷while (minesPlaced < gameConfig.mines) {const row = Math.floor(Math.random() * gameConfig.rows);const col = Math.floor(Math.random() * gameConfig.cols);// 跳过第一次点击及其周围的格子if (row === firstRow && col === firstCol) continue;if (safeCells.includes(`${row},${col}`)) continue;if (board[row][col] === -1) continue;board[row][col] = -1;mines.push({ row, col });minesPlaced++;// 更新周围格子的数字for (let i = -1; i <= 1; i++) {for (let j = -1; j <= 1; j++) {const newRow = row + i;const newCol = col + j;if (newRow >= 0 && newRow < gameConfig.rows && newCol >= 0 && newCol < gameConfig.cols && board[newRow][newCol] !== -1) {board[newRow][newCol]++;}}}}}// 处理格子点击function handleCellClick(row, col) {if (gameOver || gameWon || flagged[row][col]) return;// 如果是第一次点击,放置地雷if (firstClick) {placeMines(row, col);firstClick = false;// 开始计时timerInterval = setInterval(() => {timer++;timerElement.textContent = timer;}, 1000);}// 如果点击到地雷if (board[row][col] === -1) {revealMines();gameOver = true;statusElement.textContent = "游戏结束!你踩到地雷了 💥";statusElement.classList.add("game-over");clearInterval(timerInterval);return;}// 揭示格子revealCell(row, col);// 检查胜利条件checkWin();}// 处理右键点击(标记旗帜)function handleRightClick(row, col) {if (gameOver || gameWon || revealed[row][col]) return;// 切换旗帜状态flagged[row][col] = !flagged[row][col];flagCount += flagged[row][col] ? 1 : -1;flagCountElement.textContent = flagCount;// 更新UIconst cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);if (flagged[row][col]) {cell.classList.add('flagged');cell.textContent = '🚩';} else {cell.classList.remove('flagged');cell.textContent = '';}// 更新剩余地雷计数remainingMines = gameConfig.mines - flagCount;mineCountElement.textContent = remainingMines;// 检查胜利条件checkWin();}// 揭示格子function revealCell(row, col) {if (revealed[row][col] || flagged[row][col]) return;revealed[row][col] = true;const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);cell.classList.add('revealed');if (board[row][col] > 0) {cell.textContent = board[row][col];cell.classList.add(`num-${board[row][col]}`);} else if (board[row][col] === 0) {// 如果是空白格子,递归揭示周围格子for (let i = -1; i <= 1; i++) {for (let j = -1; j <= 1; j++) {const newRow = row + i;const newCol = col + j;if (newRow >= 0 && newRow < gameConfig.rows && newCol >= 0 && newCol < gameConfig.cols) {revealCell(newRow, newCol);}}}}}// 揭示所有地雷(游戏结束时)function revealMines() {mines.forEach(mine => {const cell = document.querySelector(`.cell[data-row="${mine.row}"][data-col="${mine.col}"]`);if (!flagged[mine.row][mine.col]) {cell.classList.add('mine', 'revealed');cell.textContent = '💣';}});}// 检查胜利条件function checkWin() {// 检查是否所有非地雷格子都被揭示let allRevealed = true;for (let i = 0; i < gameConfig.rows; i++) {for (let j = 0; j < gameConfig.cols; j++) {if (board[i][j] !== -1 && !revealed[i][j]) {allRevealed = false;break;}}if (!allRevealed) break;}// 检查是否所有地雷都被正确标记let allMinesFlagged = true;mines.forEach(mine => {if (!flagged[mine.row][mine.col]) {allMinesFlagged = false;}});if (allRevealed || allMinesFlagged) {gameWon = true;clearInterval(timerInterval);statusElement.textContent = `恭喜获胜!用时 ${timer} 秒 🎉`;statusElement.classList.add("win");revealMines();}}// 设置难度function setDifficulty(difficulty) {gameConfig = { ...DIFFICULTY[difficulty], sizeFactor: gameConfig.sizeFactor };// 更新UIdocument.querySelectorAll('.difficulty-btn').forEach(btn => {btn.classList.toggle('active', btn.dataset.difficulty === difficulty);});initGame();}// 设置棋盘大小(确保调用initGame重新初始化)function setSize(size) {gameConfig.sizeFactor = SIZE_FACTOR[size];// 更新UIdocument.querySelectorAll('.size-btn').forEach(btn => {btn.classList.toggle('active', btn.dataset.size === size);});// 重新初始化游戏以应用新尺寸initGame();}// 事件监听document.querySelectorAll('.difficulty-btn').forEach(btn => {btn.addEventListener('click', () => setDifficulty(btn.dataset.difficulty));});document.querySelectorAll('.size-btn').forEach(btn => {btn.addEventListener('click', () => setSize(btn.dataset.size));});newGameBtn.addEventListener('click', initGame);// 初始化游戏initGame();</script>
</body>
</html>