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

Next.js中Minimax函数异常求助:AI落子仅按顺序选择

井字棋AI修复方案

核心问题分析

你的AI无法计算最优落子,主要是以下逻辑错误导致:

1. 胜负判断函数逻辑错误

checkWinner中的平局判断逻辑完全错误,会在列填满但未分出胜负时错误判定为平局,且会覆盖已找到的胜者;同时未正确检查所有格子是否填满来判定平局。

2. Minimax得分体系颠倒

当前得分设置scores = {X:10, O:-10}与AI角色(O)的目标矛盾:AI需要最大化自身得分,但当前设置AI获胜得负分、人类获胜得正分,导致AI主动选择最差走法。

3. 最佳落子初始化逻辑不严谨

findBestMove未处理初始move未赋值的边界情况,且依赖的useEffect触发条件冗余。


修复步骤

1. 修复checkWinner函数

先检查是否有胜者,无胜者时再通过判断所有格子是否填满来确认平局:

const checkWinner = (board: board) => {
  let winner = null;
  // 检查胜负组合
  for (const combo of winCondition) {
    const [a, b, c] = combo;
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      winner = board[a];
      break;
    }
  }
  // 检查平局:无胜者且所有格子已填满
  if (winner === null) {
    let isBoardFull = true;
    for (let i = 0; i < 9; i++) {
      if (board[i] === "") {
        isBoardFull = false;
        break;
      }
    }
    winner = isBoardFull ? "tie" : null;
  }
  return winner;
};

2. 修正Minimax得分体系

调整得分让AI(O)获胜得正分,人类(X)获胜得负分,匹配AI的目标:

const scores = {
  X: -10,
  O: 10,
  tie: 0,
};

3. 优化findBestMove与useEffect逻辑

  • 给move设置初始值并增加边界判断,避免无落子位置时出错
  • 精简useEffect依赖,只保留必要的触发条件:
// 优化后的useEffect
useEffect(() => {
  if (!won && turn === "bot") {
    findBestMove();
  }
}, [boardData, turn, won]);

// 修复后的findBestMove
const findBestMove = () => {
  let bestScore = -Infinity;
  let move = -1;
  const board = { ...boardData };

  for (let i = 0; i < 9; i++) {
    if (board[i] === "") {
      board[i] = ai;
      const score = minimax(board, false);
      board[i] = "";

      if (score > bestScore) {
        bestScore = score;
        move = i;
      }
    }
  }

  if (move !== -1) {
    setBoardData(prev => ({ ...prev, [move]: ai }));
    setTries(prev => prev + 1);
  }
  setTurn("human");
};

完整修复后的代码

"use client";
import React, { useState, useEffect } from "react";
import "./GameMenu.css";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faRefresh } from "@fortawesome/free-solid-svg-icons/faRefresh";
import WonScreen from "./WonScreen";
import { Tooltip } from "react-tooltip";

const GameMenu = () => {
  type board = {
    [key: number]: string;
  };

  const defaultBoard: board = {
    0: "",
    1: "",
    2: "",
    3: "",
    4: "",
    5: "",
    6: "",
    7: "",
    8: "",
  };

  const [turn, setTurn] = useState<"human" | "bot">("human");
  const [won, setWon] = useState(false);
  const [boardData, setBoardData] = useState<board>(defaultBoard);
  const ai = "O";
  const human = "X";
  const [tries, setTries] = useState(0);

  const winCondition = [
    [0, 1, 2],
    [0, 4, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [3, 4, 5],
    [2, 4, 6],
    [6, 7, 8],
  ];

  const scores = {
    X: -10,
    O: 10,
    tie: 0,
  };

  const updateBoardData = (idx: keyof board) => {
    if (won || boardData[idx] !== "") return;
    setTurn("bot");
    setTries(prev => prev + 1);
    setBoardData({ ...boardData, [idx]: human });
  };

  useEffect(() => {
    if (!won && turn === "bot") {
      findBestMove();
    }
  }, [boardData, turn, won]);

  const checkWinner = (board: board) => {
    let winner = null;
    // 检查胜负组合
    for (const combo of winCondition) {
      const [a, b, c] = combo;
      if (board[a] && board[a] === board[b] && board[a] === board[c]) {
        winner = board[a];
        break;
      }
    }
    // 检查平局
    if (winner === null) {
      let isFull = true;
      for (let i = 0; i < 9; i++) {
        if (board[i] === "") {
          isFull = false;
          break;
        }
      }
      winner = isFull ? "tie" : null;
    }
    return winner;
  };

  useEffect(() => {
    const win = checkWinner(boardData);
    if (win === human || win === ai) {
      setWon(true);
    }
  }, [boardData]);

  const restart = () => {
    setBoardData(defaultBoard);
    setWon(false);
    setTries(0);
    setTurn("human");
  };

  const minimax = (board: board, isMaximizing: boolean): number => {
    const result = checkWinner(board);
    if (result !== null) {
      return scores[result];
    }

    let bestScore = isMaximizing ? -Infinity : Infinity;

    for (let i = 0; i < 9; i++) {
      if (board[i] === "") {
        board[i] = isMaximizing ? ai : human;
        const score = minimax(board, !isMaximizing);
        board[i] = "";

        if (isMaximizing) {
          bestScore = Math.max(score, bestScore);
        } else {
          bestScore = Math.min(score, bestScore);
        }
      }
    }

    return bestScore;
  };

  const findBestMove = () => {
    let bestScore = -Infinity;
    let move = -1;
    const board = { ...boardData };

    for (let i = 0; i < 9; i++) {
      if (board[i] === "") {
        board[i] = ai;
        const score = minimax(board, false);
        board[i] = "";

        if (score > bestScore) {
          bestScore = score;
          move = i;
        }
      }
    }

    if (move !== -1) {
      setBoardData(prev => ({ ...prev, [move]: ai }));
      setTries(prev => prev + 1);
    }
    setTurn("human");
  };

  // 补充棋盘渲染逻辑
  return (
    <div className="game-menu">
      <div className="board">
        {Object.keys(boardData).map((key) => (
          <div
            key={key}
            className="cell"
            onClick={() => updateBoardData(Number(key))}
          >
            {boardData[Number(key)]}
          </div>
        ))}
      </div>
      <button onClick={restart} className="restart-btn">
        <FontAwesomeIcon icon={faRefresh} /> 重启游戏
      </button>
      {won && <WonScreen winner={checkWinner(boardData)} onRestart={restart} />}
      <Tooltip />
    </div>
  );
};

export default GameMenu;

内容的提问来源于stack exchange,提问作者Animesh Acharya

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 17:04:56