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.
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.
I am only going to address the runtime cost problem of your algorithm, because I don't know the implementation details of your board evaluation function.
In order to keep things as simple as possible, I will assume the worst case for the algorithm.
The getMove function makes len1 calls to the alphaBeta function, which in turn makes len2 calls to itself, which in turn makes len3 calls to itself and so on until depth reaches 0 and the recursion stops. Because of the worst case assumption, let's say n = max(len1, len2, ...), so you have
n * n * n * ... * n calls to alphaBeta with number of multiplications depending on depth d, which leads to n^d calls to alphaBeta which means that you have an exponential runtime behavior. This is ultra slow and only beaten by factorial runtime behavior.
I think you should take a look at the Big O notation for that purpose and try to optimize your algorithm accordingly to get much faster results.
Best regards, OPM
链接地址: http://www.djcxy.com/p/84690.html上一篇: 如何为国际象棋编程一个神经网络?
下一篇: 国际象棋AI与alpha beta算法