Suppose we have the following position (FEN 8/1K6/8/4q2P/8/8/5k2/8 b - - 3 2
):
My chess engine produces the correct move of Qxh5 when the search depth is below 3. After that, it seems the problem is that it thinks the capture can be made later (considers Qh2 as the best move). I cannot see any obvious ways to prefer the branches where the capture is made earlier in the evaluation algorithm, since that would break the evaluation symmetry needed for negamax (and minimax) to work.
Just for reference, here is my actual negamax code (copied from wikipedia):
int Search::negamaxSearch (Board& positionToSearch, int depth, int alpha, int beta) {
std::vector<Move> moves = positionToSearch.getMoves();
if (moves.empty()) {
if (positionToSearch.isCheck()) {
return EvaluationConstants::LOSE;
} else {
return EvaluationConstants::DRAW;
}
}
if (depth == 0) {
return BoardEvaluator::evaluateSimple(positionToSearch);
}
orderMoves(positionToSearch, moves, depth);
int positionValue = -1e9;
for (auto move : moves) {
positionToSearch.executeMove(move);
int newValue = -negamaxSearch(positionToSearch, depth - 1, -beta, -alpha);
positionToSearch.unmakeMove();
positionValue = std::max(positionValue, newValue);
alpha = std::max(alpha, newValue);
if (alpha >= beta) {
++cutoffs;
break;
}
}
return positionValue;
}
And the evaluation function:
int BoardEvaluator::evaluateSimpleOneSide (const Board& board, PieceColor perspective) {
if (board.isCheckmate()) return EvaluationConstants::LOSE;
int value = 0;
for (int pieceType = 0; pieceType < PieceTypes::KING; ++pieceType) {
value += board.getPieces(perspective).boards[pieceType].popCount() * pieceValues[pieceType];
}
return value;
}
int BoardEvaluator::evaluateSimple (const Board& board) {
return evaluateSimpleOneSide(board, board.getTurn()) - evaluateSimpleOneSide(board, flip(board.getTurn()));
}
EDIT:
constexpr int MVV_LVA[7][7] = {
{0, 0, 0, 0, 0, 0, 0}, // victim K, attacker K, Q, R, B, N, P, None
{10, 100, 180, 300, 300, 900, 0}, // victim Q, attacker K, Q, R, B, N, P, None
{6, 56, 100, 166, 166, 500, 0}, // victim R, attacker K, Q, R, B, N, P, None
{3, 33, 60, 100, 100, 300, 0}, // victim B, attacker K, Q, R, B, N, P, None
{3, 33, 60, 100, 100, 300, 0}, // victim N, attacker K, Q, R, B, N, P, None
{1, 11, 20, 33, 33, 100, 0}, // victim P, attacker K, Q, R, B, N, P, None
{0, 0, 0, 0, 0, 0, 0}, // victim None, attacker K, Q, R, B, N, P, None
};
int scoreMove (const Board& context, const Move& move) {
int moveScoreGuess = 0;
moveScoreGuess += MVV_LVA[context.getPieceAt(move.getDestination()).type][context.getPieceAt(move.getOrigin()).type];
moveScoreGuess += BoardEvaluator::pieceValues[move.getPromotedPiece()];
return moveScoreGuess;
}
void Search::orderMoves (Board& positionToSearch, std::vector<Move>& moves, int depth) {
std::sort(moves.begin(), moves.end(), [&] (const Move& move1, const Move& move2) {
return scoreMove(positionToSearch, move1) > scoreMove(positionToSearch, move2);
});
}```
Is there something obvious wrong that I haven't noticed?
EDIT:
I actually managed to solve the mate problem by stopping the search inside my iterative deepening framework, if a checkmate is found at a lower than max level. This helped somewhat, but the capture problem still persists.