【HTML】五子棋(精美版)

发布于:2025-07-17 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

第一版:双人对战

执行代码

运行结果

第二版本:人机对战

执行代码

优化点

第三版:人机对战

 执行代码

运行结果 

优化点

第四版:人机对战

执行代码

运行结果

优化点


第一版:双人对战

执行代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>五子棋</title>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: Arial, sans-serif;
    }
    canvas {
      border: 1px solid black;
    }
    #status {
      margin-top: 10px;
      font-size: 18px;
    }
    #restart {
      margin-top: 10px;
      padding: 10px 20px;
      font-size: 16px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <canvas id="board" width="450" height="450"></canvas>
  <div id="status">当前玩家:黑棋</div>
  <button id="restart">重新开始</button>
  <script>
    const canvas = document.getElementById('board');
    const ctx = canvas.getContext('2d');
    const status = document.getElementById('status');
    const restartBtn = document.getElementById('restart');
    const size = 15; // 15x15 棋盘
    const cellSize = 30; // 每个格子大小
    const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
    let currentPlayer = 1; // 1:黑棋, 2:白棋
    let gameOver = false;

    // 绘制棋盘
    function drawBoard() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.beginPath();
      for (let i = 0; i < size; i++) {
        // 画横线
        ctx.moveTo(0, i * cellSize);
        ctx.lineTo(canvas.width, i * cellSize);
        // 画竖线
        ctx.moveTo(i * cellSize, 0);
        ctx.lineTo(i * cellSize, canvas.height);
      }
      ctx.strokeStyle = '#000';
      ctx.stroke();

      // 绘制棋子
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 1) {
            ctx.beginPath();
            ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'black';
            ctx.fill();
          } else if (board[i][j] === 2) {
            ctx.beginPath();
            ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'white';
            ctx.strokeStyle = 'black';
            ctx.fill();
            ctx.stroke();
          }
        }
      }
    }

    // 检查胜利
    function checkWin(row, col, player) {
      const directions = [
        [0, 1], [1, 0], [1, 1], [1, -1] // 水平、垂直、斜线
      ];
      for (let [dx, dy] of directions) {
        let count = 1;
        // 正方向
        for (let i = 1; i < 5; i++) {
          let newRow = row + i * dx;
          let newCol = col + i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        // 反方向
        for (let i = 1; i < 5; i++) {
          let newRow = row - i * dx;
          let newCol = col - i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        if (count >= 5) return true;
      }
      return false;
    }

    // 处理点击
    canvas.addEventListener('click', (e) => {
      if (gameOver) return;
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const row = Math.round(y / cellSize);
      const col = Math.round(x / cellSize);
      if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
        board[row][col] = currentPlayer;
        drawBoard();
        if (checkWin(row, col, currentPlayer)) {
          status.textContent = `游戏结束!${currentPlayer === 1 ? '黑棋' : '白棋'}获胜!`;
          gameOver = true;
        } else {
          currentPlayer = currentPlayer === 1 ? 2 : 1;
          status.textContent = `当前玩家:${currentPlayer === 1 ? '黑棋' : '白棋'}`;
        }
      }
    });

    // 重新开始
    restartBtn.addEventListener('click', () => {
      board.forEach(row => row.fill(0));
      currentPlayer = 1;
      gameOver = false;
      status.textContent = '当前玩家:黑棋';
      drawBoard();
    });

    // 初始化棋盘
    drawBoard();
  </script>
</body>
</html>

运行结果


第二版本:人机对战

执行代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>五子棋 - 人机对战</title>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: Arial, sans-serif;
    }
    canvas {
      border: 1px solid black;
    }
    #status {
      margin-top: 10px;
      font-size: 18px;
    }
    #timer {
      margin-top: 5px;
      font-size: 16px;
      color: red;
    }
    #restart {
      margin-top: 10px;
      padding: 10px 20px;
      font-size: 16px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <canvas id="board" width="450" height="450"></canvas>
  <div id="status">当前玩家:黑棋(玩家)</div>
  <div id="timer">剩余时间:20秒</div>
  <button id="restart">重新开始</button>
  <script>
    const canvas = document.getElementById('board');
    const ctx = canvas.getContext('2d');
    const status = document.getElementById('status');
    const timerDisplay = document.getElementById('timer');
    const restartBtn = document.getElementById('restart');
    const size = 15; // 15x15 棋盘
    const cellSize = 30; // 每个格子大小
    const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
    let currentPlayer = 1; // 1:黑棋(玩家), 2:白棋(AI)
    let gameOver = false;
    let timer;
    let timeLeft = 20; // 玩家20秒,AI 5秒

    // 绘制棋盘
    function drawBoard() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.beginPath();
      for (let i = 0; i < size; i++) {
        ctx.moveTo(0, i * cellSize);
        ctx.lineTo(canvas.width, i * cellSize);
        ctx.moveTo(i * cellSize, 0);
        ctx.lineTo(i * cellSize, canvas.height);
      }
      ctx.strokeStyle = '#000';
      ctx.stroke();

      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 1) {
            ctx.beginPath();
            ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'black';
            ctx.fill();
          } else if (board[i][j] === 2) {
            ctx.beginPath();
            ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'white';
            ctx.strokeStyle = 'black';
            ctx.fill();
            ctx.stroke();
          }
        }
      }
    }

    // 检查胜利
    function checkWin(row, col, player) {
      const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
      for (let [dx, dy] of directions) {
        let count = 1;
        for (let i = 1; i < 5; i++) {
          let newRow = row + i * dx;
          let newCol = col + i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        for (let i = 1; i < 5; i++) {
          let newRow = row - i * dx;
          let newCol = col - i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        if (count >= 5) return true;
      }
      return false;
    }

    // 评估棋盘得分
    function evaluateBoard() {
      let score = 0;
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 0) continue;
          const player = board[i][j];
          const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
          for (let [dx, dy] of directions) {
            let count = 1;
            let openEnds = 0;
            for (let k = 1; k < 5; k++) {
              let newRow = i + k * dx;
              let newCol = j + k * dy;
              if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
                count++;
              } else {
                if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
                  openEnds++;
                }
                break;
              }
            }
            for (let k = 1; k < 5; k++) {
              let newRow = i - k * dx;
              let newCol = j - k * dy;
              if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
                count++;
              } else {
                if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
                  openEnds++;
                }
                break;
              }
            }
            if (count >= 5) return player === 2 ? Infinity : -Infinity;
            if (player === 2) {
              if (count === 4 && openEnds >= 1) score += 10000;
              else if (count === 3 && openEnds === 2) score += 1000;
              else if (count === 3 && openEnds === 1) score += 100;
              else if (count === 2 && openEnds === 2) score += 10;
            } else {
              if (count === 4 && openEnds >= 1) score -= 12000;
              else if (count === 3 && openEnds === 2) score -= 1200;
              else if (count === 3 && openEnds === 1) score -= 120;
              else if (count === 2 && openEnds === 2) score -= 12;
            }
          }
        }
      }
      return score;
    }

    // 获取可落子位置(优化:仅考虑邻近棋子的空位)
    function getValidMoves() {
      const moves = [];
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 0) {
            let hasNeighbor = false;
            for (let di = -1; di <= 1; di++) {
              for (let dj = -1; dj <= 1; dj++) {
                let ni = i + di, nj = j + dj;
                if (ni >= 0 && ni < size && nj >= 0 && nj < size && board[ni][nj] !== 0) {
                  hasNeighbor = true;
                  break;
                }
              }
              if (hasNeighbor) break;
            }
            if (hasNeighbor || (i === Math.floor(size/2) && j === Math.floor(size/2))) {
              moves.push([i, j]);
            }
          }
        }
      }
      return moves;
    }

    // Minimax 算法 + Alpha-Beta 剪枝
    function minimax(depth, alpha, beta, isMaximizing) {
      if (depth === 0) return evaluateBoard();
      let validMoves = getValidMoves();
      if (validMoves.length === 0) return evaluateBoard();

      if (isMaximizing) {
        let maxEval = -Infinity;
        for (let [row, col] of validMoves) {
          board[row][col] = 2;
          if (checkWin(row, col, 2)) {
            board[row][col] = 0;
            return Infinity;
          }
          let evalScore = minimax(depth - 1, alpha, beta, false);
          board[row][col] = 0;
          maxEval = Math.max(maxEval, evalScore);
          alpha = Math.max(alpha, evalScore);
          if (beta <= alpha) break;
        }
        return maxEval;
      } else {
        let minEval = Infinity;
        for (let [row, col] of validMoves) {
          board[row][col] = 1;
          if (checkWin(row, col, 1)) {
            board[row][col] = 0;
            return -Infinity;
          }
          let evalScore = minimax(depth - 1, alpha, beta, true);
          board[row][col] = 0;
          minEval = Math.min(minEval, evalScore);
          beta = Math.min(beta, evalScore);
          if (beta <= alpha) break;
        }
        return minEval;
      }
    }

    // AI 选择最佳落子
    function aiMove() {
      let bestScore = -Infinity;
      let bestMove = null;
      const validMoves = getValidMoves();
      const depth = 2;

      const startTime = performance.now();
      for (let [row, col] of validMoves) {
        board[row][col] = 2;
        let score = minimax(depth, -Infinity, Infinity, false);
        board[row][col] = 0;
        if (score > bestScore) {
          bestScore = score;
          bestMove = [row, col];
        }
        if (performance.now() - startTime > 4500) break; // 限制 AI 计算时间
      }

      if (bestMove) {
        const [row, col] = bestMove;
        board[row][col] = 2;
        drawBoard();
        clearInterval(timer);
        if (checkWin(row, col, 2)) {
          status.textContent = '游戏结束!白棋(AI)获胜!';
          timerDisplay.textContent = '';
          gameOver = true;
        } else {
          currentPlayer = 1;
          status.textContent = '当前玩家:黑棋(玩家)';
          startTimer(20); // 玩家 20 秒
        }
      } else {
        // AI 超时或无合法落子
        clearInterval(timer);
        status.textContent = '游戏结束!白棋(AI)超时或无合法落子,黑棋(玩家)获胜!';
        timerDisplay.textContent = '';
        gameOver = true;
      }
    }

    // 计时器
    function startTimer(seconds) {
      timeLeft = seconds;
      timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
      clearInterval(timer);
      timer = setInterval(() => {
        timeLeft--;
        timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
        if (timeLeft <= 0) {
          clearInterval(timer);
          if (currentPlayer === 1) {
            status.textContent = '游戏结束!黑棋(玩家)超时,白棋(AI)获胜!';
            timerDisplay.textContent = '';
            gameOver = true;
          } else {
            status.textContent = '游戏结束!白棋(AI)超时,黑棋(玩家)获胜!';
            timerDisplay.textContent = '';
            gameOver = true;
          }
        }
      }, 1000);
    }

    // 处理玩家点击
    canvas.addEventListener('click', (e) => {
      if (gameOver || currentPlayer !== 1) return;
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const row = Math.round(y / cellSize);
      const col = Math.round(x / cellSize);
      if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
        board[row][col] = 1;
        drawBoard();
        clearInterval(timer);
        if (checkWin(row, col, 1)) {
          status.textContent = '游戏结束!黑棋(玩家)获胜!';
          timerDisplay.textContent = '';
          gameOver = true;
        } else {
          currentPlayer = 2;
          status.textContent = 'AI(白棋)思考中...';
          timerDisplay.textContent = '剩余时间:5秒';
          startTimer(5); // AI 5 秒
          setTimeout(() => {
            aiMove();
          }, 500);
        }
      }
    });

    // 重新开始
    restartBtn.addEventListener('click', () => {
      board.forEach(row => row.fill(0));
      currentPlayer = 1;
      gameOver = false;
      status.textContent = '当前玩家:黑棋(玩家)';
      timerDisplay.textContent = '剩余时间:20秒';
      clearInterval(timer);
      drawBoard();
      startTimer(20); // 玩家 20 秒
    });

    // 初始化棋盘并启动计时器
    drawBoard();
    startTimer(20); // 玩家 20 秒
  </script>
</body>
</html>

优化点

  • 玩家超时(20 秒未落子)会导致 AI 获胜,AI 超时(5 秒未落子)会导致玩家获胜。
  • AI 使用 minimax 算法(深度 2)结合 alpha-beta 剪枝,计算时间受限以确保 5 秒内完成。
  • 计时器通过 setInterval 实现,每秒更新,AI 计算时间通过 performance.now() 监控,限制在 4.5 秒内以留出余量。

第三版:人机对战

 执行代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>五子棋 - 人机对战</title>
  <style>
    body {
      display: flex;
      flex-direction: column;
      align-items: center;
      font-family: Arial, sans-serif;
    }
    canvas {
      border: 1px solid black;
    }
    #status {
      margin-top: 10px;
      font-size: 18px;
    }
    #timer {
      margin-top: 5px;
      font-size: 16px;
      color: red;
    }
    #buttons {
      margin-top: 10px;
    }
    #undo, #restart {
      padding: 10px 20px;
      font-size: 16px;
      cursor: pointer;
      margin: 0 5px;
    }
    #undo:disabled {
      cursor: not-allowed;
      opacity: 0.5;
    }
  </style>
</head>
<body>
  <canvas id="board" width="450" height="450"></canvas>
  <div id="status">当前玩家:黑棋(玩家)</div>
  <div id="timer">剩余时间:20秒</div>
  <div id="buttons">
    <button id="undo">悔棋(剩余3次)</button>
    <button id="restart">重新开始</button>
  </div>
  <script>
    const canvas = document.getElementById('board');
    const ctx = canvas.getContext('2d');
    const status = document.getElementById('status');
    const timerDisplay = document.getElementById('timer');
    const undoBtn = document.getElementById('undo');
    const restartBtn = document.getElementById('restart');
    const size = 15; // 15x15 棋盘
    const cellSize = 30; // 每个格子大小
    const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
    let currentPlayer = 1; // 1:黑棋(玩家), 2:白棋(AI)
    let gameOver = false;
    let timer;
    let timeLeft = 20; // 玩家20秒,AI 5秒
    let undoCount = 3; // 悔棋次数
    let moveHistory = []; // 存储落子历史
    let lastMove = null; // 最新落子位置

    // 绘制棋盘
    function drawBoard() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.beginPath();
      for (let i = 0; i < size; i++) {
        ctx.moveTo(0, i * cellSize);
        ctx.lineTo(canvas.width, i * cellSize);
        ctx.moveTo(i * cellSize, 0);
        ctx.lineTo(i * cellSize, canvas.height);
      }
      ctx.strokeStyle = '#000';
      ctx.stroke();

      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 1) {
            ctx.beginPath();
            ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'black';
            ctx.fill();
          } else if (board[i][j] === 2) {
            ctx.beginPath();
            ctx.arc(j * cellSize, i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'white';
            ctx.strokeStyle = 'black';
            ctx.fill();
            ctx.stroke();
          }
        }
      }

      // 高亮最新落子
      if (lastMove) {
        const [row, col] = lastMove;
        ctx.beginPath();
        ctx.rect(col * cellSize - cellSize / 2, row * cellSize - cellSize / 2, cellSize, cellSize);
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.lineWidth = 1; // 重置线宽
        setTimeout(() => {
          drawBoard(); // 2秒后清除高亮
        }, 2000);
      }
    }

    // 检查胜利
    function checkWin(row, col, player) {
      const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
      for (let [dx, dy] of directions) {
        let count = 1;
        for (let i = 1; i < 5; i++) {
          let newRow = row + i * dx;
          let newCol = col + i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        for (let i = 1; i < 5; i++) {
          let newRow = row - i * dx;
          let newCol = col - i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        if (count >= 5) return true;
      }
      return false;
    }

    // 评估棋盘得分
    function evaluateBoard() {
      let score = 0;
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 0) continue;
          const player = board[i][j];
          const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
          for (let [dx, dy] of directions) {
            let count = 1;
            let openEnds = 0;
            for (let k = 1; k < 5; k++) {
              let newRow = i + k * dx;
              let newCol = j + k * dy;
              if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
                count++;
              } else {
                if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
                  openEnds++;
                }
                break;
              }
            }
            for (let k = 1; k < 5; k++) {
              let newRow = i - k * dx;
              let newCol = j - k * dy;
              if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
                count++;
              } else {
                if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
                  openEnds++;
                }
                break;
              }
            }
            if (count >= 5) return player === 2 ? Infinity : -Infinity;
            if (player === 2) {
              if (count === 4 && openEnds >= 1) score += 10000;
              else if (count === 3 && openEnds === 2) score += 1000;
              else if (count === 3 && openEnds === 1) score += 100;
              else if (count === 2 && openEnds === 2) score += 10;
            } else {
              if (count === 4 && openEnds >= 1) score -= 12000;
              else if (count === 3 && openEnds === 2) score -= 1200;
              else if (count === 3 && openEnds === 1) score -= 120;
              else if (count === 2 && openEnds === 2) score -= 12;
            }
          }
        }
      }
      return score;
    }

    // 获取可落子位置
    function getValidMoves() {
      const moves = [];
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 0) {
            let hasNeighbor = false;
            for (let di = -1; di <= 1; di++) {
              for (let dj = -1; dj <= 1; dj++) {
                let ni = i + di, nj = j + dj;
                if (ni >= 0 && ni < size && nj >= 0 && nj < size && board[ni][nj] !== 0) {
                  hasNeighbor = true;
                  break;
                }
              }
              if (hasNeighbor) break;
            }
            if (hasNeighbor || (i === Math.floor(size/2) && j === Math.floor(size/2))) {
              moves.push([i, j]);
            }
          }
        }
      }
      return moves;
    }

    // Minimax 算法 + Alpha-Beta 剪枝
    function minimax(depth, alpha, beta, isMaximizing) {
      if (depth === 0) return evaluateBoard();
      let validMoves = getValidMoves();
      if (validMoves.length === 0) return evaluateBoard();

      if (isMaximizing) {
        let maxEval = -Infinity;
        for (let [row, col] of validMoves) {
          board[row][col] = 2;
          if (checkWin(row, col, 2)) {
            board[row][col] = 0;
            return Infinity;
          }
          let evalScore = minimax(depth - 1, alpha, beta, false);
          board[row][col] = 0;
          maxEval = Math.max(maxEval, evalScore);
          alpha = Math.max(alpha, evalScore);
          if (beta <= alpha) break;
        }
        return maxEval;
      } else {
        let minEval = Infinity;
        for (let [row, col] of validMoves) {
          board[row][col] = 1;
          if (checkWin(row, col, 1)) {
            board[row][col] = 0;
            return -Infinity;
          }
          let evalScore = minimax(depth - 1, alpha, beta, true);
          board[row][col] = 0;
          minEval = Math.min(minEval, evalScore);
          beta = Math.min(beta, evalScore);
          if (beta <= alpha) break;
        }
        return minEval;
      }
    }

    // AI 选择最佳落子
    function aiMove() {
      let bestScore = -Infinity;
      let bestMove = null;
      const validMoves = getValidMoves();
      const depth = 2;
      const startTime = performance.now();

      for (let [row, col] of validMoves) {
        board[row][col] = 2;
        let score = minimax(depth, -Infinity, Infinity, false);
        board[row][col] = 0;
        if (score > bestScore) {
          bestScore = score;
          bestMove = [row, col];
        }
        if (performance.now() - startTime > 4500) break;
      }

      if (bestMove) {
        const [row, col] = bestMove;
        board[row][col] = 2;
        moveHistory.push({ player: 2, row, col });
        lastMove = [row, col];
        drawBoard();
        clearInterval(timer);
        status.textContent = `白棋(AI)落子:(${row}, ${col})`;
        if (checkWin(row, col, 2)) {
          status.textContent = `游戏结束!白棋(AI)获胜!`;
          timerDisplay.textContent = '';
          gameOver = true;
        } else {
          currentPlayer = 1;
          setTimeout(() => {
            status.textContent = '当前玩家:黑棋(玩家)';
            startTimer(20);
          }, 2000);
        }
      } else {
        clearInterval(timer);
        status.textContent = '游戏结束!白棋(AI)超时或无合法落子,黑棋(玩家)获胜!';
        timerDisplay.textContent = '';
        gameOver = true;
      }
    }

    // 计时器
    function startTimer(seconds) {
      timeLeft = seconds;
      timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
      clearInterval(timer);
      timer = setInterval(() => {
        timeLeft--;
        timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
        if (timeLeft <= 0) {
          clearInterval(timer);
          if (currentPlayer === 1) {
            status.textContent = '游戏结束!黑棋(玩家)超时,白棋(AI)获胜!';
            timerDisplay.textContent = '';
            gameOver = true;
          } else {
            status.textContent = '游戏结束!白棋(AI)超时,黑棋(玩家)获胜!';
            timerDisplay.textContent = '';
            gameOver = true;
          }
        }
      }, 1000);
    }

    // 处理玩家点击
    canvas.addEventListener('click', (e) => {
      if (gameOver || currentPlayer !== 1) return;
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const row = Math.round(y / cellSize);
      const col = Math.round(x / cellSize);
      if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
        board[row][col] = 1;
        moveHistory.push({ player: 1, row, col });
        lastMove = [row, col];
        drawBoard();
        clearInterval(timer);
        status.textContent = `黑棋(玩家)落子:(${row}, ${col})`;
        if (checkWin(row, col, 1)) {
          status.textContent = `游戏结束!黑棋(玩家)获胜!`;
          timerDisplay.textContent = '';
          gameOver = true;
        } else {
          currentPlayer = 2;
          setTimeout(() => {
            status.textContent = 'AI(白棋)思考中...';
            timerDisplay.textContent = '剩余时间:5秒';
            startTimer(5);
            setTimeout(() => {
              aiMove();
            }, 500);
          }, 2000);
        }
      }
    });

    // 悔棋
    undoBtn.addEventListener('click', () => {
      if (gameOver || undoCount <= 0 || currentPlayer !== 1 || moveHistory.length < 2) return;
      // 撤销最后两次落子(AI 和玩家各一次)
      for (let i = 0; i < 2; i++) {
        if (moveHistory.length > 0) {
          const last = moveHistory.pop();
          board[last.row][last.col] = 0;
        }
      }
      undoCount--;
      undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
      if (undoCount === 0) undoBtn.disabled = true;
      lastMove = moveHistory.length > 0 ? [moveHistory[moveHistory.length - 1].row, moveHistory[moveHistory.length - 1].col] : null;
      drawBoard();
      clearInterval(timer);
      status.textContent = '当前玩家:黑棋(玩家)';
      startTimer(20);
    });

    // 重新开始
    restartBtn.addEventListener('click', () => {
      board.forEach(row => row.fill(0));
      currentPlayer = 1;
      gameOver = false;
      undoCount = 3;
      moveHistory = [];
      lastMove = null;
      undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
      undoBtn.disabled = false;
      status.textContent = '当前玩家:黑棋(玩家)';
      timerDisplay.textContent = '剩余时间:20秒';
      clearInterval(timer);
      drawBoard();
      startTimer(20);
    });

    // 初始化棋盘并启动计时器
    drawBoard();
    startTimer(20);
  </script>
</body>
</html>

运行结果 

优化点

  • 落子提示:最新落子以红色边框高亮 2 秒,状态栏显示落子坐标,确保玩家清楚落子位置。
  • 悔棋限制:最多 3 次,需在玩家回合使用,撤销最近的玩家和 AI 落子,保持游戏公平。

第四版:人机对战

执行代码

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>五子棋 - 人机对战</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <style>
    body {
      background: linear-gradient(to bottom, #f0f4f8, #d9e2ec);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      font-family: 'Arial', sans-serif;
    }
    canvas {
      background: url('https://www.transparenttextures.com/patterns/wood-pattern.png');
      border: 4px solid #4a3728;
      border-radius: 8px;
      box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
    }
  </style>
</head>
<body>
  <div class="flex flex-col items-center p-6">
    <h1 class="text-3xl font-bold text-gray-800 mb-4">五子棋 - 人机对战</h1>
    <canvas id="board" width="480" height="480" class="mb-4"></canvas>
    <div id="status" class="text-xl text-gray-700 mb-2">当前玩家:黑棋(玩家)</div>
    <div id="timer" class="text-lg text-red-600 mb-4">剩余时间:20秒</div>
    <div id="buttons" class="flex space-x-4">
      <button id="undo" class="bg-blue-500 text-white px-6 py-2 rounded-lg hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed">悔棋(剩余3次)</button>
      <button id="restart" class="bg-green-500 text-white px-6 py-2 rounded-lg hover:bg-green-600">重新开始</button>
    </div>
  </div>
  <script>
    const canvas = document.getElementById('board');
    const ctx = canvas.getContext('2d');
    const status = document.getElementById('status');
    const timerDisplay = document.getElementById('timer');
    const undoBtn = document.getElementById('undo');
    const restartBtn = document.getElementById('restart');
    const size = 15; // 15x15 棋盘
    const cellSize = 30; // 每个格子大小
    const offset = 30; // 边缘偏移,确保棋子完整显示
    const board = Array(size).fill().map(() => Array(size).fill(0)); // 0:空, 1:黑棋, 2:白棋
    let currentPlayer = 1; // 1:黑棋(玩家), 2:白棋(AI)
    let gameOver = false;
    let timer;
    let timeLeft = 20; // 玩家20秒,AI 5秒
    let undoCount = 3; // 悔棋次数
    let moveHistory = []; // 落子历史
    let lastMove = null; // 最新落子

    // 绘制棋盘
    function drawBoard() {
      ctx.clearRect(0, 0, canvas.width, canvas.height);
      ctx.beginPath();
      for (let i = 0; i < size; i++) {
        // 横线
        ctx.moveTo(offset, offset + i * cellSize);
        ctx.lineTo(offset + (size - 1) * cellSize, offset + i * cellSize);
        // 竖线
        ctx.moveTo(offset + i * cellSize, offset);
        ctx.lineTo(offset + i * cellSize, offset + (size - 1) * cellSize);
      }
      ctx.strokeStyle = '#000';
      ctx.lineWidth = 1;
      ctx.stroke();

      // 绘制棋子
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 1) {
            ctx.beginPath();
            ctx.arc(offset + j * cellSize, offset + i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'black';
            ctx.fill();
          } else if (board[i][j] === 2) {
            ctx.beginPath();
            ctx.arc(offset + j * cellSize, offset + i * cellSize, cellSize / 2 - 2, 0, Math.PI * 2);
            ctx.fillStyle = 'white';
            ctx.strokeStyle = 'black';
            ctx.fill();
            ctx.stroke();
          }
        }
      }

      // 高亮最新落子
      if (lastMove) {
        const [row, col] = lastMove;
        ctx.beginPath();
        ctx.rect(offset + col * cellSize - cellSize / 2, offset + row * cellSize - cellSize / 2, cellSize, cellSize);
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 2;
        ctx.stroke();
        ctx.lineWidth = 1;
        setTimeout(() => {
          drawBoard();
        }, 2000);
      }
    }

    // 检查胜利
    function checkWin(row, col, player) {
      const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
      for (let [dx, dy] of directions) {
        let count = 1;
        for (let i = 1; i < 5; i++) {
          let newRow = row + i * dx;
          let newCol = col + i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        for (let i = 1; i < 5; i++) {
          let newRow = row - i * dx;
          let newCol = col - i * dy;
          if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
            count++;
          } else {
            break;
          }
        }
        if (count >= 5) return true;
      }
      return false;
    }

    // 评估棋盘得分
    function evaluateBoard() {
      let score = 0;
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 0) continue;
          const player = board[i][j];
          const directions = [[0, 1], [1, 0], [1, 1], [1, -1]];
          for (let [dx, dy] of directions) {
            let count = 1;
            let openEnds = 0;
            for (let k = 1; k < 5; k++) {
              let newRow = i + k * dx;
              let newCol = j + k * dy;
              if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
                count++;
              } else {
                if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
                  openEnds++;
                }
                break;
              }
            }
            for (let k = 1; k < 5; k++) {
              let newRow = i - k * dx;
              let newCol = j - k * dy;
              if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === player) {
                count++;
              } else {
                if (newRow >= 0 && newRow < size && newCol >= 0 && newCol < size && board[newRow][newCol] === 0) {
                  openEnds++;
                }
                break;
              }
            }
            if (count >= 5) return player === 2 ? Infinity : -Infinity;
            if (player === 2) {
              if (count === 4 && openEnds >= 1) score += 10000;
              else if (count === 3 && openEnds === 2) score += 1000;
              else if (count === 3 && openEnds === 1) score += 100;
              else if (count === 2 && openEnds === 2) score += 10;
            } else {
              if (count === 4 && openEnds >= 1) score -= 12000;
              else if (count === 3 && openEnds === 2) score -= 1200;
              else if (count === 3 && openEnds === 1) score -= 120;
              else if (count === 2 && openEnds === 2) score -= 12;
            }
          }
        }
      }
      return score;
    }

    // 获取可落子位置
    function getValidMoves() {
      const moves = [];
      for (let i = 0; i < size; i++) {
        for (let j = 0; j < size; j++) {
          if (board[i][j] === 0) {
            let hasNeighbor = false;
            for (let di = -1; di <= 1; di++) {
              for (let dj = -1; dj <= 1; dj++) {
                let ni = i + di, nj = j + dj;
                if (ni >= 0 && ni < size && nj >= 0 && nj < size && board[ni][nj] !== 0) {
                  hasNeighbor = true;
                  break;
                }
              }
              if (hasNeighbor) break;
            }
            if (hasNeighbor || (i === Math.floor(size/2) && j === Math.floor(size/2))) {
              moves.push([i, j]);
            }
          }
        }
      }
      return moves;
    }

    // Minimax 算法 + Alpha-Beta 剪枝
    function minimax(depth, alpha, beta, isMaximizing) {
      if (depth === 0) return evaluateBoard();
      let validMoves = getValidMoves();
      if (validMoves.length === 0) return evaluateBoard();

      if (isMaximizing) {
        let maxEval = -Infinity;
        for (let [row, col] of validMoves) {
          board[row][col] = 2;
          if (checkWin(row, col, 2)) {
            board[row][col] = 0;
            return Infinity;
          }
          let evalScore = minimax(depth - 1, alpha, beta, false);
          board[row][col] = 0;
          maxEval = Math.max(maxEval, evalScore);
          alpha = Math.max(alpha, evalScore);
          if (beta <= alpha) break;
        }
        return maxEval;
      } else {
        let minEval = Infinity;
        for (let [row, col] of validMoves) {
          board[row][col] = 1;
          if (checkWin(row, col, 1)) {
            board[row][col] = 0;
            return -Infinity;
          }
          let evalScore = minimax(depth - 1, alpha, beta, true);
          board[row][col] = 0;
          minEval = Math.min(minEval, evalScore);
          beta = Math.min(beta, evalScore);
          if (beta <= alpha) break;
        }
        return minEval;
      }
    }

    // AI 选择最佳落子
    function aiMove() {
      let bestScore = -Infinity;
      let bestMove = null;
      const validMoves = getValidMoves();
      const depth = 2;
      const startTime = performance.now();

      for (let [row, col] of validMoves) {
        board[row][col] = 2;
        let score = minimax(depth, -Infinity, Infinity, false);
        board[row][col] = 0;
        if (score > bestScore) {
          bestScore = score;
          bestMove = [row, col];
        }
        if (performance.now() - startTime > 4500) break;
      }

      if (bestMove) {
        const [row, col] = bestMove;
        board[row][col] = 2;
        moveHistory.push({ player: 2, row, col });
        lastMove = [row, col];
        drawBoard();
        clearInterval(timer);
        status.textContent = `白棋(AI)落子:(${row}, ${col})`;
        if (checkWin(row, col, 2)) {
          status.textContent = `游戏结束!白棋(AI)获胜!`;
          timerDisplay.textContent = '';
          gameOver = true;
        } else {
          currentPlayer = 1;
          setTimeout(() => {
            status.textContent = '当前玩家:黑棋(玩家)';
            startTimer(20);
          }, 2000);
        }
      } else {
        clearInterval(timer);
        status.textContent = '游戏结束!白棋(AI)超时或无合法落子,黑棋(玩家)获胜!';
        timerDisplay.textContent = '';
        gameOver = true;
      }
    }

    // 计时器
    function startTimer(seconds) {
      timeLeft = seconds;
      timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
      clearInterval(timer);
      timer = setInterval(() => {
        timeLeft--;
        timerDisplay.textContent = `剩余时间:${timeLeft}秒`;
        if (timeLeft <= 0) {
          clearInterval(timer);
          if (currentPlayer === 1) {
            status.textContent = '游戏结束!黑棋(玩家)超时,白棋(AI)获胜!';
            timerDisplay.textContent = '';
            gameOver = true;
          } else {
            status.textContent = '游戏结束!白棋(AI)超时,黑棋(玩家)获胜!';
            timerDisplay.textContent = '';
            gameOver = true;
          }
        }
      }, 1000);
    }

    // 处理玩家点击
    canvas.addEventListener('click', (e) => {
      if (gameOver || currentPlayer !== 1) return;
      const rect = canvas.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      const row = Math.round((y - offset) / cellSize);
      const col = Math.round((x - offset) / cellSize);
      if (row >= 0 && row < size && col >= 0 && col < size && board[row][col] === 0) {
        board[row][col] = 1;
        moveHistory.push({ player: 1, row, col });
        lastMove = [row, col];
        drawBoard();
        clearInterval(timer);
        status.textContent = `黑棋(玩家)落子:(${row}, ${col})`;
        if (checkWin(row, col, 1)) {
          status.textContent = `游戏结束!黑棋(玩家)获胜!`;
          timerDisplay.textContent = '';
          gameOver = true;
        } else {
          currentPlayer = 2;
          setTimeout(() => {
            status.textContent = 'AI(白棋)思考中...';
            timerDisplay.textContent = '剩余时间:5秒';
            startTimer(5);
            setTimeout(() => {
              aiMove();
            }, 500);
          }, 2000);
        }
      }
    });

    // 悔棋
    undoBtn.addEventListener('click', () => {
      if (gameOver || undoCount <= 0 || currentPlayer !== 1 || moveHistory.length < 2) return;
      for (let i = 0; i < 2; i++) {
        if (moveHistory.length > 0) {
          const last = moveHistory.pop();
          board[last.row][last.col] = 0;
        }
      }
      undoCount--;
      undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
      if (undoCount === 0) undoBtn.disabled = true;
      lastMove = moveHistory.length > 0 ? [moveHistory[moveHistory.length - 1].row, moveHistory[moveHistory.length - 1].col] : null;
      drawBoard();
      clearInterval(timer);
      status.textContent = '当前玩家:黑棋(玩家)';
      startTimer(20);
    });

    // 重新开始
    restartBtn.addEventListener('click', () => {
      board.forEach(row => row.fill(0));
      currentPlayer = 1;
      gameOver = false;
      undoCount = 3;
      moveHistory = [];
      lastMove = null;
      undoBtn.textContent = `悔棋(剩余${undoCount}次)`;
      undoBtn.disabled = false;
      status.textContent = '当前玩家:黑棋(玩家)';
      timerDisplay.textContent = '剩余时间:20秒';
      clearInterval(timer);
      drawBoard();
      startTimer(20);
    });

    // 初始化棋盘并启动计时器
    drawBoard();
    startTimer(20);
  </script>
</body>
</html>

运行结果

优化点

  • 美观性:使用 Tailwind CSS 优化布局,添加渐变背景、木纹棋盘、按钮悬停效果,提升视觉体验。
  • 边缘棋子:棋盘格线和棋子从偏移 30px 开始绘制,canvas 尺寸调整为 480x480,确保边缘棋子完整显示。


网站公告

今日签到

点亮在社区的每一天
去签到