Search code examples
c++algorithmartificial-intelligencechessalpha-beta-pruning

Chess AI with alpha beta algorithm


I have implemented the alpha beta algorithm for my chess game, however it takes a lot of time (minutes for 4-ply) to finally make a rather stupid move.

I've been trying to find the mistake (I assume I made one) for 2 days now, I would very much appreciate some outside input on my code.

getMove function: is called for the root node, it calls alphaBeta function for all it's child nodes (possible moves) and then chooses the move with the highest score.

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 function:

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;
    }
}

EDIT: I should note that the evaluateBoard only evaluates the mobility of pieces (number of possible moves, capture moves get a higher score depending on the piece captured)

Thank you.


Solution

  • I can see that you're trying to implement a mini-max algorithm. However, there is something in the code that makes me suspicious. We'll compare the code with the open-source Stockfish chess engine. Please refer to the search algorithm at https://github.com/mcostalba/Stockfish/blob/master/src/search.cpp

    1. Passing Board b by value

    You have this in your code:

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

    I don't know what exactly "Board" is. But it doesn't look right to me. Let's look at Stockfish:

    Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode)

    The position object is passed by reference in Stockfish. If "Board" is a class, the program will need to make a new copy everytime the alpha-beta function is called. In chess, when we have to evaluate many number of nodes, this is obviously unacceptable.

    2. No hashing

    Hashing is done in Stockfish as:

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

    Without hashing, you'll need to evaluate the same position again and again and again and again. You won't go anywhere without hashing implemented.

    3. Checking for checkmate

    Probably not the most significant slow-down, but we should never check for checkmate in every single node. In 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
    

    This is done AFTER all possible moves are searched. We do it because we usually have many more non-checkmates node than checkmate-nodes.

    4. Board bTemp(false);

    This looks like a major slow-down. Let's take at Stockfish:

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

    You should not create a temporary object in every node (creating an object of bTemp). The machine would need to allocate some stack space to save bTemp. This could be a serious performance penalty in particular if bTemp is not a primary variable (ie, not likely be cached by the processor). Stockfish simply modifies the internal data-structure without creating a new one.

    5. bTemp.cloneBoard(b);

    Similar to 4, even worse, this is done for every move in the node.

    6. std::cout << "max cutoff" << std::endl;

    Maybe it's hard to believe, printing to a terminal is much slower than processing. Here you're creating a potential slow-down that the string would need to be saved to an IO buffer. The function might (I'm not 100% sure) even block your program until the text is shown on the terminal. Stockfish only does it for statistic summary, definitely not everytime when you have a fail-high or fail-low.

    7. Not sorting the PV move

    Probably not something that you want to do before addressing the other issues. In Stockfish, they have:

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

    This is done for every iteration in an iterative-deepening framework.