<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>智能扫雷(多主题优化版)</title>
<style>
/* ============ 主题系统 ============ */
:root {
--bg-color: #f0f0f0;
--cell-color: #c0c0c0;
--text-color: #333;
--border-color: #808080;
--danger: #ff4444;
--safe: #4CAF50;
--header-bg: #e0e0e0;
--button-bg: #bbb;
--button-hover: #999;
--flag-color: #ffecb3;
}
[data-theme="dark"] {
--bg-color: #2d2d2d;
--cell-color: #404040;
--text-color: #ddd;
--border-color: #666;
--danger: #d32f2f;
--safe: #388e3c;
--header-bg: #3a3a3a;
--button-bg: #555;
--button-hover: #777;
--flag-color: #5d4037;
}
[data-theme="forest-light"] {
--bg-color: #e8f5e9;
--cell-color: #a5d6a7;
--text-color: #1b5e20;
--border-color: #4caf50;
--danger: #e53935;
--safe: #2e7d32;
--header-bg: #c8e6c9;
--button-bg: #81c784;
--button-hover: #66bb6a;
--flag-color: #fff176;
}
[data-theme="forest-dark"] {
--bg-color: #1b5e20;
--cell-color: #388e3c;
--text-color: #e8f5e8;
--border-color: #66bb6a;
--danger: #f44336;
--safe: #81c784;
--header-bg: #2e7d32;
--button-bg: #1b5e20;
--button-hover: #388e3c;
--flag-color: #ffd54f;
}
[data-theme="ocean-light"] {
--bg-color: #e0f7fa;
--cell-color: #80deea;
--text-color: #006064;
--border-color: #00acc1;
--danger: #ff7043;
--safe: #00838f;
--header-bg: #b2ebf2;
--button-bg: #4dd0e1;
--button-hover: #26c6da;
--flag-color: #ffcc80;
}
[data-theme="ocean-dark"] {
--bg-color: #004d40;
--cell-color: #00838f;
--text-color: #e0f7fa;
--border-color: #26c6da;
--danger: #ff7043;
--safe: #80deea;
--header-bg: #006064;
--button-bg: #004d40;
--button-hover: #00838f;
--flag-color: #ffcc80;
}
/* ============ 全局样式 ============ */
body {
background: var(--bg-color);
color: var(--text-color);
transition: background 0.5s ease, color 0.5s ease;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 0;
}
.mode-select-container,
.game-container,
.custom-mode-container {
max-width: 500px;
margin: 2rem auto;
padding: 20px;
border-radius: 15px;
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.25);
background: var(--bg-color);
transition: background 0.5s;
}
.header {
display: flex;
flex-wrap: wrap;
gap: 10px;
justify-content: space-between;
align-items: center;
padding: 12px;
background: var(--header-bg);
border-radius: 10px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
button {
background: var(--button-bg);
color: var(--text-color);
border: none;
padding: 8px 14px;
border-radius: 8px;
cursor: pointer;
font-weight: bold;
transition: background 0.3s, transform 0.2s;
}
button:hover {
background: var(--button-hover);
transform: translateY(-2px);
}
.grid {
display: grid;
gap: 2px;
margin-top: 1rem;
}
.cell {
aspect-ratio: 1;
background: var(--cell-color);
border: 2px solid var(--border-color);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 1.2em;
cursor: pointer;
transition: all 0.3s ease;
user-select: none;
border-radius: 4px;
}
.cell:hover:not(.revealed) {
transform: scale(1.08);
box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
}
.cell.revealed {
background: var(--bg-color);
box-shadow: inset 0 0 4px rgba(0, 0, 0, 0.1);
}
.cell.mine {
background: var(--danger) !important;
}
.cell.flag {
background: var(--flag-color) !important;
}
#status {
margin-top: 15px;
font-weight: bold;
text-align: center;
font-size: 1.1em;
padding: 8px;
border-radius: 8px;
background: rgba(0, 0, 0, 0.05);
}
.theme-select-container {
position: fixed;
top: 15px;
right: 15px;
z-index: 10;
}
.theme-select-container select {
padding: 6px 10px;
border-radius: 20px;
background: var(--button-bg);
color: var(--text-color);
border: none;
font-weight: bold;
cursor: pointer;
}
</style>
</head>
<body>
<!-- 主题选择器 -->
<div class="theme-select-container">
<select id="theme-select" onchange="changeTheme(this.value)">
<option value="light">经典浅色</option>
<option value="dark">经典深色</option>
<option value="forest-light">🌲 森林(浅)</option>
<option value="forest-dark">🌲 森林(深)</option>
<option value="ocean-light">🌊 海洋(浅)</option>
<option value="ocean-dark">🌊 海洋(深)</option>
</select>
</div>
<!-- 开始页面 -->
<div class="mode-select-container" id="mode-select">
<h1>🎮 智能扫雷</h1>
<button onclick="selectMode('beginner')">初级(9×9,10雷)</button>
<button onclick="selectMode('intermediate')">中级(16×16,40雷)</button>
<button onclick="selectMode('expert')">高级(16×30,99雷)</button>
<button onclick="selectMode('custom')">自定义模式</button>
</div>
<!-- 游戏页面 -->
<div class="game-container" id="game-container" style="display: none;">
<button onclick="returnToModeSelect()">⬅️ 返回开始页面</button>
<div class="header">
<span id="mine-count">💣10</span>
<button onclick="initGame()">🔄 新游戏</button>
<button onclick="resetGame()">🔁 重来</button>
<span id="timer">⏱<span id="time">0</span></span>
<button id="swapBtn" onclick="swapClickMode()">↔️ 正常模式</button>
</div>
<div class="grid" id="grid"></div>
<div id="status"></div>
</div>
<!-- 自定义页面 -->
<div class="custom-mode-container" id="custom-mode-container" style="display: none;">
<h1>🛠 自定义游戏</h1>
<label for="width">宽度(5-20):</label>
<input type="number" id="width" min="5" max="20" value="10"><br><br>
<label for="height">高度(5-20):</label>
<input type="number" id="height" min="5" max="20" value="10"><br><br>
<label for="mines">地雷数量(1-100):</label>
<input type="number" id="mines" min="1" max="100" value="10"><br><br>
<button onclick="startCustomGame()">🚀 开始游戏</button>
<button onclick="returnToModeSelect()">⬅️ 返回开始页面</button>
</div>
<script>
let GRID_SIZE = 10;
let GRID_WIDTH = 10;
let GRID_HEIGHT = 10;
let MINES_COUNT = 10;
let gameActive = true;
let timerInterval;
let bestTime = localStorage.getItem('bestTime') || Infinity;
let isSwapped = false;
let remainingMines = MINES_COUNT;
// mineMap 里存 'X' 或 数字 0-8(字符串/数字都行,这里统一用字符串 'X' + 数字)
let mineMap = [];
let minesPlaced = false; // ✅ 第一次点击后才布雷
let firstClickDone = false; // ✅ 用于确保“第一步不踩雷”只影响第一次“点开”动作
// 初始化主题
let currentTheme = localStorage.getItem('theme') || 'light';
document.body.dataset.theme = currentTheme;
document.getElementById('theme-select').value = currentTheme;
function changeTheme(theme) {
document.body.dataset.theme = theme;
localStorage.setItem('theme', theme);
}
function selectMode(mode) {
document.getElementById('mode-select').style.display = 'none';
if (mode === 'custom') {
document.getElementById('custom-mode-container').style.display = 'block';
return;
}
// 设置Windows 7经典模式
if (mode === 'beginner') {
GRID_WIDTH = 9;
GRID_HEIGHT = 9;
MINES_COUNT = 10;
} else if (mode === 'intermediate') {
GRID_WIDTH = 16;
GRID_HEIGHT = 16;
MINES_COUNT = 40;
} else if (mode === 'expert') {
GRID_WIDTH = 30;
GRID_HEIGHT = 16;
MINES_COUNT = 99;
}
// 对于非自定义模式,使用宽度作为GRID_SIZE(保持原逻辑)
GRID_SIZE = Math.max(GRID_WIDTH, GRID_HEIGHT);
document.getElementById('game-container').style.display = 'block';
initGame();
}
function startCustomGame() {
const w = parseInt(document.getElementById('width').value);
const h = parseInt(document.getElementById('height').value);
const m = parseInt(document.getElementById('mines').value);
if (m >= w * h) {
alert("地雷数量不能大于或等于总格子数!");
return;
}
// 说明:你原代码实际上只支持正方形(GRID_SIZE×GRID_SIZE)
// 这里保持原逻辑:只用 width 作为 GRID_SIZE
GRID_WIDTH = Math.max(5, Math.min(20, w));
GRID_HEIGHT = Math.max(5, Math.min(20, h));
MINES_COUNT = Math.max(1, Math.min(100, m));
if (MINES_COUNT >= GRID_WIDTH * GRID_HEIGHT) {
alert("地雷数量不能大于或等于总格子数!");
return;
}
// 对于非自定义模式,使用宽度作为GRID_SIZE(保持原逻辑)
GRID_SIZE = Math.max(GRID_WIDTH, GRID_HEIGHT);
document.getElementById('custom-mode-container').style.display = 'none';
document.getElementById('game-container').style.display = 'block';
initGame();
}
function returnToModeSelect() {
clearInterval(timerInterval);
document.getElementById('game-container').style.display = 'none';
document.getElementById('custom-mode-container').style.display = 'none';
document.getElementById('mode-select').style.display = 'block';
}
function initGame() {
clearInterval(timerInterval);
document.getElementById('time').textContent = '0';
document.getElementById('status').textContent = '';
gameActive = true;
remainingMines = MINES_COUNT;
document.getElementById('mine-count').textContent = `💣${remainingMines}`;
minesPlaced = false;
firstClickDone = false;
const grid = document.getElementById('grid');
grid.innerHTML = '';
grid.style.gridTemplateColumns = `repeat(${GRID_WIDTH}, 1fr)`;
// ✅ 先创建一个空图(全 0),等第一次点开再布雷并计算数字
mineMap = createEmptyMap();
for (let i = 0; i < GRID_HEIGHT; i++) {
for (let j = 0; j < GRID_WIDTH; j++) {
const div = document.createElement('div');
div.className = 'cell';
div.dataset.value = mineMap[i][j]; // 先是 0
div.dataset.x = i;
div.dataset.y = j;
div.addEventListener('click', handleClick);
div.addEventListener('contextmenu', handleRightClick);
grid.appendChild(div);
}
}
let seconds = 0;
timerInterval = setInterval(() => {
if (gameActive) document.getElementById('time').textContent = ++seconds;
}, 1000);
}
function createEmptyMap() {
return Array.from({
length: GRID_HEIGHT
}, () => Array.from({
length: GRID_WIDTH
}, () => 0));
}
// ✅ 根据首次点击位置布雷:保证首点格子(以及八邻域)不含雷
function placeMinesAvoiding(firstX, firstY) {
// 保护区:首次点击格子 + 八邻域
const forbidden = new Set();
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const x = firstX + dx;
const y = firstY + dy;
if (x >= 0 && x < GRID_HEIGHT && y >= 0 && y < GRID_WIDTH) {
forbidden.add(`${x},${y}`);
}
}
}
// 先清空再布雷(安全起见)
mineMap = createEmptyMap();
let mines = 0;
const totalCells = GRID_HEIGHT * GRID_WIDTH;
const maxPossible = totalCells - forbidden.size;
const minesToPlace = Math.min(MINES_COUNT, maxPossible);
while (mines < minesToPlace) {
const x = Math.floor(Math.random() * GRID_HEIGHT);
const y = Math.floor(Math.random() * GRID_WIDTH);
const key = `${x},${y}`;
if (forbidden.has(key)) continue;
if (mineMap[x][y] !== 'X') {
mineMap[x][y] = 'X';
mines++;
}
}
// 计算数字
for (let i = 0; i < GRID_HEIGHT; i++) {
for (let j = 0; j < GRID_WIDTH; j++) {
if (mineMap[i][j] === 'X') continue;
let count = 0;
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const x = i + dx;
const y = j + dy;
if (x >= 0 && x < GRID_HEIGHT && y >= 0 && y < GRID_WIDTH) {
if (mineMap[x][y] === 'X') count++;
}
}
}
mineMap[i][j] = count;
}
}
// 同步到 DOM 的 dataset.value
for (let i = 0; i < GRID_HEIGHT; i++) {
for (let j = 0; j < GRID_WIDTH; j++) {
const cellEl = document.querySelector(`[data-x="${i}"][data-y="${j}"]`);
if (cellEl) cellEl.dataset.value = mineMap[i][j];
}
}
minesPlaced = true;
}
function handleClick(e) {
if (!gameActive) return;
const cell = e.target;
isSwapped ? handleRightClickAction(cell) : handleLeftClickAction(cell);
}
function handleRightClick(e) {
e.preventDefault();
if (!gameActive) return;
const cell = e.target;
isSwapped ? handleLeftClickAction(cell) : handleRightClickAction(cell);
}
function handleLeftClickAction(cell) {
if (cell.classList.contains('flag')) return;
// ✅ 第一次“点开”之前先布雷,保证首点安全
if (!firstClickDone) {
firstClickDone = true;
const x = parseInt(cell.dataset.x);
const y = parseInt(cell.dataset.y);
placeMinesAvoiding(x, y);
}
const value = cell.dataset.value;
if (value === 'X') {
gameOver();
cell.classList.add('mine');
cell.textContent = '💥';
} else {
revealCell(cell);
checkWinConditions();
}
}
function handleRightClickAction(cell) {
if (!cell.classList.contains('revealed')) {
const wasFlagged = cell.classList.contains('flag');
cell.classList.toggle('flag');
cell.textContent = cell.classList.contains('flag') ? '🚩' : '';
if (wasFlagged) {
remainingMines += 1;
} else {
remainingMines -= 1;
}
remainingMines = Math.max(0, Math.min(MINES_COUNT, remainingMines));
document.getElementById('mine-count').textContent = `💣${remainingMines}`;
checkWinConditions();
}
}
function revealCell(cell) {
if (cell.classList.contains('revealed')) return;
cell.classList.add('revealed');
const v = cell.dataset.value;
// 只有布雷后才显示真实数字;未布雷时这里不会被调用(因为左键首点会先布雷)
if (v === '0' || v === 0) {
cell.textContent = '';
revealNeighbors(cell);
} else {
cell.textContent = v;
}
checkWinConditions();
}
function revealNeighbors(cell) {
const x = parseInt(cell.dataset.x);
const y = parseInt(cell.dataset.y);
for (let dx = -1; dx <= 1; dx++) {
for (let dy = -1; dy <= 1; dy++) {
const nx = x + dx;
const ny = y + dy;
const neighbor = document.querySelector(`[data-x="${nx}"][data-y="${ny}"]`);
if (neighbor && !neighbor.classList.contains('revealed') && !neighbor.classList.contains('flag')) {
// 只递归展开非雷(dataset.value 已是最终)
if (neighbor.dataset.value !== 'X') {
revealCell(neighbor);
}
}
}
}
}
function checkWinConditions() {
// 如果还没布雷(只右键插旗但没左键点开),不判胜
if (!firstClickDone) return;
const allNonMinesRevealed = Array.from(document.querySelectorAll('.cell'))
.filter(cell => cell.dataset.value !== 'X')
.every(cell => cell.classList.contains('revealed'));
if (allNonMinesRevealed) {
gameActive = false;
clearInterval(timerInterval);
const time = parseInt(document.getElementById('time').textContent);
if (time < bestTime) {
bestTime = time;
localStorage.setItem('bestTime', bestTime);
}
document.getElementById('status').textContent = `🎉 胜利!用时:${time}s | 最佳成绩:${bestTime}s`;
document.querySelectorAll('.cell').forEach(cell => {
if (cell.dataset.value === 'X') {
cell.textContent = '💣';
cell.classList.add('revealed');
}
});
}
}
function gameOver() {
gameActive = false;
clearInterval(timerInterval);
document.querySelectorAll('.cell').forEach(cell => {
if (cell.dataset.value === 'X') {
cell.classList.add('mine');
cell.textContent = '💥';
} else if (cell.classList.contains('flag') && cell.dataset.value !== 'X') {
cell.textContent = '❌'; // 错误标记
}
});
document.getElementById('status').textContent = '💥 踩到地雷了!游戏结束!';
}
function swapClickMode() {
isSwapped = !isSwapped;
document.getElementById('swapBtn').textContent = isSwapped ?
"↔️ 左键标记 / 右键点击(标记模式)" :
"↔️ 正常模式";
}
function resetGame() {
// ✅ “重来”:保留当前这盘布局(mineMap 不变),清空显示和计时
// 如果你希望“重来”也重新洗牌,把下面这一行取消注释即可:
// initGame(); return;
clearInterval(timerInterval);
document.getElementById('time').textContent = '0';
document.getElementById('status').textContent = '';
gameActive = true;
remainingMines = MINES_COUNT;
document.getElementById('mine-count').textContent = `💣${remainingMines}`;
const grid = document.getElementById('grid');
Array.from(grid.children).forEach(cell => {
cell.classList.remove('revealed', 'mine', 'flag');
cell.textContent = '';
});
// 这里的关键:如果这盘还没开始(没左键点开),重来也仍然没布雷
// 如果已经开始过(布雷了),重来保持同一盘
// 不改变 firstClickDone/minesPlaced:让玩家继续在同一盘开局重试更符合“重来”
let seconds = 0;
timerInterval = setInterval(() => {
if (gameActive) document.getElementById('time').textContent = ++seconds;
}, 1000);
}
</script>
</body>
</html>
index.html
index.html