Perft stats discrepencies

Discussion of chess software programming and technical issues.

Moderator: Ras

benvining
Posts: 22
Joined: Fri May 30, 2025 10:18 pm
Full name: Ben Vining

Perft stats discrepencies

Post by benvining »

I'm working on my first engine, and I've got fairly extensive unit testing for the move generator, as well as the test cases from https://github.com/schnitzi/rampart. Perft is returning the correct number of nodes up to 6 (haven't run 7 yet...)

My issue is that in some positions, my perft reports the correct number of total nodes, but incorrect stats. Some example output:

Code: Select all

Running perft depth 4...
90: 
90: Wrote JSON results to /Users/benvining/Documents/libchess/Builds/ninja/tests/perft/results/start_pos/depth_4.json
90: 
90: a2a3 8457
90: b2b3 9345
90: c2c3 9272
90: d2d3 11959
90: e2e3 13134
90: f2f3 8457
90: g2g3 9345
90: h2h3 8457
90: a2a4 9329
90: b2b4 9332
90: c2c4 9744
90: d2d4 12435
90: e2e4 13160
90: f2f4 8929
90: g2g4 9328
90: h2h4 9329
90: b1a3 8885
90: b1c3 9755
90: g1f3 9748
90: g1h3 8881
90: 
90: Nodes: 197281
90: Captures: 1610
90: En passant captures: 0
90: Castles: 0
90: Promotions: 0
90: Checks: 481
90: Checkmates: 8
90: Stalemates: 0
90: 
90: Search time: 1s
90: ERROR: Expected 1576 captures, got 1610
90: ERROR: Expected 469 checks, got 481
Has anyone ever encountered something similar? I'm wondering if my perft function is wrong:

Code: Select all

struct PerftResult final {
    size_t nodes { 0uz };

    size_t captures { 0uz };

    size_t enPassantCaptures { 0uz };

    size_t castles { 0uz };

    size_t promotions { 0uz };

    size_t checks { 0uz };

    size_t checkmates { 0uz };

    size_t stalemates { 0uz };

    std::vector<std::pair<Move, size_t>> rootNodes;

    constexpr PerftResult& operator+=(const PerftResult& rhs) noexcept;
};

template <bool IsRoot>
constexpr PerftResult perft(const size_t depth, const Position& startingPosition) // NOLINT(misc-no-recursion)
{
    if (depth == 0uz)
        return { .nodes = 1uz };

    PerftResult result;

    for (const auto& move : generate(startingPosition)) {
        if (startingPosition.is_capture(move)) {
            ++result.captures;

            if (startingPosition.is_en_passant(move))
                ++result.enPassantCaptures;
        }

        if (move.is_castling())
            ++result.castles;

        if (move.promotedType.has_value())
            ++result.promotions;

        const auto newPosition = game::after_move(startingPosition, move);

        const bool isCheck = newPosition.is_check();

        if (isCheck)
            ++result.checks;

        if (! any_legal_moves(newPosition)) {
            if (isCheck)
                ++result.checkmates;
            else
                ++result.stalemates;
        }

        const auto childResult = perft<false>(depth - 1uz, newPosition);

        if constexpr (IsRoot) {
            result.rootNodes.emplace_back(move, childResult.nodes);
        }

        result += childResult;
    }

    return result;
}
tmokonen
Posts: 1355
Joined: Sun Mar 12, 2006 6:46 pm
Location: Kelowna
Full name: Tony Mokonen

Re: Perft stats discrepencies

Post by tmokonen »

It looks like you are totalling captures and checks in leaf nodes and interior nodes. Your expected numbers are only for leaf nodes.

https://www.chessprogramming.org/Perft_Results

1576 captures at depth 4 + 34 captures at depth 3 = 1610.
469 checks at depth 4 + 12 checks at depth 3 = 481.
benvining
Posts: 22
Joined: Fri May 30, 2025 10:18 pm
Full name: Ben Vining

Re: Perft stats discrepencies

Post by benvining »

That was it, thank you! Making this small change to my perft function fixed it:

Code: Select all

template <bool IsRoot>
constexpr PerftResult perft(const size_t depth, const Position& startingPosition) // NOLINT(misc-no-recursion)
{
    if (depth == 0uz)
        return { .nodes = 1uz };

    PerftResult result;

    for (const auto& move : generate(startingPosition)) {
        const auto newPosition = game::after_move(startingPosition, move);

        // we want stats only for leaf nodes
        if (depth == 1uz) {
            if (startingPosition.is_capture(move)) {
                ++result.captures;

                if (startingPosition.is_en_passant(move))
                    ++result.enPassantCaptures;
            }

            if (move.is_castling())
                ++result.castles;

            if (move.promotedType.has_value())
                ++result.promotions;

            const bool isCheck = newPosition.is_check();

            if (isCheck)
                ++result.checks;

            if (! any_legal_moves(newPosition)) {
                if (isCheck)
                    ++result.checkmates;
                else
                    ++result.stalemates;
            }
        }

        const auto childResult = perft<false>(depth - 1uz, newPosition);

        if constexpr (IsRoot) {
            result.rootNodes.emplace_back(move, childResult.nodes);
        }

        result += childResult;
    }

    return result;
}
And now all my test cases pass! Cheers!