国际象棋AI与alpha beta算法

时间:2015-05-29 22:01:57

标签: c++ algorithm artificial-intelligence chess alpha-beta-pruning

我已经为我的国际象棋游戏实现了alpha beta算法,但是最后做出一个相当愚蠢的举动需要花费很多时间(4层分钟)。

我已经试图找到错误(我假设我已经犯了一个)2天了,我非常感谢我的代码上的一些外部输入。

getMove函数:为根节点调用,它为所有它的子节点调用alphaBeta函数(可能的移动),然后选择得分最高的移动。

Move AIPlayer::getMove(Board b, MoveGenerator& gen)
{
    // defined constants: ALPHA=-20000 and BETA= 20000
    int alpha = ALPHA; 
    Board bTemp(false); // test Board
    Move BestMov;
    int i = -1; int temp;
    int len = gen.moves.getLength();  // moves is a linked list holding all legal moves
    BoardCounter++; // private attribute of AIPlayer object, counts analyzed boards
    Move mTemp;     // mTemp is used to apply the nextmove in the list to the temporary test Board
    gen.mouvements.Begin();   // sets the list counter to the first element in the list
    while (++i < len && alpha < BETA){
        mTemp = gen.moves.nextElement();
        bTemp.cloneBoard(b);
        bTemp.applyMove(mTemp);
        temp = MAX(alpha, alphaBeta(bTemp, alpha, BETA, depth, MIN_NODE));
        if (temp > alpha){
            alpha = temp;
            BestMov = mTemp;
        }
    }
    return BestMov;
}

alphaBeta功能:

int AIPlayer::alphaBeta(Board b, int alpha, int beta, char depth, bool nodeType)
{
    Move m;
    b.changeSide();
    compteurBoards++;
    MoveGenerator genMoves(b); // when the constructor is given a board, it automatically generates possible moves
    // the Board object has a player attribute that holds the current player
    if (genMoves.checkMate(b, b.getSide(), moves)){ // if the current player is in checkmate
        return 100000;
    }
    else if (genMoves.checkMate(b, ((b.getSide() == BLACK) ? BLACK : WHITE), moves)){ // if the other player is in checkmate
        return -100000;
    }
    else if (!depth){
        return b.evaluateBoard(nodeType);

    }
    else{
        int scoreMove = alpha;
        int best;
        genMoves.moves.Begin();
        short i = -1, len = genMoves.moves.getLength();
        Board bTemp(false);

        if (nodeType == MAX_NODE){
            best = ALPHA;
            while (++i < len){
                bTemp.cloneBoard(b);
                if (bTemp.applyMove(genMoves.moves.nextElement())){ 
                    scoreMove = alphaBeta(bTemp, alpha, beta, depth - 1, !nodeType);
                    best = MAX(best, scoreMove);
                    alpha = MAX(alpha, best);

                    if (beta <= alpha){ 
                        std::cout << "max cutoff" << std::endl;
                        break;
                    }
                }
            }
            return scoreMove;
        //return alpha;
        }
        else{
            best = BETA;
            while (++i < len){
                bTemp.cloneBoard(b);
                if (bTemp.applyMove(genMoves.moves.nextElement())){ 
                    scoreMove = alphaBeta(bTemp, alpha, beta, depth - 1, !nodeType);
                    best = MIN(best, scoreMove);
                    beta = MIN(beta, best);
                    if (beta <= alpha){ 
                        std::cout << "min cutoff" << std::endl;
                        break;
                    }
                }
            }
            return scoreMove;
            //return beta;
        }
        return meilleur;
    }
}

编辑:我应该注意,evaluateBoard只评估棋子的移动性(可能的移动次数,捕获移动得分越高,取决于捕获的棋子)

谢谢。

2 个答案:

答案 0 :(得分:3)

我可以看到您正在尝试实施mini-max算法。但是,代码中有一些东西让我怀疑。我们将代码与开源Stockfish国际象棋引擎进行比较。请参阅https://github.com/mcostalba/Stockfish/blob/master/src/search.cpp

上的搜索算法

<强> 1。按值传递Board b

你的代码中有这个:

  

alphaBeta(Board b,int alpha,int beta,char depth,bool nodeType)

我不知道究竟是什么&#34; Board&#34;是。但它对我来说并不合适。让我们来看看Stockfish:

  

值搜索(位置和位置,堆栈* ss,值alpha,值beta,深度   深度,bool cutNode)

在Stockfish中,位置对象通过引用传递。如果&#34;董事会&#34;是一个类,每次调用alpha-beta函数时,程序都需要制作一个新的副本。在国际象棋中,当我们必须评估许多节点时,这显然是不可接受的。

<强> 2。没有散​​列

哈希在Stockfish中完成:

  

ttValue = ttHit? value_from_tt(tte-&gt; value(),ss-&gt; ply):VALUE_NONE;

如果没有散列,您需要一次又一次地反复评估相同的位置。在没有实现散列的情况下,你不会去任何地方。

第3。检查将死的人

可能不是最重要的减速,但我们永远不应该检查每个节点的将军。在Stockfish:

// All legal moves have been searched. A special case: If we're in check
// and no legal moves were found, it is checkmate.
if (InCheck && bestValue == -VALUE_INFINITE)
    return mated_in(ss->ply); // Plies to mate from the root

这样做 AFTER 搜索所有可能的动作。我们这样做是因为我们通常拥有比checkmate-nodes更多的非checkmate节点。

<强> 4。董事会bTemp(虚假);

这看起来像是一个重大的减速。让我们来看看Stockfish:

  // Step 14. Make the move
  pos.do_move(move, st, ci, givesCheck);

您不应在每个节点中创建临时对象(创建bTemp的对象)。机器需要分配一些堆栈空间来保存bTemp。如果bTemp不是主要变量(即,不太可能被处理器缓存),这可能是严重的性能损失。 Stockfish只是修改内部数据结构而不创建新的数据结构。

<强> 5。 bTemp.cloneBoard(B);

与4类似,更糟糕的是,这是针对节点中的每次移动完成的。

<强> 6。 std :: cout&lt;&lt; &#34;最大截止&#34; &LT;&LT;的std :: ENDL;

也许很难相信,打印到终端比处理慢得多。在这里,您需要创建一个潜在的减速,以便将字符串保存到IO缓冲区。功能可能(我不是100%肯定)甚至阻止您的程序,直到文本显示在终端上。 Stockfish仅用于统计汇总,绝对不是每次有故障高或失败低时。

<强> 7。不对PV移动进行排序

在解决其他问题之前,可能不是您想要做的事情。在Stockfish,他们有:

  

std :: stable_sort(RootMoves.begin()+ PVIdx,RootMoves.end());

这是针对迭代深化框架中的每次迭代完成的。

答案 1 :(得分:0)

我只是要解决算法的运行时成本问题,因为我不知道你的评估函数的实现细节。

为了使事情尽可能简单,我将假设算法的最坏情况。

getMove函数对alphaBeta函数进行len1调用,这反过来使len2调用自身,这反过来使len3调用自身,依此类推,直到深度达到0并且递归停止。 由于最坏的情况假设,让我们说n = max(len1,len2,...),所以你有

n * n * n * ... * n根据深度d调用带有乘法次数的alphaBeta,这导致对alphaBeta的n ^ d调用,这意味着您具有指数运行时行为。这非常慢,只能通过阶乘运行时行为来打败。

我认为您应该为此目的查看Big O表示法,并尝试相应地优化您的算法以获得更快的结果。

祝你好运, OPM