diff --git a/.gitattributes b/.gitattributes index aae67d9..6313b56 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1 @@ -./** text=auto eol=lf +* text=auto eol=lf diff --git a/.github/workflows/doxygen.yml b/.github/workflows/doxygen.yml index b4eb127..89d0798 100644 --- a/.github/workflows/doxygen.yml +++ b/.github/workflows/doxygen.yml @@ -1,32 +1,32 @@ -name: Doxygen docs - -on: - push: - branches: [ main ] - -jobs: - build-docs: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - with: - persist-credentials: false - - - name: Install dependencies - run: | - sudo apt-get update -qq - sudo apt-get install -y doxygen graphviz - - - name: Build docs - working-directory: ${{ github.workspace }} - run: | - if [ ! -f Doxyfile ]; then echo "Doxyfile not found in repo root"; exit 1; fi - doxygen Doxyfile - - - name: Upload docs artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a - with: - name: doxygen-html - path: doc/html - retention-days: 7 +name: Doxygen docs + +on: + push: + branches: [ main ] + +jobs: + build-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 + with: + persist-credentials: false + + - name: Install dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y doxygen graphviz + + - name: Build docs + working-directory: ${{ github.workspace }} + run: | + if [ ! -f Doxyfile ]; then echo "Doxyfile not found in repo root"; exit 1; fi + doxygen Doxyfile + + - name: Upload docs artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + with: + name: doxygen-html + path: doc/html + retention-days: 7 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5525b77..d195cfd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 with: ref: ${{ github.head_ref }} - persist-credentials: false + persist-credentials: true # commit - name: Install clang-format run: sudo apt-get update && sudo apt-get install -y clang-format diff --git a/.gitignore b/.gitignore index fe2d04a..fd90f7a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ !.github/** !.github/ !Doxyfile -!doc/ # Ignore build/editor junk build/ out/ @@ -21,4 +20,4 @@ out/ .cache/ cmake-*/ # Ignore asm outputs -*.s \ No newline at end of file +*.s diff --git a/Doxyfile b/Doxyfile index fe00865..5015cd1 100644 --- a/Doxyfile +++ b/Doxyfile @@ -1,20 +1,20 @@ -# Doxyfile 1.9.1 -PROJECT_NAME = "chesslib" -OUTPUT_DIRECTORY = doc -INPUT = . -RECURSIVE = YES -EXTRACT_ALL = YES -EXTRACT_PRIVATE = YES -GENERATE_HTML = YES -GENERATE_LATEX = NO -GENERATE_XML = YES -XML_OUTPUT = xml -QUIET = YES -GENERATE_TREEVIEW = YES -INLINE_SOURCES = YES -STRIP_FROM_PATH = "$(PWD)" -FILE_PATTERNS = *.h *.hpp *.cpp -EXCLUDE = build -WARNINGS = YES -WARN_IF_UNDOCUMENTED = YES -WARN_AS_ERROR = YES \ No newline at end of file +# Doxyfile 1.9.1 +PROJECT_NAME = "chesslib" +OUTPUT_DIRECTORY = doc +INPUT = . +RECURSIVE = NO +EXTRACT_ALL = YES +EXTRACT_PRIVATE = YES +GENERATE_HTML = YES +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_OUTPUT = xml +QUIET = YES +GENERATE_TREEVIEW = YES +INLINE_SOURCES = YES +STRIP_FROM_PATH = "$(PWD)" +FILE_PATTERNS = *.h *.hpp *.cpp +EXCLUDE = build +WARNINGS = YES +WARN_IF_UNDOCUMENTED = YES +WARN_AS_ERROR = YES diff --git a/attacks.cpp b/attacks.cpp index 032b882..9a85b60 100644 --- a/attacks.cpp +++ b/attacks.cpp @@ -326,9 +326,9 @@ _POSSIBLY_CONSTEXPR std::array RookAttacks = rookData.second; /** * @brief Returns the attack bitboard for a bishop on the given square. * - * @param sq The square where the bishop is located. - * @param occupied The occupied squares that block the bishop's attack paths. - * @return Bitboard with bits set for each square the bishop attacks. + * @param sq Bishop square. + * @param occupied Occupancy bitboard. + * @return Bitboard of squares attacked. */ [[nodiscard]] Bitboard bishop(Square sq, Bitboard occupied) { const auto &entry = BishopTable[(int)sq]; @@ -337,9 +337,9 @@ _POSSIBLY_CONSTEXPR std::array RookAttacks = rookData.second; /** * @brief Look up rook attacks from the precomputed magic table. - * @param sq The square where the rook is located. - * @param occupied A bitboard representing occupied squares. - * @return A bitboard of squares the rook can attack. + * @param sq Rook square. + * @param occupied Occupancy bitboard. + * @return Bitboard of squares attacked. */ [[nodiscard]] Bitboard rook(Square sq, Bitboard occupied) { const auto &entry = RookTable[(int)sq]; diff --git a/attacks.h b/attacks.h index 6a2fa3f..f82ed6d 100644 --- a/attacks.h +++ b/attacks.h @@ -257,13 +257,7 @@ template [[nodiscard]] constexpr Bitboard pawn(const Bitboard pawns) { /// @param sq Square. /// @param occupied Occupancy bitboard. /// @return Bitboard of squares attacked. -template /** - * Computes attack squares for a slider piece. - * @param sq Square the piece occupies. - * @param occupied Squares currently occupied on the board. - * @return Bitboard of squares attacked by the piece. - */ -[[nodiscard]] inline Bitboard slider(Square sq, Bitboard occupied) { +template [[nodiscard]] inline Bitboard slider(Square sq, Bitboard occupied) { static_assert(pt == PieceType::BISHOP || pt == PieceType::ROOK || pt == PieceType::QUEEN, "PieceType must be a slider!"); if constexpr (pt == PieceType::BISHOP) diff --git a/fwd_decl.h b/fwd_decl.h index 0379e4d..8771119 100644 --- a/fwd_decl.h +++ b/fwd_decl.h @@ -108,8 +108,8 @@ enum class ContiguousMappingPiece : uint8_t; /// @brief Default chess position type (uses EnginePiece). using Position = _Position; -/// @typedef Board /// @brief Alias for Position. +/// @deprecated Use Position instead using Board [[deprecated("Use Position instead")]] = Position; } // namespace chess diff --git a/movegen.cpp b/movegen.cpp index c64adca..ce239a6 100644 --- a/movegen.cpp +++ b/movegen.cpp @@ -153,17 +153,17 @@ inline Move *splat_moves(Move *moveList, Square from, Bitboard to_bb) { } #endif } // namespace _chess - -template /** - * @brief Appends moves from a source square to destination squares, or counts them. - * - * For Movelist, generates and stores all moves efficiently. For CountOnlyList, - * only increments the count. For other list types, appends placeholder moves. - * - * @param from Source square for all moves. - * @param targets Bitboard of destination squares. - */ -inline void record_moves(ListT &list, Square from, Bitboard targets) { +/** + * @brief Appends moves from a source square to destination squares, or counts them. + * + * For Movelist, generates and stores all moves efficiently. For CountOnlyList, + * only increments the count. For other list types, appends placeholder moves. + * + * @param list The list of moves + * @param from Source square for all moves. + * @param targets Bitboard of destination squares. + */ +template inline void record_moves(ListT &list, Square from, Bitboard targets) { if constexpr (std::is_same_v) { _chess::splat_moves(list.data() + list.size_, from, targets); list.size_ += popcount(targets); @@ -366,6 +366,8 @@ template * Generates all knight moves subject to pin and check constraints. * If `capturesOnly` is true, restricts to capture moves only. * + * @param pos The position. + * @param list The move list to record moves into. * @param _pin_mask Bitboard of pinned pieces; pinned knights are excluded. * @param _check_mask Bitboard indicating squares that resolve checks. */ @@ -462,6 +464,8 @@ template * constraints and check restrictions. Pieces pinned along rook lines are confined to those lines; * pieces pinned along bishop lines are confined to those diagonals. * + * @param pos The position. + * @param moves The move list to record moves into. * @param _rook_pin Bitboard of squares pinned along rook lines (vertical/horizontal). * @param _bishop_pin Bitboard of squares pinned along bishop lines (diagonals). * @param _check_mask Bitboard of legal destination squares when in check. diff --git a/movegen.h b/movegen.h index 6cc00de..e2a3e25 100644 --- a/movegen.h +++ b/movegen.h @@ -27,26 +27,27 @@ namespace chess::movegen { /// @brief Generate en-passant captures for the given colour. -template void genEP(const _Position &, ListT &); +template HOTFUNC void genEP(const _Position &, ListT &); /// @brief Generate double-pawn pushes (from the starting rank). -template void genPawnDoubleMoves(const _Position &, ListT &, Bitboard, Bitboard); +template +HOTFUNC void genPawnDoubleMoves(const _Position &, ListT &, Bitboard, Bitboard); /// @brief Generate single-pawn moves (pushes and captures). template -void genPawnSingleMoves(const _Position &, ListT &, Bitboard, Bitboard, Bitboard); +HOTFUNC void genPawnSingleMoves(const _Position &, ListT &, Bitboard, Bitboard, Bitboard); /// @brief Generate knight moves. template -void genKnightMoves(const _Position &, ListT &, Bitboard, Bitboard); +HOTFUNC void genKnightMoves(const _Position &, ListT &, Bitboard, Bitboard); /// @brief Generate king moves. template -void genKingMoves(const _Position &, ListT &, Bitboard); +HOTFUNC void genKingMoves(const _Position &, ListT &, Bitboard); /// @brief Generate sliding-piece moves (bishop, rook, queen). template -void genSlidingMoves(const _Position &, ListT &, Bitboard, Bitboard, Bitboard); +HOTFUNC void genSlidingMoves(const _Position &, ListT &, Bitboard, Bitboard, Bitboard); /// @brief Precomputed between-square bitboards. /// @details squares_between_bb[sq1][sq2] contains a bitboard of all squares diff --git a/moves_io.cpp b/moves_io.cpp index 5cca8fa..abe6520 100644 --- a/moves_io.cpp +++ b/moves_io.cpp @@ -1,552 +1,534 @@ -/* - a chess library (bonus: you can integrate more piece types!) which - supports Chess960 and is decently fast enough - Copyright (C) 2025-2026 winapiadmin - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . -*/ -// UCI moves parsing - -// License: https://github.com/Disservin/chess-library/blob/master/LICENSE - -/// @file moves_io.cpp -/// @brief UCI move parsing and conversion (moveToUci, uciToMove). - -#include "moves_io.h" -#include "position.h" -#include "types.h" -#include -#include -#if defined(_CHESSLIB_ERROR_MODE_THROW) -#define INVALID_ARG_IF(c, exception) \ - do { \ - if (c) \ - throw(exception); \ - } while (0) -#elif defined(_CHESSLIB_ERROR_MODE_ASSERT) -#define INVALID_ARG_IF(c, exception) \ - do { \ - assert(!(c) && #exception); \ - } while (0) -#elif defined(_DEBUG) && !defined(NDEBUG) -#include -#define INVALID_ARG_IF(c, exception) \ - do { \ - if (c) \ - std::cerr << #c << ", message: " << #exception << " (at " << __FILE__ << ":" << __LINE__ << ")\n"; \ - } while (0) -#else -#define INVALID_ARG_IF(c, exception) \ - do { \ - (void)(c); \ - } while (0) -#endif -namespace chess { -namespace uci { -/// @brief Convert a Square to algebraic notation string (e.g. 0 -> "a1"). -std::string squareToString(Square sq) { - constexpr std::string_view fileChars[65] = { - "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "a3", - "b3", "c3", "d3", "e3", "f3", "g3", "h3", "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "a5", "b5", - "c5", "d5", "e5", "f5", "g5", "h5", "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "a7", "b7", "c7", - "d7", "e7", "f7", "g7", "h7", "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", "none" - }; - return std::string{ fileChars[sq] }; -} -/// @brief Convert a Move to UCI string representation. -std::string moveToUci(Move mv, bool chess960) { - if (!mv.is_ok()) { - // null move - static const std::string nullMove = "0000"; - return nullMove; - } - constexpr char PieceTypeChar[] = " pnbrqk"; - static thread_local std::string move; - move.clear(); - // Source square - move += squareToString(mv.from_sq()); - // To square, special: castlings - switch (mv.type_of()) { - case CASTLING: { - if (chess960) - move += squareToString(mv.to_sq()); - else { - switch (mv.to_sq()) { - case SQ_H1: - move += "g1"; // White kingside castling - break; - case SQ_A1: - move += "c1"; // white queenside castling - break; - case SQ_H8: - move += "g8"; // black kingside castling - break; - case SQ_A8: - move += "c8"; // black queenside castling - break; - default: - INVALID_ARG_IF(true, std::runtime_error("This isn't Chess960")); - return {}; - } - } - } break; - case PROMOTION: - move += squareToString(mv.to_sq()); - move += PieceTypeChar[mv.promotion_type()]; - break; - default: - move += squareToString(mv.to_sq()); - break; - } - return move; -} -/// @brief Convert a UCI string (e.g. "e2e4") to a Move object. -template Move uciToMove(const _Position &pos, std::string_view uci) { - if (uci.length() < 4) { - INVALID_ARG_IF(uci.length() < 4, IllegalMoveException("example: a2a4 or d7d8q")); - return Move::NO_MOVE; - } - - Square source = parse_square(uci.substr(0, 2)); - Square target = parse_square(uci.substr(2, 2)); - - if (!is_valid(source) || !is_valid(target)) { - INVALID_ARG_IF(!is_valid(source) || !is_valid(target), - IllegalMoveException("source !in [a1, h8], target !in [a1, h8]")); - return Move::NO_MOVE; - } - auto move = (uci.length() == 4) ? Move::make(source, target) : Move::NO_MOVE; - auto pt = piece_of(pos.at(source)); - if (pt == NO_PIECE_TYPE) { - INVALID_ARG_IF(pt == NO_PIECE_TYPE, IllegalMoveException("source need to be a existing piece, got nothing")); - return Move::NO_MOVE; - } - // castling in chess960 - if (pos.chess960() && pt == PieceType::KING && pos.template at(target) == PieceType::ROOK && - pos.template at(target) == pos.side_to_move()) { - move = Move::make(source, target); - } - - // convert to king captures rook - // in chess960 the move should be sent as king captures rook already! - else if (!pos.chess960() && pt == PieceType::KING && square_distance(target, source) == 2) { - target = make_sq(target > source ? File::FILE_H : File::FILE_A, rank_of(source)); - move = Move::make(source, target); - } - // en passant - else if (pt == PAWN && target == pos.ep_square()) { - move = Move::make(source, target); - } - - // promotion - else if (pt == PAWN && uci.length() == 5 && (rank_of(target) == (pos.side_to_move() == WHITE ? RANK_8 : RANK_1))) { - auto promotion = parse_pt(uci[4]); - - if (promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN) { - INVALID_ARG_IF(promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN, - IllegalMoveException("promotions: [NRBQ]")); - return Move::NO_MOVE; - } - - move = Move::make(source, target, promotion); - } - Movelist moves; - pos.legals(moves); - auto it = std::find(moves.begin(), moves.end(), move); - if (it == moves.end()) { - INVALID_ARG_IF(true, IllegalMoveException("Move is illegal")); - return Move::NO_MOVE; - } - return move; -} -/// @brief Parse a SAN (Standard Algebraic Notation) move string. -template /** - * @brief Parses a SAN move string into a Move, validating against legal moves. - * - * Handles castling - notations (`O-O`, `0-0`, `O-O-O`, `0-0-0`), check/checkmate suffixes, - * promotions (`c8=Q` or `c8Q`), - and disambiguates moves using piece letters, file/rank hints, - * or full source squares (LAN - notation). - * - * @param pos The position context for validating legality and resolving ambiguity. - * - @param raw_san The SAN move string to parse (e.g., "e4", "Nf3", "exd5", "e8=Q+"). - * @param - remove_illegals If `true`, progressively removes trailing characters from the input - * until a legal - move is found or the string is empty; if `false`, - * parses the full string and - returns `Move::none()` on any error. - * @return The parsed `Move`, or `Move::none()` if parsing fails - or no legal move matches. - */ -Move parseSan(const _Position &pos, std::string_view raw_san, bool remove_illegals) { - auto do_parse = [&](std::string_view input_san) -> Move { - if (input_san.empty()) - return Move::none(); - Movelist moves; - pos.legals(moves); - - // Make a local mutable copy we can trim safely. - std::string san(input_san), _san(raw_san); - - // 1) Castling shortcuts - if (san == "O-O" || san == "0-0" || san == "O-O+" || san == "0-0+" || san == "O-O#" || san == "0-0#") { - const auto from = pos.king_sq(pos.side_to_move()); - const auto to = pos.get_castling_metadata(pos.side_to_move()).rook_start_ks; - Move km = chess::Move::make(from, to); - - if (std::find(moves.begin(), moves.end(), km) != moves.end()) - return km; - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); - return Move::none(); - } - if (san == "O-O-O" || san == "0-0-0" || san == "O-O-O+" || san == "0-0-0+" || san == "O-O-O#" || san == "0-0-0#") { - const auto from = pos.king_sq(pos.side_to_move()); - const auto to = pos.get_castling_metadata(pos.side_to_move()).rook_start_qs; - Move qm = chess::Move::make(from, to); - - if (std::find(moves.begin(), moves.end(), qm) != moves.end()) - return qm; - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); - return Move::none(); - } - // 2) Strip trailing annotations (+, #) that aren't required in the standard (except "e.p. "). Repeated occurrences too. - while (!san.empty()) { - char c = san.back(); - if (c == '+' || c == '#') - san.pop_back(); - else - break; - } - if (san.empty()) { - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); - return Move::none(); - } - - // 3) Extract promotion if present (e.g. c8=Q or c8Q) - PieceType promotion = NO_PIECE_TYPE; - if (san.size() >= 3) { - // look for "=Q" or similar at the very end, or single letter promotion (historical) - char penult = san[san.size() - 2]; - char last = san.back(); - if (penult == '=') { - promotion = parse_pt(last); - if (promotion == NO_PIECE_TYPE) { - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - san.pop_back(); // remove piece letter - san.pop_back(); // remove '=' - } else if ((last == 'Q' || last == 'R' || last == 'B' || last == 'N' || last == 'q' || last == 'r' || last == 'b' || - last == 'n')) { - // allow c8Q or c8q as shorthand (optional) - promotion = parse_pt(last); - san.pop_back(); - } - } - - // 4) Destination square: always the last [file][rank] - if (san.size() < 2) { - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - char dfile = san[san.size() - 2]; - char drank = san[san.size() - 1]; - if (!(dfile >= 'a' && dfile <= 'h' && drank >= '1' && drank <= '8')) { - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - std::string dest_sq_str = san.substr(san.size() - 2, 2); - Square to_square = parse_square(dest_sq_str); - if (to_square == SQ_NONE) { - if (!remove_illegals) - INVALID_ARG_IF(to_square == SQ_NONE, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - san.resize(san.size() - 2); // chop off destination - - // 5) Now san contains everything before the dest: - // possible piece letter, possible source square, possible disambiguation, - // optional 'x' capture markers (we will ignore 'x'). - // Remove all 'x' characters (capture indicators) from the remainder - std::string prefix; - prefix.reserve(san.size()); - for (char c : san) - if (c != 'x' && c != 'X') - prefix.push_back(c); - // prefix now holds the pre-destination token (e.g. "Nbd" from "Nbd2" or "Pe2" from "Pe2e4") - - // 6) Detect a fully specified source square at the end of prefix (LAN) - bool has_src_square = false; - Square src_square = SQ_NONE; - if (prefix.size() >= 2) { - char sfile = prefix[prefix.size() - 2]; - char srank = prefix[prefix.size() - 1]; - if (sfile >= 'a' && sfile <= 'h' && srank >= '1' && srank <= '8') { - // consume it - std::string src_sq_str = prefix.substr(prefix.size() - 2, 2); - src_square = parse_square(src_sq_str); - if (src_square == SQ_NONE) { - if (!remove_illegals) - INVALID_ARG_IF(src_square == SQ_NONE, - IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - prefix.resize(prefix.size() - 2); - } - } - has_src_square = src_square != SQ_NONE; - // 7) Detect piece letter at front if present - PieceType piece_type = NO_PIECE_TYPE; - if (!prefix.empty()) { - char front = prefix.front(); - PieceType pt = parse_pt(front); - if (pt != NO_PIECE_TYPE) { - piece_type = pt; - // remove leading piece letter - prefix.erase(prefix.begin()); - } - } - // If no explicit piece letter, it's a pawn move - if (piece_type == NO_PIECE_TYPE) - piece_type = PAWN; - - // 8) The remaining prefix is disambiguation: can be file, rank, or file+rank (rare) - int dis_file = -1; // 0..7 or -1 - int dis_rank = -1; // 0..7 or -1 - for (char c : prefix) { - if (c >= 'a' && c <= 'h') - dis_file = c - 'a'; - else if (c >= '1' && c <= '8') - dis_rank = c - '1'; - else { - // unexpected char in prefix - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - } - - // 9) Build candidate filter and scan legal moves - Move matched = Move::null(); - bool found = false; - // Bitboard to_mask = (1ULL << to_square) & ~pos.occ(pos.side_to_move()); // mask excluding own pieces on destination - - // If pawn and no disambiguation file, restrict pawns to dest file (avoid ambiguous pawn non-file forms) - // This matches python-chess behavior described earlier. - Bitboard from_mask = ~0ULL; - if (piece_type == PAWN) { - from_mask &= pos.pieces(PAWN, pos.side_to_move()); - if (dis_file == -1 && !has_src_square) { - // restrict to same file as destination (non-capture pawns must be on same file) - int dest_file = file_of(to_square); - from_mask &= attacks::MASK_FILE[dest_file]; - } - } else { - from_mask &= pos.pieces(piece_type, pos.side_to_move()); - } - - // Additional disambiguation masks: - if (dis_file != -1) - from_mask &= attacks::MASK_FILE[dis_file]; - if (dis_rank != -1) - from_mask &= attacks::MASK_RANK[dis_rank]; - if (has_src_square) { - // If fully specified source given, narrow to that square only. - from_mask &= (1ULL << src_square); - } - - for (Move m : moves) { - // match destination - if (m.to_sq() != to_square) - continue; - - // match promotion - if (promotion != NO_PIECE_TYPE) { - if (m.type_of() != PROMOTION || m.promotion_type() != promotion) - continue; - } else { - // if move is promotion but SAN lacked piece, reject (require explicit promotion) - if (m.type_of() == PROMOTION) - continue; - } - - // match piece type: check the piece that is on m.from_sq() in pos - PieceType src_pt = piece_of(pos.piece_on(m.from_sq())); - if (src_pt != piece_type) - continue; - - // match from_mask (disambiguation and pawn filtering) - if (((1ULL << m.from_sq()) & from_mask) == 0) - continue; - - // Everything matches -> accept candidate - if (found) { - if (!remove_illegals) - INVALID_ARG_IF(found, AmbiguousMoveException("ambiguous san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - matched = m; - found = true; - } - - if (!found) { - if (!remove_illegals) - INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); - return Move::none(); - } - - return matched; - }; - - if (remove_illegals) { - std::string trimmed_san(raw_san); - while (!trimmed_san.empty()) { - Move attempt = do_parse(trimmed_san); - if (attempt.is_ok()) - return attempt; - trimmed_san.pop_back(); - } - INVALID_ARG_IF(trimmed_san.empty(), - IllegalMoveException("illegal san: '" + std::string(raw_san) + "' in " + pos.fen())); - return Move::none(); - } else - return do_parse(raw_san); -} -/// @brief Convert a Move to SAN or LAN (Long Algebraic Notation) string. -template std::string moveToSan(const _Position &pos, Move move, bool long_, bool suffix) { - constexpr char FILE_NAMES[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; - - constexpr char PieceTypeChar[] = " pnbrqk"; - // Null move. (or none) - if (!move.is_ok()) { - return "--"; - } - - std::string san; - PieceType piece_type = piece_of(pos.at(move.from_sq())); - bool capture = pos.is_capture(move); - // Castling. - if (pos.is_castling(move)) { - if (file_of(move.to_sq()) < file_of(move.from_sq())) { - san = "O-O-O"; - goto appendCheck; - } else { - san = "O-O"; - goto appendCheck; - } - } - if (piece_type == NO_PIECE_TYPE) { - INVALID_ARG_IF(piece_type == NO_PIECE_TYPE, - IllegalMoveException("moveToSan() expect move to be pseudo-legal or null, but got " + moveToUci(move) + - " in " + pos.fen())); - return ""; - } - - if (piece_type != PAWN) { - san = std::toupper(PieceTypeChar[piece_type]); - } - if (long_) { - san += squareToString(move.from_sq()); - } else if (piece_type != PAWN) { - // Get ambiguous move candidates. - // Relevant candidates: not exactly the current move, - // but to the same square. - Movelist moves; - pos.legals(moves); - Bitboard others = 0; - Bitboard from_mask = pos.pieces(piece_type, pos.side_to_move()); - from_mask &= ~(1ULL << move.from_sq()); - Bitboard to_mask = 1ULL << move.to_sq(); - for (const Move &candidate : moves) { - Bitboard cand_from_bb = 1ULL << candidate.from_sq(); - // Only consider other pieces of same type that can move to the same destination. - if ((cand_from_bb & from_mask) && ((1ULL << candidate.to_sq()) & to_mask)) - others |= cand_from_bb; - } - - // Disambiguate only if there are other candidates that can move to the same square. - if (others) { - const char RANK_NAMES[] = { '1', '2', '3', '4', '5', '6', '7', '8' }; - bool need_file = false, need_rank = false; - for (Square sq = SQ_A1; sq < SQ_NONE; ++sq) { - if (others & (1ULL << sq)) { - if (file_of(sq) == file_of(move.from_sq())) - need_rank = true; - if (rank_of(sq) == rank_of(move.from_sq())) - need_file = true; - } - } - // If neither shares file nor rank, include file by default. - if (!need_file && !need_rank) - need_file = true; - if (need_file) - san += FILE_NAMES[file_of(move.from_sq())]; - if (need_rank) - san += RANK_NAMES[rank_of(move.from_sq())]; - } - } else if (capture) { - san += FILE_NAMES[file_of(move.from_sq())]; - } - - // Captures. - if (capture) { - san += "x"; - } else if (long_) { - san += "-"; - } - - // Destination square. - san += squareToString(move.to_sq()); - - // Promotion. - if (move.type_of() == PROMOTION) { - san += "=" + std::string(1, std::toupper(PieceTypeChar[move.promotion_type()])); - } -appendCheck: - if (!suffix) - return san; - _Position p = pos; - p.do_move(move); - const bool _check = p.is_check(); - Movelist moves; - p.legals(moves); - // Checkmate: no legal moves and in check; Stalemate: no legal moves and not in check - if (moves.size() == 0 && _check) - san += "#"; - else if (_check) - san += "+"; - return san; -} -#define INSTANTITATE(PieceC) \ - template Move uciToMove(const _Position &, std::string_view); \ - template Move parseSan(const _Position &, std::string_view, bool); \ - template std::string moveToSan(const _Position &, Move, bool, bool); -INSTANTITATE(PolyglotPiece) -INSTANTITATE(EnginePiece) -INSTANTITATE(ContiguousMappingPiece) -#undef INSTANTITATE -} // namespace uci -std::string Move::uci() const { return uci::moveToUci(*this); } -} // namespace chess +/* + a chess library (bonus: you can integrate more piece types!) which + supports Chess960 and is decently fast enough + Copyright (C) 2025-2026 winapiadmin + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ +// UCI moves parsing + +// License: https://github.com/Disservin/chess-library/blob/master/LICENSE + +/// @file moves_io.cpp +/// @brief UCI move parsing and conversion (moveToUci, uciToMove). + +#include "moves_io.h" +#include "position.h" +#include "types.h" +#include +#include +#if defined(_CHESSLIB_ERROR_MODE_THROW) +#define INVALID_ARG_IF(c, exception) \ + do { \ + if (c) \ + throw(exception); \ + } while (0) +#elif defined(_CHESSLIB_ERROR_MODE_ASSERT) +#define INVALID_ARG_IF(c, exception) \ + do { \ + assert(!(c) && #exception); \ + } while (0) +#elif defined(_DEBUG) && !defined(NDEBUG) +#include +#define INVALID_ARG_IF(c, exception) \ + do { \ + if (c) \ + std::cerr << #c << ", message: " << #exception << " (at " << __FILE__ << ":" << __LINE__ << ")\n"; \ + } while (0) +#else +#define INVALID_ARG_IF(c, exception) \ + do { \ + (void)(c); \ + } while (0) +#endif +namespace chess { +namespace uci { +/// @brief Convert a Square to algebraic notation string (e.g. 0 -> "a1"). +std::string squareToString(Square sq) { + constexpr std::string_view fileChars[65] = { + "a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1", "a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2", "a3", + "b3", "c3", "d3", "e3", "f3", "g3", "h3", "a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4", "a5", "b5", + "c5", "d5", "e5", "f5", "g5", "h5", "a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6", "a7", "b7", "c7", + "d7", "e7", "f7", "g7", "h7", "a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8", "none" + }; + return std::string{ fileChars[sq] }; +} +/// @brief Convert a Move to UCI string representation. +std::string moveToUci(Move mv, bool chess960) { + if (!mv.is_ok()) { + // null move + static const std::string nullMove = "0000"; + return nullMove; + } + constexpr char PieceTypeChar[] = " pnbrqk"; + static thread_local std::string move; + move.clear(); + // Source square + move += squareToString(mv.from_sq()); + // To square, special: castlings + switch (mv.type_of()) { + case CASTLING: { + if (chess960) + move += squareToString(mv.to_sq()); + else { + switch (mv.to_sq()) { + case SQ_H1: + move += "g1"; // White kingside castling + break; + case SQ_A1: + move += "c1"; // white queenside castling + break; + case SQ_H8: + move += "g8"; // black kingside castling + break; + case SQ_A8: + move += "c8"; // black queenside castling + break; + default: + INVALID_ARG_IF(true, std::runtime_error("This isn't Chess960")); + return {}; + } + } + } break; + case PROMOTION: + move += squareToString(mv.to_sq()); + move += PieceTypeChar[mv.promotion_type()]; + break; + default: + move += squareToString(mv.to_sq()); + break; + } + return move; +} +/// @brief Convert a UCI string (e.g. "e2e4") to a Move object. +template Move uciToMove(const _Position &pos, std::string_view uci) { + if (uci.length() < 4) { + INVALID_ARG_IF(uci.length() < 4, IllegalMoveException("example: a2a4 or d7d8q")); + return Move::NO_MOVE; + } + + Square source = parse_square(uci.substr(0, 2)); + Square target = parse_square(uci.substr(2, 2)); + + if (!is_valid(source) || !is_valid(target)) { + INVALID_ARG_IF(!is_valid(source) || !is_valid(target), + IllegalMoveException("source !in [a1, h8], target !in [a1, h8]")); + return Move::NO_MOVE; + } + auto move = (uci.length() == 4) ? Move::make(source, target) : Move::NO_MOVE; + auto pt = piece_of(pos.at(source)); + if (pt == NO_PIECE_TYPE) { + INVALID_ARG_IF(pt == NO_PIECE_TYPE, IllegalMoveException("source need to be a existing piece, got nothing")); + return Move::NO_MOVE; + } + // castling in chess960 + if (pos.chess960() && pt == PieceType::KING && pos.template at(target) == PieceType::ROOK && + pos.template at(target) == pos.side_to_move()) { + move = Move::make(source, target); + } + + // convert to king captures rook + // in chess960 the move should be sent as king captures rook already! + else if (!pos.chess960() && pt == PieceType::KING && square_distance(target, source) == 2) { + target = make_sq(target > source ? File::FILE_H : File::FILE_A, rank_of(source)); + move = Move::make(source, target); + } + // en passant + else if (pt == PAWN && target == pos.ep_square()) { + move = Move::make(source, target); + } + + // promotion + else if (pt == PAWN && uci.length() == 5 && (rank_of(target) == (pos.side_to_move() == WHITE ? RANK_8 : RANK_1))) { + auto promotion = parse_pt(uci[4]); + + if (promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN) { + INVALID_ARG_IF(promotion == NO_PIECE_TYPE || promotion == KING || promotion == PAWN, + IllegalMoveException("promotions: [NRBQ]")); + return Move::NO_MOVE; + } + + move = Move::make(source, target, promotion); + } + Movelist moves; + pos.legals(moves); + auto it = std::find(moves.begin(), moves.end(), move); + if (it == moves.end()) { + INVALID_ARG_IF(true, IllegalMoveException("Move is illegal")); + return Move::NO_MOVE; + } + return move; +} +/// @brief Parse a SAN string into a Move for the given position. +/// @tparam T Piece enum type. +/// @tparam P Position tag. +/// @param pos The position. +/// @param san SAN string (e.g. "Nf3", "O-O"). +/// @param remove_illegals If true, return Move::NO_MOVE instead of throwing. +/// @return The parsed Move. +template Move parseSan(const _Position &pos, std::string_view san, bool remove_illegals) { + auto do_parse = [&](std::string_view input_san) -> Move { + if (input_san.empty()) + return Move::none(); + Movelist moves; + pos.legals(moves); + + // Make a local mutable copy we can trim safely. + std::string san(input_san), _san(san); + + // 1) Castling shortcuts + if (san == "O-O" || san == "0-0" || san == "O-O+" || san == "0-0+" || san == "O-O#" || san == "0-0#") { + const auto from = pos.king_sq(pos.side_to_move()); + const auto to = pos.get_castling_metadata(pos.side_to_move()).rook_start_ks; + Move km = chess::Move::make(from, to); + + if (std::find(moves.begin(), moves.end(), km) != moves.end()) + return km; + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); + return Move::none(); + } + if (san == "O-O-O" || san == "0-0-0" || san == "O-O-O+" || san == "0-0-0+" || san == "O-O-O#" || san == "0-0-0#") { + const auto from = pos.king_sq(pos.side_to_move()); + const auto to = pos.get_castling_metadata(pos.side_to_move()).rook_start_qs; + Move qm = chess::Move::make(from, to); + + if (std::find(moves.begin(), moves.end(), qm) != moves.end()) + return qm; + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); + return Move::none(); + } + // 2) Strip trailing annotations (+, #) that aren't required in the standard (except "e.p. "). Repeated occurrences too. + while (!san.empty()) { + char c = san.back(); + if (c == '+' || c == '#') + san.pop_back(); + else + break; + } + if (san.empty()) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + san + "' in " + pos.fen())); + return Move::none(); + } + + // 3) Extract promotion if present (e.g. c8=Q or c8Q) + PieceType promotion = NO_PIECE_TYPE; + if (san.size() >= 3) { + // look for "=Q" or similar at the very end, or single letter promotion (historical) + char penult = san[san.size() - 2]; + char last = san.back(); + if (penult == '=') { + promotion = parse_pt(last); + if (promotion == NO_PIECE_TYPE) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + san.pop_back(); // remove piece letter + san.pop_back(); // remove '=' + } else if ((last == 'Q' || last == 'R' || last == 'B' || last == 'N' || last == 'q' || last == 'r' || last == 'b' || + last == 'n')) { + // allow c8Q or c8q as shorthand (optional) + promotion = parse_pt(last); + san.pop_back(); + } + } + + // 4) Destination square: always the last [file][rank] + if (san.size() < 2) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + char dfile = san[san.size() - 2]; + char drank = san[san.size() - 1]; + if (!(dfile >= 'a' && dfile <= 'h' && drank >= '1' && drank <= '8')) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + std::string dest_sq_str = san.substr(san.size() - 2, 2); + Square to_square = parse_square(dest_sq_str); + if (to_square == SQ_NONE) { + if (!remove_illegals) + INVALID_ARG_IF(to_square == SQ_NONE, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + san.resize(san.size() - 2); // chop off destination + + // 5) Now san contains everything before the dest: + // possible piece letter, possible source square, possible disambiguation, + // optional 'x' capture markers (we will ignore 'x'). + // Remove all 'x' characters (capture indicators) from the remainder + std::string prefix; + prefix.reserve(san.size()); + for (char c : san) + if (c != 'x' && c != 'X') + prefix.push_back(c); + // prefix now holds the pre-destination token (e.g. "Nbd" from "Nbd2" or "Pe2" from "Pe2e4") + + // 6) Detect a fully specified source square at the end of prefix (LAN) + bool has_src_square = false; + Square src_square = SQ_NONE; + if (prefix.size() >= 2) { + char sfile = prefix[prefix.size() - 2]; + char srank = prefix[prefix.size() - 1]; + if (sfile >= 'a' && sfile <= 'h' && srank >= '1' && srank <= '8') { + // consume it + std::string src_sq_str = prefix.substr(prefix.size() - 2, 2); + src_square = parse_square(src_sq_str); + if (src_square == SQ_NONE) { + if (!remove_illegals) + INVALID_ARG_IF(src_square == SQ_NONE, + IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + prefix.resize(prefix.size() - 2); + } + } + has_src_square = src_square != SQ_NONE; + // 7) Detect piece letter at front if present + PieceType piece_type = NO_PIECE_TYPE; + if (!prefix.empty()) { + char front = prefix.front(); + PieceType pt = parse_pt(front); + if (pt != NO_PIECE_TYPE) { + piece_type = pt; + // remove leading piece letter + prefix.erase(prefix.begin()); + } + } + // If no explicit piece letter, it's a pawn move + if (piece_type == NO_PIECE_TYPE) + piece_type = PAWN; + + // 8) The remaining prefix is disambiguation: can be file, rank, or file+rank (rare) + int dis_file = -1; // 0..7 or -1 + int dis_rank = -1; // 0..7 or -1 + for (char c : prefix) { + if (c >= 'a' && c <= 'h') + dis_file = c - 'a'; + else if (c >= '1' && c <= '8') + dis_rank = c - '1'; + else { + // unexpected char in prefix + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + } + + // 9) Build candidate filter and scan legal moves + Move matched = Move::null(); + bool found = false; + // Bitboard to_mask = (1ULL << to_square) & ~pos.occ(pos.side_to_move()); // mask excluding own pieces on destination + + // If pawn and no disambiguation file, restrict pawns to dest file (avoid ambiguous pawn non-file forms) + // This matches python-chess behavior described earlier. + Bitboard from_mask = ~0ULL; + if (piece_type == PAWN) { + from_mask &= pos.pieces(PAWN, pos.side_to_move()); + if (dis_file == -1 && !has_src_square) { + // restrict to same file as destination (non-capture pawns must be on same file) + int dest_file = file_of(to_square); + from_mask &= attacks::MASK_FILE[dest_file]; + } + } else { + from_mask &= pos.pieces(piece_type, pos.side_to_move()); + } + + // Additional disambiguation masks: + if (dis_file != -1) + from_mask &= attacks::MASK_FILE[dis_file]; + if (dis_rank != -1) + from_mask &= attacks::MASK_RANK[dis_rank]; + if (has_src_square) { + // If fully specified source given, narrow to that square only. + from_mask &= (1ULL << src_square); + } + + for (Move m : moves) { + // match destination + if (m.to_sq() != to_square) + continue; + + // match promotion + if (promotion != NO_PIECE_TYPE) { + if (m.type_of() != PROMOTION || m.promotion_type() != promotion) + continue; + } else { + // if move is promotion but SAN lacked piece, reject (require explicit promotion) + if (m.type_of() == PROMOTION) + continue; + } + + // match piece type: check the piece that is on m.from_sq() in pos + PieceType src_pt = piece_of(pos.piece_on(m.from_sq())); + if (src_pt != piece_type) + continue; + + // match from_mask (disambiguation and pawn filtering) + if (((1ULL << m.from_sq()) & from_mask) == 0) + continue; + + // Everything matches -> accept candidate + if (found) { + if (!remove_illegals) + INVALID_ARG_IF(found, AmbiguousMoveException("ambiguous san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + matched = m; + found = true; + } + + if (!found) { + if (!remove_illegals) + INVALID_ARG_IF(true, IllegalMoveException("illegal san: '" + _san + "' in " + pos.fen())); + return Move::none(); + } + + return matched; + }; + + if (remove_illegals) { + std::string trimmed_san(san); + while (!trimmed_san.empty()) { + Move attempt = do_parse(trimmed_san); + if (attempt.is_ok()) + return attempt; + trimmed_san.pop_back(); + } + INVALID_ARG_IF(trimmed_san.empty(), IllegalMoveException("illegal san: '" + std::string(san) + "' in " + pos.fen())); + return Move::none(); + } else + return do_parse(san); +} +/// @brief Convert a Move to SAN or LAN (Long Algebraic Notation) string. +template std::string moveToSan(const _Position &pos, Move move, bool long_, bool suffix) { + constexpr char FILE_NAMES[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; + + constexpr char PieceTypeChar[] = " pnbrqk"; + // Null move. (or none) + if (!move.is_ok()) { + return "--"; + } + + std::string san; + PieceType piece_type = piece_of(pos.at(move.from_sq())); + bool capture = pos.is_capture(move); + // Castling. + if (pos.is_castling(move)) { + if (file_of(move.to_sq()) < file_of(move.from_sq())) { + san = "O-O-O"; + goto appendCheck; + } else { + san = "O-O"; + goto appendCheck; + } + } + if (piece_type == NO_PIECE_TYPE) { + INVALID_ARG_IF(piece_type == NO_PIECE_TYPE, + IllegalMoveException("moveToSan() expect move to be pseudo-legal or null, but got " + moveToUci(move) + + " in " + pos.fen())); + return ""; + } + + if (piece_type != PAWN) { + san = std::toupper(PieceTypeChar[piece_type]); + } + if (long_) { + san += squareToString(move.from_sq()); + } else if (piece_type != PAWN) { + // Get ambiguous move candidates. + // Relevant candidates: not exactly the current move, + // but to the same square. + Movelist moves; + pos.legals(moves); + Bitboard others = 0; + Bitboard from_mask = pos.pieces(piece_type, pos.side_to_move()); + from_mask &= ~(1ULL << move.from_sq()); + Bitboard to_mask = 1ULL << move.to_sq(); + for (const Move &candidate : moves) { + Bitboard cand_from_bb = 1ULL << candidate.from_sq(); + // Only consider other pieces of same type that can move to the same destination. + if ((cand_from_bb & from_mask) && ((1ULL << candidate.to_sq()) & to_mask)) + others |= cand_from_bb; + } + + // Disambiguate only if there are other candidates that can move to the same square. + if (others) { + const char RANK_NAMES[] = { '1', '2', '3', '4', '5', '6', '7', '8' }; + bool need_file = false, need_rank = false; + for (Square sq = SQ_A1; sq < SQ_NONE; ++sq) { + if (others & (1ULL << sq)) { + if (file_of(sq) == file_of(move.from_sq())) + need_rank = true; + if (rank_of(sq) == rank_of(move.from_sq())) + need_file = true; + } + } + // If neither shares file nor rank, include file by default. + if (!need_file && !need_rank) + need_file = true; + if (need_file) + san += FILE_NAMES[file_of(move.from_sq())]; + if (need_rank) + san += RANK_NAMES[rank_of(move.from_sq())]; + } + } else if (capture) { + san += FILE_NAMES[file_of(move.from_sq())]; + } + + // Captures. + if (capture) { + san += "x"; + } else if (long_) { + san += "-"; + } + + // Destination square. + san += squareToString(move.to_sq()); + + // Promotion. + if (move.type_of() == PROMOTION) { + san += "=" + std::string(1, std::toupper(PieceTypeChar[move.promotion_type()])); + } +appendCheck: + if (!suffix) + return san; + _Position p = pos; + p.do_move(move); + const bool _check = p.is_check(); + Movelist moves; + p.legals(moves); + // Checkmate: no legal moves and in check; Stalemate: no legal moves and not in check + if (moves.size() == 0 && _check) + san += "#"; + else if (_check) + san += "+"; + return san; +} +#define INSTANTITATE(PieceC) \ + template Move uciToMove(const _Position &, std::string_view); \ + template Move parseSan(const _Position &, std::string_view, bool); \ + template std::string moveToSan(const _Position &, Move, bool, bool); +INSTANTITATE(PolyglotPiece) +INSTANTITATE(EnginePiece) +INSTANTITATE(ContiguousMappingPiece) +#undef INSTANTITATE +} // namespace uci +std::string Move::uci() const { return uci::moveToUci(*this); } +} // namespace chess diff --git a/moves_io.h b/moves_io.h index 630ff90..a9064f7 100644 --- a/moves_io.h +++ b/moves_io.h @@ -25,18 +25,6 @@ #include /// @file moves_io.h -/** - * Parse a SAN string into a Move for the given position. - * @tparam T Piece enum type. - * @tparam P Position tag. - * @param pos The position. - * @param san SAN string (e.g. "Nf3", "O-O"). - * @param remove_illegals If true, return Move::NO_MOVE instead of throwing. - * @return The parsed Move. - * @throws IllegalMoveException if the SAN string represents an illegal move and remove_illegals is false. - * @throws AmbiguousMoveException if the SAN string is ambiguous. - */ - namespace chess::uci { /// @brief Convert a Move to UCI coordinate string (e.g. "e2e4", "e7e8q"). diff --git a/position.cpp b/position.cpp index 1e16dd3..5ba9489 100644 --- a/position.cpp +++ b/position.cpp @@ -245,10 +245,6 @@ template template void _Position /** * @brief Loads a position from a FEN string. diff --git a/position.h b/position.h index 68695f5..fb8be73 100644 --- a/position.h +++ b/position.h @@ -18,7 +18,6 @@ */ #pragma once #include "attacks.h" -#include "bitboard.h" #include "movegen.h" #include "types.h" #include "zobrist.h" @@ -49,7 +48,6 @@ namespace attacks { /// @note This function assumes that the occupancy bitboards have already been masked to only include pieces on the relevant /// ray, which allows it to use simple bit operations to find the first blocker and potential attackers without needing to /// iterate over squares. -/// @return nothing (modified via refs) template inline void scan_attacks_ray(Square ksq, Bitboard occ_masked, Bitboard slider_mask, Bitboard occ_us, Bitboard &checkers, Bitboard &pin_bb) { @@ -466,6 +464,8 @@ template -class ValueList { +template class ValueList { static_assert(MaxSize, "what are you doing with 0 items"); public: @@ -652,7 +644,6 @@ using Movelist = ValueList; /// @brief Counting-only move list — same interface as Movelist but discards move data. class CountOnlyList { - public: public: /// @brief Size type for CountOnlyList. using size_type = std::size_t;