2

Suppose we have the following position (FEN 8/1K6/8/4q2P/8/8/5k2/8 b - - 3 2): 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.
K. Raivio
  • 100
  • 8
  • I haven't looked at the code. This is really an algorithm question. You can find useful chess programming information [here](https://www.chessprogramming.org/Main_Page). The usual approach to this problem is to sort the move list so that captures and checks are examined before other moves; combined with alpha-beta pruning, that typically gives a major speed improvement. – Pete Becker Jan 09 '22 at 16:29
  • I'm not really looking for speed improvement, just I'd like my engine to play correctly. I only ask in stackoverflow as a last resort so, believe me, I have gone through chessprogramming.org multiple times looking for any clues that might be related to my problem. – K. Raivio Jan 09 '22 at 16:32
  • Whoops, sorry, didn't read the question completely. :-( – Pete Becker Jan 09 '22 at 16:33
  • If the pruning algorithm is implemented correctly, I'm guessing that the evaluation function needs some tweaks. Did you write it by hand or is it an ANN of some kind? If it's an ANN, did you train it for playing with depths below 3? – Ted Lyngmo Jan 09 '22 at 16:45
  • I’ve seen that with a chess program in the eighties, where with the right search depth the computer always thought it could take a piece later (which was always correct) and never took it. You might change your valuation so that taking a piece after two moves is valued just a little bit higher than taking the piece in the third move. – gnasher729 Jan 09 '22 at 16:48
  • Of course that means valuation isn’t based just on the position but also on the history. – gnasher729 Jan 09 '22 at 16:50
  • *Why* do you want the engine to capture earlier? In order to move the game along? – Beta Jan 09 '22 at 16:53
  • @Beta because it's objectively a better move – K. Raivio Jan 09 '22 at 17:05
  • The best move is Ke3 - try this position on a tablebase ([lichess analysis](https://lichess.org/analysis/fromPosition/8/1K6/8/4q2P/8/8/5k2/8_b_-_-_3_2#3)). – double-beep Jan 09 '22 at 18:14
  • You mean black can force a win after playing Qxh5, but white can force a win or draw after black plays Qh2? – Beta Jan 09 '22 at 22:47
  • What is inside the orderMoves()? – ferdy Jan 10 '22 at 01:19
  • @double-beep My point is that even though it would be ultimately better to Ke3, in my engine that must be a bug since it's only counting material – K. Raivio Jan 10 '22 at 07:34
  • @ferdy edited the question – K. Raivio Jan 10 '22 at 07:37
  • Qh2 as the best move - how many seconds is this? What is `EvaluationConstants::LOSE;` – ferdy Jan 10 '22 at 09:55

1 Answers1

0

Handle mate score by defining a ply variable that is incremented in make() and decremented in unmake(), set it to 0 when starting the search.

VALUE_MATED = your_value; // the worst score of mated player, say -10000

int Search::negamaxSearch (Board& positionToSearch, int depth, int alpha, int beta) {
    std::vector<Move> moves = positionToSearch.getMoves();

    if (depth == 0) {
        return BoardEvaluator::evaluateSimple(positionToSearch);
    }

    orderMoves(positionToSearch, moves, depth);

    int positionValue = -1e9;
    for (auto move : moves) {
        positionToSearch.executeMove(move);
        ply++;
        int newValue = -negamaxSearch(positionToSearch, depth - 1, -beta, -alpha);
        positionToSearch.unmakeMove();
        ply--;

        positionValue = std::max(positionValue, newValue);
        alpha = std::max(alpha, newValue);

        if (alpha >= beta) {
            ++cutoffs;
            break;
        }
    }

    if (positionValue == -1e9) {
        if (positionToSearch.isCheck()) {
            return VALUE_MATED + ply
        } else {
            return EvaluationConstants::DRAW;
        }        
    }

    return positionValue;
}
ferdy
  • 4,396
  • 2
  • 4
  • 16
  • Thank you for you answer! However, 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. – K. Raivio Jan 11 '22 at 17:43
  • Try this position `1k6/7Q/1K6/8/8/8/8/8 w - - 0 1` What is the score and bestmove? – ferdy Jan 11 '22 at 23:37