diff --git a/README.md b/README.md index e402dd9..3745af8 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# An extensive SHL Chess Library for C++ +# An extensive SHL Chess Library for C++ [![Chess Library](https://github.com/Disservin/chess-library/actions/workflows/chess-library.yml/badge.svg)](https://github.com/Disservin/chess-library/actions/workflows/chess-library.yml) @@ -15,6 +15,7 @@ It can be used for any type of chess program, be it a chess engine, a chess GUI, - **Robust**: Unit Tests & it has been tested on millions of chess positions, while developing the C++ part of [Stockfish's Winrate Model](https://github.com/official-stockfish/WDL_model). - **PGN Support**: Parse basic PGN files. - **Namespace**: Everything is in the `chess::` namespace, so it won't pollute your namespace. +- **Compact Board Representation in 24bytes**: The board state can be compressed into 24 bytes, using `PackedBoard` and `Board::Compact::encode`/`Board::Compact::decode`. ### Usage @@ -140,6 +141,7 @@ depth 5 time 3403 nodes 164075551 nps 48200808 fen r4rk1/1pp1qppp/p1np1n2/ ### Development Setup This project is using the meson build system. https://mesonbuild.com/ + #### Setup ```bash diff --git a/docs/pages/board.md b/docs/pages/board.md index a50af7b..3ebafbc 100644 --- a/docs/pages/board.md +++ b/docs/pages/board.md @@ -1,6 +1,8 @@ # Board ```cpp +using PackedBoard = std::array; + class Board { public: Board::Board(std::string_view fen) @@ -90,5 +92,16 @@ class Board { /// @brief Recalculates the zobrist hash and return it. /// If you want get the zobrist hash use hash(). U64 zobrist(); + + class Compact { + public: + /// @brief Compresses the board into a PackedBoard + static PackedBoard encode(const Board &board); + + /// @brief Creates a Board object from a PackedBoard + /// @param compressed + /// @param chess960 If the board is a chess960 position, set this to true + static Board decode(const PackedBoard &compressed, bool chess960 = false); + } }; ``` diff --git a/include/chess.hpp b/include/chess.hpp index 15eec58..3f69a65 100644 --- a/include/chess.hpp +++ b/include/chess.hpp @@ -25,7 +25,7 @@ THIS FILE IS AUTO GENERATED DO NOT CHANGE MANUALLY. Source: https://github.com/Disservin/chess-library -VERSION: 0.6.56 +VERSION: 0.6.57 */ #ifndef CHESS_HPP @@ -1661,6 +1661,10 @@ enum class GameResultReason { NONE }; +// A compact representation of the board in 24 bytes, +// does not include the half-move clock or full move number. +using PackedBoard = std::array; + class Board { using U64 = std::uint64_t; @@ -1738,10 +1742,12 @@ class Board { }; public: - explicit Board(std::string_view fen = constants::STARTPOS) { + explicit Board(std::string_view fen = constants::STARTPOS, bool chess960 = false) { prev_states_.reserve(256); + chess960_ = chess960; setFenInternal(fen); } + virtual void setFen(std::string_view fen) { setFenInternal(fen); } static Board fromFen(std::string_view fen) { return Board(fen); } @@ -2228,7 +2234,7 @@ class Board { void set960(bool is960) { chess960_ = is960; - setFen(original_fen_); + if (!original_fen_.empty()) setFen(original_fen_); } /// @brief Checks if the current position is a chess960, aka. FRC/DFRC position. @@ -2428,6 +2434,191 @@ class Board { friend std::ostream &operator<<(std::ostream &os, const Board &board); + /// @brief Compresses the board into a PackedBoard + class Compact { + friend class Board; + Compact() = default; + + public: + /// @brief Compresses the board into a PackedBoard + static PackedBoard encode(const Board &board) { return encodeState(board); } + + /// @brief Creates a Board object from a PackedBoard + /// @param compressed + /// @param chess960 If the board is a chess960 position, set this to true + static Board decode(const PackedBoard &compressed, bool chess960 = false) { + Board board{}; + board.chess960_ = chess960; + decode(board, compressed); + return board; + } + + private: + /** + * A compact board representation can be achieved in 24 bytes, + * we use 8 bytes (64bit) to store the occupancy bitboard, + * and 16 bytes (128bit) to store the pieces (plus some special information). + * + * Each of the 16 bytes can store 2 pieces, since chess only has 12 different pieces, + * we can represent the pieces from 0 to 11 in 4 bits (a nibble) and use the other 4 bit for + * the next piece. + * Since we need to store information about enpassant, castling rights and the side to move, + * we can use the remaining 4 bits to store this information. + * + * However we need to store the information and the piece information together. + * This means in our case that + * 12 -> enpassant + a pawn, we can deduce the color of the pawn from the rank of the square + * 13 -> white rook with castling rights, we later use the file to deduce if it's a short or long castle + * 14 -> black rook with castling rights, we later use the file to deduce if it's a short or long castle + * 15 -> black king and black is side to move + * + * We will later deduce the square of the pieces from the occupancy bitboard. + */ + static PackedBoard encodeState(const Board &board) { + PackedBoard packed{}; + + packed[0] = board.occ().getBits() >> 56; + packed[1] = (board.occ().getBits() >> 48) & 0xFF; + packed[2] = (board.occ().getBits() >> 40) & 0xFF; + packed[3] = (board.occ().getBits() >> 32) & 0xFF; + packed[4] = (board.occ().getBits() >> 24) & 0xFF; + packed[5] = (board.occ().getBits() >> 16) & 0xFF; + packed[6] = (board.occ().getBits() >> 8) & 0xFF; + packed[7] = board.occ().getBits() & 0xFF; + + auto offset = 8 * 2; + auto occ = board.occ(); + + while (occ) { + // we now fill the packed array, since our convertedpiece only actually needs 4 bits, + // we can store 2 pieces in one byte. + const auto sq = Square(occ.pop()); + const auto shift = (offset % 2 == 0 ? 4 : 0); + packed[offset / 2] |= convertMeaning(board, sq, board.at(sq)) << shift; + offset++; + } + + return packed; + } + + static void decode(Board &board, const PackedBoard &compressed) { + Bitboard occupied = 0ull; + + for (int i = 0; i < 8; i++) { + occupied |= Bitboard(compressed[i]) << (56 - i * 8); + } + + int offset = 16; + int white_castle_idx = 0, black_castle_idx = 0; + File white_castle[2] = {File::NO_FILE, File::NO_FILE}; + File black_castle[2] = {File::NO_FILE, File::NO_FILE}; + + // clear board state + + board.stm_ = Color::WHITE; + board.occ_bb_.fill(0ULL); + board.pieces_bb_.fill(0ULL); + board.board_.fill(Piece::NONE); + board.cr_.clear(); + board.original_fen_.clear(); + + // place pieces back on the board + while (occupied) { + const auto sq = Square(occupied.pop()); + const auto nibble = compressed[offset / 2] >> (offset % 2 == 0 ? 4 : 0) & 0b1111; + const auto piece = convertPiece(nibble); + + if (piece != Piece::NONE) { + board.placePiece(piece, sq); + + offset++; + continue; + } + + // Piece has a special meaning, interpret it from the raw integer + // pawn with ep square behind it + if (nibble == 12) { + board.ep_sq_ = sq.ep_square(); + // depending on the rank this is a white or black pawn + auto color = sq.rank() == Rank::RANK_4 ? Color::WHITE : Color::BLACK; + board.placePiece(Piece(PieceType::PAWN, color), sq); + } + // castling rights for white + else if (nibble == 13) { + white_castle[white_castle_idx++] = sq.file(); + board.placePiece(Piece(PieceType::ROOK, Color::WHITE), sq); + } + // castling rights for black + else if (nibble == 14) { + black_castle[black_castle_idx++] = sq.file(); + board.placePiece(Piece(PieceType::ROOK, Color::BLACK), sq); + } + // black to move + else if (nibble == 15) { + board.stm_ = Color::BLACK; + board.placePiece(Piece(PieceType::KING, Color::BLACK), sq); + } + + offset++; + } + + // reapply castling + for (int i = 0; i < 2; i++) { + if (white_castle[i] != File::NO_FILE) { + const auto king_sq = board.kingSq(Color::WHITE); + const auto file = white_castle[i]; + const auto side = CastlingRights::closestSide(file, king_sq.file()); + + board.cr_.setCastlingRight(Color::WHITE, side, file); + } + + if (black_castle[i] != File::NO_FILE) { + const auto king_sq = board.kingSq(Color::BLACK); + const auto file = black_castle[i]; + const auto side = CastlingRights::closestSide(file, king_sq.file()); + + board.cr_.setCastlingRight(Color::BLACK, side, file); + } + } + } + + // 1:1 mapping of Piece::internal() to the compressed piece + static std::uint8_t convertPiece(Piece piece) { return int(piece.internal()); } + + // for pieces with a special meaning return Piece::NONE since this is otherwise not used + static Piece convertPiece(std::uint8_t piece) { + if (piece >= 12) return Piece::NONE; + return Piece(Piece::underlying(piece)); + } + + // 12 => theres an ep square behind the pawn, rank will be deduced from the rank + // 13 => any white rook with castling rights, side will be deduced from the file + // 14 => any black rook with castling rights, side will be deduced from the file + // 15 => black king and black is side to move + static std::uint8_t convertMeaning(const Board &board, Square sq, Piece piece) { + if (piece.type() == PieceType::PAWN && board.ep_sq_ != Square::underlying::NO_SQ) { + if (Square(static_cast(sq.index()) ^ 8) == board.ep_sq_) return 12; + } + + if (piece.type() == PieceType::ROOK) { + if (piece.color() == Color::WHITE && + (board.cr_.getRookFile(Color::WHITE, CastlingRights::Side::KING_SIDE) == sq.file() || + board.cr_.getRookFile(Color::WHITE, CastlingRights::Side::QUEEN_SIDE) == sq.file())) + return 13; + if (piece.color() == Color::BLACK && + (board.cr_.getRookFile(Color::BLACK, CastlingRights::Side::KING_SIDE) == sq.file() || + board.cr_.getRookFile(Color::BLACK, CastlingRights::Side::QUEEN_SIDE) == sq.file())) + return 14; + } + + if (piece.type() == PieceType::KING && piece.color() == Color::BLACK && board.stm_ == Color::BLACK) { + return 15; + } + + return convertPiece(piece); + } + }; + protected: virtual void placePiece(Piece piece, Square sq) { assert(board_[sq.index()] == Piece::NONE); diff --git a/src/board.hpp b/src/board.hpp index 247bf81..2b43a83 100644 --- a/src/board.hpp +++ b/src/board.hpp @@ -32,6 +32,10 @@ enum class GameResultReason { NONE }; +// A compact representation of the board in 24 bytes, +// does not include the half-move clock or full move number. +using PackedBoard = std::array; + class Board { using U64 = std::uint64_t; @@ -109,10 +113,12 @@ class Board { }; public: - explicit Board(std::string_view fen = constants::STARTPOS) { + explicit Board(std::string_view fen = constants::STARTPOS, bool chess960 = false) { prev_states_.reserve(256); + chess960_ = chess960; setFenInternal(fen); } + virtual void setFen(std::string_view fen) { setFenInternal(fen); } static Board fromFen(std::string_view fen) { return Board(fen); } @@ -599,7 +605,7 @@ class Board { void set960(bool is960) { chess960_ = is960; - setFen(original_fen_); + if (!original_fen_.empty()) setFen(original_fen_); } /// @brief Checks if the current position is a chess960, aka. FRC/DFRC position. @@ -799,6 +805,191 @@ class Board { friend std::ostream &operator<<(std::ostream &os, const Board &board); + /// @brief Compresses the board into a PackedBoard + class Compact { + friend class Board; + Compact() = default; + + public: + /// @brief Compresses the board into a PackedBoard + static PackedBoard encode(const Board &board) { return encodeState(board); } + + /// @brief Creates a Board object from a PackedBoard + /// @param compressed + /// @param chess960 If the board is a chess960 position, set this to true + static Board decode(const PackedBoard &compressed, bool chess960 = false) { + Board board{}; + board.chess960_ = chess960; + decode(board, compressed); + return board; + } + + private: + /** + * A compact board representation can be achieved in 24 bytes, + * we use 8 bytes (64bit) to store the occupancy bitboard, + * and 16 bytes (128bit) to store the pieces (plus some special information). + * + * Each of the 16 bytes can store 2 pieces, since chess only has 12 different pieces, + * we can represent the pieces from 0 to 11 in 4 bits (a nibble) and use the other 4 bit for + * the next piece. + * Since we need to store information about enpassant, castling rights and the side to move, + * we can use the remaining 4 bits to store this information. + * + * However we need to store the information and the piece information together. + * This means in our case that + * 12 -> enpassant + a pawn, we can deduce the color of the pawn from the rank of the square + * 13 -> white rook with castling rights, we later use the file to deduce if it's a short or long castle + * 14 -> black rook with castling rights, we later use the file to deduce if it's a short or long castle + * 15 -> black king and black is side to move + * + * We will later deduce the square of the pieces from the occupancy bitboard. + */ + static PackedBoard encodeState(const Board &board) { + PackedBoard packed{}; + + packed[0] = board.occ().getBits() >> 56; + packed[1] = (board.occ().getBits() >> 48) & 0xFF; + packed[2] = (board.occ().getBits() >> 40) & 0xFF; + packed[3] = (board.occ().getBits() >> 32) & 0xFF; + packed[4] = (board.occ().getBits() >> 24) & 0xFF; + packed[5] = (board.occ().getBits() >> 16) & 0xFF; + packed[6] = (board.occ().getBits() >> 8) & 0xFF; + packed[7] = board.occ().getBits() & 0xFF; + + auto offset = 8 * 2; + auto occ = board.occ(); + + while (occ) { + // we now fill the packed array, since our convertedpiece only actually needs 4 bits, + // we can store 2 pieces in one byte. + const auto sq = Square(occ.pop()); + const auto shift = (offset % 2 == 0 ? 4 : 0); + packed[offset / 2] |= convertMeaning(board, sq, board.at(sq)) << shift; + offset++; + } + + return packed; + } + + static void decode(Board &board, const PackedBoard &compressed) { + Bitboard occupied = 0ull; + + for (int i = 0; i < 8; i++) { + occupied |= Bitboard(compressed[i]) << (56 - i * 8); + } + + int offset = 16; + int white_castle_idx = 0, black_castle_idx = 0; + File white_castle[2] = {File::NO_FILE, File::NO_FILE}; + File black_castle[2] = {File::NO_FILE, File::NO_FILE}; + + // clear board state + + board.stm_ = Color::WHITE; + board.occ_bb_.fill(0ULL); + board.pieces_bb_.fill(0ULL); + board.board_.fill(Piece::NONE); + board.cr_.clear(); + board.original_fen_.clear(); + + // place pieces back on the board + while (occupied) { + const auto sq = Square(occupied.pop()); + const auto nibble = compressed[offset / 2] >> (offset % 2 == 0 ? 4 : 0) & 0b1111; + const auto piece = convertPiece(nibble); + + if (piece != Piece::NONE) { + board.placePiece(piece, sq); + + offset++; + continue; + } + + // Piece has a special meaning, interpret it from the raw integer + // pawn with ep square behind it + if (nibble == 12) { + board.ep_sq_ = sq.ep_square(); + // depending on the rank this is a white or black pawn + auto color = sq.rank() == Rank::RANK_4 ? Color::WHITE : Color::BLACK; + board.placePiece(Piece(PieceType::PAWN, color), sq); + } + // castling rights for white + else if (nibble == 13) { + white_castle[white_castle_idx++] = sq.file(); + board.placePiece(Piece(PieceType::ROOK, Color::WHITE), sq); + } + // castling rights for black + else if (nibble == 14) { + black_castle[black_castle_idx++] = sq.file(); + board.placePiece(Piece(PieceType::ROOK, Color::BLACK), sq); + } + // black to move + else if (nibble == 15) { + board.stm_ = Color::BLACK; + board.placePiece(Piece(PieceType::KING, Color::BLACK), sq); + } + + offset++; + } + + // reapply castling + for (int i = 0; i < 2; i++) { + if (white_castle[i] != File::NO_FILE) { + const auto king_sq = board.kingSq(Color::WHITE); + const auto file = white_castle[i]; + const auto side = CastlingRights::closestSide(file, king_sq.file()); + + board.cr_.setCastlingRight(Color::WHITE, side, file); + } + + if (black_castle[i] != File::NO_FILE) { + const auto king_sq = board.kingSq(Color::BLACK); + const auto file = black_castle[i]; + const auto side = CastlingRights::closestSide(file, king_sq.file()); + + board.cr_.setCastlingRight(Color::BLACK, side, file); + } + } + } + + // 1:1 mapping of Piece::internal() to the compressed piece + static std::uint8_t convertPiece(Piece piece) { return int(piece.internal()); } + + // for pieces with a special meaning return Piece::NONE since this is otherwise not used + static Piece convertPiece(std::uint8_t piece) { + if (piece >= 12) return Piece::NONE; + return Piece(Piece::underlying(piece)); + } + + // 12 => theres an ep square behind the pawn, rank will be deduced from the rank + // 13 => any white rook with castling rights, side will be deduced from the file + // 14 => any black rook with castling rights, side will be deduced from the file + // 15 => black king and black is side to move + static std::uint8_t convertMeaning(const Board &board, Square sq, Piece piece) { + if (piece.type() == PieceType::PAWN && board.ep_sq_ != Square::underlying::NO_SQ) { + if (Square(static_cast(sq.index()) ^ 8) == board.ep_sq_) return 12; + } + + if (piece.type() == PieceType::ROOK) { + if (piece.color() == Color::WHITE && + (board.cr_.getRookFile(Color::WHITE, CastlingRights::Side::KING_SIDE) == sq.file() || + board.cr_.getRookFile(Color::WHITE, CastlingRights::Side::QUEEN_SIDE) == sq.file())) + return 13; + if (piece.color() == Color::BLACK && + (board.cr_.getRookFile(Color::BLACK, CastlingRights::Side::KING_SIDE) == sq.file() || + board.cr_.getRookFile(Color::BLACK, CastlingRights::Side::QUEEN_SIDE) == sq.file())) + return 14; + } + + if (piece.type() == PieceType::KING && piece.color() == Color::BLACK && board.stm_ == Color::BLACK) { + return 15; + } + + return convertPiece(piece); + } + }; + protected: virtual void placePiece(Piece piece, Square sq) { assert(board_[sq.index()] == Piece::NONE); diff --git a/src/include.hpp b/src/include.hpp index a8b9f85..6d8222f 100644 --- a/src/include.hpp +++ b/src/include.hpp @@ -25,7 +25,7 @@ THIS FILE IS AUTO GENERATED DO NOT CHANGE MANUALLY. Source: https://github.com/Disservin/chess-library -VERSION: 0.6.56 +VERSION: 0.6.57 */ #ifndef CHESS_HPP diff --git a/tests/board.cpp b/tests/board.cpp index bb13ea6..c8f2c05 100644 --- a/tests/board.cpp +++ b/tests/board.cpp @@ -1,3 +1,5 @@ +#include + #include "../src/include.hpp" #include "doctest/doctest.hpp" @@ -141,4 +143,150 @@ TEST_SUITE("Board") { Board board = Board("8/7k/8/8/3BB3/8/8/1K6 w - - 0 1"); CHECK(board.isInsufficientMaterial() == false); } -} \ No newline at end of file + + TEST_CASE("Compressed State Normal") { + Board board = Board("4k1n1/pppppppp/8/8/8/8/PPPPPPPP/4K3 w - - 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + CHECK(sizeof(compressed) == 24); + } + + TEST_CASE("Compressed State EP ") { + Board board = Board("4k1n1/ppp1pppp/8/8/3pP3/8/PPPP1PPP/4K3 b - e3 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Compressed State White Castling Queen") { + Board board = Board("4k1n1/pppppppp/8/8/8/8/PPPPPPPP/R3K3 w Q - 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Compressed State White Castling King") { + Board board = Board("4k1n1/pppppppp/8/8/8/8/PPPPPPPP/4K2R w K - 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Compressed State Black Castling Queen") { + Board board = Board("r3k1n1/pppppppp/8/8/8/8/PPPPPPPP/4K3 w q - 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Compressed State Black Castling King") { + Board board = Board("4k1nr/pppppppp/8/8/8/8/PPPPPPPP/4K3 w k - 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Compressed State Black Side to Move") { + Board board = Board("4k1n1/pppppppp/8/8/8/8/PPPPPPPP/4K3 b - - 0 1"); + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Compressed State Usable in Map") { + std::map compressed; + + compressed.emplace(0, Board::Compact::encode(Board("4k1n1/pppppppp/8/8/8/8/PPPPPPPP/4K3 w - - 0 1"))); + + CHECK(Board::Compact::decode(compressed.at(0)).getFen() == "4k1n1/pppppppp/8/8/8/8/PPPPPPPP/4K3 w - - 0 1"); + } + + TEST_CASE("Chess960 Castling") { + Board board = Board("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w AHah - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 2") { + Board board = Board("1rqbkrbn/1ppppp1p/1n6/p1N3p1/8/2P4P/PP1PPPP1/1RQBKRBN w FBfb - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 3") { + Board board = Board("rbbqn1kr/pp2p1pp/6n1/2pp1p2/2P4P/P7/BP1PPPP1/R1BQNNKR w HAha - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 4") { + Board board = Board("rqbbknr1/1ppp2pp/p5n1/4pp2/P7/1PP5/1Q1PPPPP/R1BBKNRN w GAga - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 5") { + Board board = Board("4rrb1/1kp3b1/1p1p4/pP1Pn2p/5p2/1PR2P2/2P1NB1P/2KR1B2 w D - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 6") { + Board board = Board("1rkr3b/1ppn3p/3pB1n1/6q1/R2P4/4N1P1/1P5P/2KRQ1B1 b Dbd - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 7") { + Board board = Board("qbbnrkr1/p1pppppp/1p4n1/8/2P5/6N1/PPNPPPPP/1BRKBRQ1 b FCge - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 8") { + Board board = Board("rr6/2kpp3/1ppn2p1/p2b1q1p/P4P1P/1PNN2P1/2PP4/1K2R2R b E - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } + + TEST_CASE("Chess960 Castling 9") { + Board board = Board("rr6/2kpp3/1ppnb1p1/p2Q1q1p/P4P1P/1PNN2P1/2PP4/1K2RR2 b E - 0 1", true); + + auto compressed = Board::Compact::encode(board); + auto newboard = Board::Compact::decode(compressed, true); + + CHECK(board.getFen() == newboard.getFen()); + } +}