我们将使用 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;
// 更新UI
mineCountElement.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"