【问题标题】:Return bestMove in minimax algorithm for tictactoe返回井字游戏的极小极大算法中的最佳移动
【发布时间】:2012-11-23 04:20:34
【问题描述】:

我尝试编写 Russel Norvig 关于人工智能的书中给出的井字游戏的极小极大算法。除了将 bestMove 返回给用户的方式之外,它拥有一切。我正在努力返回 bestMove,但无法决定何时选择 bestMove。帮助,有人吗?

moveT MiniMax(stateT state)
{
    moveT bestMove;

    max_move(state,bestMove);

    return bestMove;

}

int max_move(stateT state,int & bestMove)
{
    int v = -10000;
    if(GameIsOver(state))
    {
        return EvaluateStaticPosition(state);

    }

    vector<moveT> moveList;
    GenerateMoveList(state, moveList);
    int nMoves = moveList.size();

    for(int i = 0 ; i < nMoves ; i++)
    {
        moveT move = moveList[i];
        MakeMove(state, move);

        int curValue = min_move(state,bestMove);

            if(curValue > v)
            {
              v = curValue;
              bestMove = move;
            }
        RetractMove(state, move);

    }

    return v;

}

int min_move(stateT state, int &bestMove)
{
    int v = 10000;
    if(GameIsOver(state))
    {
      return EvaluateStaticPosition(state);

    }
    vector<moveT> moveList;
    GenerateMoveList(state, moveList);

    int nMoves = moveList.size();

    for(int i = 0 ; i < nMoves; i++)
    {
        moveT move = moveList[i];
        MakeMove(state, move);

        int curValue = max_move(state,depth+1,bestMove);

            if(curValue < v)
            {
              curValue = v;
            }
        RetractMove(state, move);

    }
    return v;
}

P.S.:还有其他伪代码可以找到 minmax 值。但是,他们只专注于井字游戏,我正在尝试将其扩展到其他游戏。谢谢。

更新:整个代码可以在这里找到:http://ideone.com/XPswCl

【问题讨论】:

  • 您在上面发布的代码是最新的吗?因为它看起来不应该编译。在min_move 中,您使用三个参数调用max_move,但 max_move 只能使用两个参数。
  • @Kevin:哎呀,现在更新了。我试图在某个时候限制深度。
  • 感谢更新,但错误的行仍然存在:int curValue = max_move(state,depth+1,bestMove); 这让我担心;这让我怀疑您发布的代码不是您正在编译的代码。这使得潜在的回答者发现问题变得更加困难。我们将在发布的代码中识别出真实代码中不存在的错误,如果它们不在发布的代码中,我们将无法在真实代码中找到错误。
  • 请查看更新,给您造成的困扰,抱歉;整个代码在这里:ideone.com/XPswCl
  • 感谢您发布整个代码。为了其他回答者的利益,这里是一个计算机播放不完美的示例:选择移动 5,然后选择 7。计算机应该将其第二个棋子放在右上方的插槽中以阻止您的对角线获胜,但它却选择了左上方的插槽.

标签: c++ artificial-intelligence minimax


【解决方案1】:

在极小极大的最简单版本中,第一个玩家希望最大化他的分数,而第二个玩家希望最小化第一个玩家的分数。 由于第一个和第二个玩家都只关心第一个玩家的分数,EvaluateStaticPosition 应该返回一个值,指示第一个玩家的棋盘状态有多好。轮到谁不重要。

int EvaluateStaticPosition(stateT state)
{
        if(CheckForWin(state, FIRST_PLAYER))
        {
                return WINNING_POSITION;
        } 
        if(CheckForWin(state, Opponent(FIRST_PLAYER)))
        {
                return LOSING_POSITION;
        } 
        return NEUTRAL_POSITION;
}

现在,当您想要最适合第一个玩家的移动时,请调用 MaxMove。如果您想要最适合第二名玩家的移动,请调用 MinMove。

moveT MiniMax(stateT state)
{
    moveT bestMove;
    int i = 0;
    if (state.whoseTurn == FIRST_PLAYER){
        i = MaxMove(state, bestMove);
    }
    else{
        i = MinMove(state,bestMove);
    }
    cout<<"i is "<<i<<endl;
    return bestMove;
}

最后,MinMoveMaxMove 内部存在一些问题。当您在其中任何一个中分配curRating 时,您不应将bestMove 作为第二个参数传递给MaxMoveMinMove。然后它将对手的最佳移动放入bestMove,这是没有意义的。相反,声明一个opponentsBestMove 对象并将其作为第二个参数传递。 (您实际上不会使用该对象,甚至不会在之后查看它的值,但这没关系)。通过该更改,您永远不会在 MinMove 内为 bestMove 分配任何内容,因此您应该在 if(curRating &lt; v) 块内这样做。

int MaxMove(stateT state, moveT &bestMove)
{
        if(GameIsOver(state))
        {
            return EvaluateStaticPosition(state);
        }
        vector<moveT> moveList;
        GenerateMoveList(state, moveList);
        int nMoves = moveList.size();
        int v = -1000;
        for(int i = 0 ;i<nMoves; i++)
        {
                moveT move = moveList[i];
                MakeMove(state, move);
                moveT opponentsBestMove;
                int curRating = MinMove(state, opponentsBestMove);
                if (curRating > v)
                {
                        v = curRating;
                        bestMove = move;
                }
                RetractMove(state, move);
        }
        return v;

}
int MinMove(stateT state,  moveT &bestMove)
{
        if(GameIsOver(state))
        {
                return EvaluateStaticPosition(state);
        }
        vector<moveT>moveList;
        GenerateMoveList(state, moveList);
        int nMoves = moveList.size();
        int v = 1000;
        for(int i = 0 ; i<nMoves; i++)
        {
                moveT move = moveList[i];
                MakeMove(state , move);
                moveT opponentsBestMove;
                int curRating = MaxMove(state,opponentsBestMove);
                if(curRating < v)
                {
                        v = curRating;
                        bestMove = move;
                }
                RetractMove(state, move);
        }
        return v;
}

此时你应该拥有无与伦比的 AI!

The final position looks like this:

 O | O | X
---+---+---
 X | X | O
---+---+---
 O | X | X

Cat's game.

另一种方法利用井字游戏是零和游戏这一事实。换句话说,在游戏结束时,玩家的得分总和将为零。对于两人游戏,这意味着一个玩家的分数将永远是另一个玩家的负数。这对我们来说很方便,因为最小化其他玩家的分数与最大化自己的分数是相同的。因此,我们可以让两个玩家都尝试最大化自己的分数,而不是让一个玩家最大化自己的分数,而一个玩家最小化另一个玩家的分数。

EvaluateStaticPosition 改回其原始形式,以便根据当前玩家的棋盘状态的好坏给出分数。

int EvaluateStaticPosition(stateT state)
{
        if(CheckForWin(state, state.whoseTurn))
        {
                return WINNING_POSITION;
        }
        if(CheckForWin(state, Opponent(state.whoseTurn)))
        {
                return LOSING_POSITION;
        }
        return NEUTRAL_POSITION;
}

删除MinMove,因为我们只关心最大化。 重写MaxMove,以便它选择让对手得分最差的动作。最好棋的得分是其他玩家最差得分的负数。

int MaxMove(stateT state, moveT &bestMove)
{
        if(GameIsOver(state))
        {
                return EvaluateStaticPosition(state);
        }
        vector<moveT> moveList;
        GenerateMoveList(state, moveList);
        int nMoves = moveList.size();
        int v = -1000;
        for(int i = 0 ;i<nMoves; i++)
        {
                moveT move = moveList[i];
                MakeMove(state, move);
                moveT opponentsBestMove;
                int curRating = -MaxMove(state, opponentsBestMove);
                if (curRating > v)
                {
                        v = curRating;
                        bestMove = move;
                }
                RetractMove(state, move);
        }
        return v;

}

由于MaxMove 用于两个玩家,我们不再需要在MiniMax 函数中区分玩家。

moveT MiniMax(stateT state)
{
    moveT bestMove;
    int i = 0;
    i = MaxMove(state, bestMove);
    cout<<"i is "<<i<<endl;
    return bestMove;
}

【讨论】:

  • 如果我没记错的话,你是不是在curRating时故意把bestMove=移出MinMove
  • 是的,我故意将bestMove = move 放入MinMove 函数中,在curRating &lt; v 块内。那是它合乎逻辑的地方。如果一个动作的评分是迄今为止发现的最低值,那么该动作是最好的动作。至少在找到另一个更低的评分之前。
  • 工作的人,我希望我能给你一个拥抱;无论你在哪里,谢谢。让我进一步测试,等等。
【解决方案2】:

好吧,看起来MiniMax 为您正确选择了它,只需使用初始状态和深度调用它即可。 (除非根据状态的第一个玩家是第二个玩家,那么你应该在MiniMax中调用min_move。)

编辑: 是的,我忽略了一些东西,bestMove 目前没有多大意义。在 max_move 中的程序中,您可以像这样更改循环:

for(int i = 0 ; i < nMoves ; i++)
{
    moveT move = moveList[i];
    MakeMove(state, move);

    int new_value = min_move(state, depth+1);
    if(new_value > v)
    {
      v=new_value;
    }
    RetractMove(state, move);

}

在那之后你可以想想你bestMove是什么意思?我的想法是,您有兴趣找到井字游戏的“最佳”系列动作之一。为此,您需要一个向量,甚至更好的是stack。但这也意味着将std::stack&lt;int&gt;* best_moves 作为最后一个参数。

对于堆栈实现,在 min_move 中,您返回下一个动作,如果它们的值是最好的,您将把 move 推到 best_moves 堆栈的顶部。当然,在游戏结束时,您只需返回空堆栈即可。它需要一个面向对象的方法来正确地完成它,我有时间会做的。

如果您需要的只是最好的下一步,那么我建议您将 min_move 和 max_moe 的返回类型更改为以下结构:

struct Value_move{
  int value;
  moveT best_move;
};

那么 max_move 的新实现如下所示:

const int MOVE_INVALID = -12345;
const int MOVE_NOTHING = -12346;

Value_move max_move(stateT state, int depth)
{
    Value_move best;
    best.value = -10000; best.best_move = MOVE_INVALID;

    if(GameIsOver(state))
    {
        best.value = EvaluateStaticPosition(state);
        best.best_move = MOVE_NOTHING;
        return best;
    }

    vector<moveT> moveList;
    GenerateMoveList(state, moveList);
    int nMoves = moveList.size();

    for(int i = 0 ; i < nMoves ; i++)
    {
        moveT move = moveList[i];
        MakeMove(state, move);
        Value_move curr = min_move(state, depth+1);
        if(curr.value > best.value)
        {
            best.value = curr.value;
            best.best_move = move;
        }
        RetractMove(state, move);

    }

    return v;

}

你只需要在MiniMax函数中取出返回的struct中的best_move字段即可。

备注:
您必须承认,尽管这在许多方面都不像一个 c++ 程序,而是一个 c 程序。否则,CapitalCamelCase 中的所有函数都应该是类方法,您应该通过 (const) ref 而不是 value 传递状态——但只有当状态确实是 typedef 后面的指针时,整个代码才有意义。

【讨论】:

  • 正确,已更正,抱歉,+1。不过,我希望我们在这里就 c++ 程序的不良做法达成一致。这段代码介于两者之间。它也开始在某些地方使用引用。 :)
  • @BarnabasSzabolcs 我担心 curValue > v 是否正确放置在 for 循环中。 curValue 是未初始化的,它不可能大于 v ,到目前为止获得的最大值,比如 +10 。如何更改代码以使 'curValue' 代表 v = max(v,min_move(state, depth+1,bestMove)) 以及存储和比较迄今为止获得的最佳值的方法 curValue 。我这里有点模糊。
  • 我现在尝试整理语言并使其更重要,我对您的反馈很感兴趣,如果有不清楚的地方请告诉我,也许我可以改进并更好地解释它。
  • 谢谢@BarnabasSzabolcs:我的问题已经解决了,只是有点小题大做。
【解决方案3】:

您的代码找到了正确的值,然后通过向下传递相同的引用来覆盖它。

int curValue = min_move(state,bestMove);

应该变成

moveT nextMove; // No need to actually do anything with this value
int curValue = min_move(state,nextMove);

您还需要在 min_move 函数中进行同样的更改。

注意:在 min_move 中,您的代码调用 max_move 的参数比您为函数定义的参数多。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-09-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-07-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多