2
\$\begingroup\$

I'm playing around with a chess engine that I wanted it to know all the rules such as pawn promotion, en-passant, and castling. Currently, I'm using a basic Negamax for the AI but I plan to improve at a later stage.

I have not spent too much effort on the interface as I plan to make it compatible with XBoard / Winboard when I'm bored enough.

Any ideas to get the basics more efficient or dry?

#include <array>
#include <cstring> 
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

// bit-util.h

typedef std::uint8_t uint8;
typedef std::uint64_t uint64;


#ifdef _MSC_VER
#define lzcnt(a) __lzcnt64(a)
#define popcnt(a) __popcnt64(a)
#else
#define lzcnt(a) __builtin_clzl(a)
#define popcnt(a) __builtin_popcountl(a)
#endif

constexpr uint64 one = 1ul;

uint64 bitcalc(int col, int row) {
    return one << (col + row * 8);
}

bool excludes(uint64 bits, uint64 flag) {
    return !(bits & flag);
}

bool includes(uint64 bits, uint64 flag) {
    return bits & flag;
}

uint8 square(uint64 index) {
    return 63 - static_cast<uint8>(lzcnt(index));
}

struct Cell
{
    Cell(uint8 col, uint8 row)
        : col{ col }
        , row{ row }
    {
    }

    Cell(uint8 index) {
        col = index % 8;
        row = index / 8;
    }

    bool valid()
    {
        return col >= 0 && col < 8 && row >= 0 && row < 8;
    }

    bool move(int icol, int irow) {
        col += icol;
        row += irow;
        return valid();
    }

    uint8 index() {
        return col + row * 8;
    }

    uint64 flag()
    {
        return one << index();
    }

    uint8 col;
    uint8 row;
};

std::optional<uint64> onboard(int col, int row)
{
    if (col >= 0 && col < 8 && row >= 0 && row < 8)
        return bitcalc(col, row);
    return std::nullopt;
}

int rank(int player, int row)
{
    return player ? 7 - row : row;
}

// bitboard.h

enum Piece
{
    Pawn,
    Rook,
    Knight,
    Bishop,
    Queen,
    King,
    Max
};

struct BState
{
    bool attacks;
    uint64 excludes;
};

class BitBoard
{
public:
    BitBoard()
        : rookMoves{ {-1, 0}, {1,  0}, {0, -1}, {0,  1} }
        , bishopMoves{ {-1, -1}, {1, -1}, {-1, 1}, {1, 1} }
        , knightMoves{ {-1, 2}, {1,  2} , {-1, -2} , {1, -2}, {-2,  1} ,  {2,  1} , {-2, -1} , {2, -1} }
        , queenMoves{ {-1, -1}, {0, -1} , {1, -1} , {-1, 1} , {0, 1} , {1, 1} , {-1, 0} , {1, 0} }

    {
        std::memset(state, 0, sizeof(state));
        for (int s = 0; s < 2; s++)
        {
            for (int p = 0; p < Piece::Max; p++)
            {
                for (uint8 i = 0; i < 64; i++)
                {
                    for (uint8 j = 0; j < 64; j++)
                    {
                        generate(s, s ? -1 : 1, p, i, j);
                    }
                }
            }
        }
    }

    bool check(int side, int piece, uint8 src, uint8 dst, uint64 all) {
        BState& st = state[side][piece][src][dst];
        return st.attacks && excludes(all, st.excludes);
    }

private:
    void generate(int side, int dir, int piece, uint8 src, uint8 dst) {
        BState& st = state[side][piece][src][dst];
        switch (piece)
        {
        case Pawn:
            genray(st, dir, src, dst, -1, dir, true);
            genray(st, dir, src, dst, 1, dir, true);
            break;
        case Rook:
            for (const auto& move : rookMoves)
                genray(st, dir, src, dst, move[0], move[1], false);
            break;
        case Knight:
            for (const auto& move : knightMoves)
                genray(st, dir, src, dst, move[0], move[1], true);
            break;
        case Bishop:
            for (const auto& move : bishopMoves)
                genray(st, dir, src, dst, move[0], move[1], false);
            break;
        case Queen:
            for (const auto& move : queenMoves)
                genray(st, dir, src, dst, move[0], move[1], false);
            break;
        case King:
            for (const auto& move : queenMoves)
                genray(st, dir, src, dst, move[0], move[1], true);
            break;
        default:
            break;
        }
    }

    void genray(BState& state, int dir, uint8 src, uint8 dst, int inccol, int incrow, bool single)
    {
        Cell csrc(src);
        Cell cdst(dst);
        if (csrc.move(inccol, incrow))
        {
            if (csrc.index() == dst)
            {
                state.attacks = true;
                state.excludes = 0;
            }
            if (single)
                return;
            uint64 excludes = csrc.flag();
            while (csrc.move(inccol, incrow))
            {
                if (csrc.index() == dst)
                {
                    state.attacks = true;
                    state.excludes = excludes;
                    break;
                }
                else
                {
                    excludes |= csrc.flag();
                }
            }
        }
    }

private:
    BState state[2][Piece::Max][64][64];

public:
    const int knightMoves[8][2];
    const int rookMoves[4][2];
    const int bishopMoves[4][2];
    const int queenMoves[8][2];
};


// board.h

const static std::string pieces[] = { "PRNBQK", "prnbqk" };

enum Special
{
    None,
    Promote,
    EnPassant,
    CastShort,
    CastLong,
    Draw,
    Fail
};

struct Move
{
    Move(Piece piece, uint64 src, uint64 dst, uint8 special, int score = 0)
        : piece{ piece }
        , src{ src }
        , dst{ dst }
        , special{ special }
        , score{ score }
    { }
    int score;
    uint64 src;
    uint64 dst;
    uint8 special;
    Piece piece;
};

struct Player
{
    uint64 prev;
    uint64 all;
    uint64 pieces[Piece::Max];
};

class Board
{
public:
    uint64 moved;
    uint64 all;
    Player players[2];

    void init(std::string_view str)
    {
        std::memset(this, 0, sizeof(*this));
        for (uint64 i = 0; i < str.size(); i++) {
            for (int p = 0; p < 2; p++) {
                const auto& color = pieces[p];
                auto index = color.find(str[i]);
                if (index != std::string::npos) {
                    uint64 bit = one << i;
                    all |= bit;
                    players[p].all |= bit;
                    players[p].pieces[index] |= bit;
                }
            }
        }
    }

    void print(int depth = 0) {
        std::string header = "  A  B  C  D  E  F  G  H\n";
        std::cout << header;
        for (int r = 0; r < 8; r++) {
            std::cout << 8 - r;
            char bk = r % 2;
            for (int c = 0; c < 8; c++) {
                char ch = ' ';
                for (int p = 0; p < 2; p++) {
                    auto& player = players[p];
                    for (int i = 0; i < Piece::Max; i++) {
                        if (player.pieces[i] & bitcalc(c, r))
                            ch = pieces[p][i];
                    }
                }
                std::cout << (bk ? "\033[0;47;30m" : "\033[0;30;107m") << ' ' << ch << ' ';
                bk = !bk;
            }
            std::cout << "\033[m" << 8 - r << '\n';
        }
        std::cout << header << '\n';
    }

    void apply(int player, Move move) {
        Player& me = players[player];
        Player& op = players[!player];
        moved |= move.src | move.dst;
        me.prev = square(move.src) + square(move.dst) * 64;
        switch (move.special) {
        case Special::Promote:
            removePiece(op, Piece::Max, move.dst);
            removePiece(me, Piece::Pawn, move.src);
            insertPiece(me, move.piece, move.dst);
            break;
        case Special::EnPassant:
            removePiece(op, Piece::Pawn, bitcalc(Cell(square(move.dst)).col, Cell(square(move.src)).row));
            removePiece(me, Piece::Pawn, move.src);
            insertPiece(me, Piece::Pawn, move.dst);
            break;
        case Special::CastShort:
            removePiece(me, Piece::Rook, bitcalc(7, rank(player, 0)));
            insertPiece(me, Piece::Rook, bitcalc(5, rank(player, 0)));
            removePiece(me, Piece::King, move.src);
            insertPiece(me, Piece::King, move.dst);
            break;
        case Special::CastLong:
            removePiece(me, Piece::Rook, bitcalc(0, rank(player, 0)));
            insertPiece(me, Piece::Rook, bitcalc(3, rank(player, 0)));
            removePiece(me, Piece::King, move.src);
            insertPiece(me, Piece::King, move.dst);
            break;
        default:
            removePiece(op, Piece::Max, move.dst);
            removePiece(me, move.piece, move.src);
            insertPiece(me, move.piece, move.dst);
        }
    }

    int eval(Player& player) {
        int values[] = { 1, 5, 3, 3, 10, 200 };
        int total = 0;
        for (int i = 0; i < Piece::Max; i++)
            total += static_cast<int>(popcnt(player.pieces[i])) * values[i];
        return total;
    }

private:
    void insertPiece(Player& player, Piece piece, uint64 bit) {
        all |= bit;
        player.all |= bit;
        player.pieces[piece] |= bit;
    }

    void changePiece(Player& player, uint64 bit, Piece from, Piece to) {
        player.pieces[from] &= ~bit;
        player.pieces[to] |= bit;
    }

    void removePiece(Player& player, Piece piece, uint64 bit) {
        all &= ~bit;
        player.all &= ~bit;
        if (piece == Piece::Max) {
            for (auto& piece : player.pieces)
                piece &= ~bit;
        }
        else {
            player.pieces[piece] &= ~bit;
        }
    }
};

// AI.h

class AI
{
public:
    AI(BitBoard& bitboard)
        : bitboard{ bitboard }
    { }

    Move move(Board& board, int player, int maxDepth) {
        allMoves.clear();
        bestMoves.clear();
        negaMax(board, player, maxDepth, 0);
        if (bestMoves.empty())
            return Move(Piece::Max, 0, 0, Special::Draw, 0);
        return bestMoves[rand() % bestMoves.size()];
    }

    int negaMax(Board& board, int player, int maxDepth, int depth) {
        int scoreMe = board.eval(board.players[player]);
        int scoreOp = board.eval(board.players[!player]);
        int max = -9999;
        if (depth == maxDepth)
            return scoreMe - scoreOp;
        for (int i = 0; i < Piece::Max; i++) {
            Piece piece = static_cast<Piece>(i);
            uint64 bits = board.players[player].pieces[piece];
            while (bits) {
                uint64 src = bits & (0ul - bits);
                int index = square(src);
                Cell cell(index);
                switch (i) {
                case Piece::Pawn:
                    movePawn(board, player, maxDepth, depth, src, cell, piece, max);
                    break;
                case Piece::Rook:
                    moveRook(board, player, maxDepth, depth, src, cell, piece, max);
                    break;
                case Piece::Bishop:
                    moveBishop(board, player, maxDepth, depth, src, cell, piece, max);
                    break;
                case Piece::Knight:
                    moveKnight(board, player, maxDepth, depth, src, cell, piece, max);
                    break;
                case Piece::Queen:
                    moveQueen(board, player, maxDepth, depth, src, cell, piece, max);
                    break;
                case Piece::King:
                    moveKing(board, player, maxDepth, depth, src, cell, piece, max);
                    break;
                }
                bits ^= src;
            }
        }
        return max;
    }

    void recurse(Board board, int player, int maxDepth, int depth, Move move, int& max) {
        Board b = board;
        b.apply(player, move);
        move.score = -negaMax(b, !player, maxDepth, depth + 1);
        if (depth == 0) {
            allMoves.push_back(move);
            if (bestMoves.size() && bestMoves[0].score < move.score)
                bestMoves.clear();
            if (bestMoves.empty() || bestMoves[0].score == move.score)
                bestMoves.push_back(move);
        }
        else {
            if (move.score > max)
                max = move.score;
        }
    }

    void pawnPromotion(Board board, int player, int maxDepth, int depth, uint64 src, uint64 dst, int row, Piece piece, int& max) {
        if (rank(player, row) == 7) {
            for (int p = Piece::Rook; p < Piece::King; p++)
                recurse(board, player, maxDepth, depth, Move((Piece)p, src, dst, Special::Promote), max);
        }
        else {
            recurse(board, player, maxDepth, depth, Move(piece, src, dst, 0), max);
        }
    }

    void movePawn(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
        Player& opponent = board.players[!player];
        int direction = player ? -1 : 1;
        // forward moves
        auto dst = onboard(cell.col, cell.row + direction * 1);
        if (dst != std::nullopt && excludes(board.all, *dst)) {
            pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction * 1, piece, max);
            if (rank(player, cell.row) == 1)
            {
                dst = onboard(cell.col, cell.row + direction * 2);
                if (dst != std::nullopt && excludes(board.all, *dst)) {
                    pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction * 2, piece, max);
                }
            }
        }
        // sideways captures
        dst = onboard(cell.col - 1, cell.row + direction);
        if (dst != std::nullopt && includes(opponent.all, *dst)) {
            pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction, piece, max);
        }
        dst = onboard(cell.col + 1, cell.row + direction);
        if (dst != std::nullopt && includes(opponent.all, *dst)) {
            pawnPromotion(board, player, maxDepth, depth, src, *dst, cell.row + direction, piece, max);
        }
        // en-passant
        uint8 psrc = static_cast<uint8>(opponent.prev % 64);
        uint8 pdst = static_cast<uint8>(opponent.prev / 64);
        if (opponent.pieces[Piece::Pawn] & (one << pdst)) {
            Cell ps(psrc);
            Cell pd(pdst);
            if (ps.row == rank(!player, 1) && pd.row == rank(!player, 3)) {
                if (ps.col == cell.col - 1) {
                    dst = onboard(cell.col - 1, cell.row + direction);
                    recurse(board, player, maxDepth, depth, Move(piece, src, *dst, Special::EnPassant), max);
                }
                if (ps.col == cell.col + 1) {
                    dst = onboard(cell.col + 1, cell.row + direction);
                    recurse(board, player, maxDepth, depth, Move(piece, src, *dst, Special::EnPassant), max);
                }
            }
        }
    }

    void moveRook(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
        for (auto move : bitboard.rookMoves)
            moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], false, piece, max);
    }

    void moveKnight(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
        for (auto move : bitboard.knightMoves)
            moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], true, piece, max);
    }

    void moveBishop(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
        for (auto move : bitboard.bishopMoves)
            moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], false, piece, max);
    }

    void moveQueen(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
        for (auto move : bitboard.queenMoves)
            moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], false, piece, max);
    }


    void moveKing(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, Piece piece, int& max) {
        for (auto move : bitboard.queenMoves)
            moveDir(board, player, maxDepth, depth, src, cell, move[0], move[1], true, piece, max);
        int col = cell.col;
        int row = cell.row;
        if (rank(player, row) == 0 && col == 4 && excludes(board.moved, src))
        {
            Player& me = board.players[player];
            // castling queenside
            if (includes(me.pieces[Piece::Rook], bitcalc(0, row)) && excludes(board.moved, bitcalc(0, row)))
            {
                if (excludes(board.all, bitcalc(1, row) | bitcalc(2, row) | bitcalc(3, row)))
                {
                    if (canCastle(board, player, row, { 4, 3, 2 }))
                    {
                        recurse(board, player, maxDepth, depth, Move(Piece::King, src, bitcalc(col - 2, row), Special::CastLong), max);
                    }
                }
            }
            // castling otherside
            if (includes(me.pieces[Piece::Rook], bitcalc(7, row)) && excludes(board.moved, bitcalc(7, row)))
            {
                if (excludes(board.all, bitcalc(5, row) | bitcalc(6, row)))
                {
                    if (canCastle(board, player, row, { 4, 5, 6 }))
                    {
                        recurse(board, player, maxDepth, depth, Move(Piece::King, src, bitcalc(col + 2, row), Special::CastShort), max);
                    }
                }
            }
        }
    }

    bool canCastle(Board& board, int player, int row, std::array<int, 3> cols) {
        Player& opponent = board.players[!player];
        for (int p = 0; p < Piece::Max; p++) {
            uint64 bits = opponent.pieces[p];
            while (bits) {
                uint64 bit = bits & (0ul - bits);
                uint8 src = square(bit);
                for (int col : cols) {
                    if (bitboard.check(!player, p, src, Cell(col, row).index(), board.all))
                        return false;
                }
                bits ^= bit;
            }
        }
        return true;
    }

    void moveDir(Board& board, int player, int maxDepth, int depth, uint64 src, Cell cell, int colInc, int rowInc, bool single, Piece piece, int& max) {
        Player& me = board.players[player];
        Player& opponent = board.players[!player];
        while (cell.move(colInc, rowInc))
        {
            uint64 dst = cell.flag();
            if (includes(me.all, dst))
                break;
            recurse(board, player, maxDepth, depth, Move(piece, src, dst, 0), max);
            if (single || includes(board.all, dst))
                break;
        }
    }
public:
    BitBoard& bitboard;
    std::vector<Move> bestMoves;
    std::vector<Move> allMoves;
};

// chess.cpp

class GamePlayer
{
public:
    virtual std::optional<Move> play(AI& ai, Board& board, int side) = 0;
};

class PlayerHuman : public GamePlayer
{
public:
    virtual std::optional<Move> play(AI& ai, Board& board, int side) override {
        for (;;) {
            // validate
            ai.move(board, side, 1);

            for (;;)
            {
                auto src = getSquare("Select source: ");
                if (src == std::nullopt)
                    return std::nullopt;

                
                std::string seperator = "";
                bool found = false;
                for (auto move : ai.allMoves) {
                    if (move.src == *src) {
                        if (found == false)
                        {
                            std::cout << "Valid destinations: ";
                            found = true;
                        }
                        Cell cell(square(move.dst));
                        std::cout << seperator << (char)('a' + cell.col) << (char)('8' - cell.row);
                        seperator = ", ";
                    }
                }
                if (found == false)
                {
                    std::printf("No valid moves found from source.\n");
                    continue;
                }
                else
                {
                    std::cout << '\n';
                }

                auto dst = getSquare("Select destination: ");
                if (dst == std::nullopt)
                    return std::nullopt;
                for (auto move : ai.allMoves) {
                    if (move.src == *src && move.dst == *dst) {
                        if (move.special == Special::Promote) {
                            auto piece = selectPiece();
                            if (piece == std::nullopt)
                                return std::nullopt;
                            move.piece = *piece;
                        }
                        return move;
                    }
                }
                std::cout << "Invalid move.\n";
            }
        }
    }

private:
    std::optional<uint64> getSquare(std::string_view prompt) {
        for (;;) {
            std::cout << prompt;
            std::string line;
            std::getline(std::cin, line);
            if (std::cin.bad() || std::cin.eof())
                return std::nullopt;
            if (line.length() == 2) {
                Cell cell(toupper(line[0]) - 'A', '8' - line[1]);
                if (cell.valid())
                    return cell.flag();
            }
            std::cout << "Invalid square.\n";
        }
    }

    std::optional<Piece> selectPiece() {

        std::string prompt = "Select Piece: (R)ook, k(N)ight, (B)ishop or (Q)ueen\n";
        for (;;) {
            std::cout << prompt;
            std::string line;
            std::getline(std::cin, line);
            if (std::cin.bad() || std::cin.eof())
                return std::nullopt;
            if (line.length() == 1) {
                auto index = pieces[0].find(std::toupper(line[0]));
                if (index != std::string::npos)
                    return static_cast<Piece>(index);
            }
            std::cout << "Invalid square.\n";
        }
    }
};

class PlayerAI : public GamePlayer
{
public:
    virtual std::optional<Move> play(AI& ai, Board& board, int side) override {
        std::cout << "Thinking...";
        auto move = ai.move(board, side, 5);
        Cell src(square(move.src));
        Cell dst(square(move.dst));
        std::cout << '\n' << char('a' + src.col) << char('8' - src.row) << " -> " << char('a' + dst.col) << char('8' - dst.row) << "\n";
        return move;
    }
};

void playGame(GamePlayer& pl1, GamePlayer& pl0) {

    Board board;
    board.init(
        "RNBQKBNR"
        "PPPPPPPP"
        "        "
        "        "
        "        "
        "        "
        "pppppppp"
        "rnbqkbnr"
    );

    BitBoard bitboard;
    AI ai(bitboard);

    int side = 1;
    for (;;) {
        board.print();

        GamePlayer& player = side ? pl1 : pl0;
        auto move = player.play(ai, board, side);
        if (move == std::nullopt)
            return;

        board.apply(side, *move);
        if ((*move).special == Special::Draw || (*move).score < -100)
        {
            bool check = ai.move(board, !side, 1).score > 100;
            std::cout << (check ? "Checkmate" : "Draw");
            return;
        }

        board.apply(side, *move);
        side = !side;
    }
}

int main() {
    srand(static_cast<unsigned>(time(0)));
    PlayerHuman human;
    PlayerAI comp;
    playGame(human, comp);
}
\$\endgroup\$
1
  • 2
    \$\begingroup\$ canCastle shall also verify that neither King nor Rook ever moved. \$\endgroup\$
    – vnp
    Commented Dec 9, 2021 at 21:54

2 Answers 2

3
\$\begingroup\$

There is a lot of code, so please forgive a very cursory look.

  • Purely chess-related problems:

    • Castling privileges require that neither King nor Rook ever moved
    • End-of game is not detected (mate, three-fold repetition, 50 moves rule... the player cannot even resign!)
  • Detecting transpositions is a must. Do not waste time to analyze the same position twice.

  • I don't understand why bestMoves.empty() results in Special::Draw. In case of a mate on the board, bestMoves is certainly empty.

    That said, I am not sure that Board.apply() handles Special.Draw correctly.

  • It is very surprising that PlayerHuman needs AI. Something is wrong at the design level.

  • AI::moveDir line is \$152\$ characters long. This is a bit too much.

  • recurse is being called from too many places. Consider building a list of legal moves, and recurse over it.

  • Some of your braces are at the same line with a keyword, some are at the next. Stick to an uniform style.

Of course eval is not to be reviewed

\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the review and sorry for the amount of code. I had illusions of grandeur that it would be much simpler to create a half-decent engine =) Regarding checking if the king or rook was ever moved, I think this is handled by board.moved, but perhaps my logic is not sound? Special::Draw was a hack until I thought about making GamePlayer::play optional. \$\endgroup\$
    – jdt
    Commented Dec 10, 2021 at 12:57
2
\$\begingroup\$

Moves should not be member variables

The list of valid moves for each piece is not something that needs to be duplicated for every instance of BitBoard. Consider making them static constexpr variables instead:

class BitBoard {
public:
    BitBoard() {
        ...
    }
    ...
private:
    static constexpr int rookMoves[1][2] = {{-1, 0}, {1,  0}, {0, -1}, {0,  1}};
    ...
}

It can also be moved out of the class entirely.

Reducing code repetition

In generate(), there is a lot of code duplication. Ideally, you would be able to rewrite it like so:

void generate(...) {
    static constexpr struct {
        int[][2] &moves; // this is problematic
        bool single;
    } pieceMoves[Piece::MAX] = {
        {pawnMoves, true},
        {rookMoves, false},
        ...
    };
    BState& st = state[side][piece][src][dst];
    for (const auto& move: pieceMoves[piece].moves)
         genray(st, dir, src, dst, move[0], move[3], pieceMoves[piece].single);
};

The problem is how to store (references to) the valid move arrays into moves. The above code doesn't work; as it doesn't store the length of the array it references. It might work though if you can use something like C++20's std::span.

The trick used above is to store the non-redundant parts of the code in an array. You can also use this inside negaMax():

using MoveFunction = void (AI::*)(Board&, int, int, int, uint64, Cell, Piece, int&);
static constexpr MoveFunction moveFunctions[] = {
    &AI::movePawn,
    &AI::moveRook,
    ...
};
...
while (bits) {
    uint64 src = bits & (0ul - bits);
    int index = square(src);
    Cell cell(index);
    moveFunctions[i](board, player, maxDepth, depth, src, cell, piece, max);
    bits ^= src;
}

Arguably, with just the handful of different pieces to deal with, it might not be worth the extra complexity.

Prefer enum class where possible

Instead of using regular enums, prefer using enum class where possible, as it improves type safety.

Avoid mixing C and C++ functions

I see both std::cout and std::printf() being used. Avoid calling C functions in C++ code where possible. This also goes for things like std::memset(): you can use std::fill_n() instead. Instead of rand(), use C++'s pseudo-random number generation functions.

Prefer proper initializers over std::memset()

Avoid using std::memset() if you can just use regular initialization of variables. This is much safer, and the compiler will likely be able to optimize it into a memset() itself if possible.

For class BitBoard, just write:

class BitBoard {
    ...
    BState state[2][Piece::Max][64][64] = {};
    ...
};

For initializing a class Board, write:

void init(std::string_view str) {
    moved = {};
    all = {};
    std::fill_n(players, 2, Player{});
    ...
}

Consider using std::array for all arrays

Using raw arrays is not problematic on its own, but you have to be aware of pointer decay, it is not easy to get the size of them, and declaring multi-dimensional arrays can be confusing as well. While std::array is more verbose, it has none of these problems. It also has a fill() member function that makes clearing them easy.

Use std::getline()'s return value

std::getline() returns the input stream you pass it, but it is convertible to bool, which will tell you if the stream is still good. You can use this to read a line and check if you actually read something in one go, like so:

if (!std::getline(std::cin, line))
    return std::nullopt;

Use foo->bar instead of (*foo).bar

This is not only true for regular pointers, but also for std::optional types:

auto move = player.play(ai, board, side);
...
if (move->special == Special::Draw || move->score < -100) {
    ...
}
\$\endgroup\$

Not the answer you're looking for? Browse other questions tagged or ask your own question.