五子棋(javascript)

这里人机的算法还是不太完美,目前也找不到更好的,大家有想法可以一起交流一下。

html 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>五子棋</title>
  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    .game_title {
      margin-top: 30px;
      font-size: 24px;
      text-align: center;
    }

    .game_canvas {
      display: block;
      height: 450px;
      width: 450px;
      margin: 40px auto;
      box-shadow: 5px 5px 5px #b9b9b9, -2px -2px 2px #efefef;
      cursor: pointer;
    }

    .interaction {
      width: 100px;
      height: 50px;
      margin: 0 auto;
      display: flex;
    }

    .interaction .restart {
      flex: auto;
      border-radius: 15px;
      background-color: rgb(217, 223, 224);
      color: #333;
      font-weight: bolder;
      transition: box-shadow 0.5s;
      font-size: 16px;
      border: none;
    }

    .interaction .restart:hover {
      color: black;
      box-shadow: 0px 1px 3px #a8a7a7;
      cursor: pointer;
    }

    .result {
      width: 200px;
      height: 100px;
      background-color: rgba(206, 207, 206, 0.95);
      position: fixed;
      left: 0;
      right: 0;
      top: 0;
      bottom: 0;
      margin: auto;
      border-radius: 80px;
      font-size: 40px;
      line-height: 100px;
      text-align: center;
      display: none;
    }
  </style>
</head>
<body>
  <h3 class="game_title">五子棋</h3>
  <div class="result"></div>
  <canvas class="game_canvas"></canvas>
  <div class="interaction">
    <button class="restart">重新开始</button>
  </div>

  <script>
    /* ----- 基本画布与棋盘绘制 ----- */
    const game_canvas = document.querySelector(".game_canvas");
    // 必须设置 canvas 内部绘图分辨率(与 CSS 显示尺寸分开)
    game_canvas.width = 450;
    game_canvas.height = 450;
    const context = game_canvas.getContext("2d");
    context.strokeStyle = "#b9b9b9";
    context.lineWidth = 1;

    const drawChessBoard = () => {
      context.clearRect(0, 0, 450, 450);
      // 背景(木纹色)
      context.fillStyle = "#f2d79b";
      context.fillRect(0, 0, 450, 450);

      for (let i = 0; i < 15; i++) {
        // 横线
        context.beginPath();
        context.moveTo(15, 15 + i * 30);
        context.lineTo(435, 15 + i * 30);
        context.stroke();
        context.closePath();

        // 竖线
        context.beginPath();
        context.moveTo(15 + i * 30, 15);
        context.lineTo(15 + i * 30, 435);
        context.stroke();
        context.closePath();
      }

      // 星位(可选)
      const stars = [3, 7, 11];
      context.fillStyle = "#000";
      for (let i of stars) {
        for (let j of stars) {
          context.beginPath();
          context.arc(15 + i * 30, 15 + j * 30, 3, 0, Math.PI * 2);
          context.fill();
          context.closePath();
        }
      }
    }

    drawChessBoard();

    /* ----- 赢法统计(15x15) ----- */
    let wins = [];
    for (let i = 0; i < 15; i++) {
      wins[i] = [];
      for (let j = 0; j < 15; j++) {
        wins[i][j] = [];
      }
    }

    let count = 0;
    // 横
    for (let i = 0; i < 15; i++) {
      for (let j = 0; j < 11; j++) {
        for (let k = 0; k < 5; k++) {
          wins[j + k][i][count] = true;
        }
        count++;
      }
    }
    // 竖
    for (let i = 0; i < 15; i++) {
      for (let j = 0; j < 11; j++) {
        for (let k = 0; k < 5; k++) {
          wins[i][j + k][count] = true;
        }
        count++;
      }
    }
    // 正斜
    for (let i = 0; i < 11; i++) {
      for (let j = 0; j < 11; j++) {
        for (let k = 0; k < 5; k++) {
          wins[i + k][j + k][count] = true;
        }
        count++;
      }
    }
    // 反斜
    for (let i = 0; i < 11; i++) {
      for (let j = 14; j > 3; j--) {
        for (let k = 0; k < 5; k++) {
          wins[i + k][j - k][count] = true;
        }
        count++;
      }
    }

    /* ----- 棋盘状态 ----- */
    let chessboard = [];
    for (let i = 0; i < 15; i++) {
      chessboard[i] = [];
      for (let j = 0; j < 15; j++) {
        chessboard[i][j] = 0; // 0 空,1 人(黑),2 机(白/红)
      }
    }

    let me = true;        // true 表示人的回合(黑子)
    let over = false;     // 游戏是否结束
    let myWin = [];       // 人在每种赢法上的进度
    let computerWin = []; // 机在每种赢法上的进度
    for (let i = 0; i < count; i++) {
      myWin[i] = 0;
      computerWin[i] = 0;
    }

    /* ----- 绘子 ----- */
    // me = true 表示黑子(人),否则为机器
    const onestep = (i, j, me) => {
      context.beginPath();
      context.arc(15 + i * 30, 15 + j * 30, 13, 0, 2 * Math.PI);
      context.closePath();

      // 黑子为 #000,机器为白色
      if (me) {
        // 人下的黑子
        context.fillStyle = "#000";
        context.fill();
      } else {
        // 机器下的颜色
        let grad = context.createRadialGradient(15 + i * 30 - 3, 15 + j * 30 - 3, 2, 15 + i * 30, 15 + j * 30, 13);
        grad.addColorStop(0, '#fff');
        grad.addColorStop(1, '#ddd'); // 白色
        context.fillStyle = grad;
        context.fill();
      }
    }

    /* ----- 辅助:计算到最近已有棋子的曼哈顿距离 ----- */
    const minDistanceToPieces = (i, j) => {
      let minDist = Infinity;
      for (let x = 0; x < 15; x++) {
        for (let y = 0; y < 15; y++) {
          if (chessboard[x][y] !== 0) {
            let d = Math.abs(x - i) + Math.abs(y - j);
            if (d < minDist) minDist = d;
          }
        }
      }
      // 如果棋盘为空(刚开始),返回 0
      return minDist === Infinity ? 0 : minDist;
    }

    /* ----- 修改后的 AI:先检查直接获胜/必须堵点,然后评分(含距离惩罚) ----- */
    const computeAI = () => {
      if (over) return;

      // 1) 先检查是否有直接获胜的落子(机方)
      for (let i = 0; i < 15; i++) {
        for (let j = 0; j < 15; j++) {
          if (chessboard[i][j] === 0) {
            for (let k = 0; k < count; k++) {
              if (wins[i][j][k] && computerWin[k] + 1 === 5) {
                // 下这个点直接获胜
                onestep(i, j, false);
                chessboard[i][j] = 2;
                for (let t = 0; t < count; t++) {
                  if (wins[i][j][t]) computerWin[t] += 1;
                }
                document.querySelectorAll(".result")[0].textContent = `你输了`;
                document.querySelectorAll(".result")[0].style.display = "block";
                over = true;
                return;
              }
            }
          }
        }
      }

      // 2) 再检查是否有必须堵住的点(对方下一步能成五)
      for (let i = 0; i < 15; i++) {
        for (let j = 0; j < 15; j++) {
          if (chessboard[i][j] === 0) {
            for (let k = 0; k < count; k++) {
              if (wins[i][j][k] && myWin[k] + 1 === 5) {
                // 必须堵住这个点
                onestep(i, j, false);
                chessboard[i][j] = 2;
                for (let t = 0; t < count; t++) {
                  if (wins[i][j][t]) computerWin[t] += 1;
                }
                me = !me;
                return;
              }
            }
          }
        }
      }

      // 3) 否则按评分选择最佳点(含距离惩罚和活/眠简单处理)
      let myScore = [], computeScore = [];
      for (let i = 0; i < 15; i++) {
        myScore[i] = [];
        computeScore[i] = [];
        for (let j = 0; j < 15; j++) {
          myScore[i][j] = 0;
          computeScore[i][j] = 0;
        }
      }

      // 简单的基础权值(可调整)
      const weights = {
        1: 200,
        2: 400,
        3: 2000,
        4: 10000
      };

      const compWeights = {
        1: 220,
        2: 420,
        3: 2200,
        4: 20000
      };

      // 评分遍历
      for (let i = 0; i < 15; i++) {
        for (let j = 0; j < 15; j++) {
          if (chessboard[i][j] === 0) {
            for (let k = 0; k < count; k++) {
              if (wins[i][j][k]) {
                // 简单的活/眠判断:看这条5连的两侧是否被堵(边界或对手)
                // 找到该赢法覆盖的5个点后判断两端格子状态
                // 为效率起见,尝试根据 wins 的三种类型来简单判断周边被堵情况
                // 这里实现一个轻量版本:若 myWin[k] 或 computerWin[k] 的进度被双方同时占,则视为被部分堵,降低权值
                // 详细活眠区分可以在更复杂实现中替换
                // 对我方(玩家)的影响
                if (myWin[k] > 0) {
                  // 若该赢法中已有对方棋子则根据进度累加
                  myScore[i][j] += weights[myWin[k]] || 0;
                }
                // 对机器
                if (computerWin[k] > 0) {
                  computeScore[i][j] += compWeights[computerWin[k]] || 0;
                }
              }
            }

            // 距离惩罚:优先靠近已有棋子的点(避免远处落子)
            let dist = minDistanceToPieces(i, j); // 曼哈顿距离
            let distancePenalty = 0;
            // 距离越远惩罚越大(可调)
            if (dist > 0) {
              distancePenalty = (dist - 1) * 10; // 第一个邻近格不惩罚,越远惩罚越多
            }

            // 合并策略:进攻与防守兼顾。给防守一个较高权重但不过度盖过进攻。
            // 也可以用 max(compute, my*factor) 类策略,这里取线性组合并扣除距离惩罚。
            let totalScore = computeScore[i][j] * 1.0 + myScore[i][j] * 0.8 - distancePenalty;

            // 把评分暂存在 computeScore[i][j] 中以便后续比较(也可以用独立数组)
            computeScore[i][j] = totalScore;
          }
        }
      }

      // 选择最大分点(若多个,优先 compute(进攻)分高者,再优先 my(防守)分高者)
      let max = -Infinity;
      let bestX = 0, bestY = 0;
      for (let i = 0; i < 15; i++) {
        for (let j = 0; j < 15; j++) {
          if (chessboard[i][j] === 0) {
            let score = computeScore[i][j];
            if (score > max) {
              max = score;
              bestX = i; bestY = j;
            } else if (score === max) {
              // 评分相等时按进攻得分优先,再按防守得分
              // 重新计算原始分以比较(因为我们把 total 存回 computeScore)
              let myS = myScore[i][j], mySbest = myScore[bestX][bestY];
              let compS = (computeScore[i][j] - myScore[i][j] * 0.8 + minDistanceToPieces(i, j) * 10) || 0;
              let compSbest = (computeScore[bestX][bestY] - myScorebestAdjustment()) || 0;

              // 为避免过度复杂,这里简化比较:优先距离更近的点
              if (minDistanceToPieces(i, j) < minDistanceToPieces(bestX, bestY)) {
                bestX = i; bestY = j;
              }
            }
          }
        }
      }

      // 辅助:用于上面比较时得到 best 的 myScorebest adjustment safe access
      function myScorebestAdjustment() {
        return myScore[bestX] && myScore[bestX][bestY] ? myScore[bestX][bestY] * 0.8 : 0;
      }

      // 最后落子
      onestep(bestX, bestY, false);
      chessboard[bestX][bestY] = 2;
      for (let k = 0; k < count; k++) {
        if (wins[bestX][bestY][k]) {
          computerWin[k] += 1;
          // 如果机器达成 5
          if (computerWin[k] === 5) {
            document.querySelectorAll(".result")[0].textContent = `你输了`;
            document.querySelectorAll(".result")[0].style.display = "block";
            over = true;
          }
        }
      }
      if (!over) me = !me;
    }

    /* ----- 点击下子(人) ----- */
    game_canvas.addEventListener("click", (event) => {
      if (over) return;
      if (!me) return;

      // 取得画布内坐标
      const rect = game_canvas.getBoundingClientRect();
      const x = event.clientX - rect.left;
      const y = event.clientY - rect.top;
      // 将像素坐标转换为棋盘索引(交叉点)
      const i = Math.round((x - 15) / 30);
      const j = Math.round((y - 15) / 30);

      if (i < 0 || i >= 15 || j < 0 || j >= 15) return;
      if (chessboard[i][j] !== 0) return;

      // 落子(人)
      onestep(i, j, true);
      chessboard[i][j] = 1;

      // 更新 myWin
      for (let k = 0; k < count; k++) {
        if (wins[i][j][k]) {
          myWin[k] += 1;
          if (myWin[k] === 5) {
            document.querySelectorAll(".result")[0].textContent = `你赢了`;
            document.querySelectorAll(".result")[0].style.display = "block";
            over = true;
          }
        }
      }

      if (!over) {
        me = !me;
        // 让机器思考并落子(可以延迟以便更自然)
        setTimeout(computeAI, 200);
      }
    });

    /* ----- 重新开始 ----- */
    const restartBtn = document.querySelectorAll(".restart")[0];
    restartBtn.addEventListener("click", () => {
      // 简单重载页面
      window.location.reload();
    });

  </script>
</body>
</html>
相关推荐
2601_949847752 小时前
Flutter for OpenHarmony 剧本杀组队App实战:关于我们页面实现
开发语言·javascript·flutter
weixin_422201302 小时前
Element Plus中el-tree组件默认选中第一个节点的实现方法
前端·javascript·vue.js
_OP_CHEN2 小时前
【前端开发之CSS】(六)CSS 弹性布局(Flex)完全指南:从入门到精通,搞定所有布局需求
前端·css·html·flex布局·弹性布局·界面美化·页面开发
css趣多多2 小时前
组件没有原生事件
前端·javascript·vue.js
IT陈图图2 小时前
Flutter × OpenHarmony 实战:优雅构建确认对话框的组件化方案
开发语言·javascript·flutter
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony 简易文本末尾字符查看器开发指南
开发语言·javascript·flutter
想起你的日子2 小时前
CSS3 弹性盒子(Flex Box)
前端·css3
萧曵 丶2 小时前
CSS3 业务开发高频样式
前端·css·css3
渡我白衣2 小时前
从线性到非线性——神经网络的原理、训练与可解释性探索
开发语言·javascript·人工智能·深度学习·神经网络·机器学习·数字电路