diff --git a/chesslib/include/chessboard.h b/chesslib/include/chessboard.h index 08ebad4..f515504 100644 --- a/chesslib/include/chessboard.h +++ b/chesslib/include/chessboard.h @@ -41,6 +41,14 @@ D E F I N E M A K R O S ==================================================== */ +/* The chess bitboards in start formation */ +#define START_FORMATION {\ + 0x0000000000000010uLL, 0x0000000000000008uLL, 0x0000000000000081uLL,\ + 0x0000000000000024uLL, 0x0000000000000042uLL, 0x000000000000FF00uLL,\ + 0x1000000000000000uLL, 0x0800000000000000uLL, 0x8100000000000000uLL,\ + 0x2400000000000000uLL, 0x4200000000000000uLL, 0x00FF000000000000uLL,\ + 0x0000FFFFFFFF0000uLL } + /* row masks */ #define ROW_1 0x00000000000000FFuLL #define ROW_2 0x000000000000FF00uLL @@ -115,4 +123,8 @@ void to_simple_board(const Bitboard board[], ChessPiece* target); void compress_pieces_array(const ChessPiece pieces[], uint8_t* compr_bytes); void uncompress_pieces_array(const uint8_t compr_bytes[], ChessPiece* out_pieces); +Bitboard get_captured_fields(const Bitboard bitboards[], ChessColor side); +void get_board_positions(Bitboard bitboard, ChessPosition* out_positions, size_t* out_length); +ChessPosition get_board_position(Bitboard bitboard); + #endif diff --git a/chesslib/include/chessgamesession.h b/chesslib/include/chessgamesession.h new file mode 100644 index 0000000..1be295a --- /dev/null +++ b/chesslib/include/chessgamesession.h @@ -0,0 +1,61 @@ +/* + * MIT License + * + * Copyright(c) 2020 Marco Tröster + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef CHESSGAMESESSION_H +#define CHESSGAMESESSION_H + +#include "chesstypes.h" +#include "chessboard.h" +#include "chessdraw.h" + +/* A chess game session semantically at least as powerful as the FEN notation. + It is supposed to help carrying out professional chess matches. */ +typedef struct _CHESS_GAME_SESSION { + Bitboard board[13]; + ChessGameContext context; +} ChessGameSession; + +/* default game context bits: 00000000 00001000 00011110 00000000 + * + * meaning: + * first game round, all rochades possible, no en-passant possible, + * white drawing and zero halfdraws since the last pawn draw. + */ +#define DEFAULT_GAME_CONTEXT 0x00081E00uL + +/* initial game session: board in start formation and initial game context */ +#define INIT_GAME_SESSION {START_FORMATION, DEFAULT_GAME_CONTEXT} + +ChessGameContext create_context(ChessColor side, uint8_t en_passants, + uint8_t rochades, uint8_t halfdraws_since_last_pawn_draw, int game_round); + +Bitboard get_en_passant_mask(ChessGameContext context); +Bitboard get_rochade_mask(ChessGameContext context); +uint8_t get_hdslpd(ChessGameContext context); +int get_game_rounds(ChessGameContext context); + +void apply_draw_to_context(ChessDraw draw, ChessGameContext* context); +void apply_game_context_to_board(Bitboard* board, ChessGameContext context); + +#endif \ No newline at end of file diff --git a/chesslib/include/chesslibmodule.h b/chesslib/include/chesslibmodule.h index c61989e..6ab228d 100644 --- a/chesslib/include/chesslibmodule.h +++ b/chesslib/include/chesslibmodule.h @@ -49,5 +49,6 @@ #include "chesspieceatpos.h" #include "chessposition.h" #include "chesstypes.h" +#include "chessxformat.h" #endif diff --git a/chesslib/include/chessposition.h b/chesslib/include/chessposition.h index 8e5789f..9add8a6 100644 --- a/chesslib/include/chessposition.h +++ b/chesslib/include/chessposition.h @@ -33,6 +33,7 @@ #include #include +#include /* ==================================================== D E F I N E F U N C T I O N S @@ -45,6 +46,7 @@ ChessPosition create_position(int8_t row, int8_t column); int8_t get_row(ChessPosition position); int8_t get_column(ChessPosition position); +int position_from_string(const char* pos_str, ChessPosition* pos); void position_to_string(ChessPosition position, char* pos_str); #endif diff --git a/chesslib/include/chesstypes.h b/chesslib/include/chesstypes.h index 61116cc..cb67ac6 100644 --- a/chesslib/include/chesstypes.h +++ b/chesslib/include/chesstypes.h @@ -95,4 +95,15 @@ typedef uint16_t CompactChessDraw; highest bit as H8 (indexes A1=0, B1=1, ..., A2=8, ..., H8=63). */ typedef uint64_t Bitboard; +/* TODO: add a definition for a FEN game session context to replace the was_moved bitboards + e.g. 1 bit for drawing_side, 8 bits for en-passants, 4 bits for rochades, + 6 bits for draws_since_last_pawn_draw, remaining bits for game_round + -> new 32-bit integer bitwise type */ + +/* | game round | halfdraws since pawn draw | rochades | en-passants | side | + | ------------- | ------------------------- | -------- | ----------- | ---- | + | xxxxxxxxxxxxx | xxxxxx | xxxx | xxxxxxxx | x | */ +typedef uint32_t ChessGameContext; +/* TODO: think of compressing the en-passant part to 3 bits, pos indices [0-7] */ + #endif diff --git a/chesslib/include/chessxformat.h b/chesslib/include/chessxformat.h new file mode 100644 index 0000000..75a3051 --- /dev/null +++ b/chesslib/include/chessxformat.h @@ -0,0 +1,45 @@ +/* + * MIT License + * + * Copyright(c) 2020 Marco Tröster + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#ifndef CHESSXFORMAT_H +#define CHESSXFORMAT_H + +#include +#include +#include +#include + +#include "chesstypes.h" +#include "chesspiece.h" +#include "chessboard.h" +#include "chessgamesession.h" + +/* TODO: move the board hashing code here */ + +int chess_session_from_fen(const char fen_str[], ChessGameSession* session); +int chess_session_to_fen(char* fen_str, const ChessGameSession* session); +int chess_session_from_pgn(const char pgn_str[], ChessGameSession* session); +int chess_session_to_pgn(char* pgn_str, const ChessGameSession* session); + +#endif \ No newline at end of file diff --git a/chesslib/src/chessboard.c b/chesslib/src/chessboard.c index 4e0495a..fc9536d 100644 --- a/chesslib/src/chessboard.c +++ b/chesslib/src/chessboard.c @@ -350,3 +350,67 @@ void uncompress_pieces_array(const uint8_t compr_bytes[], ChessPiece* out_pieces out_pieces[pos] = piece_bits; } } + +/* ==================================================== + C H E S S P O S I T I O N S + O N B I T B O A R D + ==================================================== */ + +Bitboard get_captured_fields(const Bitboard bitboards[], ChessColor side) +{ + uint8_t offset = SIDE_OFFSET(side); + + return bitboards[offset] | bitboards[offset + 1] | bitboards[offset + 2] + | bitboards[offset + 3] | bitboards[offset + 4] | bitboards[offset + 5]; +} + +void get_board_positions(Bitboard bitboard, ChessPosition* out_positions, size_t* out_length) +{ + uint8_t pos; + *out_length = 0; + + /* loop through all bits of the board */ + for (pos = 0; pos < 64; pos++) + { + if ((bitboard & 0x1uLL << pos)) { out_positions[(*out_length)++] = (ChessPosition)pos; } + } +} + +/************************************************************************************************** + this returns the index of the highest bit set on the given bitboard. + if the given bitboard has multiple bits set, only the position of the highest bit is returned. + info: the result is mathematically equal to floor(log2(x)) + **************************************************************************************************/ +ChessPosition get_board_position(Bitboard bitboard) +{ + /* code was taken from https://stackoverflow.com/questions/11376288/fast-computing-of-log2-for-64-bit-integers */ + +#ifdef __GNUC__ + /* use built-in leading zeros function for GCC Linux build + (this compiles to the very fast 'bsr' instruction on x86 AMD processors) */ + return (ChessPosition)((unsigned)(8 * sizeof(unsigned long long) - __builtin_clzll(bitboard) - 1)); +#else + /* use abstract DeBruijn algorithm with table lookup */ + /* TODO: think of implementing this as assembler code */ + + const uint8_t tab64[64] = { + 63, 0, 58, 1, 59, 47, 53, 2, + 60, 39, 48, 27, 54, 33, 42, 3, + 61, 51, 37, 40, 49, 18, 28, 20, + 55, 30, 34, 11, 43, 14, 22, 4, + 62, 57, 46, 52, 38, 26, 32, 41, + 50, 36, 17, 19, 29, 10, 13, 21, + 56, 45, 25, 31, 35, 16, 9, 12, + 44, 24, 15, 8, 23, 7, 6, 5 + }; + + bitboard |= bitboard >> 1; + bitboard |= bitboard >> 2; + bitboard |= bitboard >> 4; + bitboard |= bitboard >> 8; + bitboard |= bitboard >> 16; + bitboard |= bitboard >> 32; + + return (ChessPosition)tab64[((Bitboard)((bitboard - (bitboard >> 1)) * 0x07EDD5E59A4E28C2uLL)) >> 58]; +#endif +} diff --git a/chesslib/src/chessdrawgen.c b/chesslib/src/chessdrawgen.c index 064f2d2..450ca2d 100644 --- a/chesslib/src/chessdrawgen.c +++ b/chesslib/src/chessdrawgen.c @@ -490,67 +490,3 @@ Bitboard get_peasant_draw_positions(const Bitboard bitboards[], return draws; } - -/* ==================================================== - C H E S S P O S I T I O N S - O N B I T B O A R D - ==================================================== */ - -Bitboard get_captured_fields(const Bitboard bitboards[], ChessColor side) -{ - uint8_t offset = SIDE_OFFSET(side); - - return bitboards[offset] | bitboards[offset + 1] | bitboards[offset + 2] - | bitboards[offset + 3] | bitboards[offset + 4] | bitboards[offset + 5]; -} - -void get_board_positions(Bitboard bitboard, ChessPosition* out_positions, size_t* out_length) -{ - uint8_t pos; - *out_length = 0; - - /* loop through all bits of the board */ - for (pos = 0; pos < 64; pos++) - { - if ((bitboard & 0x1uLL << pos)) { out_positions[(*out_length)++] = (ChessPosition)pos; } - } -} - -/************************************************************************************************** - this returns the index of the highest bit set on the given bitboard. - if the given bitboard has multiple bits set, only the position of the highest bit is returned. - info: the result is mathematically equal to floor(log2(x)) - **************************************************************************************************/ -ChessPosition get_board_position(Bitboard bitboard) -{ - /* code was taken from https://stackoverflow.com/questions/11376288/fast-computing-of-log2-for-64-bit-integers */ - -#ifdef __GNUC__ - /* use built-in leading zeros function for GCC Linux build - (this compiles to the very fast 'bsr' instruction on x86 AMD processors) */ - return (ChessPosition)((unsigned)(8 * sizeof(unsigned long long) - __builtin_clzll(bitboard) - 1)); -#else - /* use abstract DeBruijn algorithm with table lookup */ - /* TODO: think of implementing this as assembler code */ - - const uint8_t tab64[64] = { - 63, 0, 58, 1, 59, 47, 53, 2, - 60, 39, 48, 27, 54, 33, 42, 3, - 61, 51, 37, 40, 49, 18, 28, 20, - 55, 30, 34, 11, 43, 14, 22, 4, - 62, 57, 46, 52, 38, 26, 32, 41, - 50, 36, 17, 19, 29, 10, 13, 21, - 56, 45, 25, 31, 35, 16, 9, 12, - 44, 24, 15, 8, 23, 7, 6, 5 - }; - - bitboard |= bitboard >> 1; - bitboard |= bitboard >> 2; - bitboard |= bitboard >> 4; - bitboard |= bitboard >> 8; - bitboard |= bitboard >> 16; - bitboard |= bitboard >> 32; - - return (ChessPosition)tab64[((Bitboard)((bitboard - (bitboard >> 1)) * 0x07EDD5E59A4E28C2uLL)) >> 58]; -#endif -} diff --git a/chesslib/src/chessgamesession.c b/chesslib/src/chessgamesession.c new file mode 100644 index 0000000..b144255 --- /dev/null +++ b/chesslib/src/chessgamesession.c @@ -0,0 +1,134 @@ +/* + * MIT License + * + * Copyright(c) 2020 Marco Tröster + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "chessgamesession.h" + +#define GC_DRAWING_SIDE(context) (ChessColor)((context) & 0x1uL) + +const ChessGameContext SIDE_MASK = 0x00000001uL; +const ChessGameContext EN_PASSANT_MASK = 0x000001FEuL; +const ChessGameContext ROCHADE_MASK = 0x00001E00uL; +const ChessGameContext HDSLPD_MASK = 0x0007E000uL; +const ChessGameContext GAME_ROUND_MASK = 0xFFF80000uL; + +ChessGameContext create_context(ChessColor side, uint8_t en_passants, + uint8_t rochades, uint8_t hdslpd, int game_round) +{ + /* combine the attributes to a single 32-bit integer */ + return ((ChessGameContext)side & 0x1uL) + | (((ChessGameContext)en_passants & 0xFFuL) << 1) + | (((ChessGameContext)rochades & 0xFuL) << 9) + | (((ChessGameContext)hdslpd & 0x3FuL) << 13) + | ((ChessGameContext)game_round << 19); +} + +Bitboard get_en_passant_mask(ChessGameContext context) +{ + /* extract the 8 en-passant bits and transform it into a bit mask */ + uint8_t shift = 8 * (GC_DRAWING_SIDE(context) == White ? 2 : 5); + return ((Bitboard)((context) >> 1) & ROW_1) << shift; +} + +Bitboard get_rochade_mask(ChessGameContext context) +{ + /* extract the 4 bits indicating possible rochades */ + Bitboard rochades = (Bitboard)((context) >> 9) & 0xFuLL; + + /* transform the rochade bits into a bitboard mask */ + return (rochades & 0x1uLL) /* rook on A1 */ + | (rochades & 0x2uLL) << 6 /* rook on H1 */ + | (rochades & 0x4uLL) << 54 /* rook on A8 */ + | (rochades & 0x8uLL) << 60; /* rook on H8 */ +} + +uint8_t get_hdslpd(ChessGameContext context) +{ + /* extract the 6 bits counting the halfdraws since the last pawn draw */ + return (uint8_t)((context & HDSLPD_MASK) >> 13); +} + +int get_game_rounds(ChessGameContext context) +{ + /* extract the 13 bits counting the game rounds */ + return (int)((context & GAME_ROUND_MASK) >> 19); +} + +void apply_draw_to_context(ChessDraw draw, ChessGameContext* context) +{ + ChessGameContext temp = *context; + + /* alternate the drawing side */ + *context ^= 0x1uL; + + /* increment the game round counter (if white is drawing) */ + if (GC_DRAWING_SIDE(*context) == White) { + *context = (((ChessGameContext)get_game_rounds(temp) + 1) << 19) + | (*context & ~GAME_ROUND_MASK); + } + + /* update the hdslpd counter (either increment or reset) */ + *context = *context & ~HDSLPD_MASK; /* reset bits */ + if (get_drawing_piece_type(draw) != Peasant) { + *context |= (((ChessGameContext)get_hdslpd(temp) + 1) << 13); + } + + /* handle en-passant (if a peasant moved double-forward) */ + *context = *context & ~EN_PASSANT_MASK; /* reset bits */ + if (get_drawing_piece_type(draw) == Peasant + && abs(get_new_position(draw) - get_old_position(draw)) == 16 + && ((get_drawing_side(draw) ? ROW_5 : ROW_4) & get_new_position(draw))) + { + /* determine the column of the possible en-passant and set the bit */ + *context |= ((ChessGameContext)0x1uL << (get_new_position(draw) % 8 + 1)); + } + + /* handle rochade (if a king or a rook moved) */ + *context = *context & ~ROCHADE_MASK; /* reset bits */ + if (get_drawing_piece_type(draw) == King + && (get_old_position(draw) & (FIELD_E1 | FIELD_E8))) + { + /* disable the rochade bits of the given side accordingly */ + *context |= (get_drawing_side(draw) ? 0x1800uL : 0x600uL); + } + if (get_drawing_piece_type(draw) == Rook + && (get_old_position(draw) & (FIELD_A1 | FIELD_H1 | FIELD_A8 | FIELD_H8))) + { + /* disable the rochade bit of a single rook accordingly */ + *context |= (ChessGameContext)( + ((get_old_position(draw) & FIELD_A1) << 9) /* shift to 10th bit */ + | ((get_old_position(draw) & FIELD_H1) << 3) /* shift to 11th bit */ + | ((get_old_position(draw) & FIELD_A8) >> 54) /* shift to 12th bit */ + | ((get_old_position(draw) & FIELD_H8) >> 60)); /* shift to 13th bit */ + /* TODO: make sure the bit shifts are correct */ + } +} + +void apply_game_context_to_board(Bitboard* board, ChessGameContext context) +{ + Bitboard rochade_mask = 0; + + /* set was_moved bits for rooks */ + rochade_mask = get_rochade_mask(context); + board[12] &= ~rochade_mask; /* TODO: check if this works */ +} diff --git a/chesslib/src/chesslibmodule.c b/chesslib/src/chesslibmodule.c index 88c5b35..ce0b904 100644 --- a/chesslib/src/chesslibmodule.c +++ b/chesslib/src/chesslibmodule.c @@ -34,11 +34,18 @@ static PyObject* chesslib_create_chesspieceatpos(PyObject* self, PyObject* args, static PyObject* chesslib_create_chessboard(PyObject* self, PyObject* args, PyObject *keywds); static PyObject* chesslib_create_startformation(PyObject* self, PyObject* args, PyObject *keywds); static PyObject* chesslib_create_chessdraw(PyObject* self, PyObject* args, PyObject *keywds); + static PyObject* chesslib_get_all_draws(PyObject* self, PyObject* args, PyObject *keywds); -static PyObject* chesslib_board_to_hash(PyObject* self, PyObject* args, PyObject *keywds); -static PyObject* chesslib_board_from_hash(PyObject* self, PyObject* args, PyObject *keywds); static PyObject* chesslib_apply_draw(PyObject* self, PyObject* args, PyObject *keywds); static PyObject* chesslib_get_game_state(PyObject* self, PyObject* args, PyObject *keywds); + +static PyObject* chesslib_board_to_hash(PyObject* self, PyObject* args, PyObject *keywds); +static PyObject* chesslib_board_from_hash(PyObject* self, PyObject* args, PyObject *keywds); +static PyObject* chesslib_gamesession_from_fen(PyObject* self, PyObject* args, PyObject *keywds); +static PyObject* chesslib_board_to_fen(PyObject* self, PyObject* args, PyObject *keywds); +static PyObject* chesslib_draw_from_pgn(PyObject* self, PyObject* args, PyObject *keywds); +static PyObject* chesslib_draw_to_pgn(PyObject* self, PyObject* args, PyObject *keywds); + static PyObject* chesslib_visualize_board(PyObject* self, PyObject* args, PyObject *keywds); static PyObject* chesslib_visualize_draw(PyObject* self, PyObject* args, PyObject *keywds); @@ -223,7 +230,20 @@ Args:\n\ is_simple_board: Indicates whether the resulting chess board should be of the simple board format, defaults to False\n\ \n\ Returns:\n\ - the game state related to the given game situation, encoded as integer/ASCII byte"; + the chess board represented by the given hash, as numpy array"; + +/* if (!PyArg_ParseTuple(args, "s|i", &fen_str, &is_simple_board)) { return NULL; } */ +const char Board_FromFen_Docstring[] = +"Board_FromFen(fen_str: str, is_simple_board: bool=False) -> int\n\ +\n\ +Convert the given FEN string to a chess board.\n\ +\n\ +Args:\n\ + fen_str: The FEN string representation to be imported\n\ + is_simple_board: Indicates whether the resulting chess board should be of the simple board format, defaults to False\n\ +\n\ +Returns:\n\ + the chess board represented by the given FEN string, as numpy array"; /* if (!PyArg_ParseTuple(args, "O|i", &bitboards, &is_simple_board)) { return NULL; } */ const char VisualizeBoard_Docstring[] = @@ -264,25 +284,26 @@ Returns:\n\ static PyMethodDef chesslib_methods[] = { /* data types and structures */ - {"ChessPosition", chesslib_create_chessposition, METH_VARARGS | METH_KEYWORDS, ChessPosition_Docstring}, - {"ChessPiece", chesslib_create_chesspiece, METH_VARARGS | METH_KEYWORDS, ChessPiece_Docstring}, - {"ChessPieceAtPos", chesslib_create_chesspieceatpos, METH_VARARGS | METH_KEYWORDS, ChessPieceAtPos_Docstring}, - {"ChessBoard", chesslib_create_chessboard, METH_VARARGS | METH_KEYWORDS, ChessBoard_Docstring}, - {"ChessBoard_StartFormation", chesslib_create_startformation, METH_VARARGS | METH_KEYWORDS, ChessBoard_StartFormation_Docstring}, - {"ChessDraw", chesslib_create_chessdraw, METH_VARARGS | METH_KEYWORDS, ChessDraw_Docstring}, + {"ChessPosition", (PyCFunction)chesslib_create_chessposition, METH_VARARGS | METH_KEYWORDS, ChessPosition_Docstring}, + {"ChessPiece", (PyCFunction)chesslib_create_chesspiece, METH_VARARGS | METH_KEYWORDS, ChessPiece_Docstring}, + {"ChessPieceAtPos", (PyCFunction)chesslib_create_chesspieceatpos, METH_VARARGS | METH_KEYWORDS, ChessPieceAtPos_Docstring}, + {"ChessBoard", (PyCFunction)chesslib_create_chessboard, METH_VARARGS | METH_KEYWORDS, ChessBoard_Docstring}, + {"ChessBoard_StartFormation", (PyCFunction)chesslib_create_startformation, METH_VARARGS | METH_KEYWORDS, ChessBoard_StartFormation_Docstring}, + {"ChessDraw", (PyCFunction)chesslib_create_chessdraw, METH_VARARGS | METH_KEYWORDS, ChessDraw_Docstring}, /* core chess logic for gameplay */ - {"GenerateDraws", chesslib_get_all_draws, METH_VARARGS | METH_KEYWORDS, GenerateDraws_Docstring}, - {"ApplyDraw", chesslib_apply_draw, METH_VARARGS | METH_KEYWORDS, ApplyDraw_Docstring}, - {"GameState", chesslib_get_game_state, METH_VARARGS | METH_KEYWORDS, GameState_Docstring}, + {"GenerateDraws", (PyCFunction)chesslib_get_all_draws, METH_VARARGS | METH_KEYWORDS, GenerateDraws_Docstring}, + {"ApplyDraw", (PyCFunction)chesslib_apply_draw, METH_VARARGS | METH_KEYWORDS, ApplyDraw_Docstring}, + {"GameState", (PyCFunction)chesslib_get_game_state, METH_VARARGS | METH_KEYWORDS, GameState_Docstring}, /* extensions for data compression */ - {"Board_ToHash", chesslib_board_to_hash, METH_VARARGS | METH_KEYWORDS, Board_ToHash_Docstring}, - {"Board_FromHash", chesslib_board_from_hash, METH_VARARGS | METH_KEYWORDS, Board_FromHash_Docstring}, + {"Board_ToHash", (PyCFunction)chesslib_board_to_hash, METH_VARARGS | METH_KEYWORDS, Board_ToHash_Docstring}, + {"Board_FromHash", (PyCFunction)chesslib_board_from_hash, METH_VARARGS | METH_KEYWORDS, Board_FromHash_Docstring}, + {"Board_FromFen", (PyCFunction)chesslib_gamesession_from_fen, METH_VARARGS | METH_KEYWORDS, Board_FromFen_Docstring}, /* extensions for data visualization of complex type encodings */ - {"VisualizeBoard", chesslib_visualize_board, METH_VARARGS | METH_KEYWORDS, VisualizeBoard_Docstring}, - {"VisualizeDraw", chesslib_visualize_draw, METH_VARARGS | METH_KEYWORDS, VisualizeDraw_Docstring}, + {"VisualizeBoard", (PyCFunction)chesslib_visualize_board, METH_VARARGS | METH_KEYWORDS, VisualizeBoard_Docstring}, + {"VisualizeDraw", (PyCFunction)chesslib_visualize_draw, METH_VARARGS | METH_KEYWORDS, VisualizeDraw_Docstring}, /* TODO: add functions for visualizing remaining data structures like chess piece, chess pos, piece@pos */ /* TODO: add functions for converting from/to portable formats like FEN / PGN, etc. */ @@ -352,25 +373,17 @@ PyMODINIT_FUNC PyInit_chesslib(void) **************************************************************************/ static PyObject* chesslib_create_chessposition(PyObject* self, PyObject* args, PyObject *keywds) { - const char* pos_as_str; - uint8_t row = 0, column = 0; + const char* pos_as_str; ChessPosition pos = 0; static char *kwlist[] = {"pos_as_str", NULL}; /* read position string, quit if the parameter does not exist */ if (!PyArg_ParseTupleAndKeywords(args, keywds, "s", kwlist, &pos_as_str)) { return NULL; } - /* make sure that the overloaded string is of the correct format */ - if (*(pos_as_str + 2) != '\0' - || (!isalpha(pos_as_str[0]) || toupper(pos_as_str[0]) - 'A' >= 8 - || toupper(pos_as_str[0]) - 'A' < 0) - || (!isdigit(pos_as_str[1]) || pos_as_str[1] - '1' >= 8)) { return NULL; } - - /* parse position from position string */ - row = pos_as_str[1] - '1'; - column = toupper(pos_as_str[0]) - 'A'; + /* parse the position index from string */ + if (!position_from_string(pos_as_str, &pos)) { return NULL; } /* create uint32 python object and return it */ - return PyLong_FromUnsignedLong(create_position(row, column)); + return PyLong_FromUnsignedLong(pos); } /************************************************************************** @@ -433,21 +446,7 @@ static PyObject* chesslib_create_startformation(PyObject* self, PyObject* args, static char* kwlist[] = {"is_simple", NULL}; /* create the chess board */ - const Bitboard start_formation[] = { - 0x0000000000000010uLL, /* white king */ - 0x0000000000000008uLL, /* white queen(s) */ - 0x0000000000000081uLL, /* white rooks */ - 0x0000000000000024uLL, /* white bishops */ - 0x0000000000000042uLL, /* white knights */ - 0x000000000000FF00uLL, /* white pawns */ - 0x1000000000000000uLL, /* black king */ - 0x0800000000000000uLL, /* black queen(s) */ - 0x8100000000000000uLL, /* black rooks */ - 0x2400000000000000uLL, /* black bishops */ - 0x4200000000000000uLL, /* black knights */ - 0x00FF000000000000uLL, /* black pawns */ - 0x0000FFFFFFFF0000uLL /* was_moved mask */ - }; + const Bitboard start_formation[] = START_FORMATION; /* parse all args */ if (!PyArg_ParseTupleAndKeywords(args, keywds, "|i", kwlist, &is_simple_board)) { return NULL; } @@ -720,10 +719,64 @@ static PyObject* chesslib_board_from_hash(PyObject* self, PyObject* args, PyObje /* signal python that the PyObject is no longer used by this function */ Py_DecRef(hash_orig); - + return chessboard; } +/* ================================================= + E X C H A N G E F O R M A T S + ================================================= */ + +static PyObject* chesslib_gamesession_from_fen(PyObject* self, PyObject* args, PyObject *keywds) +{ + /* start formation in FEN: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' */ + + PyObject* chessboard, * tuple; + char* fen_str; int is_simple_board = 0; + ChessGameSession session = INIT_GAME_SESSION; ChessPiece simple_board[64]; + static char* kwlist[] = {"fen_str", "is_simple", NULL}; + + /* parse bitboards as ChessBoard struct */ + if (!PyArg_ParseTupleAndKeywords(args, keywds, "s|i", kwlist, + &fen_str, &is_simple_board)) { return NULL; } + + /* convert the FEN string to a game session */ + if (!chess_session_from_fen(fen_str, &session)) { return NULL; } + + /* convert the session's bitboard to a simple board if required */ + if (is_simple_board) { to_simple_board(session.board, simple_board); } + + /* return as simple board or bitboards format */ + chessboard = is_simple_board + ? serialize_as_pieces(simple_board) + : serialize_as_bitboards(session.board); + + /* zip the chess board and game context as a PyTuple */ + tuple = PyTuple_New(2); + PyTuple_SetItem(tuple, 0, chessboard); + PyTuple_SetItem(tuple, 1, PyLong_FromUnsignedLong(session.context)); + + return tuple; +} + +static PyObject* chesslib_board_to_fen(PyObject* self, PyObject* args, PyObject *keywds) +{ + /* TODO: implement logic here ... */ + return NULL; +} + +static PyObject* chesslib_draw_from_pgn(PyObject* self, PyObject* args, PyObject *keywds) +{ + /* TODO: implement logic here ... */ + return NULL; +} + +static PyObject* chesslib_draw_to_pgn(PyObject* self, PyObject* args, PyObject *keywds) +{ + /* TODO: implement logic here ... */ + return NULL; +} + /* ================================================= V I S U A L I Z E ================================================= */ diff --git a/chesslib/src/chesspiece.c b/chesslib/src/chesspiece.c index e9d63be..d571ec1 100644 --- a/chesslib/src/chesspiece.c +++ b/chesslib/src/chesspiece.c @@ -55,7 +55,7 @@ ChessColor color_from_char(char c) { case 'W': return White; case 'B': return Black; - default: return White; + default: return White; /* TODO: think of throwing an error instead */ } } diff --git a/chesslib/src/chessposition.c b/chesslib/src/chessposition.c index ca68978..d08d239 100644 --- a/chesslib/src/chessposition.c +++ b/chesslib/src/chessposition.c @@ -39,6 +39,31 @@ int8_t get_column(ChessPosition position) return (position & 7); } +int position_from_string(const char* pos_str, ChessPosition* pos) +{ + uint8_t row, column; + + /* make sure the first character is within [a-h] or [A-H] */ + if (!isalpha(pos_str[0]) + || toupper(pos_str[0]) - 'A' >= 8 + || toupper(pos_str[0]) - 'A' < 0) + { return 0; } + + /* make sure the second character is within [1-8] */ + if (!isdigit(pos_str[1]) || pos_str[1] - '1' >= 8) { return 0; } + + /* make sure the third character is a zero-terminal */ + if (pos_str[2] != '\0') { return 0; } + + /* finally, do the actual parsing */ + row = pos_str[1] - '1'; + column = toupper(pos_str[0]) - 'A'; + *pos = create_position(row, column); + + /* parsing successful! */ + return 1; +} + void position_to_string(ChessPosition position, char* pos_str) { /* serialize position as string*/ diff --git a/chesslib/src/chessxformat.c b/chesslib/src/chessxformat.c new file mode 100644 index 0000000..6d5bbc5 --- /dev/null +++ b/chesslib/src/chessxformat.c @@ -0,0 +1,301 @@ +/* + * MIT License + * + * Copyright(c) 2020 Marco Tröster + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +#include "chessxformat.h" + +/* start formation in FEN: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' */ + +#define DECIMAL_LEN(num) ((int)((ceil(log10((num))) + 1) * sizeof(char))) + +/* Parse a positive integer value from the given decimal numeric string + and return the length of the characters parsed (return 0 on format error). */ +int parse_uint(const char* value_str, int* out_value, char term) +{ + int value = 0; size_t i = 0; + + /* make sure there are no heading zeros for non-zero values */ + if (value_str[i] == '0' && value_str[++i] != term) { return 0; } + + /* parse the uint value from decimal notation */ + while (isdigit(value_str[i]) && value_str[i] != term && value_str[i] != '\0') + { value = value * 10 + (value_str[i++] - '0'); } + + *out_value = value; + return value_str[i] == term && i > 0 ? i : 0; +} + +/* Look up the next appearance of the given character in the given zero-terminated string. + Return 0 if the string does not contain the character searched. */ +int str_index_of(char* search_str, char find) +{ + size_t i = 0; char temp; + while ((temp = search_str[i++]) != find && temp != '\0') { } + return temp == find ? i - 1 : 0; +} + +int parse_first_fen_section(const char fen_str[], Bitboard* board) +{ + size_t i = 0, sep_count = 0; char temp; int is_terminal = 0; + ChessPosition pos = 56; ChessColor color; ChessPieceType type; int was_moved; + ChessPiece simple_board[64] = { 0 }; + + /* parse the first FEN section (positions of pieces on the board) */ + do + { + /* access the next FEN character to be parsed */ + temp = fen_str[i++]; + + switch (temp) + { + /* handle termination character */ + case '\0': if (!is_terminal) { return 0; } break; + + /* ensure that the row bounds are not violated */ + case '/': if (pos != (8 - (sep_count++)) * 8) { return 0; } pos -= 16; break; + + /* handle empty fields declaration */ + case '1': case '2': case '3': case '4': + case '5': case '6': case '7': case '8': + pos += (ChessPosition)(temp - '0'); break; + + /* put another piece on the chess board */ + case 'K': case 'Q': case 'R': case 'B': case 'N': case 'P': + case 'k': case 'q': case 'r': case 'b': case 'n': case 'p': + color = (ChessColor)(isupper(temp) ? White : Black); + type = piece_type_from_char(temp); + was_moved = (START_POSITIONS & (1uLL << pos)) ? 0 : 1; + simple_board[pos++] = create_piece(type, color, was_moved); + break; + + /* handle invalid / unexpected characters */ + default: return 0; + } + + /* check if the next state is supposed to be terminal */ + is_terminal = (pos == 8 && sep_count == 7); + + /* loop until the end of the FEN string's first section */ + } while (temp != '\0'); + + /* convert the simple board to a bitboard representation */ + from_simple_board(simple_board, board); + + return 1; +} + +int parse_second_fen_section(const char fen_str[], ChessColor* side) +{ + ChessColor color = color_from_char(fen_str[0]); + if (fen_str[1] != '\0') { return 0; } + *side = color; + return 1; +} + +int parse_third_fen_section(const char fen_str[], uint8_t* rochades) +{ + size_t i = 0; uint8_t poss_rochades = 0x0; + + /* handle case with no castlings */ + if (fen_str[i] == '-' && fen_str[i+1] == '\0') { /* use default value */ } + + /* handle case with rochades */ + else + { + /* enable castlings (ensure the correct order) */ + if (fen_str[i] == 'K') { poss_rochades ^= 0x2; i++; } + if (fen_str[i] == 'Q') { poss_rochades ^= 0x1; i++; } + if (fen_str[i] == 'k') { poss_rochades ^= 0x8; i++; } + if (fen_str[i] == 'q') { poss_rochades ^= 0x4; i++; } + + /* ensure any rochade is possible and the terminal symbol is hit */ + if (!poss_rochades || fen_str[i] != '\0') { return 0; } + } + + /* apply the parsed rochades to the game context */ + *rochades = poss_rochades; + + return 1; +} + +int parse_fourth_fen_section(const char fen_str[], uint8_t* en_passants) +{ + size_t i = 0; uint8_t poss_en_passants = 0x00; ChessPosition pos = 0; + + /* handle case with no en-passant */ + if (fen_str[i] == '-' && fen_str[i+1] == '\0') { /* use default value */ } + + /* handle case with rochades */ + else + { + /* parse the en-passant position from string */ + if (!position_from_string(fen_str, &pos)) { return 0; } + + /* set the en-passant bit accordingly */ + poss_en_passants = 0x1 << (pos % 8); + } + + /* apply the parsed en-passant to the game context */ + *en_passants = poss_en_passants; + + return 1; +} + +int chess_session_from_fen(const char fen_str[], ChessGameSession* session) +{ + size_t end = 0; size_t len; int game_round; + ChessColor side; uint8_t rochades, en_passants, hdslpd; + char cache[100]; char* temp_str = cache; Bitboard board[13] = { 0 }; + /* TODO: figure out the exact cap for the cache memory */ + + /* create a carbon copy of the fen string (that can be safely modified) */ + strcpy(temp_str, fen_str); + + /* parse the first FEN section (positions of pieces on the board) */ + if (!(end = str_index_of(temp_str, ' '))) { return 0; } + temp_str[end] = '\0'; + if (!parse_first_fen_section(temp_str, board)) { return 0; } + temp_str += end + 1; + + /* parse the second FEN section (drawing side) */ + if (!(end = str_index_of(temp_str, ' '))) { return 0; } + temp_str[end] = '\0'; + if (!parse_second_fen_section(temp_str, &side)) { return 0; } + temp_str += end + 1; + + /* parse the third FEN section (possible castlings) */ + if (!(end = str_index_of(temp_str, ' '))) { return 0; } + temp_str[end] = '\0'; + if (!parse_third_fen_section(temp_str, &rochades)) { return 0; } + temp_str += end + 1; + + /* parse the fourth FEN section (en-passant) */ + if (!(end = str_index_of(temp_str, ' '))) { return 0; } + temp_str[end] = '\0'; + if (!parse_fourth_fen_section(temp_str, &en_passants)) { return 0; } + temp_str += end + 1; + + /* parse the fifth FEN section (halfdraws since last pawn draw) */ + len = parse_uint(temp_str, (int*)&hdslpd, ' '); + if (len > 0) { temp_str += len + 1; } else { return 0; } + + /* parse the sixth FEN section (game round) */ + len = parse_uint(temp_str, &game_round, '\0'); + if (len <= 0) { return 0; } + + /* assign the parsed FEN string content to the game session object */ + copy_board(board, session->board); + session->context = create_context(side, + en_passants, rochades, hdslpd, game_round); + apply_game_context_to_board(session->board, session->context); + + return 1; +} + +int chess_session_to_fen(char* fen_str, const ChessGameSession* session) +{ + size_t i = 0, empty_cnt = 0; ChessPosition pos; ChessPiece piece; char temp; + Bitboard rochades_mask = 0, en_passant_mask = 0; int i_temp; + ChessPiece simple_board[64] = { 0 }; + + /* write the first section to the FEN string (pieces on board) */ + + /* convert the session's board to the simple board format */ + to_simple_board(session->board, simple_board); + + /* loop through each field on the chess board */ + for (pos = 0; pos < 64; pos++) + { + piece = simple_board[pos]; + + /* handle empty field spaces symbol */ + if ((piece != CHESS_PIECE_NULL || (pos + 1) % 8 == 0) && empty_cnt > 0) + { fen_str[i++] = '0' + empty_cnt; empty_cnt = 0; } + + /* handle piece symbol */ + if (piece != CHESS_PIECE_NULL) + { + /* write the piece type (uppercase -> white, lowercase -> black) */ + temp = piece_type_to_char(get_piece_type(piece)); + fen_str[i++] = get_piece_color(piece) ? tolower(temp) : toupper(temp); + } + /* handle empty field -> increment counter */ + else { empty_cnt++; } + + /* handle end-of-row separator */ + if ((pos + 1) % 8 == 0 && pos < 63) { fen_str[i++] = '/'; } + } + + /* write the second section to the FEN string (drawing side) */ + fen_str[i++] = ' '; + fen_str[i++] = tolower(color_to_char((ChessColor)(session->context & 1uL))); + + /* write the third section to the FEN string (rochades) */ + fen_str[i++] = ' '; + rochades_mask = get_rochade_mask(session->context); + if (rochades_mask & FIELD_H1) { fen_str[i++] = 'K'; } + if (rochades_mask & FIELD_A1) { fen_str[i++] = 'Q'; } + if (rochades_mask & FIELD_H8) { fen_str[i++] = 'k'; } + if (rochades_mask & FIELD_A8) { fen_str[i++] = 'q'; } + if (!rochades_mask) { fen_str[i++] = '-'; } + + /* write the fourth section to the FEN string (en-passant) */ + fen_str[i++] = ' '; + en_passant_mask = get_en_passant_mask(session->context); + if (!en_passant_mask) { fen_str[i++] = '-'; } + else + { + /* get the set bit's position as index and convert it to a string */ + pos = get_board_position(en_passant_mask); + position_to_string(pos, (fen_str + i)); + i += 2; + } + + /* write the fifth section to the FEN string (hdslpd) */ + fen_str[i++] = ' '; + i_temp = (int)get_hdslpd(session->context); + sprintf((fen_str + i), "%d", i_temp); + i += DECIMAL_LEN(i_temp); + + /* write the sixth section to the FEN string (game round) */ + fen_str[i++] = ' '; + i_temp = (int)get_hdslpd(session->context); + sprintf((fen_str + i), "%d", i_temp); + i += DECIMAL_LEN(i_temp); + + /* write the zero-terminal char and return success! */ + fen_str[i] = '\0'; + return 1; +} + +int chess_session_from_pgn(const char pgn_str[], ChessGameSession* session) +{ + /* TODO: implement logic */ + return 0; +} + +int chess_session_to_pgn(char* pgn_str, const ChessGameSession* session) +{ + /* TODO: implement logic */ + return 0; +} diff --git a/setup.py b/setup.py index 16305d8..0be0460 100644 --- a/setup.py +++ b/setup.py @@ -98,7 +98,9 @@ def main(): "chesslib/src/chessdraw.c", "chesslib/src/chessdrawgen.c", "chesslib/src/chesspieceatpos.c", - "chesslib/src/chessgamestate.c" + "chesslib/src/chessgamestate.c", + "chesslib/src/chessxformat.c", + "chesslib/src/chessgamesession.c", ] # define extension module settings (for cross-plattform builds) diff --git a/tests/test.py b/tests/test.py index d07e01d..6d0d887 100644 --- a/tests/test.py +++ b/tests/test.py @@ -46,6 +46,7 @@ def test_module(): test_apply_draw() test_game_state() test_board_hash() + test_board_from_fen() # # test visualization functions test_visualize_board() @@ -189,7 +190,7 @@ def test_create_chessdraw(): # board = chesslib.ChessBoard_StartFormation(True) gen_draw = chesslib.ChessDraw(board, chesslib.ChessPosition('E2'), chesslib.ChessPosition('E4'), is_compact_draw=True, is_simple=True) assert_equal(gen_draw, exp_compact_draw) - print(gen_draw, exp_compact_draw) + # print(gen_draw, exp_compact_draw) assert_equal(sys.getrefcount(board), 2) # TODO: add a peasant prom. test case @@ -660,5 +661,49 @@ def test_visualize_draw(): # TODO: add more tests for edge cases (rochade, en-passant, promotion) +def test_board_from_fen(): + + # TODO: make this test work + + print("testing FEN to chess board") + + # FEN string representing the initial game state + fen_str = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1' + board_exp = chesslib.ChessBoard_StartFormation() + + # parse the FEN string and make sure it's correct + board, context = chesslib.Board_FromFen(fen_str) + assert_true(np.array_equal(board, board_exp)) + assert_equal(531968, context) + + # FEN string representing the game state after first draw 'white pawn F2-F4' + fen_str = 'rnbqkbnr/pppppppp/8/8/5P2/8/PPPPP1PP/RNBQKBNR b KQkq f3 10 12' + board_exp = chesslib.ApplyDraw(board, chesslib.ChessDraw( + board, chesslib.ChessPosition('F2'), chesslib.ChessPosition('F4'))) + + # parse the FEN string and make sure it's correct + board, context = chesslib.Board_FromFen(fen_str) + assert_true(np.array_equal(board, board_exp)) + assert_equal(6381121, context) + + # FEN string representing the game state after first draw 'white pawn F2-F4' + fen_str = '5k2/8/8/8/8/8/8/3K4 b - - 41 71' + board_exp = np.array([ + 1 << chesslib.ChessPosition('D1'), + 0, 0, 0, 0, 0, + 1 << chesslib.ChessPosition('F8'), + 0, 0, 0, 0, 0, + 0xFFFF00000000FFFF # info: was_moved mask cannot be 100% accurately restored + # -> draw-gen needs to be the same + ], dtype=np.uint64) + + # parse the FEN string and make sure it's correct + board, context = chesslib.Board_FromFen(fen_str) + assert_true(np.array_equal(board[:12], board_exp[:12])) + assert_equal(37560321, context) + + print("test passed!") + + if __name__ == '__main__': test_module()