扫雷游戏
一个使用纯HTML、CSS和JavaScript实现的经典扫雷游戏。
游戏特点
- 🎮 经典玩法:与Windows经典扫雷游戏相同的规则和体验
- 🎚️ 多级难度:提供初级(12x12)、中级(16x16)和高级(19x19)三种难度选择
- 🎯 第一次点击安全:首次点击永远不会触雷
- 🚩 标记功能:右键点击可标记可疑的地雷位置
- 🔢 数字提示:显示周围地雷数量的彩色数字
- 📱 响应式设计:适配不同屏幕尺寸,包括移动设备
在线体验
游戏截图
安装与运行
喜欢的话,可以去我的github上Fork一下
扫雷github仓库
使用git复制下面代码即可下载到本地
git clone https://github.com/Forminio/minesweeper-game.git
技术实现详解
1. 游戏核心数据结构
游戏使用两个二维数组作为核心数据结构:
- mineBoard :记录每个格子是地雷(‘M’)还是数字(0-8)
- showBoard :记录每个格子的显示状态(‘hidden’、‘revealed’、‘flag’、‘mine’)
2. 地雷生成算法
关键实现点:
- 使用随机数生成地雷位置
- 确保首次点击位置及其周围8个格子不会有地雷
- 地雷放置完成后,计算每个非地雷格子周围的地雷数量
3. 自动展开算法(递归实现)
关键实现点:
- 使用递归算法自动展开空白区域
- 当点击到数字0(周围无雷)的格子时,自动展开周围的8个格子
- 递归继续展开,直到遇到数字格子为止
- 边界检查确保不会越界
4. 胜利检测算法
关键实现点:
- 计算已揭示的格子数量
- 当已揭示格子数量等于总格子数减去地雷数时,判定为胜利
- 胜利时自动标记所有未标记的地雷
5. 渲染与DOM操作
关键实现点:
- 使用CSS Grid布局实现游戏网格
- 动态调整网格大小以适应不同难度
- 为每个单元格添加点击和右键点击事件
- 根据单元格状态设置不同的样式和内容
6. 响应式设计
关键实现点:
- 使用媒体查询适配不同屏幕尺寸
- 在小屏幕设备上调整按钮布局和单元格大小
- 确保游戏在移动设备上也能良好运行
7. 健壮性与错误处理
关键实现点:
- 添加多种事件监听确保游戏在各种情况下都能正常运行
- 定期检查游戏状态,自动修复可能的问题
- 处理页面可见性变化、缓存加载等特殊情况
设计亮点
- 经典Windows风格UI :使用灰色背景、凸起和凹陷的边框,重现经典Windows扫雷游戏的视觉风格
- 彩色数字提示 :不同数字使用不同颜色,提高游戏可读性
- 游戏状态反馈 :通过模态窗口提供游戏结束反馈,显示游戏结果和用时
- 自适应布局 :游戏界面能够适应不同屏幕尺寸,在桌面和移动设备上都有良好体验
以下是完整实现代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JavaScript扫雷</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 创建隐形盒子撑开页面 */
.page-container {
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
background-color: transparent;
}
body {
font-family: 'Microsoft YaHei', Arial, sans-serif;
background-color: transparent;
color: #333;
line-height: 1.6;
margin: 0;
padding: 0;
width: 100%;
min-height: 100vh;
}
.game-container {
width: 100%;
max-width: 1200px; /* 与编译器容器宽度一致 */
margin: 0 auto;
background: #c0c0c0;
border: 3px outset #fff;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
padding: 20px;
border-radius: 8px;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
padding: 10px;
background: #c0c0c0;
border: 2px inset #fff;
border-radius: 4px;
color: #333; /* 添加文字颜色 */
}
.mine-count {
font-weight: bold;
font-size: 18px;
}
.difficulty-controls {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
justify-content: center;
}
.difficulty-btn, .new-game-btn {
padding: 8px 15px;
background: #f0f0f0;
border: 2px outset #fff;
cursor: pointer;
font-family: 'Microsoft YaHei', Arial, sans-serif;
border-radius: 4px;
transition: all 0.3s;
color: #333; /* 添加默认文字颜色 */
font-weight: bold; /* 加粗文字 */
}
.difficulty-btn:active, .new-game-btn:active {
border: 2px inset #fff;
}
.difficulty-btn.active {
background: #4568dc; /* 更改为蓝色背景 */
border: 2px inset #fff;
color: white; /* 白色文字 */
}
.new-game-btn {
font-weight: bold;
background: #4568dc;
color: white;
}
.new-game-btn:hover {
background: #3a5bc9;
}
.grid-container {
width: 100%;
overflow: auto;
margin: 0 auto;
display: flex;
justify-content: center;
padding: 10px;
background: #a0a0a0;
border: 2px inset #fff;
border-radius: 4px;
min-height: 400px;
height: auto; /* 允许高度自适应 */
}
.grid {
display: grid;
gap: 1px;
background: #808080;
border: 2px inset #fff;
padding: 2px;
margin: 0 auto;
/* 确保网格在初始加载时有固定尺寸 */
min-width: 300px;
min-height: 300px;
}
.cell {
width: 25px;
height: 25px;
background: #c0c0c0;
border: 2px outset #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-weight: bold;
font-size: 16px; /* 增大字体大小 */
}
.revealed {
border: 1px solid #808080;
background: #c0c0c0;
}
.flagged {
color: red;
}
.game-over-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.game-over-modal {
background: #c0c0c0;
border: 3px outset #fff;
padding: 20px;
text-align: center;
width: 300px;
border-radius: 8px;
}
.game-over-modal h2 {
margin-top: 0;
margin-bottom: 15px;
}
.game-over-modal p {
margin-bottom: 20px;
}
.hidden {
display: none;
}
/* 数字颜色 */
.num-1 { color: #0066ff; font-weight: bold; } /* 更亮的蓝色 */
.num-2 { color: #00aa00; font-weight: bold; } /* 更亮的绿色 */
.num-3 { color: #ff0000; font-weight: bold; } /* 鲜红色 */
.num-4 { color: #000099; font-weight: bold; } /* 深蓝色 */
.num-5 { color: #aa0000; font-weight: bold; } /* 深红色 */
.num-6 { color: #008080; font-weight: bold; } /* 青色 */
.num-7 { color: #000000; font-weight: bold; } /* 黑色 */
.num-8 { color: #666666; font-weight: bold; } /* 灰色 */
@media (max-width: 768px) {
.difficulty-controls {
flex-direction: column;
align-items: center;
}
.difficulty-btn, .new-game-btn {
width: 100%;
max-width: 200px;
}
.cell {
width: 20px;
height: 20px;
font-size: 12px;
}
}
</style>
</head>
<body>
<div class="page-container">
<div class="game-container">
<div class="status-bar">
<div class="mine-count">剩余雷数: <span id="mine-count">20</span></div>
<button class="new-game-btn" onclick="initGame()">新游戏</button>
</div>
<div class="difficulty-controls">
<button class="difficulty-btn active" onclick="setDifficulty('beginner')">初级 (12x12)</button>
<button class="difficulty-btn" onclick="setDifficulty('intermediate')">中级 (16x16)</button>
<button class="difficulty-btn" onclick="setDifficulty('expert')">高级 (19x19)</button>
</div>
<div class="grid-container">
<div id="grid" class="grid"></div>
</div>
<div id="game-over" class="game-over-overlay hidden">
<div class="game-over-modal">
<h2 id="game-result">游戏结束</h2>
<p id="game-message"></p>
<button class="new-game-btn" onclick="closeModal(); initGame();">再来一局</button>
</div>
</div>
</div>
<script>
// 游戏配置
const DIFFICULTY = {
beginner: { rows: 12, cols: 12, mines: 20 },
intermediate: { rows: 16, cols: 16, mines: 40 },
expert: { rows: 19, cols: 19, mines: 99 }
};
let currentDifficulty = 'beginner';
let ROWS = DIFFICULTY[currentDifficulty].rows;
let COLS = DIFFICULTY[currentDifficulty].cols;
let MINE_COUNT = DIFFICULTY[currentDifficulty].mines;
let mineBoard = [];
let showBoard = [];
let gameOver = false;
let flaggedCount = 0;
let startTime = 0;
let firstClick = true;
let isGridRendered = false; // 添加标志来跟踪网格是否已渲染
function setDifficulty(difficulty) {
// 更新当前难度按钮样式
document.querySelectorAll('.difficulty-btn').forEach(btn => {
btn.classList.remove('active');
});
event.target.classList.add('active');
currentDifficulty = difficulty;
ROWS = DIFFICULTY[difficulty].rows;
COLS = DIFFICULTY[difficulty].cols;
MINE_COUNT = DIFFICULTY[difficulty].mines;
initGame();
}
function initGame() {
gameOver = false;
flaggedCount = 0;
firstClick = true;
document.getElementById('mine-count').textContent = MINE_COUNT;
// 隐藏游戏结束弹窗
document.getElementById('game-over').classList.add('hidden');
// 初始化棋盘
mineBoard = Array(ROWS).fill().map(() => Array(COLS).fill(0));
showBoard = Array(ROWS).fill().map(() => Array(COLS).fill('hidden'));
renderGrid();
isGridRendered = true; // 设置网格已渲染标志
}
function placeMines(firstX, firstY) {
// 布置雷(确保第一次点击不是雷)
let minesPlaced = 0;
while (minesPlaced < MINE_COUNT) {
const x = Math.floor(Math.random() * ROWS);
const y = Math.floor(Math.random() * COLS);
// 确保第一次点击的位置及其周围没有雷
if (mineBoard[x][y] !== 'M' &&
(Math.abs(x - firstX) > 1 || Math.abs(y - firstY) > 1)) {
mineBoard[x][y] = 'M';
minesPlaced++;
}
}
// 计算数字
for (let i = 0; i < ROWS; i++) {
for (let j = 0; j < COLS; j++) {
if (mineBoard[i][j] !== 'M') {
mineBoard[i][j] = countAdjacentMines(i, j);
}
}
}
}
function countAdjacentMines(x, y) {
let count = 0;
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
const newX = x + i;
const newY = y + j;
if (newX >= 0 && newX < ROWS &&
newY >= 0 && newY < COLS &&
mineBoard[newX][newY] === 'M') {
count++;
}
}
}
return count;
}
function reveal(x, y) {
if (gameOver || showBoard[x][y] === 'revealed' || showBoard[x][y] === 'flag') return;
// 第一次点击时布置雷
if (firstClick) {
placeMines(x, y);
firstClick = false;
startTime = Date.now();
}
if (mineBoard[x][y] === 'M') {
gameOver = true;
revealAllMines();
showGameOver(false);
return;
}
showBoard[x][y] = 'revealed';
// 自动展开空白区域
if (mineBoard[x][y] === 0) {
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
const newX = x + i;
const newY = y + j;
if (newX >= 0 && newX < ROWS &&
newY >= 0 && newY < COLS &&
showBoard[newX][newY] !== 'revealed') {
reveal(newX, newY);
}
}
}
}
checkWin();
renderGrid();
}
function toggleFlag(x, y, event) {
event.preventDefault();
if (gameOver || showBoard[x][y] === 'revealed') return;
if (showBoard[x][y] === 'flag') {
showBoard[x][y] = 'hidden';
flaggedCount--;
} else {
showBoard[x][y] = 'flag';
flaggedCount++;
}
document.getElementById('mine-count').textContent = MINE_COUNT - flaggedCount;
renderGrid();
checkWin();
}
function checkWin() {
if (gameOver) return;
// 检查是否所有非雷格子都已揭开
let revealedCount = 0;
for (let i = 0; i < ROWS; i++) {
for (let j = 0; j < COLS; j++) {
if (showBoard[i][j] === 'revealed') revealedCount++;
}
}
if (revealedCount === ROWS * COLS - MINE_COUNT) {
gameOver = true;
// 自动标记所有未标记的雷
for (let i = 0; i < ROWS; i++) {
for (let j = 0; j < COLS; j++) {
if (mineBoard[i][j] === 'M' && showBoard[i][j] !== 'flag') {
showBoard[i][j] = 'flag';
}
}
}
renderGrid();
showGameOver(true);
}
}
function showGameOver(isWin) {
const modal = document.getElementById('game-over');
const result = document.getElementById('game-result');
const message = document.getElementById('game-message');
modal.classList.remove('hidden');
if (isWin) {
result.textContent = '恭喜你赢了!';
const timeTaken = Math.floor((Date.now() - startTime) / 1000);
message.textContent = `你用了${timeTaken}秒完成了游戏!`;
} else {
result.textContent = '游戏结束';
message.textContent = '很遗憾,你触雷了!';
}
}
function closeModal() {
document.getElementById('game-over').classList.add('hidden');
}
function revealAllMines() {
for (let i = 0; i < ROWS; i++) {
for (let j = 0; j < COLS; j++) {
if (mineBoard[i][j] === 'M') {
showBoard[i][j] = 'mine';
}
}
}
renderGrid();
}
function renderGrid() {
const grid = document.getElementById('grid');
if (!grid) return; // 添加安全检查
grid.style.gridTemplateColumns = `repeat(${COLS}, 25px)`;
grid.innerHTML = '';
// 确保网格有固定尺寸
const gridWidth = COLS * 25 + 4;
const gridHeight = ROWS * 25 + 4;
grid.style.width = `${gridWidth}px`;
grid.style.height = `${gridHeight}px`;
// 设置网格容器的高度,确保能完全显示网格,增加10px以消除滚动条
const gridContainer = document.querySelector('.grid-container');
if (gridContainer) {
gridContainer.style.height = `${gridHeight + 35}px`; // 从+20px改为+30px
}
// 调整游戏容器宽度以匹配父容器
const gameContainer = document.querySelector('.game-container');
if (gameContainer) {
gameContainer.style.width = '100%';
}
// 创建初始网格
for (let i = 0; i < ROWS; i++) {
for (let j = 0; j < COLS; j++) {
const cell = document.createElement('div');
cell.className = 'cell';
if (showBoard[i][j] === 'revealed') {
cell.classList.add('revealed');
if (mineBoard[i][j] !== 0) {
cell.textContent = mineBoard[i][j];
cell.classList.add(`num-${mineBoard[i][j]}`);
}
} else if (showBoard[i][j] === 'flag') {
cell.textContent = '🚩';
cell.classList.add('flagged');
}
if (showBoard[i][j] === 'mine') {
cell.textContent = '💣';
cell.style.backgroundColor = '#ff6666';
}
cell.addEventListener('click', () => reveal(i, j));
cell.addEventListener('contextmenu', (e) => toggleFlag(i, j, e));
grid.appendChild(cell);
}
}
}
// 检查网格是否为空,如果为空则重新初始化
function checkAndReinitializeGrid() {
const grid = document.getElementById('grid');
if (grid && (!grid.children.length || grid.children.length === 0)) {
console.log('检测到空网格,重新初始化游戏');
// 重置为初级难度
resetToBeginner();
initGame();
}
}
// 新增:重置为初级难度的函数
function resetToBeginner() {
currentDifficulty = 'beginner';
ROWS = DIFFICULTY[currentDifficulty].rows;
COLS = DIFFICULTY[currentDifficulty].cols;
MINE_COUNT = DIFFICULTY[currentDifficulty].mines;
// 更新难度按钮样式
document.querySelectorAll('.difficulty-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.textContent.includes('初级')) {
btn.classList.add('active');
}
});
// 更新剩余雷数显示
document.getElementById('mine-count').textContent = MINE_COUNT;
}
// 初始化游戏
// 确保DOM完全加载后立即初始化游戏
document.addEventListener('DOMContentLoaded', function() {
initGame();
// 强制立即渲染网格,确保游戏加载时显示画面
renderGrid();
});
// 添加备份初始化方法,以防DOMContentLoaded事件已经触发
if (document.readyState === 'complete' || document.readyState === 'interactive') {
setTimeout(function() {
initGame();
renderGrid();
}, 1);
}
// 添加页面可见性变化监听
document.addEventListener('visibilitychange', function() {
if (document.visibilityState === 'visible') {
// 当页面变为可见时,检查网格是否需要重新初始化
setTimeout(checkAndReinitializeGrid, 100);
}
});
// 添加自定义事件监听,用于处理导航栏刷新
window.addEventListener('pageshow', function(event) {
// pageshow事件在每次页面显示时触发,包括从缓存加载
if (event.persisted) {
// 页面从缓存加载时,重置为初级难度
resetToBeginner();
setTimeout(checkAndReinitializeGrid, 100);
}
});
// 修改:添加导航栏刷新检测
window.addEventListener('load', function() {
// 检查是否是通过导航栏刷新加载的页面
if (performance && performance.navigation) {
// navigation.type: 0=直接访问, 1=刷新, 2=前进/后退
if (performance.navigation.type === 1) {
console.log('检测到页面刷新,重置为初级难度');
resetToBeginner();
initGame();
}
}
});
// 添加额外的安全措施,定期检查网格是否为空
setInterval(function() {
const grid = document.getElementById('grid');
if (grid && (!grid.children.length || grid.children.length === 0) && isGridRendered) {
console.log('定期检查:检测到空网格,重新初始化游戏');
initGame();
}
}, 1000); // 每秒检查一次
</script>
</div>
</body>
</html>