智能扫雷(还原特性)edit icon

作者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
现在支持上传本地图片了!
index.html
            
            <!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('default')">默认模式(10×10,10雷)</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 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 === 'default') {
        GRID_SIZE = 10;
        MINES_COUNT = 10;
        document.getElementById('game-container').style.display = 'block';
        initGame();
      } else {
        document.getElementById('custom-mode-container').style.display = 'block';
      }
    }

    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_SIZE = Math.max(5, Math.min(20, w));
      MINES_COUNT = Math.max(1, Math.min(100, m));
      if (MINES_COUNT >= GRID_SIZE * GRID_SIZE) {
        alert("地雷数量不能大于或等于总格子数!");
        return;
      }
      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_SIZE}, 1fr)`;
      // ✅ 先创建一个空图(全 0),等第一次点开再布雷并计算数字
      mineMap = createEmptyMap();
      mineMap.forEach((row, i) => {
        row.forEach((cell, j) => {
          const div = document.createElement('div');
          div.className = 'cell';
          div.dataset.value = cell; // 先是 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_SIZE
      }, () => Array.from({
        length: GRID_SIZE
      }, () => 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_SIZE && y >= 0 && y < GRID_SIZE) {
            forbidden.add(`${x},${y}`);
          }
        }
      }
      // 先清空再布雷(安全起见)
      mineMap = createEmptyMap();
      let mines = 0;
      const totalCells = GRID_SIZE * GRID_SIZE;
      const maxPossible = totalCells - forbidden.size;
      const minesToPlace = Math.min(MINES_COUNT, maxPossible);
      while (mines < minesToPlace) {
        const x = Math.floor(Math.random() * GRID_SIZE);
        const y = Math.floor(Math.random() * GRID_SIZE);
        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_SIZE; i++) {
        for (let j = 0; j < GRID_SIZE; 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_SIZE && y >= 0 && y < GRID_SIZE) {
                if (mineMap[x][y] === 'X') count++;
              }
            }
          }
          mineMap[i][j] = count;
        }
      }
      // 同步到 DOM 的 dataset.value
      for (let i = 0; i < GRID_SIZE; i++) {
        for (let j = 0; j < GRID_SIZE; 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>
        
编辑器加载中
预览
控制台