You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JS实现井字棋Minimax AI遇多子同时落子问题求助

井字棋Minimax AI异常问题排查与修复方案

核心问题原因

  1. 异步逻辑未正确处理:minimax被定义为async函数,但调用它的bestMove没有使用await,导致bestMove会立即返回undefined,后续place(bestMove(), player)执行时会出错。同时,minimax内部的await会让函数暂停执行,此时其他递归分支或用户操作可能同时修改DOM,引发多棋子放置的混乱。
  2. 棋盘状态与DOM耦合:直接使用DOM元素的innerText存储棋盘状态,而非独立的纯数据结构。异步过程中DOM状态可能被多个递归分支同时修改,或者被用户的重复点击干扰,导致状态逻辑混乱。

修复步骤与代码调整

1. 分离状态与视图

改用纯二维数组存储棋盘状态,DOM仅负责渲染状态,避免直接操作DOM作为状态载体:

// 纯数据棋盘状态,初始为空
let board = [
  ['', '', ''],
  ['', '', ''],
  ['', '', '']
];
const human = 'X';
const ai = 'O';
let currentPlayer = human;

// 新增DOM更新函数,统一处理视图渲染
function updateDOM(row, col, player) {
  document.getElementById(`row${row+1}`).children[col].innerText = player;
}

2. 修正异步调用逻辑

将bestMove改为async函数,并在点击事件中用await等待AI计算完成,确保流程顺序执行:

// 初始化点击事件时修改为async回调
cell.addEventListener('click', async function(event) {
  const pos = getPos(event.target);
  // 校验:单元格为空且当前是人类玩家
  if (board[pos.y][pos.x] === '' && currentPlayer === human) {
    // 更新状态与视图
    board[pos.y][pos.x] = human;
    updateDOM(pos.y, pos.x, human);
    // 检查游戏是否结束
    if (checkWinner(board)) return;
    
    currentPlayer = ai;
    // 等待AI计算最佳落子
    const bestMovePos = await bestMove();
    board[bestMovePos.i][bestMovePos.j] = ai;
    updateDOM(bestMovePos.i, bestMovePos.j, ai);
    if (checkWinner(board)) return;
    
    currentPlayer = human;
  }
});

// 修改bestMove为async函数,等待minimax计算
async function bestMove() {
  let bestScore = -Infinity;
  let move;
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      if (board[i][j] === '') {
        board[i][j] = ai;
        // 等待minimax递归计算完成
        const score = await minimax(board, 0, false);
        board[i][j] = '';
        if (score > bestScore) {
          bestScore = score;
          move = {i, j};
        }
      }
    }
  }
  return move;
}

3. 完善Minimax函数的异步与逻辑

确保递归调用minimax时使用await,同时补充平局判断逻辑:

const values = {
  X: -1,
  O: 1,
  tie: 0
};

async function minimax(board, depth, isMaximizing) {
  let result = checkWinner(board);
  if (result !== null) {
    return values[result];
  }
  // 新增平局判断
  if (isBoardFull(board)) {
    return values.tie;
  }

  if (isMaximizing) {
    let bestScore = -Infinity;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (board[i][j] === '') {
          board[i][j] = ai;
          // 可选:添加小延迟观察计算过程,正式版可移除
          await new Promise(r => setTimeout(r, 100));
          const score = await minimax(board, depth + 1, false);
          board[i][j] = '';
          bestScore = Math.max(score, bestScore);
        }
      }
    }
    return bestScore;
  } else {
    let bestScore = Infinity;
    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        if (board[i][j] === '') {
          board[i][j] = human;
          await new Promise(r => setTimeout(r, 100));
          const score = await minimax(board, depth + 1, true);
          board[i][j] = '';
          bestScore = Math.min(score, bestScore);
        }
      }
    }
    return bestScore;
  }
}

// 新增棋盘满格判断函数
function isBoardFull(board) {
  return board.every(row => row.every(cell => cell !== ''));
}

// 修改checkWinner为接收纯数据棋盘
function checkWinner(board){
  // 检查行
  for (let i = 0; i < 3; i++) {
    if (board[i][0] !== '' && board[i][0] === board[i][1] && board[i][1] === board[i][2]) {
      return board[i][0];
    }
  }
  // 检查列
  for (let i = 0; i < 3; i++) {
    if (board[0][i] !== '' && board[0][i] === board[1][i] && board[1][i] === board[2][i]) {
      return board[0][i];
    }
  }
  // 检查对角线
  if (board[0][0] !== '' && board[0][0] === board[1][1] && board[1][1] === board[2][2]) {
    return board[0][0];
  }
  if (board[0][2] !== '' && board[0][2] === board[1][1] && board[1][1] === board[2][0]) {
    return board[0][2];
  }
  return null;
}

重构与优化建议

  • 状态与视图彻底分离:始终用纯数据管理核心状态,DOM只做渲染,避免任何直接操作DOM修改状态的逻辑,这是前端应用避免状态混乱的关键。
  • 移除不必要的异步:Minimax算法本身是同步递归计算,添加await延迟仅用于演示过程,正式发布版本可移除,大幅提升AI响应速度。若需要演示计算过程,建议用CSS动画而非阻塞递归的延迟。
  • Alpha-Beta剪枝优化:为Minimax添加Alpha-Beta剪枝逻辑,减少不必要的递归计算,提升AI的计算效率,尤其是当棋盘规模扩大时效果显著。
  • 缓存计算结果:对已经计算过的棋盘状态进行缓存(比如用对象或Map存储状态哈希与对应得分),避免重复递归计算相同状态。
  • 函数参数化:将棋盘、玩家标识等作为函数参数传递,减少对全局变量的依赖,让函数更独立、易于测试和复用。
  • 模块化拆分:将初始化、AI计算、状态校验、DOM渲染等功能拆分为独立函数或模块,提升代码的可读性和维护性。

内容的提问来源于stack exchange,提问作者Pr1nglz.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 06:13:10