五子棋edit icon

创建者:
邓朝元
Fork(复制)
下载
嵌入
BUG反馈
index.html
md
README.md
index.html
            
            <!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
  <title>🎮 增强版五子棋</title>
  <style>
    :root {
      --safe-bottom: env(safe-area-inset-bottom, 0px);
    }

    * { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

    body {
      background: #f4f4f4;
      font-family: -apple-system, 'Microsoft Yahei', sans-serif;
      text-align: center;
      margin: 0;
      padding: 0;
      padding-top: 10px;
      padding-bottom: calc(80px + var(--safe-bottom));
      transition: all 0.3s ease;
      touch-action: manipulation; /* 禁止双击缩放 */
      overscroll-behavior: none;
    }

    h1 {
      color: #333;
      font-size: clamp(16px, 4vw, 24px);
      margin: 8px 0;
      padding: 0 10px;
      line-height: 1.3;
    }

    /* 顶部控制区 - 移动端紧凑排列 */
    .top-controls {
      display: flex;
      justify-content: center;
      gap: 8px;
      padding: 5px 10px;
      flex-wrap: wrap;
    }

    .top-controls label {
      font-size: 13px;
      display: flex;
      align-items: center;
      gap: 4px;
    }

    select {
      font-size: 13px;
      padding: 4px 6px;
      border-radius: 6px;
      border: 1px solid #999;
      max-width: 120px;
      appearance: auto;
    }

    /* Canvas 响应式 */
    .canvas-wrapper {
      width: 100%;
      display: flex;
      justify-content: center;
      padding: 8px;
    }

    canvas {
      border: 2px solid #333;
      background-color: #fdd;
      box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
      transition: all 0.3s ease;
      /* 关键:Canvas大小由JS动态设置,CSS只约束最大值 */
      max-width: calc(100vw - 16px);
      max-height: calc(100vw - 16px);
      touch-action: none; /* 禁止Canvas上的浏览器默认手势 */
    }

    #status {
      font-size: clamp(14px, 3.5vw, 18px);
      margin: 6px 0;
      color: green;
      min-height: 24px;
      padding: 0 10px;
    }

    #score {
      font-size: clamp(12px, 3vw, 16px);
      margin: 4px 0;
      padding: 0 10px;
      line-height: 1.5;
    }

    /* 底部固定操作栏 - 移动端核心UI */
    .bottom-bar {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      background: inherit;
      border-top: 1px solid rgba(0,0,0,0.1);
      padding: 10px 10px calc(10px + var(--safe-bottom));
      display: flex;
      justify-content: center;
      gap: 8px;
      z-index: 100;
      backdrop-filter: blur(10px);
      -webkit-backdrop-filter: blur(10px);
    }

    button {
      padding: 10px 14px;
      font-size: clamp(13px, 3vw, 16px);
      cursor: pointer;
      border-radius: 8px;
      border: 1px solid #999;
      background: #fff;
      transition: all 0.15s ease;
      flex: 1;
      max-width: 140px;
      white-space: nowrap;
      /* 移动端触摸反馈 */
      -webkit-user-select: none;
      user-select: none;
    }

    button:active:not(:disabled) {
      transform: scale(0.95);
      opacity: 0.8;
    }

    button:disabled {
      opacity: 0.4;
      cursor: not-allowed;
    }

    .footer-info {
      font-size: 11px;
      color: #888;
      margin-top: 4px;
      padding: 0 10px;
    }

    /* 横屏适配 */
    @media (orientation: landscape) and (max-height: 500px) {
      body { padding-top: 5px; padding-bottom: calc(60px + var(--safe-bottom)); }
      h1 { font-size: 14px; margin: 4px 0; }
      .top-controls { padding: 2px 10px; }
      .canvas-wrapper { padding: 4px; }
      canvas {
        max-width: calc(100vh - 120px);
        max-height: calc(100vh - 120px);
      }
      .bottom-bar { padding: 6px 10px calc(6px + var(--safe-bottom)); }
      button { padding: 8px 10px; }
      #status { margin: 3px 0; min-height: 20px; }
      #score { display: none; } /* 横屏隐藏战绩节省空间 */
      .footer-info { display: none; }
    }

    /* 超小屏幕适配 */
    @media (max-width: 360px) {
      button { padding: 8px 8px; font-size: 12px; }
      .top-controls { gap: 4px; }
      select { max-width: 100px; font-size: 12px; }
    }
  </style>
</head>

<body>

  <h1>🎮 增强版五子棋</h1>

  <div class="top-controls">
    <label>模式:
      <select id="gameMode">
        <option value="local">双人对战</option>
        <option value="ai">挑战电脑</option>
      </select>
    </label>
    <label>主题:
      <select id="themeSelect">
        <option value="classic">🟤 经典</option>
        <option value="dark">🌙 暗夜</option>
        <option value="modern">💎 现代蓝</option>
        <option value="nature">🌿 自然绿</option>
        <option value="sunset">🌅 夕阳橙</option>
        <option value="purple">🌸 梦幻紫</option>
      </select>
    </label>
  </div>

  <div class="canvas-wrapper">
    <canvas id="chessBoard"></canvas>
  </div>

  <div id="status">轮到黑棋落子</div>
  <div id="score">🎮 你(黑):0胜 &nbsp; 电脑(白):0胜</div>
  <div class="footer-info">💡 AI模式撤销会同时撤回电脑棋步</div>

  <!-- 底部固定操作栏 -->
  <div class="bottom-bar">
    <button id="undoBtn" onclick="undoMove()" disabled>⬅️ 撤销</button>
    <button onclick="restartGame()">🔄 重来</button>
    <button onclick="resetScores()">🗑️ 清战绩</button>
  </div>

  <audio id="placeSound">
    <source src="https://cdn.pixabay.com/audio/2022/03/15/audio_4b7f9a1ba6.mp3" type="audio/mp3">
  </audio>
  <audio id="winSound">
    <source src="https://cdn.pixabay.com/audio/2022/01/17/audio_c46dc4b0ac.mp3" type="audio/mp3">
  </audio>

  <script>
    // ========== 主题定义 ==========
    const themes = {
      classic: {
        background: "#f4f4f4", canvasBg: "#fdd", canvasBorder: "#333",
        boardLines: "#333", textColor: "#333", statusColor: "green",
        blackGradient: ["#333", "#000"], whiteGradient: ["#fff", "#ddd"],
        whiteBorder: "#999", starColor: "#333",
        buttonBg: "#fff", buttonBorder: "#999",
        shadow: "0 0 15px rgba(0,0,0,0.2)", barBorder: "rgba(0,0,0,0.1)"
      },
      dark: {
        background: "#1a1a2e", canvasBg: "#2d2d44", canvasBorder: "#4a4a6a",
        boardLines: "#7a7a9a", textColor: "#e0e0e0", statusColor: "#4ade80",
        blackGradient: ["#222", "#000"], whiteGradient: ["#f0f0f0", "#c0c0c0"],
        whiteBorder: "#888", starColor: "#7a7a9a",
        buttonBg: "#2d2d44", buttonBorder: "#4a4a6a",
        shadow: "0 0 20px rgba(100,100,255,0.3)", barBorder: "rgba(255,255,255,0.1)"
      },
      modern: {
        background: "#e8f4f8", canvasBg: "#b8d8e8", canvasBorder: "#2c3e50",
        boardLines: "#2c3e50", textColor: "#2c3e50", statusColor: "#2980b9",
        blackGradient: ["#34495e", "#1a252f"], whiteGradient: ["#fff", "#bdc3c7"],
        whiteBorder: "#95a5a6", starColor: "#2c3e50",
        buttonBg: "#ecf0f1", buttonBorder: "#bdc3c7",
        shadow: "0 0 15px rgba(44,62,80,0.2)", barBorder: "rgba(44,62,80,0.1)"
      },
      nature: {
        background: "#e8f5e9", canvasBg: "#c8e6c9", canvasBorder: "#1b5e20",
        boardLines: "#2e7d32", textColor: "#1b5e20", statusColor: "#2e7d32",
        blackGradient: ["#333", "#111"], whiteGradient: ["#fff", "#ddd"],
        whiteBorder: "#757575", starColor: "#1b5e20",
        buttonBg: "#e8f5e9", buttonBorder: "#81c784",
        shadow: "0 0 15px rgba(27,94,32,0.2)", barBorder: "rgba(27,94,32,0.1)"
      },
      sunset: {
        background: "#fff3e0", canvasBg: "#ffe0b2", canvasBorder: "#e65100",
        boardLines: "#bf360c", textColor: "#bf360c", statusColor: "#e65100",
        blackGradient: ["#4e342e", "#1b0000"], whiteGradient: ["#fff", "#ffe082"],
        whiteBorder: "#f9a825", starColor: "#bf360c",
        buttonBg: "#fff3e0", buttonBorder: "#ff8f00",
        shadow: "0 0 15px rgba(230,81,0,0.2)", barBorder: "rgba(230,81,0,0.1)"
      },
      purple: {
        background: "#f3e5f5", canvasBg: "#e1bee7", canvasBorder: "#4a148c",
        boardLines: "#6a1b9a", textColor: "#4a148c", statusColor: "#7b1fa2",
        blackGradient: ["#2d0052", "#0a0015"], whiteGradient: ["#fff", "#f8bbd0"],
        whiteBorder: "#ad1457", starColor: "#6a1b9a",
        buttonBg: "#f3e5f5", buttonBorder: "#ab47bc",
        shadow: "0 0 15px rgba(106,27,154,0.2)", barBorder: "rgba(106,27,154,0.1)"
      }
    };

    // ========== 全局变量 ==========
    const canvas = document.getElementById("chessBoard");
    const ctx = canvas.getContext("2d");
    const statusDiv = document.getElementById("status");
    const scoreDiv = document.getElementById("score");
    const undoBtn = document.getElementById("undoBtn");
    const boardSize = 15;
    let cellSize = 0; // 动态计算
    let chessBoard = [];
    let gameOver = false;
    let currentPlayer = "black";
    let gameMode = "local";
    let moveHistory = [];
    let currentTheme = themes.classic;
    let aiThinking = false;
    let aiTimeout = null;

    // ========== 响应式Canvas尺寸 ==========
    function resizeCanvas() {
      const wrapper = document.querySelector('.canvas-wrapper');
      const maxSize = Math.min(wrapper.clientWidth - 16, window.innerHeight * 0.65);
      // 确保是整数避免模糊
      const size = Math.floor(Math.max(280, maxSize));

      // 处理高清屏
      const dpr = window.devicePixelRatio || 1;
      canvas.width = size * dpr;
      canvas.height = size * dpr;
      canvas.style.width = size + 'px';
      canvas.style.height = size + 'px';
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);

      cellSize = size / (boardSize + 1);
      drawBoard();
    }

    // 监听窗口变化,防抖处理
    let resizeTimer;
    window.addEventListener('resize', () => {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(resizeCanvas, 100);
    });

    // ========== 初始化 ==========
    function initScores() {
      if (!localStorage.getItem("blackWins")) localStorage.setItem("blackWins", "0");
      if (!localStorage.getItem("whiteWins")) localStorage.setItem("whiteWins", "0");
      updateScoreDisplay();
    }

    function resetScores() {
      if (confirm("确定清除所有战绩?")) {
        localStorage.setItem("blackWins", "0");
        localStorage.setItem("whiteWins", "0");
        updateScoreDisplay();
      }
    }

    function updateScoreDisplay() {
      const b = localStorage.getItem("blackWins");
      const w = localStorage.getItem("whiteWins");
      scoreDiv.innerHTML = `🎮 你(黑):${b}胜 &nbsp; 电脑(白):${w}胜`;
    }

    function playPlaceSound() {
      const s = document.getElementById("placeSound");
      s.currentTime = 0;
      s.play().catch(() => {});
    }

    function playWinSound() {
      const s = document.getElementById("winSound");
      s.currentTime = 0;
      s.play().catch(() => {});
    }

    function updateStatus(text) { statusDiv.textContent = text; }

    function updateUndoButton() {
      undoBtn.disabled = gameOver || moveHistory.length === 0 || aiThinking;
    }

    // ========== 主题切换 ==========
    function applyTheme(themeName) {
      currentTheme = themes[themeName] || themes.classic;
      document.body.style.background = currentTheme.background;
      document.body.style.color = currentTheme.textColor;
      document.querySelector("h1").style.color = currentTheme.textColor;
      document.querySelector(".footer-info").style.color = currentTheme.textColor;
      statusDiv.style.color = currentTheme.statusColor;
      canvas.style.backgroundColor = currentTheme.canvasBg;
      canvas.style.borderColor = currentTheme.canvasBorder;
      canvas.style.boxShadow = currentTheme.shadow;

      document.querySelectorAll("button").forEach(btn => {
        btn.style.background = currentTheme.buttonBg;
        btn.style.borderColor = currentTheme.buttonBorder;
        btn.style.color = currentTheme.textColor;
      });
      document.querySelectorAll("select").forEach(sel => {
        sel.style.background = currentTheme.buttonBg;
        sel.style.borderColor = currentTheme.buttonBorder;
        sel.style.color = currentTheme.textColor;
      });
      document.querySelector(".bottom-bar").style.borderTopColor = currentTheme.barBorder;
      document.querySelector(".bottom-bar").style.background = currentTheme.background;

      drawBoard();
    }

    document.getElementById("themeSelect").addEventListener("change", function() {
      applyTheme(this.value);
    });

    // ========== 游戏核心 ==========
    function init() {
      for (let i = 0; i < boardSize; i++) {
        chessBoard[i] = [];
        for (let j = 0; j < boardSize; j++) chessBoard[i][j] = "";
      }
      moveHistory = [];
      gameOver = false;
      aiThinking = false;
      if (aiTimeout) clearTimeout(aiTimeout);
      currentPlayer = "black";
      gameMode = document.getElementById("gameMode").value;
      updateStatus(gameMode === "ai" ? "轮到你(黑棋)落子" : "轮到黑棋落子");
      resizeCanvas();
      updateUndoButton();
    }

    function restartGame() { init(); }

    function drawBoard() {
      if (cellSize === 0) return;
      const size = cellSize * (boardSize + 1);
      ctx.clearRect(0, 0, size, size);

      // 网格线
      ctx.strokeStyle = currentTheme.boardLines;
      ctx.lineWidth = 1;
      for (let i = 1; i <= boardSize; i++) {
        ctx.beginPath();
        ctx.moveTo(cellSize, i * cellSize);
        ctx.lineTo(boardSize * cellSize, i * cellSize);
        ctx.stroke();
        ctx.beginPath();
        ctx.moveTo(i * cellSize, cellSize);
        ctx.lineTo(i * cellSize, boardSize * cellSize);
        ctx.stroke();
      }

      // 星位
      ctx.fillStyle = currentTheme.starColor;
      [[3,3],[11,3],[3,11],[11,11],[7,7]].forEach(([x,y]) => {
        ctx.beginPath();
        ctx.arc((x+1)*cellSize, (y+1)*cellSize, Math.max(3, cellSize*0.12), 0, Math.PI*2);
        ctx.fill();
      });

      // 棋子
      for (let i = 0; i < boardSize; i++)
        for (let j = 0; j < boardSize; j++)
          if (chessBoard[i][j]) drawChess(i, j, chessBoard[i][j]);

      // 最后一步标记
      if (moveHistory.length > 0) {
        const last = moveHistory[moveHistory.length - 1];
        ctx.strokeStyle = "#ff0000";
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.arc((last.x+1)*cellSize, (last.y+1)*cellSize, cellSize*0.25, 0, Math.PI*2);
        ctx.stroke();
      }
    }

    function drawChess(x, y, color) {
      const cx = (x+1)*cellSize, cy = (y+1)*cellSize;
      const r = cellSize/2 - Math.max(1, cellSize*0.05);
      ctx.beginPath();
      ctx.arc(cx, cy, r, 0, Math.PI*2);

      const grad = ctx.createRadialGradient(cx-r/3, cy-r/3, 1, cx, cy, r);
      if (color === "black") {
        grad.addColorStop(0, currentTheme.blackGradient[0]);
        grad.addColorStop(1, currentTheme.blackGradient[1]);
      } else {
        grad.addColorStop(0, currentTheme.whiteGradient[0]);
        grad.addColorStop(1, currentTheme.whiteGradient[1]);
      }
      ctx.fillStyle = grad;
      ctx.fill();
      if (color === "white") {
        ctx.strokeStyle = currentTheme.whiteBorder;
        ctx.lineWidth = 1;
        ctx.stroke();
      }
    }

    // ========== 撤销 ==========
    function undoMove() {
      if (gameOver || moveHistory.length === 0 || aiThinking) return;
      if (gameMode === "ai") {
        const last = moveHistory[moveHistory.length - 1];
        if (last.player === "white" && moveHistory.length >= 2) {
          moveHistory.pop(); moveHistory.pop();
        } else {
          moveHistory.pop();
        }
        resetBoardFromHistory();
        currentPlayer = "black";
        updateStatus("轮到你(黑棋)落子");
      } else {
        moveHistory.pop();
        resetBoardFromHistory();
        currentPlayer = currentPlayer === "black" ? "white" : "black";
        updateStatus(`轮到${currentPlayer === "black" ? "黑棋" : "白棋"}落子`);
      }
      drawBoard();
      updateUndoButton();
    }

    function resetBoardFromHistory() {
      for (let i = 0; i < boardSize; i++)
        for (let j = 0; j < boardSize; j++) chessBoard[i][j] = "";
      for (const m of moveHistory) chessBoard[m.x][m.y] = m.player;
    }

    // ========== 触控/点击落子 ==========
    function handleInput(e) {
      if (gameOver || aiThinking) return;
      e.preventDefault(); // 防止触摸时触发鼠标事件

      const rect = canvas.getBoundingClientRect();
      const scaleX = canvas.offsetWidth / (cellSize * (boardSize + 1));
      const scaleY = canvas.offsetHeight / (cellSize * (boardSize + 1));

      let clientX, clientY;
      if (e.changedTouches && e.changedTouches.length > 0) {
        clientX = e.changedTouches[0].clientX;
        clientY = e.changedTouches[0].clientY;
      } else {
        clientX = e.clientX;
        clientY = e.clientY;
      }

      const x = Math.round((clientX - rect.left) / scaleX / cellSize) - 1;
      const y = Math.round((clientY - rect.top) / scaleY / cellSize) - 1;

      if (x >= 0 && x < boardSize && y >= 0 && y < boardSize && chessBoard[x][y] === "") {
        chessBoard[x][y] = currentPlayer;
        moveHistory.push({ x, y, player: currentPlayer });
        drawBoard();
        playPlaceSound();

        if (checkWin(x, y)) {
          const winner = currentPlayer === "black" ? "黑棋" : "白棋";
          updateStatus(`${winner} 获胜!🎉`);
          playWinSound();
          gameOver = true;
          if (gameMode === "ai") {
            const key = currentPlayer === "black" ? "blackWins" : "whiteWins";
            localStorage.setItem(key, parseInt(localStorage.getItem(key)) + 1);
            updateScoreDisplay();
          }
          updateUndoButton();
          return;
        }

        currentPlayer = currentPlayer === "black" ? "white" : "black";
        if (gameMode === "ai" && currentPlayer === "white") {
          updateStatus("🤔 电脑思考中...");
          aiThinking = true;
          updateUndoButton();
          aiTimeout = setTimeout(aiMove, 500);
        } else {
          updateStatus(`轮到${currentPlayer === "black" ? "黑棋" : "白棋"}落子`);
        }
        updateUndoButton();
      }
    }

    // 同时绑定touch和click,用preventDefault防重复
    canvas.addEventListener("touchend", handleInput, { passive: false });
    canvas.addEventListener("click", handleInput);

    // ========== AI ==========
    function aiMove() {
      if (gameOver) { aiThinking = false; return; }
      let bestScore = -Infinity, bestMove = null;
      for (let i = 0; i < boardSize; i++)
        for (let j = 0; j < boardSize; j++)
          if (chessBoard[i][j] === "") {
            const s = evaluate(i, j, "white");
            if (s > bestScore) { bestScore = s; bestMove = {x:i, y:j}; }
          }

      if (bestMove) {
        chessBoard[bestMove.x][bestMove.y] = "white";
        moveHistory.push({ x: bestMove.x, y: bestMove.y, player: "white" });
        drawBoard();
        playPlaceSound();
        if (checkWin(bestMove.x, bestMove.y)) {
          updateStatus("电脑获胜 😢");
          playWinSound();
          gameOver = true;
          localStorage.setItem("whiteWins", parseInt(localStorage.getItem("whiteWins")) + 1);
          updateScoreDisplay();
          aiThinking = false;
          updateUndoButton();
          return;
        }
        currentPlayer = "black";
        updateStatus("轮到你(黑棋)落子");
      }
      aiThinking = false;
      updateUndoButton();
    }

    // ========== 评估 & 胜负 ==========
    const PS = { live5:100000, live4:10000, doubleLive3:5000, live3:1000, live2:100, block4:800, block3:50 };

    function evaluate(x, y, player) {
      let score = 0;
      const dirs = [[[1,0],[-1,0]],[[0,1],[0,-1]],[[1,1],[-1,-1]],[[1,-1],[-1,1]]];
      for (const dir of dirs) {
        let line = "";
        for (let s = -4; s <= 4; s++) {
          const tx = x + dir[0][0]*s, ty = y + dir[0][1]*s;
          line += (tx>=0 && tx<boardSize && ty>=0 && ty<boardSize) ? (chessBoard[tx][ty]||"_") : "#";
        }
        line = line.replace(/_/i, "o");
        if (line.includes("ooooo")) return PS.live5;
        if (/(_|^)oooo_/.test(line)||/_oooo($|_)/.test(line)) score += PS.live4;
        else if (/oo_o_o|o_oo_o|o_o_oo|_ooo_/.test(line)) score += PS.live3;
        else if (/oo_oo/.test(line)) score += PS.block4;
        else if (/oo_/.test(line)) score += PS.live2;
      }
      // 防守分
      for (const dir of dirs) {
        let line = "";
        for (let s = -4; s <= 4; s++) {
          const tx = x + dir[0][0]*s, ty = y + dir[0][1]*s;
          line += (tx>=0 && tx<boardSize && ty>=0 && ty<boardSize) ? (chessBoard[tx][ty]||"_") : "#";
        }
        line = line.replace(/_/i, "x");
        if (line.includes("xxxxx")) score += PS.live5 * 0.9;
        else if (/(_|^)xxxx_/.test(line)||/_xxxx($|_)/.test(line)) score += PS.live4 * 0.9;
        else if (/xx_x_x|x_xx_x|x_x_xx|_xxx_/.test(line)) score += PS.live3 * 0.8;
      }
      return score;
    }

    function checkWin(x, y) {
      const dirs = [[[1,0],[-1,0]],[[0,1],[0,-1]],[[1,1],[-1,-1]],[[1,-1],[-1,1]]];
      const p = chessBoard[x][y];
      for (const dir of dirs) {
        let c = 1;
        for (const d of dir) {
          let tx = x+d[0], ty = y+d[1];
          while (tx>=0 && tx<boardSize && ty>=0 && ty<boardSize && chessBoard[tx][ty]===p) { c++; tx+=d[0]; ty+=d[1]; }
        }
        if (c >= 5) return true;
      }
      return false;
    }

    // ========== 启动 ==========
    document.getElementById("gameMode").addEventListener("change", init);
    initScores();
    applyTheme("classic");
    init();
  </script>
</body>
</html>
        
编辑器加载中
预览
控制台