<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>扫雷游戏</title>
</head>
<body>
<div class="game-container">
<h1>💣 扫雷 Minesweeper</h1>
<div class="controls">
<button data-level="easy" class="active">简单 9×9</button>
<button data-level="medium">中等 16×16</button>
<button data-level="hard">困难 20×20</button>
</div>
<div class="info-bar">
<div class="info-item">💣 <span id="mineCount">10</span></div>
<button class="reset-btn" title="重新开始">😊</button>
<div class="info-item">⏱️ <span id="timer">0</span></div>
</div>
<div class="board" id="board"></div>
<div class="instructions">
左键点击揭开 | 右键标记地雷 | 双击快速揭开周围
</div>
</div>
<div class="modal" id="modal">
<div class="modal-content">
<h2 id="modalTitle">游戏结束</h2>
<p id="modalMsg">用时: 0秒</p>
<button>再来一局</button>
</div>
</div>
</body>
</html>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
user-select: none;
}
html,
body {
height: 100%;
overflow: hidden;
font-family: 'Segoe UI', Arial, sans-serif;
}
body {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
padding: 10px;
}
.game-container {
display: flex;
flex-direction: column;
align-items: center;
max-height: 100%;
max-width: 100%;
}
h1 {
color: #eee;
font-size: clamp(1.2rem, 4vmin, 2rem);
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3);
}
.controls {
display: flex;
gap: 10px;
margin-bottom: 10px;
flex-wrap: wrap;
justify-content: center;
}
.controls button {
padding: 8px 16px;
font-size: clamp(0.7rem, 2vmin, 0.9rem);
border: none;
border-radius: 6px;
cursor: pointer;
background: #4a5568;
color: white;
transition: all 0.2s;
}
.controls button:hover {
background: #5a6578;
transform: translateY(-2px);
}
.controls button.active {
background: #3182ce;
box-shadow: 0 0 10px rgba(49, 130, 206, 0.5);
}
.info-bar {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 500px;
margin-bottom: 10px;
padding: 8px 15px;
background: #2d3748;
border-radius: 8px;
color: #fff;
font-size: clamp(0.8rem, 2.5vmin, 1rem);
}
.info-item {
display: flex;
align-items: center;
gap: 5px;
}
.info-item span {
font-weight: bold;
color: #f6e05e;
min-width: 30px;
}
.reset-btn {
font-size: clamp(1.2rem, 3vmin, 1.5rem);
padding: 5px 15px;
border: none;
border-radius: 6px;
cursor: pointer;
background: #4a5568;
transition: transform 0.2s;
}
.reset-btn:hover {
transform: scale(1.1);
}
.board {
display: grid;
gap: 2px;
padding: 10px;
background: #2d3748;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.4);
}
.cell {
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(145deg, #4a5568, #3d4452);
border-radius: 4px;
cursor: pointer;
font-weight: bold;
transition: all 0.1s;
border: 1px solid #5a6578;
}
.cell:hover:not(.revealed) {
background: linear-gradient(145deg, #5a6578, #4d5462);
transform: scale(1.05);
}
.cell.revealed {
background: #1a202c;
cursor: default;
border-color: #2d3748;
}
.cell.mine {
background: #e53e3e;
}
.cell.flagged::after {
content: "🚩";
}
.cell.question::after {
content: "❓";
}
.cell.exploded {
background: #ff0000;
animation: explode 0.3s ease-out;
}
@keyframes explode {
0% {
transform: scale(1);
}
50% {
transform: scale(1.3);
}
100% {
transform: scale(1);
}
}
.num-1 {
color: #63b3ed;
}
.num-2 {
color: #68d391;
}
.num-3 {
color: #fc8181;
}
.num-4 {
color: #b794f4;
}
.num-5 {
color: #f6ad55;
}
.num-6 {
color: #4fd1c5;
}
.num-7 {
color: #fff;
}
.num-8 {
color: #a0aec0;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
align-items: center;
justify-content: center;
z-index: 100;
}
.modal.show {
display: flex;
}
.modal-content {
background: #2d3748;
padding: 30px 50px;
border-radius: 15px;
text-align: center;
color: #fff;
animation: popIn 0.3s ease-out;
}
@keyframes popIn {
0% {
transform: scale(0.5);
opacity: 0;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.modal h2 {
font-size: 2rem;
margin-bottom: 10px;
}
.modal p {
font-size: 1.2rem;
color: #a0aec0;
margin-bottom: 20px;
}
.modal button {
padding: 10px 30px;
font-size: 1rem;
border: none;
border-radius: 8px;
cursor: pointer;
background: #3182ce;
color: white;
transition: background 0.2s;
}
.modal button:hover {
background: #4299e1;
}
.win h2 {
color: #68d391;
}
.lose h2 {
color: #fc8181;
}
.instructions {
color: #718096;
font-size: clamp(0.6rem, 1.5vmin, 0.75rem);
margin-top: 10px;
text-align: center;
}
const LEVELS = {
easy: { rows: 9, cols: 9, mines: 10 },
medium: { rows: 16, cols: 16, mines: 40 },
hard: { rows: 20, cols: 20, mines: 60 }
};
let currentLevel = 'easy';
let board = [];
let revealed = [];
let flagged = [];
let gameOver = false;
let gameStarted = false;
let timer = 0;
let timerInterval = null;
let minesLeft = 0;
const boardEl = document.getElementById('board');
const timerEl = document.getElementById('timer');
const mineCountEl = document.getElementById('mineCount');
const modal = document.getElementById('modal');
const modalTitle = document.getElementById('modalTitle');
const modalMsg = document.getElementById('modalMsg');
// 难度选择
document.querySelectorAll('.controls button').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('.controls button').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
currentLevel = btn.dataset.level;
initGame();
});
});
function initGame() {
const { rows, cols, mines } = LEVELS[currentLevel];
// 重置状态
gameOver = false;
gameStarted = false;
timer = 0;
minesLeft = mines;
clearInterval(timerInterval);
timerEl.textContent = '0';
mineCountEl.textContent = mines;
document.querySelector('.reset-btn').textContent = '😊';
// 初始化数组
board = Array(rows).fill(null).map(() => Array(cols).fill(0));
revealed = Array(rows).fill(null).map(() => Array(cols).fill(false));
flagged = Array(rows).fill(null).map(() => Array(cols).fill(0)); // 0:无, 1:旗帜, 2:问号
// 计算格子大小
const maxHeight = window.innerHeight - 200;
const maxWidth = window.innerWidth - 40;
const cellSize = Math.floor(Math.min(maxHeight / rows, maxWidth / cols, 35));
const fontSize = Math.max(cellSize * 0.5, 10);
// 创建DOM
boardEl.innerHTML = '';
boardEl.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)`;
boardEl.style.gridTemplateRows = `repeat(${rows}, ${cellSize}px)`;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const cell = document.createElement('div');
cell.className = 'cell';
cell.style.width = cellSize + 'px';
cell.style.height = cellSize + 'px';
cell.style.fontSize = fontSize + 'px';
cell.dataset.row = r;
cell.dataset.col = c;
cell.addEventListener('click', () => handleClick(r, c));
cell.addEventListener('contextmenu', (e) => {
e.preventDefault();
handleRightClick(r, c);
});
cell.addEventListener('dblclick', () => handleDoubleClick(r, c));
boardEl.appendChild(cell);
}
}
}
function placeMines(firstR, firstC) {
const { rows, cols, mines } = LEVELS[currentLevel];
let placed = 0;
while (placed < mines) {
const r = Math.floor(Math.random() * rows);
const c = Math.floor(Math.random() * cols);
// 避开第一次点击的位置及周围
if (Math.abs(r - firstR) <= 1 && Math.abs(c - firstC) <= 1) continue;
if (board[r][c] === -1) continue;
board[r][c] = -1;
placed++;
}
// 计算数字
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (board[r][c] === -1) continue;
board[r][c] = countAdjacentMines(r, c);
}
}
}
function countAdjacentMines(row, col) {
let count = 0;
forEachNeighbor(row, col, (r, c) => {
if (board[r][c] === -1) count++;
});
return count;
}
function forEachNeighbor(row, col, callback) {
const { rows, cols } = LEVELS[currentLevel];
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0) continue;
const r = row + dr, c = col + dc;
if (r >= 0 && r < rows && c >= 0 && c < cols) {
callback(r, c);
}
}
}
}
function getCell(r, c) {
return boardEl.children[r * LEVELS[currentLevel].cols + c];
}
function handleClick(row, col) {
if (gameOver || flagged[row][col] === 1 || revealed[row][col]) return;
if (!gameStarted) {
gameStarted = true;
placeMines(row, col);
timerInterval = setInterval(() => {
timer++;
timerEl.textContent = timer;
}, 1000);
}
revealCell(row, col);
checkWin();
}
function revealCell(row, col) {
if (revealed[row][col] || flagged[row][col] === 1) return;
revealed[row][col] = true;
const cell = getCell(row, col);
cell.classList.add('revealed');
cell.classList.remove('flagged', 'question');
if (board[row][col] === -1) {
// 踩雷
cell.classList.add('exploded');
cell.textContent = '💣';
endGame(false);
return;
}
if (board[row][col] > 0) {
cell.textContent = board[row][col];
cell.classList.add('num-' + board[row][col]);
} else {
// 空白格,展开周围
forEachNeighbor(row, col, (r, c) => {
if (!revealed[r][c]) revealCell(r, c);
});
}
}
function handleRightClick(row, col) {
if (gameOver || revealed[row][col]) return;
const cell = getCell(row, col);
flagged[row][col] = (flagged[row][col] + 1) % 3;
cell.classList.remove('flagged', 'question');
if (flagged[row][col] === 1) {
cell.classList.add('flagged');
minesLeft--;
} else if (flagged[row][col] === 2) {
cell.classList.add('question');
minesLeft++;
}
mineCountEl.textContent = minesLeft;
}
function handleDoubleClick(row, col) {
if (gameOver || !revealed[row][col] || board[row][col] <= 0) return;
// 计算周围旗帜数
let flagCount = 0;
forEachNeighbor(row, col, (r, c) => {
if (flagged[r][c] === 1) flagCount++;
});
// 如果旗帜数等于数字,揭开周围未标记的格子
if (flagCount === board[row][col]) {
forEachNeighbor(row, col, (r, c) => {
if (!revealed[r][c] && flagged[r][c] !== 1) {
revealCell(r, c);
}
});
checkWin();
}
}
function endGame(won) {
gameOver = true;
clearInterval(timerInterval);
document.querySelector('.reset-btn').textContent = won ? '😎' : '😵';
if (!won) {
// 显示所有地雷
const { rows, cols } = LEVELS[currentLevel];
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (board[r][c] === -1 && !revealed[r][c]) {
const cell = getCell(r, c);
cell.classList.add('revealed', 'mine');
cell.textContent = '💣';
}
}
}
}
setTimeout(() => {
showModal(won);
}, 500);
}
function checkWin() {
if (gameOver) return;
const { rows, cols, mines } = LEVELS[currentLevel];
let revealedCount = 0;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
if (revealed[r][c]) revealedCount++;
}
}
if (revealedCount === rows * cols - mines) {
endGame(true);
}
}
function showModal(won) {
modal.classList.add('show');
modal.querySelector('.modal-content').className = 'modal-content ' + (won ? 'win' : 'lose');
modalTitle.textContent = won ? '🎉 恭喜胜利!' : '💥 游戏结束';
modalMsg.textContent = `用时: ${timer} 秒`;
}
function closeModal() {
modal.classList.remove('show');
}
// 窗口大小改变时重新计算
window.addEventListener('resize', () => {
if (!gameStarted) initGame();
});
// 初始化
initGame();
// 重新开始
document.querySelector('.reset-btn').addEventListener("click", function () {
initGame();
});
// 再来一局
modal.querySelector("button").addEventListener("click", function () {
closeModal();
initGame();
})