Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: compressed board #109

Merged
merged 4 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions docs/pages/board.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Board

```cpp
using PackedBoard = std::array<std::uint8_t, 24>;

class Board {
public:
Board::Board(std::string_view fen)
Expand Down Expand Up @@ -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);
}
};
```
197 changes: 194 additions & 3 deletions include/chess.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<std::uint8_t, 24>;

class Board {
using U64 = std::uint64_t;

Expand Down Expand Up @@ -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); }
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<int>(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);
Expand Down
Loading