用vscode做一个简单的扫雷小游戏
我们将使用 HTML、CSS(通过 Tailwind CSS v3)和 JavaScript 来实现一个扫雷小游戏
step 1 准备工作
- 打开 VS Code
- 创建一个新文件夹(例如
minesweeper-game
) - 在该文件夹中创建三个文件:
index.html
游戏的 HTML 结构style.css
游戏的样式script.js
游戏的逻辑
step 2 编写代码
1.index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>扫雷小游戏</title><!-- 引入Tailwind CSS --><script src="https://cdn.tailwindcss.com"></script><!-- 引入Font Awesome --><link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet"><style type="text/tailwindcss">@layer utilities {.content-auto {content-visibility: auto;}.cell-shadow {box-shadow: inset -2px -2px 5px rgba(0, 0, 0, 0.2), inset 2px 2px 5px rgba(255, 255, 255, 0.8);}.cell-shadow-pressed {box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.3);}.game-container-shadow {box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);}}</style><style>/* 数字颜色 */.cell-number-1 { color: #0000ff; }.cell-number-2 { color: #008000; }.cell-number-3 { color: #ff0000; }.cell-number-4 { color: #000080; }.cell-number-5 { color: #800000; }.cell-number-6 { color: #008080; }.cell-number-7 { color: #000000; }.cell-number-8 { color: #808080; }/* 单元格基础样式 */.cell {display: flex;align-items: center;justify-content: center;font-weight: bold;cursor: pointer;user-select: none;transition: all 0.1s ease;}/* 高亮显示可能揭示的区域 */.bg-highlight {background-color: #d1d5db !important;}/* 棋盘容器样式 */.board-container {overflow: auto;max-height: calc(100vh - 220px);scrollbar-width: thin;display: flex;justify-content: center; /* 水平居中 */align-items: flex-start; /* 垂直顶部对齐 */padding: 15px;}.board-container::-webkit-scrollbar {width: 6px;height: 6px;}.board-container::-webkit-scrollbar-thumb {background-color: #cbd5e1;border-radius: 3px;}.board-container::-webkit-scrollbar-track {background-color: #f1f5f9;}/* 游戏主容器最大宽度限制 */.game-main-container {max-width: 90vw;margin: 0 auto;}</style>
</head>
<body class="bg-gradient-to-br from-blue-50 to-indigo-100 min-h-screen flex flex-col items-center justify-start p-4 md:p-8 pt-8"><div class="game-main-container"><!-- 游戏标题 --><h1 class="text-3xl md:text-4xl font-bold text-center mb-6 text-gray-800"><i class="fa fa-bomb mr-2 text-red-500"></i>扫雷小游戏</h1><!-- 游戏容器 --><div class="bg-white rounded-xl p-4 md:p-6 game-container-shadow"><!-- 游戏信息栏 --><div class="flex justify-between items-center mb-6 p-3 bg-gray-50 rounded-lg"><!-- 地雷计数 --><div class="flex items-center bg-gray-100 px-4 py-2 rounded-lg border border-gray-200"><i class="fa fa-flag text-red-500 mr-2"></i><span id="mine-count" class="font-mono text-xl font-bold text-gray-800">10</span></div><!-- 重置按钮 --><button id="reset-btn" class="w-12 h-12 rounded-full bg-primary hover:bg-primary/90 text-white flex items-center justify-center transition-all duration-200 transform hover:scale-105"><i class="fa fa-refresh text-xl"></i></button><!-- 计时器 --><div class="flex items-center bg-gray-100 px-4 py-2 rounded-lg border border-gray-200"><i class="fa fa-clock-o text-blue-500 mr-2"></i><span id="timer" class="font-mono text-xl font-bold text-gray-800">0</span></div></div><!-- 棋盘容器(添加滚动功能和居中) --><div class="board-container mb-6 bg-gray-100 rounded-lg border border-gray-200"><!-- 游戏棋盘 --><div id="game-board" class="grid gap-0.5"></div></div><!-- 难度选择 --><div class="flex flex-wrap justify-center gap-3"><button class="difficulty-btn bg-primary text-white px-4 py-2 rounded-lg hover:bg-primary/90 transition-colors" data-size="9" data-mines="10">初级 (9×9)</button><button class="difficulty-btn bg-neutral-300 hover:bg-neutral-400 px-4 py-2 rounded-lg transition-colors" data-size="16" data-mines="40">中级 (16×16)</button><button class="difficulty-btn bg-neutral-300 hover:bg-neutral-400 px-4 py-2 rounded-lg transition-colors" data-size="30" data-mines="99">高级 (30×30)</button></div></div></div><!-- 结果弹窗 --><div id="result-modal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 hidden"><div id="modal-content" class="bg-white rounded-xl p-6 max-w-md w-full mx-4 transform transition-all duration-300 scale-95 opacity-0"><h2 id="result-title" class="text-2xl font-bold mb-3 text-primary">恭喜你赢了!</h2><p id="result-message" class="text-lg mb-6 text-gray-600">用时: 30 秒</p><button id="play-again" class="w-full bg-primary hover:bg-primary/90 text-white py-3 rounded-lg transition-colors text-lg font-medium">再玩一次</button></div></div><script src="script.js"></script>
</body>
</html>
2.script.js
// 游戏状态变量
let gameBoard = [];
let revealed = [];
let flagged = [];
let mines = [];
let gameSize = 9; // 默认初级9x9
let mineCount = 10; // 默认10个地雷
let gameStarted = false;
let gameOver = false;
let timerInterval = null;
let seconds = 0;
let remainingMines = mineCount;
let leftButtonDown = false;
let rightButtonDown = false;// DOM 元素
const boardElement = document.getElementById('game-board');
const mineCountElement = document.getElementById('mine-count');
const timerElement = document.getElementById('timer');
const resetButton = document.getElementById('reset-btn');
const resultModal = document.getElementById('result-modal');
const modalContent = document.getElementById('modal-content');
const resultTitle = document.getElementById('result-title');
const resultMessage = document.getElementById('result-message');
const playAgainButton = document.getElementById('play-again');
const difficultyButtons = document.querySelectorAll('.difficulty-btn');
const boardContainer = document.querySelector('.board-container');// 初始化游戏
function initGame() {// 重置游戏状态gameBoard = Array(gameSize).fill().map(() => Array(gameSize).fill(0));revealed = Array(gameSize).fill().map(() => Array(gameSize).fill(false));flagged = Array(gameSize).fill().map(() => Array(gameSize).fill(false));mines = [];gameStarted = false;gameOver = false;seconds = 0;remainingMines = mineCount;leftButtonDown = false;rightButtonDown = false;// 更新UImineCountElement.textContent = remainingMines;timerElement.textContent = '0';clearInterval(timerInterval);boardElement.innerHTML = '';// 计算单元格大小const cellSize = calculateCellSize();// 设置棋盘大小和单元格样式boardElement.style.gridTemplateColumns = `repeat(${gameSize}, ${cellSize}px)`;// 创建格子for (let row = 0; row < gameSize; row++) {for (let col = 0; col < gameSize; col++) {const cell = document.createElement('div');cell.classList.add('cell', 'bg-blue-200', 'cell-shadow');cell.dataset.row = row;cell.dataset.col = col;// 设置单元格大小cell.style.width = `${cellSize}px`;cell.style.height = `${cellSize}px`;cell.style.fontSize = `${Math.max(12, cellSize * 0.6)}px`; // 字体大小随单元格调整// 添加点击事件cell.addEventListener('click', () => handleCellClick(row, col));cell.addEventListener('contextmenu', (e) => {e.preventDefault();handleRightClick(row, col);});// 添加鼠标按下和释放事件,用于检测双键点击cell.addEventListener('mousedown', (e) => {if (e.button === 0) leftButtonDown = true; // 左键if (e.button === 2) rightButtonDown = true; // 右键checkDoubleClick(row, col);});// 全局鼠标释放事件,确保状态正确重置document.addEventListener('mouseup', (e) => {if (e.button === 0) leftButtonDown = false;if (e.button === 2) rightButtonDown = false;});// 添加悬停效果,显示可能揭示的区域cell.addEventListener('mouseover', () => {if ((leftButtonDown || rightButtonDown) && revealed[row][col] && gameBoard[row][col] > 0 && !gameOver) {highlightPotentialRevealArea(row, col);}});cell.addEventListener('mouseout', () => {removeHighlightFromArea(row, col);});boardElement.appendChild(cell);}}// 隐藏结果弹窗resultModal.classList.add('hidden');
}// 计算单元格大小,使棋盘适应容器并居中
function calculateCellSize() {// 获取容器可用宽度(减去内边距)const containerWidth = boardContainer.clientWidth - 40; // 减去内边距和预留空间const containerHeight = boardContainer.clientHeight - 40;// 根据游戏大小计算最大可能的单元格尺寸const maxByWidth = Math.floor(containerWidth / gameSize);const maxByHeight = Math.floor(containerHeight / gameSize);// 取最小值,确保棋盘能完整显示let cellSize = Math.min(maxByWidth, maxByHeight);// 设置最小和最大单元格尺寸限制return Math.max(16, Math.min(cellSize, 40));
}// 当窗口大小改变时重新计算棋盘大小并保持居中
window.addEventListener('resize', () => {if (gameBoard.length === 0) return; // 游戏未初始化时不处理const cellSize = calculateCellSize();boardElement.style.gridTemplateColumns = `repeat(${gameSize}, ${cellSize}px)`;// 更新所有单元格大小const cells = document.querySelectorAll('.cell');cells.forEach(cell => {cell.style.width = `${cellSize}px`;cell.style.height = `${cellSize}px`;cell.style.fontSize = `${Math.max(12, cellSize * 0.6)}px`;});
});// 放置地雷
function placeMines(firstRow, firstCol) {let minesPlaced = 0;// 确保首次点击不是地雷,并且周围没有地雷const safeZone = [];for (let r = Math.max(0, firstRow - 1); r <= Math.min(gameSize - 1, firstRow + 1); r++) {for (let c = Math.max(0, firstCol - 1); c <= Math.min(gameSize - 1, firstCol + 1); c++) {safeZone.push(`${r},${c}`);}}while (minesPlaced < mineCount) {const row = Math.floor(Math.random() * gameSize);const col = Math.floor(Math.random() * gameSize);// 检查是否是安全区或已经放置了地雷if (!safeZone.includes(`${row},${col}`) && gameBoard[row][col] !== 'M') {gameBoard[row][col] = 'M';mines.push({ row, col });minesPlaced++;// 更新周围格子的数字updateSurroundingNumbers(row, col);}}
}// 更新周围格子的数字
function updateSurroundingNumbers(row, col) {for (let r = Math.max(0, row - 1); r <= Math.min(gameSize - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(gameSize - 1, col + 1); c++) {if (r === row && c === col) continue; // 跳过地雷本身if (gameBoard[r][c] !== 'M') {gameBoard[r][c]++;}}}
}// 处理左键点击
function handleCellClick(row, col) {// 如果游戏结束、已经揭示或已标记,则不处理if (gameOver || revealed[row][col] || flagged[row][col]) return;// 首次点击开始游戏if (!gameStarted) {gameStarted = true;placeMines(row, col);startTimer();}const cell = getCellElement(row, col);// 点击到地雷,游戏结束if (gameBoard[row][col] === 'M') {cell.classList.remove('bg-blue-200', 'cell-shadow');cell.classList.add('bg-red-500', 'cell-shadow-pressed');cell.innerHTML = '<i class="fa fa-bomb"></i>';gameOver = true;clearInterval(timerInterval);revealAllMines();showResult(false);return;}// 揭示格子revealCell(row, col);// 检查是否获胜checkWin();
}// 处理右键点击(标记地雷)
function handleRightClick(row, col) {if (gameOver || revealed[row][col]) return;// 首次右键点击也开始计时,但不放置地雷if (!gameStarted) {gameStarted = true;startTimer();}const cell = getCellElement(row, col);// 切换标记状态flagged[row][col] = !flagged[row][col];if (flagged[row][col]) {cell.classList.remove('bg-blue-200');cell.classList.add('bg-yellow-300');cell.innerHTML = '<i class="fa fa-flag text-red-600"></i>';remainingMines--;} else {cell.classList.remove('bg-yellow-300');cell.classList.add('bg-blue-200');cell.innerHTML = '';remainingMines++;}// 更新剩余地雷数mineCountElement.textContent = remainingMines;// 检查是否获胜checkWin();
}// 检查双键点击(左右键同时按下)
function checkDoubleClick(row, col) {// 只有当格子已揭示且是数字,并且同时按下左右键时才处理if (leftButtonDown && rightButtonDown && revealed[row][col] && gameBoard[row][col] > 0 && gameBoard[row][col] !== 'M' && !gameOver) {// 计算周围已标记的地雷数const flaggedCount = countSurroundingFlags(row, col);// 如果已标记的地雷数等于格子显示的数字if (flaggedCount === gameBoard[row][col]) {// 揭示周围所有未揭示的格子revealSurroundingCells(row, col);}}
}// 计算周围已标记的地雷数
function countSurroundingFlags(row, col) {let count = 0;for (let r = Math.max(0, row - 1); r <= Math.min(gameSize - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(gameSize - 1, col + 1); c++) {if (r === row && c === col) continue; // 跳过当前格子if (flagged[r][c]) {count++;}}}return count;
}// 揭示周围所有未揭示的格子
function revealSurroundingCells(row, col) {for (let r = Math.max(0, row - 1); r <= Math.min(gameSize - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(gameSize - 1, col + 1); c++) {if (r === row && c === col) continue; // 跳过当前格子// 如果点击到未标记的地雷,游戏结束if (gameBoard[r][c] === 'M' && !flagged[r][c]) {handleMineHit(r, c);return;}// 揭示未揭示且未标记的格子if (!revealed[r][c] && !flagged[r][c]) {revealCell(r, c);}}}// 检查是否获胜checkWin();
}// 处理踩到地雷的情况
function handleMineHit(row, col) {const cell = getCellElement(row, col);cell.classList.remove('bg-blue-200', 'cell-shadow');cell.classList.add('bg-red-500', 'cell-shadow-pressed');cell.innerHTML = '<i class="fa fa-bomb"></i>';gameOver = true;clearInterval(timerInterval);revealAllMines();showResult(false);
}// 高亮显示可能揭示的区域
function highlightPotentialRevealArea(row, col) {for (let r = Math.max(0, row - 1); r <= Math.min(gameSize - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(gameSize - 1, col + 1); c++) {if (r === row && c === col) continue; // 跳过当前格子const cell = getCellElement(r, c);if (!revealed[r][c] && !flagged[r][c]) {cell.classList.add('bg-blue-300');}}}
}// 移除区域高亮
function removeHighlightFromArea(row, col) {for (let r = Math.max(0, row - 1); r <= Math.min(gameSize - 1, row + 1); r++) {for (let c = Math.max(0, col - 1); c <= Math.min(gameSize - 1, col + 1); c++) {if (r === row && c === col) continue; // 跳过当前格子const cell = getCellElement(r, c);cell.classList.remove('bg-blue-300');}}
}// 揭示格子
function revealCell(row, col) {// 检查边界和是否已经揭示if (row < 0 || row >= gameSize || col < 0 || col >= gameSize || revealed[row][col]) {return;}revealed[row][col] = true;const cell = getCellElement(row, col);cell.classList.remove('bg-blue-200', 'cell-shadow', 'bg-yellow-300', 'bg-blue-300');cell.classList.add('bg-red-100', 'cell-shadow-pressed');// 显示格子内容const value = gameBoard[row][col];if (value === 0) {// 空白格子,递归揭示周围格子for (let r = row - 1; r <= row + 1; r++) {for (let c = col - 1; c <= col + 1; c++) {if (r !== row || c !== col) {revealCell(r, c);}}}} else if (value !== 'M') {// 显示数字,使用蓝色系的不同色调cell.textContent = value;const numberColors = ['', 'text-blue-600', // 1'text-green-700', // 2'text-red-600', // 3'text-purple-700', // 4'text-orange-600', // 5'text-teal-600', // 6'text-gray-800', // 7'text-gray-600' // 8];cell.classList.add(numberColors[value]);}
}// 揭示所有地雷(游戏结束时)
function revealAllMines() {for (const { row, col } of mines) {if (!flagged[row][col]) {const cell = getCellElement(row, col);cell.classList.remove('bg-blue-200', 'cell-shadow', 'bg-yellow-300', 'bg-blue-300');cell.classList.add('bg-blue-100', 'cell-shadow-pressed');if (gameBoard[row][col] === 'M') {cell.innerHTML = '<i class="fa fa-bomb text-red-600"></i>';}}}// 显示错误标记for (let row = 0; row < gameSize; row++) {for (let col = 0; col < gameSize; col++) {if (flagged[row][col] && gameBoard[row][col] !== 'M') {const cell = getCellElement(row, col);cell.classList.remove('bg-yellow-300', 'bg-blue-300');cell.classList.add('bg-blue-100');cell.innerHTML = '<i class="fa fa-times text-red-600"></i>';}}}
}// 检查是否获胜
function checkWin() {let revealedCount = 0;// 计算已揭示的非地雷格子数for (let row = 0; row < gameSize; row++) {for (let col = 0; col < gameSize; col++) {if (revealed[row][col]) {revealedCount++;}}}// 所有非地雷格子都被揭示,或者所有地雷都被正确标记const totalCells = gameSize * gameSize;const nonMineCells = totalCells - mineCount;if (revealedCount === nonMineCells) {gameOver = true;clearInterval(timerInterval);// 标记所有剩余地雷for (const { row, col } of mines) {if (!flagged[row][col]) {const cell = getCellElement(row, col);cell.classList.remove('bg-blue-200', 'bg-blue-300');cell.classList.add('bg-yellow-300');cell.innerHTML = '<i class="fa fa-flag text-red-600"></i>';flagged[row][col] = true;}}showResult(true);}
}// 开始计时器
function startTimer() {timerInterval = setInterval(() => {seconds++;timerElement.textContent = seconds;}, 1000);
}// 显示结果弹窗
function showResult(isWin) {resultTitle.textContent = isWin ? '恭喜你赢了!' : '很遗憾,踩到地雷了!';resultTitle.className = isWin ? 'text-2xl font-bold mb-3 text-blue-600' : 'text-2xl font-bold mb-3 text-red-600';resultMessage.textContent = `用时: ${seconds} 秒`;// 显示弹窗并添加动画resultModal.classList.remove('hidden');setTimeout(() => {modalContent.classList.remove('scale-95', 'opacity-0');modalContent.classList.add('scale-100', 'opacity-100');}, 10);
}// 获取格子元素
function getCellElement(row, col) {return boardElement.querySelector(`[data-row="${row}"][data-col="${col}"]`);
}// 事件监听
resetButton.addEventListener('click', () => {// 添加旋转动画resetButton.classList.add('animate-spin');setTimeout(() => {resetButton.classList.remove('animate-spin');initGame();}, 300);
});playAgainButton.addEventListener('click', () => {modalContent.classList.remove('scale-100', 'opacity-100');modalContent.classList.add('scale-95', 'opacity-0');setTimeout(() => {initGame();}, 300);
});// 难度选择
difficultyButtons.forEach(button => {button.addEventListener('click', () => {// 更新按钮样式difficultyButtons.forEach(btn => {btn.classList.remove('bg-blue-600', 'text-white');btn.classList.add('bg-blue-200', 'hover:bg-blue-300');});button.classList.remove('bg-blue-200', 'hover:bg-blue-300');button.classList.add('bg-blue-600', 'text-white');// 设置游戏难度gameSize = parseInt(button.dataset.size);mineCount = parseInt(button.dataset.mines);initGame();});
});// 阻止右键菜单在游戏区域弹出
boardElement.addEventListener('contextmenu', e => e.preventDefault());// 初始化游戏
initGame();
3.style.css
/* 格子样式 */
.cell {width: 24px;height: 24px;display: flex;align-items: center;justify-content: center;font-weight: bold;cursor: pointer;user-select: none;transition: all 0.1s ease;
}/* 数字颜色 */
.cell-number-1 { color: #0000FF; }
.cell-number-2 { color: #008000; }
.cell-number-3 { color: #FF0000; }
.cell-number-4 { color: #000080; }
.cell-number-5 { color: #800000; }
.cell-number-6 { color: #008080; }
.cell-number-7 { color: #000000; }
.cell-number-8 { color: #808080; }/* 响应式调整 */
@media (max-width: 480px) {.cell {width: 20px;height: 20px;font-size: 12px;}
}
step 3 运行
- 在 VS Code 中安装 "Live Server" 扩展(如果尚未安装)
- 右键点击
index.html
文件 - 选择 "Open with Live Server"