From 9f05ab03c172b2f870b9405b8f35786eacd45cab Mon Sep 17 00:00:00 2001 From: Job Noorman Date: Thu, 27 Oct 2022 12:29:19 +0200 Subject: [PATCH] Add assignment --- .gitignore | 4 + .gitlab-ci.yml | 44 ++ .gitmodules | 4 + Board.cpp | 62 ++ Board.hpp | 38 ++ CMakeLists.txt | 46 ++ CastlingRights.cpp | 53 ++ CastlingRights.hpp | 25 + Engine.cpp | 7 + Engine.hpp | 37 ++ EngineFactory.cpp | 5 + EngineFactory.hpp | 14 + Fen.cpp | 168 +++++ Fen.hpp | 16 + Main.cpp | 34 + Move.cpp | 46 ++ Move.hpp | 33 + Piece.cpp | 38 ++ Piece.hpp | 40 ++ PrincipalVariation.cpp | 29 + PrincipalVariation.hpp | 25 + README.md | 844 ++++++++++++++++++++++++ Square.cpp | 126 ++++ Square.hpp | 43 ++ Tests/BoardTests.cpp | 945 +++++++++++++++++++++++++++ Tests/CMakeLists.txt | 16 + Tests/Catch2 | 1 + Tests/EngineTests.cpp | 48 ++ Tests/FenTests.cpp | 117 ++++ Tests/Main.cpp | 2 + Tests/MoveTests.cpp | 111 ++++ Tests/PieceTests.cpp | 74 +++ Tests/Puzzles/crushing_castling.csv | 20 + Tests/Puzzles/crushing_enPassant.csv | 20 + Tests/Puzzles/crushing_simple.csv | 18 + Tests/Puzzles/mateIn1_castling.csv | 20 + Tests/Puzzles/mateIn1_enPassant.csv | 20 + Tests/Puzzles/mateIn1_simple.csv | 20 + Tests/Puzzles/mateIn2_castling.csv | 20 + Tests/Puzzles/mateIn2_enPassant.csv | 20 + Tests/Puzzles/mateIn2_simple.csv | 20 + Tests/SquareTests.cpp | 80 +++ Tests/TestUtils.hpp | 26 + Tests/puzzledb.py | 151 +++++ Tests/puzzlerating.py | 89 +++ Tests/puzzlerunner.py | 260 ++++++++ TimeInfo.hpp | 20 + Uci.cpp | 389 +++++++++++ Uci.hpp | 51 ++ 49 files changed, 4339 insertions(+) create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .gitmodules create mode 100644 Board.cpp create mode 100644 Board.hpp create mode 100644 CMakeLists.txt create mode 100644 CastlingRights.cpp create mode 100644 CastlingRights.hpp create mode 100644 Engine.cpp create mode 100644 Engine.hpp create mode 100644 EngineFactory.cpp create mode 100644 EngineFactory.hpp create mode 100644 Fen.cpp create mode 100644 Fen.hpp create mode 100644 Main.cpp create mode 100644 Move.cpp create mode 100644 Move.hpp create mode 100644 Piece.cpp create mode 100644 Piece.hpp create mode 100644 PrincipalVariation.cpp create mode 100644 PrincipalVariation.hpp create mode 100644 README.md create mode 100644 Square.cpp create mode 100644 Square.hpp create mode 100644 Tests/BoardTests.cpp create mode 100644 Tests/CMakeLists.txt create mode 160000 Tests/Catch2 create mode 100644 Tests/EngineTests.cpp create mode 100644 Tests/FenTests.cpp create mode 100644 Tests/Main.cpp create mode 100644 Tests/MoveTests.cpp create mode 100644 Tests/PieceTests.cpp create mode 100644 Tests/Puzzles/crushing_castling.csv create mode 100644 Tests/Puzzles/crushing_enPassant.csv create mode 100644 Tests/Puzzles/crushing_simple.csv create mode 100644 Tests/Puzzles/mateIn1_castling.csv create mode 100644 Tests/Puzzles/mateIn1_enPassant.csv create mode 100644 Tests/Puzzles/mateIn1_simple.csv create mode 100644 Tests/Puzzles/mateIn2_castling.csv create mode 100644 Tests/Puzzles/mateIn2_enPassant.csv create mode 100644 Tests/Puzzles/mateIn2_simple.csv create mode 100644 Tests/SquareTests.cpp create mode 100644 Tests/TestUtils.hpp create mode 100755 Tests/puzzledb.py create mode 100755 Tests/puzzlerating.py create mode 100755 Tests/puzzlerunner.py create mode 100644 TimeInfo.hpp create mode 100644 Uci.cpp create mode 100644 Uci.hpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1ad6f29 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/Build*/ +/build*/ +__pycache__/ +uci-log.txt diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..f97abb1 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,44 @@ +image: ubuntu:22.04 + +.prepare-apt: &prepare-apt + - apt-get update -yqq + # Prevent interactive prompt when installing tzdata + - DEBIAN_FRONTEND=noninteractive apt-get install tzdata -yqq + +build: + stage: build + before_script: + - *prepare-apt + # Install build dependencies + - apt-get install build-essential cmake git -yqq + # Update all submodules (Catch2) + - git submodule update --init + script: + # Configure CMake + - cmake -S . -B Build -DCMAKE_BUILD_TYPE=RelWithDebInfo + # Build project + - cmake --build Build/ + artifacts: + paths: + - Build/cplchess + - Build/Tests/tests + +unit_tests: + stage: test + script: + - ./Build/Tests/tests -r junit -o junit.xml + artifacts: + reports: + junit: junit.xml + +puzzle_tests: + stage: test + before_script: + - *prepare-apt + - apt-get install python3 python3-pip -yqq + - pip3 install chess junit-xml + script: + - ./Tests/puzzlerunner.py --engine ./Build/cplchess --junit junit.xml --timeout 180 ./Tests/Puzzles/* + artifacts: + reports: + junit: junit.xml diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..a4b027c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,4 @@ +[submodule "Tests/Catch2"] + path = Tests/Catch2 + url = https://github.com/catchorg/Catch2.git + branch = v2.x diff --git a/Board.cpp b/Board.cpp new file mode 100644 index 0000000..2ba3693 --- /dev/null +++ b/Board.cpp @@ -0,0 +1,62 @@ +#include "Board.hpp" + +#include +#include +#include + +Board::Board() +{ +} + +void Board::setPiece(const Square& square, const Piece::Optional& piece) { + (void)square; + (void)piece; +} + +Piece::Optional Board::piece(const Square& square) const { + (void)square; + return std::nullopt; +} + +void Board::setTurn(PieceColor turn) { + (void)turn; +} + +PieceColor Board::turn() const { + return PieceColor::White; +} + +void Board::setCastlingRights(CastlingRights cr) { + (void)cr; +} + +CastlingRights Board::castlingRights() const { + return CastlingRights::None; +} + +void Board::setEnPassantSquare(const Square::Optional& square) { + (void)square; +} + +Square::Optional Board::enPassantSquare() const { + return std::nullopt; +} + +void Board::makeMove(const Move& move) { + (void)move; +} + +void Board::pseudoLegalMoves(MoveVec& moves) const { + (void)moves; +} + +void Board::pseudoLegalMovesFrom(const Square& from, + Board::MoveVec& moves) const { + (void)from; + (void)moves; +} + +std::ostream& operator<<(std::ostream& os, const Board& board) { + (void)board; + return os; +} diff --git a/Board.hpp b/Board.hpp new file mode 100644 index 0000000..c77d449 --- /dev/null +++ b/Board.hpp @@ -0,0 +1,38 @@ +#ifndef CHESS_ENGINE_BOARD_HPP +#define CHESS_ENGINE_BOARD_HPP + +#include "Piece.hpp" +#include "Square.hpp" +#include "Move.hpp" +#include "CastlingRights.hpp" + +#include +#include +#include + +class Board { +public: + + using Optional = std::optional; + using MoveVec = std::vector; + + Board(); + + void setPiece(const Square& square, const Piece::Optional& piece); + Piece::Optional piece(const Square& square) const; + void setTurn(PieceColor turn); + PieceColor turn() const; + void setCastlingRights(CastlingRights cr); + CastlingRights castlingRights() const; + void setEnPassantSquare(const Square::Optional& square); + Square::Optional enPassantSquare() const; + + void makeMove(const Move& move); + + void pseudoLegalMoves(MoveVec& moves) const; + void pseudoLegalMovesFrom(const Square& from, MoveVec& moves) const; +}; + +std::ostream& operator<<(std::ostream& os, const Board& board); + +#endif diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..ce78233 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,46 @@ +cmake_minimum_required(VERSION 3.16) + +project(cplchess CXX) + +# Set the default build the to Debug if it's not set on the command line. +# This is not done for multi configuration generator like Visual Studio +# (detected thought CMAKE_CONFIGURATION_TYPES). +if (NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Build type" FORCE) + + # Set the possible values of build type for cmake-gui/ccmake + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Release" "MinSizeRel" "RelWithDebInfo") +endif() + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if (MSVC) + # warning level 4 and all warnings as errors + add_compile_options(/W4 /WX) +else () + # lots of warnings and all warnings as errors + add_compile_options(-Wall -Wextra -pedantic -Werror) +endif () + +add_library(cplchess_lib OBJECT + Square.cpp + Move.cpp + Piece.cpp + Board.cpp + CastlingRights.cpp + Fen.cpp + PrincipalVariation.cpp + Engine.cpp + EngineFactory.cpp + Uci.cpp +) + +target_include_directories(cplchess_lib PUBLIC .) + +add_executable(cplchess Main.cpp) +target_link_libraries(cplchess cplchess_lib) + +include(CTest) +add_subdirectory(Tests/) diff --git a/CastlingRights.cpp b/CastlingRights.cpp new file mode 100644 index 0000000..d30007e --- /dev/null +++ b/CastlingRights.cpp @@ -0,0 +1,53 @@ +#include "CastlingRights.hpp" + +#include +#include + +CastlingRights operator&(CastlingRights lhs, CastlingRights rhs) { + using T = std::underlying_type_t; + return static_cast(static_cast(lhs) & static_cast(rhs)); +} + +CastlingRights& operator&=(CastlingRights& lhs, CastlingRights rhs) { + lhs = lhs & rhs; + return lhs; +} + +CastlingRights operator|(CastlingRights lhs, CastlingRights rhs) { + using T = std::underlying_type_t; + return static_cast(static_cast(lhs) | static_cast(rhs)); +} + +CastlingRights& operator|=(CastlingRights& lhs, CastlingRights rhs) { + lhs = lhs | rhs; + return lhs; +} + +CastlingRights operator~(CastlingRights cr) { + using T = std::underlying_type_t; + return static_cast(~static_cast(cr)); +} + +std::ostream& operator<<(std::ostream& os, CastlingRights cr) { + if (cr == CastlingRights::None) { + return os << "-"; + } else { + if ((cr & CastlingRights::WhiteKingside) != CastlingRights::None) { + os << "K"; + } + + if ((cr & CastlingRights::WhiteQueenside) != CastlingRights::None) { + os << "Q"; + } + + if ((cr & CastlingRights::BlackKingside) != CastlingRights::None) { + os << "k"; + } + + if ((cr & CastlingRights::BlackQueenside) != CastlingRights::None) { + os << "q"; + } + + return os; + } +} diff --git a/CastlingRights.hpp b/CastlingRights.hpp new file mode 100644 index 0000000..210c1cb --- /dev/null +++ b/CastlingRights.hpp @@ -0,0 +1,25 @@ +#ifndef CHESS_ENGINE_CASTLINGRIGHTS_HPP +#define CHESS_ENGINE_CASTLINGRIGHTS_HPP + +#include + +enum class CastlingRights { + None = 0, + WhiteKingside = 1 << 0, + WhiteQueenside = 1 << 1, + BlackKingside = 1 << 2, + BlackQueenside = 1 << 3, + White = WhiteKingside | WhiteQueenside, + Black = BlackKingside | BlackQueenside, + All = White | Black +}; + +CastlingRights operator&(CastlingRights lhs, CastlingRights rhs); +CastlingRights& operator&=(CastlingRights& lhs, CastlingRights rhs); +CastlingRights operator|(CastlingRights lhs, CastlingRights rhs); +CastlingRights& operator|=(CastlingRights& lhs, CastlingRights rhs); +CastlingRights operator~(CastlingRights cr); + +std::ostream& operator<<(std::ostream& os, CastlingRights cr); + +#endif diff --git a/Engine.cpp b/Engine.cpp new file mode 100644 index 0000000..8373ee4 --- /dev/null +++ b/Engine.cpp @@ -0,0 +1,7 @@ +#include "Engine.hpp" + +std::optional Engine::hashInfo() const { + return std::nullopt; +} + +void Engine::setHashSize(std::size_t) {} diff --git a/Engine.hpp b/Engine.hpp new file mode 100644 index 0000000..8ad29cd --- /dev/null +++ b/Engine.hpp @@ -0,0 +1,37 @@ +#ifndef CHESS_ENGINE_ENGINE_HPP +#define CHESS_ENGINE_ENGINE_HPP + +#include "PrincipalVariation.hpp" +#include "Board.hpp" +#include "TimeInfo.hpp" + +#include +#include +#include + +struct HashInfo { + std::size_t defaultSize; + std::size_t minSize; + std::size_t maxSize; +}; + +class Engine { +public: + + virtual ~Engine() = default; + + virtual std::string name() const = 0; + virtual std::string version() const = 0; + virtual std::string author() const = 0; + + virtual void newGame() = 0; + virtual PrincipalVariation pv( + const Board& board, + const TimeInfo::Optional& timeInfo = std::nullopt + ) = 0; + + virtual std::optional hashInfo() const; + virtual void setHashSize(std::size_t size); +}; + +#endif diff --git a/EngineFactory.cpp b/EngineFactory.cpp new file mode 100644 index 0000000..72d1c76 --- /dev/null +++ b/EngineFactory.cpp @@ -0,0 +1,5 @@ +#include "EngineFactory.hpp" + +std::unique_ptr EngineFactory::createEngine() { + return nullptr; +} diff --git a/EngineFactory.hpp b/EngineFactory.hpp new file mode 100644 index 0000000..cde697e --- /dev/null +++ b/EngineFactory.hpp @@ -0,0 +1,14 @@ +#ifndef CHESS_ENGINE_ENGINEFACTORY_HPP +#define CHESS_ENGINE_ENGINEFACTORY_HPP + +#include "Engine.hpp" + +#include + +class EngineFactory { +public: + + static std::unique_ptr createEngine(); +}; + +#endif diff --git a/Fen.cpp b/Fen.cpp new file mode 100644 index 0000000..72f763c --- /dev/null +++ b/Fen.cpp @@ -0,0 +1,168 @@ +#include "Fen.hpp" + +#include "Square.hpp" + +#include +#include + +const char* const Fen::StartingPos = + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; + +static std::string nextField(std::istream& stream) { + auto field = std::string(); + stream >> field; + return field; +} + +static bool parsePlacement(const std::string& placement, Board& board) { + auto currentFile = Square::Coordinate(0); + auto currentRank = Square::Coordinate(7); + + for (auto c: placement) { + if (c >= '1' && c <='8') { + auto offset = c - '0'; + currentFile += offset; + + if (currentFile > 8) { + return false; + } + } else if (c == '/') { + if (currentRank == 0) { + return false; + } + + currentFile = 0; + currentRank--; + } else { + auto optPiece = Piece::fromSymbol(c); + + if (!optPiece.has_value()) { + return false; + } + + auto optSquare = Square::fromCoordinates(currentFile, currentRank); + + if (!optSquare.has_value()) { + return false; + } + + board.setPiece(optSquare.value(), optPiece.value()); + currentFile++; + } + } + + return true; +} + +static bool parseTurn(const std::string& turn, Board& board) { + if (turn == "w") { + board.setTurn(PieceColor::White); + return true; + } else if (turn == "b") { + board.setTurn(PieceColor::Black); + return true; + } else { + return false; + } +} + +static bool parseCastlingRights(const std::string& rights, Board& board) { + if (rights == "-") { + board.setCastlingRights(CastlingRights::None); + return true; + } + + auto cr = CastlingRights::None; + + for (auto right : rights) { + switch (right) { + case 'K': cr |= CastlingRights::WhiteKingside; break; + case 'Q': cr |= CastlingRights::WhiteQueenside; break; + case 'k': cr |= CastlingRights::BlackKingside; break; + case 'q': cr |= CastlingRights::BlackQueenside; break; + default: return false; + } + } + + board.setCastlingRights(cr); + return true; +} + +static bool parseEnPassantSquare(const std::string& ep, Board& board) { + if (ep == "-") { + // No en passant square + return true; + } + + auto square = Square::fromName(ep); + + if (square.has_value()) { + board.setEnPassantSquare(square); + return true; + } else { + return false; + } +} + +Board::Optional Fen::createBoard(std::istream& fenStream) { + auto placement = nextField(fenStream); + + if (placement.empty()) { + return std::nullopt; + } + + auto board = Board(); + + if (!parsePlacement(placement, board)) { + return std::nullopt; + } + + auto turn = nextField(fenStream); + + if (turn.empty()) { + return std::nullopt; + } + + if (!parseTurn(turn, board)) { + return std::nullopt; + } + + auto castlingRights = nextField(fenStream); + + if (castlingRights.empty()) { + return std::nullopt; + } + + if (!parseCastlingRights(castlingRights, board)) { + return std::nullopt; + } + + auto ep = nextField(fenStream); + + if (ep.empty()) { + return std::nullopt; + } + + if (!parseEnPassantSquare(ep, board)) { + return std::nullopt; + } + + auto halfmove = nextField(fenStream); + + if (halfmove.empty()) { + return std::nullopt; + } + + auto fullmove = nextField(fenStream); + + if (fullmove.empty()) { + return std::nullopt; + } + + return board; +} + +Board::Optional Fen::createBoard(const std::string& fen) { + auto fenStream = std::stringstream(fen); + return createBoard(fenStream); +} diff --git a/Fen.hpp b/Fen.hpp new file mode 100644 index 0000000..6be475c --- /dev/null +++ b/Fen.hpp @@ -0,0 +1,16 @@ +#ifndef CHESS_ENGINE_FEN_HPP +#define CHESS_ENGINE_FEN_HPP + +#include "Board.hpp" + +#include +#include + +namespace Fen { + extern const char* const StartingPos; + + Board::Optional createBoard(std::istream& fenStream); + Board::Optional createBoard(const std::string& fen); +} + +#endif diff --git a/Main.cpp b/Main.cpp new file mode 100644 index 0000000..ac04f7c --- /dev/null +++ b/Main.cpp @@ -0,0 +1,34 @@ +#include "Uci.hpp" +#include "EngineFactory.hpp" +#include "Fen.hpp" +#include "Engine.hpp" + +#include +#include +#include + +int main(int argc, char* argv[]) { + auto engine = EngineFactory::createEngine(); + + if (engine == nullptr) { + std::cerr << "Failed to create engine\n"; + return EXIT_FAILURE; + } + + if (argc > 1) { + auto fen = argv[1]; + auto board = Fen::createBoard(fen); + + if (!board.has_value()) { + std::cerr << "Parsing FEN failed\n"; + return EXIT_FAILURE; + } + + auto pv = engine->pv(board.value()); + std::cout << "PV: " << pv << '\n'; + } else { + auto uciLog = std::ofstream("uci-log.txt"); + auto uci = Uci(std::move(engine), std::cin, std::cout, uciLog); + uci.run(); + } +} diff --git a/Move.cpp b/Move.cpp new file mode 100644 index 0000000..f151b37 --- /dev/null +++ b/Move.cpp @@ -0,0 +1,46 @@ +#include "Move.hpp" + +#include + +Move::Move(const Square& from, const Square& to, + const std::optional& promotion) +{ + (void)from; + (void)to; + (void)promotion; +} + +Move::Optional Move::fromUci(const std::string& uci) { + (void)uci; + return std::nullopt; +} + +Square Move::from() const { + return Square::A1; +} + +Square Move::to() const { + return Square::A1; +} + +std::optional Move::promotion() const { + return std::nullopt; +} + +std::ostream& operator<<(std::ostream& os, const Move& move) { + (void)move; + return os; +} + + +bool operator<(const Move& lhs, const Move& rhs) { + (void)lhs; + (void)rhs; + return false; +} + +bool operator==(const Move& lhs, const Move& rhs) { + (void)lhs; + (void)rhs; + return false; +} diff --git a/Move.hpp b/Move.hpp new file mode 100644 index 0000000..a0f38b2 --- /dev/null +++ b/Move.hpp @@ -0,0 +1,33 @@ +#ifndef CHESS_ENGINE_MOVE_HPP +#define CHESS_ENGINE_MOVE_HPP + +#include "Square.hpp" +#include "Piece.hpp" + +#include +#include +#include + +class Move { +public: + + using Optional = std::optional; + + Move(const Square& from, const Square& to, + const std::optional& promotion = std::nullopt); + + static Optional fromUci(const std::string& uci); + + Square from() const; + Square to() const; + std::optional promotion() const; + +}; + +std::ostream& operator<<(std::ostream& os, const Move& move); + +// Needed for std::map, std::set +bool operator<(const Move& lhs, const Move& rhs); +bool operator==(const Move& lhs, const Move& rhs); + +#endif diff --git a/Piece.cpp b/Piece.cpp new file mode 100644 index 0000000..cf250ee --- /dev/null +++ b/Piece.cpp @@ -0,0 +1,38 @@ +#include "Piece.hpp" + +#include + +Piece::Piece(PieceColor color, PieceType type) +{ + (void)color; + (void)type; +} + +Piece::Optional Piece::fromSymbol(char symbol) { + (void)symbol; + return std::nullopt; +} + +PieceColor Piece::color() const { + return PieceColor::Black; +} + +PieceType Piece::type() const { + return PieceType::Pawn; +} + +bool operator==(const Piece& lhs, const Piece& rhs) { + (void)lhs; + (void)rhs; + return false; +} + +std::ostream& operator<<(std::ostream& os, const Piece& piece) { + (void)piece; + return os; +} + +PieceColor operator!(PieceColor color) { + (void)color; + return PieceColor::White; +} diff --git a/Piece.hpp b/Piece.hpp new file mode 100644 index 0000000..b03f268 --- /dev/null +++ b/Piece.hpp @@ -0,0 +1,40 @@ +#ifndef CHESS_ENGINE_PIECE_HPP +#define CHESS_ENGINE_PIECE_HPP + +#include +#include + +enum class PieceColor { + White, + Black +}; + +enum class PieceType { + Pawn, + Knight, + Bishop, + Rook, + Queen, + King +}; + +class Piece { +public: + + using Optional = std::optional; + + Piece(PieceColor color, PieceType type); + + static Optional fromSymbol(char symbol); + + PieceColor color() const; + PieceType type() const; +}; + +bool operator==(const Piece& lhs, const Piece& rhs); +std::ostream& operator<<(std::ostream& os, const Piece& piece); + +// Invert a color (White becomes Black and vice versa) +PieceColor operator!(PieceColor color); + +#endif diff --git a/PrincipalVariation.cpp b/PrincipalVariation.cpp new file mode 100644 index 0000000..4b8857c --- /dev/null +++ b/PrincipalVariation.cpp @@ -0,0 +1,29 @@ +#include "PrincipalVariation.hpp" + +#include + + +bool PrincipalVariation::isMate() const { + return false; +} + +int PrincipalVariation::score() const { + return 0; +} + +std::size_t PrincipalVariation::length() const { + return 0; +} + +PrincipalVariation::MoveIter PrincipalVariation::begin() const { + return nullptr; +} + +PrincipalVariation::MoveIter PrincipalVariation::end() const { + return nullptr; +} + +std::ostream& operator<<(std::ostream& os, const PrincipalVariation& pv) { + (void)pv; + return os; +} diff --git a/PrincipalVariation.hpp b/PrincipalVariation.hpp new file mode 100644 index 0000000..5dc30ee --- /dev/null +++ b/PrincipalVariation.hpp @@ -0,0 +1,25 @@ +#ifndef CHESS_ENGINE_PRINCIPALVARIATION_HPP +#define CHESS_ENGINE_PRINCIPALVARIATION_HPP + +#include "Move.hpp" +#include "Piece.hpp" + +#include +#include + +class PrincipalVariation { +public: + + using MoveIter = Move*; + + bool isMate() const; + int score() const; + + std::size_t length() const; + MoveIter begin() const; + MoveIter end() const; +}; + +std::ostream& operator<<(std::ostream& os, const PrincipalVariation& pv); + +#endif diff --git a/README.md b/README.md new file mode 100644 index 0000000..90feeba --- /dev/null +++ b/README.md @@ -0,0 +1,844 @@ +[TOC] + +# Introduction + +In this project, the goal is to develop a [*chess engine*][chess engine] in C++. +A chess engine is a computer program that analyzes chess positions and generates a move that it considers best. +It typically consists of three major parts: +- *Board representation*: the data structure that stores all relevant information about a chess position; +- *Move generation*: given a position, generate all valid moves from that position; +- *Search*: use move generation to create a game tree and search it for good moves using an *evaluation heuristic*. + +You will be implementing all three parts in this project. + +## Goal + +The main goal of this project is to create a *correct* implementation. +Many [tests](#testing) are provided to help you validate the correctness of your implementation. +*However*, performance is an important aspect of chess engines. +The search space is simply too large for a brute-force search, even at very small depths. +Therefore, you are expected to put some effort in optimizing your engine. + +Some important points about our evaluation: +- Passing all [unit tests](#unit-tests) is a good indication of a correct board representation and move generation implementation. + However, not all corner cases are tested and we *will* run more tests; +- Passing all [puzzle tests](#puzzles) is a good indication of a correct and adequately performing search/evaluation implementation. + However, we *will* use more puzzles of increasing difficulty for our evaluation; +- All given puzzles are run with a time limit of 3 minutes. + Puzzles that take longer to solve will fail. + +It is a good idea to add more tests yourself! + +## Grading + +This project counts for 5/20 points of the CPL course. +Of those 5 points, 4 are used to grade the correctness and quality of your implementation and 1 is used to assess its performance. +More concretely: + +- 3/5: Correctness. + Tested by the unit tests and "mate-in-N" puzzles. + We *will* use more tests than the ones provided. +- 1/5: Code quality. + We will check some objective measures like the absence of undefined behavior and memory leaks. +- 1/5: Performance. + This might include puzzles in the style of the provided "crushing" ones, playing games against a reference engine,... + +## Submitting + +This is an **individual assignment**! +A private Git repository will be created for you on the KU Leuven GitLab servers which you can use for development and will be used for submitting your project. +Do not share this repository with anyone. + + +**Submitting**: +- Deadline: **Friday December 23, 2022 at 23:59**; +- How: push your solution to the `main` branch of your private repository. + +> :bulb: At the time of the deadline, your repository will be archived. +> This means it's still available but read-only. + +**Important guidelines**: +- Whatever is on the `main` branch of your private repository at the time of the deadline is considered your solution, nothing else. + Make sure *all* your code is on the *correct branch* in the *correct repository*; +- Your **code must work on our test infrastructure**. + If it does not, we **cannot grade it**. + When you push to your private repository, the tests are [automatically run](#automatic-test-runs) on our infrastructure. + Do this regularly; +- Do not change how tests are run, do not remove any tests, and do not change the compiler options related to warnings in [CMakeLists.txt](CMakeLists.txt). + If you do, the automatic test runs will not be representative anymore since we will restore everything when running our evaluation tests; +- Do **not share your code** with anyone and do **not include any code not written by you** in your repository (except, of course, the initial code we provided). + +## Allowed language features + +You are allowed to use all features of [C++20][c++20] including everything in the standard library as long as the compiler on our test infrastructure supports it. +However, we recommend sticking with [C++17][c++17] as this is what was used during the lecture and exercise sessions. +See [this][c++ features] for an overview of all (library) features and in which C++ version they are available. + +> :bulb: The compiler used on our test infrastructure is GCC 11.3.0 which [supports][gcc c++ status] (nearly) all C++20 features. + +> :bulb: To use C++20, you will have to update the `CMAKE_CXX_STANDARD` variable in [CMakeLists.txt](CMakeLists.txt). + +You are **not allowed to use *any* external libraries**. +This includes header-only libraries. + +> :warning: Including *any* source files not fully written by you in your repository will be considered plagiarism! + +## Questions and issues + +All code contains bugs, so I expect that some will be found in the code provided for this assignment. +If you happen to find one, please [report][questions] it and I will try to fix it as soon as possible. +I will then publish the fix on the [assignment repository][assignment repo] from which it can be merged into your private repository. + +> :bulb: To make sure merging is as painless as possible, try not to commit any unnecessary changes to the code, especially in the provided implementation files like [Fen.cpp](Fen.cpp) and [Uci.cpp](Uci.cpp). +> Always check what you are about to commit using, for example, `git diff --staged`. + +> :bulb: The easiest way to merge changes into your private repository is to use Git: +> ``` +> git remote add assignment https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-project-assignment.git +> git fetch assignment +> git merge assignment/main +> ``` +> The first command is only needed the first time you merge. + + +The same issue tracker can be used to **ask questions** about the project. +Just open [open an issue][questions] and I will try to answer as soon as possible. + +# Setup + +The basic setup for the project is the same as for the [exercise sessions][exercises setup]. + +From now on, we use the following variables in commands: +- `$REPO_URL`: the Git URL of your private repository; +- `$REPO_DIR`: the directory where you will clone your repository; +- `$BUILD_DIR`: the directory where you will build the project. + +[Catch2][catch2] is used for unit testing and its source code is included as a [submodule][git submodules]. +Therefore, we have to clone the project repository recursively: + +``` +$ git clone --recurse-submodules $REPO_URL $REPO_DIR +``` + +If you did a normal clone, don't worry, this can be fixed: +``` +$ cd $REPO_DIR +$ git submodule update --init +``` + +Now the project can be built in the usual way: + +``` +$ mkdir $BUILD_DIR +$ cd $BUILD_DIR +$ cmake $REPO_DIR +$ cmake --build . +``` + +By default, CMake will perform a *debug build*. +This means, among others, that optimizations are disabled. +Since performance is important for a chess engine, you might want perform a *release build*. +This can be done by replacing the third command above by the following: +``` +$ cmake -DCMAKE_BUILD_TYPE=Release $REPO_DIR +``` + +> :bulb: You can have multiple build directories, for example, one with a debug build and one with a release build. +> This is useful since debug builds are preferred if you ever need to use a debugger. +> Debugging release builds is difficult since they do not contain debugging symbols (so you cannot put breakpoints on functions, for example) and are highly optimized. + +> :bulb: The above procedure for creating a debug build does not work on Visual Studio since it creates multiple builds in the same directory. +> Therefore, a release build can be created as follows (replaces the fourth command above): +> ``` +> $ cmake --build . --config Release +> ``` + +If all went well, there should now be an executable called `cplchess` for the engine, and one called `Tests/tests` for the unit tests. +Try to execute the latter to verify your build worked. +The tests will obviously fail but it should not crash. + +If you want to add extra `.cpp` files to your project (which you will have to), you should add them to the `cplchess_lib` library in [CMakeLists.txt](CMakeLists.txt). +This library is linked into both the engine and the unit tests executables. +If you want to add extra unit test files, add them to the `tests` executable in [Tests/CMakeLists.txt](Tests/CMakeLists.txt) + +> :bulb: As explained [earlier](#submitting), you are not allowed to change the compiler options related to warnings in [CMakeLists.txt](CMakeLists.txt). +> We have enabled many [compiler warnings][gcc warnings] in the build and made sure that warnings are treated as errors. +> This forces you to write warning-free code to help you discover many mistakes early. + +# Problem description + +This section describes the parts you have to implement in your chess engine. +If you are not familiar with chess, it might be worthwhile to learn its [rules][chess rules] first. + +Although it is entirely up to you in which order to implement the parts, following the order in this section is probably a good idea. +The unit tests allow you to [select certain tests](#test-tags) to focus on specific parts. +This also allows you to postpone the implementation of more difficult move types (i.e., [castling][castling], [promotion][promotion], and [en passant][en passant]). +It could be interesting, for example, to test the full search algorithm before adding those moves. + +> :bulb: Most code you have to implement can be added to existing source files. +> We have provided skeleton code to ensure that all tests compile and that you adhere to the needed API. +> All method bodies are empty but to prevent compiler warnings/errors we +> - return some arbitrary value in non-`void` methods; +> - pretend to use arguments by casting them to `void` (e.g., `(void)arg`). + +## Fundamental data structures + +> :bulb: Relevant unit test [tag](#test-tags): `[Fundamental]` + +We start with describing the fundamental data structures used to represent the board state and moves. +All these data structures need to follow the given API but how they are implemented is up to you. + +### Piece + +Pieces are the main tools at the disposal of the chess players. +Although the different kind of pieces vary in behavior, we represent them simply by their basic properties: *color* (black or white) and *type* (pawn, knight, etc). +Their behavior (i.e., how they move) is [handled elsewhere](#move-generation). + +Piece representation is implemented in [Piece.hpp](Piece.hpp) and [Piece.cpp](Piece.cpp). +Three types are defined here: +- `PieceColor` (`enum`); +- `PieceType` (`enum`); +- `Piece` (`class` that stores a color and a type). + +Besides their representation, the `Piece` class also handles conversion to and from standard piece symbols: `Piece::fromSymbol()` takes a `char` and converts it to a `Piece` (if possible) and streaming to `std::ostream` can be used for the opposite. +The following symbols are used for chess pieces: `P` (pawn), `N` (knight), `B` (bishop), `R` (rook), `Q` (queen), and `K` (king). Uppercase letters are used for white, lowercase for black. + +> :bulb: Throughout this project, [`std::optional`][std optional] is used to represent optional values. +> For example, creation methods like `Piece::fromSymbol()` return an optional if they may fail. +> Many classes that are often used as optional values define a type alias called `Optional` for brevity. + +### Square + +Chessboards consist of a grid of 8x8 *squares*. +In [*algebraic notation*][san], each square is identified by a coordinate pair from white's point of view. +Columns (called *files*) are labeled *a* through *h* while rows (called *ranks*) are labeled *1* through *8*. +So, the lower-left square is called *a1* while the upper-right square is called *h8*. + +The `Square` class (implemented in [Square.hpp](Square.hpp) and [Square.cpp](Square.cpp)) is used to identify squares. +It offers two ways of identifying squares: +- Coordinates: create using `fromCoordinates()` and get using `file()` and `rank()`. + Note that both coordinates are numbers in the range `[0, 8)`. +- Index: create using `fromIndex()` and get using `index()`. + In this representation, all squares have a unique index in the range `[0, 64)` where the index of *a1* is *0*, *b1* is *1*, and that of *h8* is *63*. + +Squares also offer conversion from (`fromName`) and to (streaming to `std::ostream`) their name. + +Two comparison operator are needed: +- `operator==` for comparing squares in the tests and elsewhere; +- `operator<` for using squares as keys in associative (sorted) containers like [`std::map`][std_map]. + You can use any total ordering. + +Constants are provided to conveniently references all squares (e.g., `Square::E4`). +These are implemented using a private constructor that takes an index as argument. +You don't have to keep this constructor, but if you don't, you will have to update the initialization of the constants. + +### Move + +Moves are how a game of chess progresses from turn to turn. +Although the concept of a move potentially contains a lot of information (e.g., which piece is moved, if the move is valid, etc.), the `Move` class (implemented in [Move.hpp](Move.hpp) and [Move.cpp](Move.cpp)) only uses the bare minimum: the *from* and *to* squares and optionally the [promoted][promotion]-to piece type. +Its constructor takes all three items (although the promotion type is optional and by default a move is not a promotion). + +As a textual representation, the `Move` class uses the [UCI][uci] notation (also sometimes called the [*long algebraic notation*][long algebraic notation]). +In this notation, moves are represented by the concatenation of the names of the from and to squares, optionally followed by the lower case symbol of the promotion piece type. +[*Castling*][castling] is considered a king move so is represented by the from and to squares of the king (e.g., white kingside is castling is *e1g1*). +Moves can be created from this representation using `fromUci` and the conversion to UCI is done by streaming to `std::ostream`. + +Two comparison operator are needed: +- `operator==` for comparing moves in the tests and elsewhere; +- `operator<` for using moves as keys in associative (sorted) containers like [`std::map`][std_map] and [`std::set`][std_set]. + You can use any total ordering. + +> :warning: If you don't correctly implement a total ordering on `Move`s with `operator<`, you might run into strange behavior. +> For example, the [unit tests](#unit-tests) for pseudo-legal move generation store moves in a `std::set` and compare sets of expected- and generated moves. +> If you ever notice that tests pass or fail depending on the order in which you generate moves, it's probably because `operator<` does not correctly implement a total ordering. + +Note that this simple representation allows the creation of illegal moves. +Indeed, since the `Move` class has no relation with a `Board`, it's impossible to validate the legality of moves in isolation. +We will see how to handle illegal moves [later](#move-making). + +> :bulb: There are some things that *can* be checked about a move's validity. +> For example, whether a promotion is certainly invalid (e.g., *a7a8k* or *e6e8q* are *never* valid). +> You are free to check this in `fromUci()` but this is not necessary, especially since the constructor has no way of reporting errors so creating such invalid moves is always possible. + +### Board + +Arguably the most important decision for chess engines is how the [board state is represented][board representation wp] as it will have a large impact on how [*move generation*](#move-generation) will be implemented and thus how efficient this will be. +The most popular among high-rated chess engines is probably a [bitboard][bitboard] representation (e.g., [Stockfish][stockfish] uses this). +Although very efficient for move generation, this representation can be difficult to grasp and work with. +Square-centric, array-based representations (e.g., [8x8 boards][8x8 board]) might be easier to work with at the cost of less efficient move generation. + +You are entirely free to choose the way the `Board` class represents its state as long as you can implement its interface: +- Getting and setting pieces: `piece(Square)`, `setPiece(Square, Piece)`; +- Getting and setting the turn: `turn()`, `setTurn(PieceColor)`; +- Getting and setting the [castling rights](#castling-rights): `castlingRights()`, `setCastlingRights(CastlingRights)`; +- Getting and setting the [en passant square](#en-passant-square): `enPassantSquare()`, `setEnPassantSquare(Square)`; +- Default construction (`Board()`): create an _empty_ board. + +You should also implement streaming a `Board` to `std::ostream` for debugging purposes. +You are free to choose how a board is printed. +I like the following format: + +``` +r n b q k b n r +p p p p p p p p +. . . . . . . . +. . . . . . . . +. . . . . . . . +. . . . . . . . +P P P P P P P P +R N B Q K B N R +``` + +#### Castling rights + +[*Castling*][castling] is probably the most complex move type in chess and requires extra state to be stored in `Board` (i.e., knowing which castling moves are valid does not depend only on the state of the pieces). +For example, once a rook has moved, its king permanently loses the right to castle on that side of the board, even when the rook returns to its original square. + +To represent *castling rights*, the `CastlingRights` enumeration is provided (see [CastlingRights.hpp](CastlingRights.hpp) and [CastlingRights.cpp](CastlingRights.cpp)). +Overloads are provided for many bitwise operations so that this type can be used as [bit flags][bit flags]. +For example: + +```c++ +CastlingRights rights = ...; + +if ((rights & CastlingRights::BlackQueenside) != CastlingRights::None) { + // Queenside castling is available for black +} + +// Add kingside castling rights for white +rights |= CastlingRights::WhiteKingside; + +// Remove all castling rights for black +rights &= ~CastlingRights::Black; +``` + +#### En passant square + +[*En passant*][en passant], probably the least-known move type in chess, also requires extra state to be stored since it is only allowed for one move after a pawn made a two-square move. +Therefore, to implement pawn captures correctly, `Board` should store the square to which an en passant capture can be made. + +## Move generation + +> :bulb: Relevant unit test [tag](#test-tags): `[MoveGen]` + +[*Move generation*][move generation] is the process of generating valid moves from a board state. +These moves will be used for [searching](#searching). + +There are generally two ways of generating moves: legal or pseudo-legal. + +### Legal move generation + +Only moves that are [legal][legal move] according to the [rules of chess][chess rules] are generated. +This is convenient for the search algorithm but might not be that easy to implement efficiently. +The main issue is verifying that the king is not left in [check][chess rules check] after a move (which is illegal). +The naive way of doing this might be to generate the opponent's moves and verifying that none of them captures the king. +While this might work, it would result in doubling the amount of generated moves. + +### Pseudo-legal move generation + +A [*pseudo-legal move*][pseudo legal move] is one that adheres to all the rules of chess *except* that it might leave the king in check. +From the description above it should be clear that this is easier to generate but might complicate the search algorithm. +Indeed, we now have to make sure that we somehow reject illegal moves while searching. +One way to tackle this could be to search until a move that captures the king and then reject the previous move. + +> :bulb: Due to the complex rules of [castling][castling], a pseudo-legal castling move is usually also legal. + +### Interface + +Move generation should be implemented by the `Board` class through the following method: + +```c++ +using MoveVec = std::vector; + +void Board::pseudoLegalMoves(MoveVec& moves) const; +``` + +This method should add all (pseudo) legal moves for the current player to the `moves` vector. + +For [testing](#unit-tests) purposes, the following method should also be implemented to generate all moves from a specific square: + +```c++ +void Board::pseudoLegalMovesFrom(const Square& from, MoveVec& moves) const; +``` + +> :bulb: You are free to use an entirely different interface to communicate between the search algorithm and `Board` as long as you can still implement this one for testing. + +> :bulb: Even though "pseudo-legal" is part of the method names, you are free to generate legal moves here. +> The tests will only verify the correct generation of pseudo-legal moves, though. + +## Move making + +> :bulb: Relevant unit test [tag](#test-tags): `[MoveMaking]` + +[*Move making*][move making] is the process of updating the board state to reflect a move. +Obviously, this should include moving the moved piece from the source square to the destination square (possibly replacing a captured piece) but it may also include updating [castling rights](#castling-rights) and the [en passant square](#en-passant-square). + +Move making is used by the search algorithm to generate child nodes after move generation. + +It should be implemented in the `Board` class by the following method: + +```c++ +void Board::makeMove(const Move& move); +``` + +> :bulb: Since this method is only used by your search algorithm, it is probably not necessary to verify that the given move is legal in this method. +> You just have to make sure that *if* a legal move is given, it is correctly executed. + +## Searching + +> :bulb: Relevant tests: [puzzles](#puzzles). + +The basic question a chess engine tries to answer is what the best move to play is at a particular position. +To answer this, a [search tree][game tree] is typically constructed with the initial position in the root node and the children at every level being positions resulting from valid moves from their parents. + +Given this tree, a search algorithm tries to find a path that *guarantees* the player will end-up in *the most favorable* position. +There are two words to discuss a bit here: +- *Guaranteed* most favorable position: what we mean here is that we must assume the opponent always makes the best possible move for them. + That is, we assume the *worst case scenario* and try to minimize our loss (or maximize our win); +- *Favorable*: we try to win the chess game but most of the time it will not feasible to search the whole tree to find a [checkmate][checkmate]. + Therefore, we have to limit the search depth and use a [heuristic][eval] to evaluate positions that are not the [end of the game][chess rules game end]. + +[*Minimax*][minimax] is a well-known search algorithm that guarantees finding the move that ends-up in the most favorable position. +You are free to use any correct algorithm but minimax is definitely a good choice. + +> :bulb: Here are some tips for using minimax: +> - Since chess is a [zero-sum game][zero-sum game], the slightly simpler [negamax][negamax] variant can be used; +> - It is highly recommended to implement [*alpha-beta pruning*][alpha-beta pruning] to improve search performance. +> Without it, it is probably impossible to solve most puzzles within the time limit; +> - When using alpha-beta pruning, [*move ordering*][move ordering] becomes important: if potentially good moves are searched first, it may lead to earlier pruning reducing the size of the search tree; +> - If you want to take time into account (see below), you might want to use [*iterative deepening*][iterative deepening] to make sure you have a reasonable move available quickly while continuing the search at higher depths to find better moves. + +[Evaluating][eval] chess positions is very complex. +The most naive way is to simply calculate the [value][piece value] of all remaining pieces. +However, this is clearly not optimal so more advanced strategies will take other aspects into account (e.g., [center control][center control], [king safety][king safety], etc). + +> :bulb: It is up to you to decide how far you want to go with improving the heuristic. +> However, it should at least be good enough to solve the provided [puzzles](#puzzles). + +Remember that if you use [pseudo-legal move generation](#pseudo-legal-move-generation), the search algorithm needs to somehow reject illegal moves. +And unless you evaluation function can detect [checkmate][checkmate] and [stalemate][stalemate], it also needs to be able to handle [end of the game][chess rules game end] conditions. + +### Principal variation + +The result of a search is a [*principal variation*][pv] (PV) which is the sequence of moves the engine considers best. +A PV is represented by the `PrincipalVariation` class (in [PrincipalVariation.hpp](PrincipalVariation.hpp) and [PrincipalVariation.cpp](PrincipalVariation.cpp)). +The main interface that has to be implemented is the following: + +```c++ +using MoveIter = /* your iterator type here */; + +std::size_t PrincipalVariation::length() const; +MoveIter PrincipalVariation::begin() const; +MoveIter PrincipalVariation::end() const; +``` + +Where `length()` returns the number of [plies][ply] in the PV and `begin()` and `end()` allows for iterating over the `Move`s. + +> :bulb: You are free to choose any container to store the `Move`s. +> Adjust `MoveIter` accordingly. + +The `PrincipalVariation` class also stores information about the evaluation [score][eval score]: + +```c++ +bool PrincipalVariation::isMate() const; +int PrincipalVariation::score() const; +``` + +`isMate()` should return `true` if the PV ends in checkmate. +In this case, `score()` should return the number of plies that leads to the checkmate. +Otherwise, `score()` returns the evaluation of the position the PV leads to. +In both cases, the score is from the point of view of the engine: positive values are advantageous for the engine, negative ones for its opponent. + +> :bulb: Your are free to choose the unit of the evaluation score as long as larger scores mean better evaluations. +> Typically, [*centipawns*][centipawns] are used where 100 centipawns corresponds to the value of one pawn. + +There is also an overload declared to stream `PrincipalVariation` to a `std::ostream`. +You can choose how PVs are printed. + +### Engine + +The main interface to the search algorithm is provided by the abstract class `Engine` (see [Engine.hpp](Engine.hpp)). +This class functions as an interface so you have to add you own derived class to implement this interface. +You should then adapt `EngineFactory::createEngine()` (in [EngineFactory.cpp](EngineFactory.cpp)) to return an instance of your class. + +The most important method in the `Engine` interface is the following: + +```c++ +virtual PrincipalVariation Engine::pv( + const Board& board, + const TimeInfo::Optional& timeInfo = std::nullopt +) = 0; +``` + +This should calculate and return the PV starting from the position represented by `board`. + +> :bulb: The second argument, `timeInfo`, provides timing information (see [TimeInfo.hpp](TimeInfo.hpp)) if it is available. +> Most chess games are played with a [time control][time control] and for those games, `timeInfo` contains the time left on the clock of both players as well as their increment. +> It is *completely optional* to use this information but will be especially useful if you choose to use [iterative deepening][iterative deepening]. + +The following method is called whenever a new game starts (not guaranteed to be called for the first game played by an `Engine` instance): + +```c++ +virtual void Engine::newGame() = 0; +``` + +> :bulb: You probably don't need this method but it could be useful if you store state inside your engine. + +The following methods are used to identify your engine over the [UCI interface](#uci): + +```c++ +virtual std::string Engine::name() const = 0; +virtual std::string Engine::version() const = 0; +virtual std::string Engine::author() const = 0; +``` + +Note that the value of `name()` will be used to identify your engine during the +tournament so make sure to chose something appropriate! + +Lastly, most chess engines use [_transposition tables_][transpositiion table] as +an optimization technique. While it is not necessary to implement this technique +for this project, if you do chose to implement it, you _have to_ respect the +maximum table size specified over UCI. In order to do this, you will have to +override the following methods: + +```c++ +virtual std::optional hashInfo() const; +virtual void setHashSize(std::size_t size); +``` + +# Testing + +A number of tests are provided in the [Tests](Tests/) directory that you can run locally and are also run automatically when pushing commits to GitLab. +You are free (even encouraged) to add more tests but **do not modify any of the existing test files**. +They will be overwritten when we run our automatic tests after the deadline. +Also remember that the tests do not try to cover all edge cases and we will run more tests when grading the projects. + +Unit tests are provided to verify the correctness of the [board representation](#fundamental-data-structures) and [pseudo-legal move generation](#move-generation). +To test the search algorithm, chess puzzles are used. + +> :bulb: It is possible to pass all the tests with a relatively shallow search depth (about 5 [plies](ply)) and a relatively simple evaluation function. +> The most naive search, however, will probably not suffice to finish most puzzles within the time limit. + +## Automatic test runs + +Whenever you push commits to your private repository, a [CI/CD pipeline][gitlab pipelines] will start automatically that builds and tests your code. +The pipelines can be viewed by going to "CI/CD -> Pipelines" in the web interface of your repository. + +When clicking on a pipeline, you can see it consist of two stages and three jobs: +- Build stage: runs the "build" job that compiles your code and produces the engine and unit tests executables for the next stage; +- Test stage: runs the "unit_tests" and "puzzle_tests" jobs that perform all automatic tests. + +> :bulb: If the build stage fails, the test stage will not run. +> Both jobs in the test stage are independent and run in parallel, though. + +You can click on a specific job to view its output which can be useful when debugging failures (e.g., to see compiler errors). + +The "Tests" tab contains detailed information about the result of the test runs. +If you want to know why a test failed, click on the job name in the "Tests" tab first (e.g., "unit_tests") and then on "View details" next to the failed test. + +> :bulb: For those who are interested, the pipeline is defined in [.gitlab-ci.yml](.gitlab-ci.yml). +> **Do not modify this file, though!** + +## Unit tests + +Unit tests are implemented using the [Catch2][catch2] framework. +After building the project, a test executable is available at `$BUILD_DIR/Tests/tests`. +Running this executable without any arguments runs all tests and looks something like this when everything passes: + +``` +$ $BUILD_DIR/Tests/tests +=============================================================================== +All tests passed (1698 assertions in 95 test cases) +``` + +### Test tags + +Since initially (almost) all tests will fail, test cases are tagged to be able to only run a subset of tests. +The following tags are provided: +- `[Fundamental]`: tests for functionality that is fundamental. + For example, for classes or methods used by the tests themselves. + This includes the `Piece`, `Square`, and `Move` classes as well as the state of the `Board` class. + **This functionality should be the first thing to implement!** +- `[ClassName`]: each class with unit tests has a corresponding tag (e.g., `[Board]`) that runs all tests of that class; +- `[MoveGen]`: all tests for pseudo-legal move generation; +- `[MoveMaking]`: all tests for move making; +- `[Castling]`, `[Promotion]`, `[EnPassant]`: all tests for these "special" moves are tagged accordingly. + This allows you to implement and test "normal" moves first; +- `[PieceType]`: tests for a specific piece type (e.g., `[Queen]`) are tagged accordingly. + This is mostly related to `[MoveGen]` tests. + +Tags can be selected by providing them as an argument to the test executable. +If one is preceded by a `~`, it is deselected. +For example, to test the move generation for pawns except en passant: + +``` +$ $BUILD_DIR/Tests/tests '[MoveGen][Pawn]~[EnPassant]' +Filters: [MoveGen][Pawn]~[EnPassant] +=============================================================================== +All tests passed (65 assertions in 11 test cases) +``` + +See [this][catch2 filter tags] for more information. + +## Puzzles + +Puzzles are chess problems that have a clear, short, and unique solution. +They are often used to train a chess player's [tactical][chess tactic] awareness. +Simple puzzles typically present a position where a checkmate can be forced or a large material advantage can be gained. +For more advanced puzzles, one might need to find moves that gain a [positional advantage][positional advantage]. + +We have selected a number of easy puzzles from the freely available [puzzle database][lichess puzzle db] of [Lichess][lichess]. +They can be found in [Tests/Puzzles/](Tests/Puzzles/) and contain three major categories: +- `mateIn1`: checkmate can be forced in one move; +- `mateIn2`: checkmate can be forced in two moves; +- `crushing`: a significant material advantage can be gained. + +For each category, three variants are provided: +- `simple`: puzzles do not involve castling or en passant moves; +- `castling`: all puzzles involve castling moves; +- `enPassant`: all puzzles involve en passant moves; + +The puzzle files are named according to the category and variant. +For example, the simple mate-in-one puzzles can be found in [Tests/Puzzles/mateIn1_simple.csv](Tests/Puzzles/mateIn1_simple.csv). + +### Running puzzles + +> :bulb: To run the puzzles locally, you need Python 3 (at least 3.7) and the `chess` and `junit-xml` packages which can be installed through `pip`: +> ``` +> pip3 install chess junit-xml +> ``` + +A Python script is provided ([Tests/puzzlerunner.py](Tests/puzzlerunner.py)) to run puzzles through an engine and verify its solution. It uses the [UCI interface](#uci) to communicate with the engine. +The script can be used as follows: +``` +$ ./puzzlerunner.py --engine /path/to/engine/executable [csv files...] +``` + +For example, to run the simple mate-in-one puzzles using your engine: + +``` +$ ./puzzlerunner.py --engine $BUILD_DIR/cplchess Puzzles/mateIn1_simple.csv +=== Running puzzles from /.../Tests/Puzzles/mateIn1_simple.csv === +Running puzzle jrJls ... OK (1.645s) +Running puzzle rZoKr ... OK (0.834s) +[snip] +Running puzzle BEUkI ... OK (0.435s) +Running puzzle EAnMf ... OK (0.027s) +Total time: 25.294s +All tests passed +``` + +> :bulb: The timing information shown is not very accurate since it's measured in wall-clock time. +> Because running a puzzle involves spawning a new process (the engine) and inter-process communication between the Python script and this process, the measured time will depend on the current load of the system. + +When a puzzle fails because the engine returned a wrong move, some information is shown to help debug the issue. +For example: + +``` +Running puzzle 93ERa ... FAIL (5.034s) +=== +Failure reason: unexpected move +URL: https://lichess.org/training/93ERa +position=4rqk1/5rp1/7R/4B3/4P1Q1/6PK/PP5P/2n5 w - - 1 39 +move=e5d6 +expected move=g4g6 +=== +``` + +It shows a link to the puzzle on Lichess, the position from which the wrong move was generated (in [FEN](#fen) notation), and the generated and expected moves. + +### Generating puzzles + +The full [Lichess puzzle database][lichess puzzle db] currently contains about three million puzzles. +We have selected only a few to be included in the automatic tests but you are encouraged to use more puzzles to test your engine. +To help you with this, we provide a script ([Tests/puzzledb.py](Tests/puzzledb.py)) that can parse and filter the database. + +Puzzles can be filtered on a number of criteria, including their [tags][lichess puzzle themes], rating, whether they include castling or en passant moves, etc. +For a full list of options, run `./puzzledb.py --help`. + +As an example, the simple mate-in-one puzzles were generated like this: + +``` +$ ./puzzledb.py --tag=mateIn1 --castling=no --en-passant=no lichess_db_puzzle.csv | sort -R | head -n 20 +``` + +> :bulb: `sort -R` randomly shuffles all matches and `head -n 20` selects the first 20. +> If you run this locally, you will (most likely) get different puzzles than the ones in [Tests/Puzzles/mateIn1_simple.csv](Tests/Puzzles/mateIn1_simple.csv). +> Also note that this is Bash syntax, the full command will not work in other (incompatible) shells but the invocation of `puzzledb.py` itself will. + +> :bulb: Note that the options `--tag` and `--not-tag` can be given multiple times and it will filter on puzzles that (don't) have *all* given tags. + +> :bulb: [Tests/puzzledb.py](Tests/puzzledb.py) is not used by our testing infrastructure so feel free to modify it to add more filter options. + +### Puzzle rating + +We also provide a script to determine your engine's puzzle rating. All puzzles +in the Lichess puzzle database have a rating and we can estimate an engine's +rating by making it "play against" a large number of puzzles and using +[Glicko 2][glicko] to update its rating. + +> :bulb: You need to install the `glicko2` Python package to run the script: +> ``` +> pip3 install glicko2 +> ``` + +You can run the script as follows: + +``` +$ ./puzzlerating.py --engine $BUILD_DIR/cplchess --puzzle-db lichess_db_puzzle.csv +``` + +Note that the script takes quite some time to start since it parses the full +database. + +# Tools + +Here we list some freely available tools that you can use while developing your chess engine. + +## FEN + +[*Forsyth–Edwards Notation*][fen] (FEN) is a standard notation for describing chess positions. +It contains information about piece placement, current turn, castling rights, en passant square, and number of moves made. + +A FEN parser is provided (see [Fen.hpp](Fen.hpp) and [Fen.cpp](Fen.cpp)) that converts a FEN string to a `Board`. + +> :warning: The FEN parser will not work properly until all `[Fundamental]` [unit tests](#unit-tests) pass. + +> :bulb: The FEN parser currently does not support parsing the move number information (nor does `Board` store it) because you don't need it for a simple engine. +> [Some extensions](#draw-conditions) might need it, though, so if you want to implement those, you'll have to extend the FEN parser. + +When running the engine executable with a FEN string as argument, the position is evaluated and the PV printed. +For example: + +``` +$ $BUILD_DIR/cplchess "2kr4/ppp2Q2/3Pp3/1q2P3/n1r4p/5P2/6PB/R1R3K1 b - - 0 29" +PV: +600 [b5c5 g1h1 c4c1 a1c1 c5c1] +``` + +> :bulb: The FEN string should be passed *as a single argument* so you have to put it in quotes. + +> :bulb: Since you are free to choose [how a PV is printed](#principal-variation), the output may look different. + +While developing your engine, you will most likely want to test it on specific positions. +To manually create positions and convert them to FEN, the Lichess [board editor][lichess board editor] is a very handy tool. +All unit tests that start from a position (which is most of them) contain a link to the board editor with that specific position in a comment. + +> :bulb: The en passant square cannot be set from the editor so you'll have to fill that in by hand if you need it. + +## UCI + +The [*Universal Chess Protocol*][uci] is a protocol that is mainly used to communicate between chess engines and user interfaces. +It allows user interfaces to send positions (and other information like timing) to an engine and for the engine to send its best move (and optionally a PV and evaluation) back. +See [this][uci protocol] for a description of the protocol. + +A UCI implementation is provided (see [Uci.hpp](Uci.hpp) and [Uci.cpp](Uci.cpp)) that allows you to use your engine with a GUI or to let it play against another engine (or itself). +Running the engine executable without any arguments starts it in UCI mode. +It will listen on stdin for commands and write replies to stdout. +It will also log some information to a file called `uci-log.txt` in its current working directory: +- All incoming and outgoing UCI commands; +- After receiving a new position and after getting a move from the engine, the board is printed; +- The PV received from the engine. + + +There are many [UCI GUIs][uci gui] and all of them should work but the one I use is [Cute Chess][cutechess]. + +To use your engine in Cute Chess, you first have to add it to its engine list. +Go to "Tools -> Settings" and then to the "Engines" tab to add your engine by clicking on the "+" symbol at the bottom. +Once you added your engine, you can use it by going to "Game -> New" and then selecting "CPU" and your engine for one or both of the players. + +> :bulb: Cute Chess also includes a command line tool (`cutechess-cli`) that you can use to make two engines play each other. +> See [this][cutechess cli] and `cutechess-cli -help` for more info. + +# Extensions + +This section describes some (optional) extensions that could give your engine an edge during the [tournament](#tournament). + +## Time control + +As described [before](#engine), timing information is passed to the engine. +This can be used to prevent the engine from timing out during games. + +## Draw conditions + +Besides [stalemate][stalemate], there are some other conditions that can draw a game. +Most notably the [*fifty-move rule*][fifty-move rule] and [*threefold repetition*][threefold repetition]. +These rules can be used to turn a losing position into a draw, especially against an engine that does not understand these rules. +They will be applied in the tournament. + +> :bulb: Last year, many games during the tournament were drawn due to threefold +> repetition. + +# Tournament + +We plan to organize a tournament between the engines that pass all tests. +More details will follow. + +[san]: https://en.wikipedia.org/wiki/Algebraic_notation_(chess) +[long algebraic notation]: https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Long_algebraic_notation +[std_map]: https://en.cppreference.com/w/cpp/container/map +[std_set]: https://en.cppreference.com/w/cpp/container/set +[promotion]: https://en.wikipedia.org/wiki/Promotion_(chess) +[uci]: https://en.wikipedia.org/wiki/Universal_Chess_Interface +[board representation wp]: https://en.wikipedia.org/wiki/Board_representation_(computer_chess) +[board representation cpw]: https://www.chessprogramming.org/Board_Representation +[bitboard]: https://en.wikipedia.org/wiki/Bitboard +[stockfish]: https://stockfishchess.org/ +[8x8 board]: https://www.chessprogramming.org/8x8_Board +[castling]: https://en.wikipedia.org/wiki/Castling +[bit flags]: https://blog.podkalicki.com/bit-level-operations-bit-flags-and-bit-masks/ +[en passant]: https://en.wikipedia.org/wiki/En_passant +[move generation]: https://www.chessprogramming.org/Move_Generation +[pseudo legal move]: https://www.chessprogramming.org/Pseudo-Legal_Move +[legal move]: https://www.chessprogramming.org/Legal_Move +[chess rules]: https://en.wikipedia.org/wiki/Rules_of_chess +[chess rules check]: https://en.wikipedia.org/wiki/Rules_of_chess#Check +[chess rules game end]: https://en.wikipedia.org/wiki/Rules_of_chess#End_of_the_game +[move making]: https://www.chessprogramming.org/Make_Move +[game tree]: https://en.wikipedia.org/wiki/Game_tree +[checkmate]: https://en.wikipedia.org/wiki/Checkmate +[stalemate]: https://en.wikipedia.org/wiki/Stalemate +[minimax]: https://en.wikipedia.org/wiki/Minimax +[zero-sum game]: https://en.wikipedia.org/wiki/Zero-sum_game +[negamax]: https://en.wikipedia.org/wiki/Negamax +[alpha-beta pruning]: https://en.wikipedia.org/wiki/Alpha-beta_pruning +[move ordering]: https://www.chessprogramming.org/Move_Ordering +[iterative deepening]: https://www.chessprogramming.org/Iterative_Deepening +[pv]: https://www.chessprogramming.org/Principal_Variation +[eval]: https://www.chessprogramming.org/Evaluation +[piece value]: https://en.wikipedia.org/wiki/Chess_piece_relative_value +[center control]: https://www.chessprogramming.org/Center_Control +[king safety]: https://www.chessprogramming.org/King_Safety +[ply]: https://en.wikipedia.org/wiki/Ply_(game_theory) +[eval score]: https://www.chessprogramming.org/Score +[centipawns]: https://www.chessprogramming.org/Centipawns +[time control]: https://en.wikipedia.org/wiki/Time_control#Chess +[catch2]: https://github.com/catchorg/Catch2 +[catch2 filter tags]: https://github.com/catchorg/Catch2/blob/v2.x/docs/command-line.md#specifying-which-tests-to-run +[chess tactic]: https://en.wikipedia.org/wiki/Chess_tactic +[positional advantage]: https://www.chessstrategyonline.com/content/tutorials/introduction-to-chess-strategy-positional-advantage +[lichess]: https://lichess.org/ +[lichess puzzle db]: https://database.lichess.org/#puzzles +[lichess puzzle themes]: https://lichess.org/training/themes +[fen]: https://en.wikipedia.org/wiki/Forsyth-Edwards_Notation +[lichess board editor]: https://lichess.org/editor +[uci]: https://en.wikipedia.org/wiki/Universal_Chess_Interface +[uci protocol]: https://backscattering.de/chess/uci/ +[uci gui]: https://www.chessprogramming.org/UCI#GUIs +[cutechess]: https://github.com/cutechess/cutechess +[cutechess cli]: https://github.com/cutechess/cutechess#running +[chess engine]: https://en.wikipedia.org/wiki/Chess_engine +[questions]: https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-project-assignment/-/issues +[assignment repo]: https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-project-assignment +[exercises setup]: https://gitlab.kuleuven.be/distrinet/education/cpl/cplusplus-exercises-assignment#setup +[git submodules]: https://git-scm.com/book/en/v2/Git-Tools-Submodules +[c++17]: https://en.cppreference.com/w/cpp/17 +[c++20]: https://en.cppreference.com/w/cpp/20 +[c++ features]: https://en.cppreference.com/w/cpp +[fifty-move rule]: https://en.wikipedia.org/wiki/Fifty-move_rule +[threefold repetition]: https://en.wikipedia.org/wiki/Threefold_repetition +[gcc c++ status]: https://gcc.gnu.org/projects/cxx-status.html +[gitlab pipelines]: https://docs.gitlab.com/ee/ci/pipelines/ +[gcc warnings]: https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html +[std optional]: https://en.cppreference.com/w/cpp/utility/optional +[transposition table]: https://www.chessprogramming.org/Transposition_Table +[glicko]: https://en.wikipedia.org/wiki/Glicko_rating_system diff --git a/Square.cpp b/Square.cpp new file mode 100644 index 0000000..729b6e4 --- /dev/null +++ b/Square.cpp @@ -0,0 +1,126 @@ +#include "Square.hpp" + +#include + +Square::Square(Index index) +{ + (void)index; +} + +Square::Optional Square::fromCoordinates(Coordinate file, Coordinate rank) { + (void)file; + (void)rank; + return std::nullopt; +} + +Square::Optional Square::fromIndex(Index index) { + (void)index; + return std::nullopt; +} + +Square::Optional Square::fromName(const std::string& name) { + (void)name; + return std::nullopt; +} + +Square::Coordinate Square::file() const { + return 0; +} + +Square::Coordinate Square::rank() const { + return 0; +} + +Square::Index Square::index() const { + return 0; +} + + +const Square Square::A1 = Square( 0 + 0); +const Square Square::B1 = Square( 0 + 1); +const Square Square::C1 = Square( 0 + 2); +const Square Square::D1 = Square( 0 + 3); +const Square Square::E1 = Square( 0 + 4); +const Square Square::F1 = Square( 0 + 5); +const Square Square::G1 = Square( 0 + 6); +const Square Square::H1 = Square( 0 + 7); + +const Square Square::A2 = Square( 8 + 0); +const Square Square::B2 = Square( 8 + 1); +const Square Square::C2 = Square( 8 + 2); +const Square Square::D2 = Square( 8 + 3); +const Square Square::E2 = Square( 8 + 4); +const Square Square::F2 = Square( 8 + 5); +const Square Square::G2 = Square( 8 + 6); +const Square Square::H2 = Square( 8 + 7); + +const Square Square::A3 = Square(16 + 0); +const Square Square::B3 = Square(16 + 1); +const Square Square::C3 = Square(16 + 2); +const Square Square::D3 = Square(16 + 3); +const Square Square::E3 = Square(16 + 4); +const Square Square::F3 = Square(16 + 5); +const Square Square::G3 = Square(16 + 6); +const Square Square::H3 = Square(16 + 7); + +const Square Square::A4 = Square(24 + 0); +const Square Square::B4 = Square(24 + 1); +const Square Square::C4 = Square(24 + 2); +const Square Square::D4 = Square(24 + 3); +const Square Square::E4 = Square(24 + 4); +const Square Square::F4 = Square(24 + 5); +const Square Square::G4 = Square(24 + 6); +const Square Square::H4 = Square(24 + 7); + +const Square Square::A5 = Square(32 + 0); +const Square Square::B5 = Square(32 + 1); +const Square Square::C5 = Square(32 + 2); +const Square Square::D5 = Square(32 + 3); +const Square Square::E5 = Square(32 + 4); +const Square Square::F5 = Square(32 + 5); +const Square Square::G5 = Square(32 + 6); +const Square Square::H5 = Square(32 + 7); + +const Square Square::A6 = Square(40 + 0); +const Square Square::B6 = Square(40 + 1); +const Square Square::C6 = Square(40 + 2); +const Square Square::D6 = Square(40 + 3); +const Square Square::E6 = Square(40 + 4); +const Square Square::F6 = Square(40 + 5); +const Square Square::G6 = Square(40 + 6); +const Square Square::H6 = Square(40 + 7); + +const Square Square::A7 = Square(48 + 0); +const Square Square::B7 = Square(48 + 1); +const Square Square::C7 = Square(48 + 2); +const Square Square::D7 = Square(48 + 3); +const Square Square::E7 = Square(48 + 4); +const Square Square::F7 = Square(48 + 5); +const Square Square::G7 = Square(48 + 6); +const Square Square::H7 = Square(48 + 7); + +const Square Square::A8 = Square(56 + 0); +const Square Square::B8 = Square(56 + 1); +const Square Square::C8 = Square(56 + 2); +const Square Square::D8 = Square(56 + 3); +const Square Square::E8 = Square(56 + 4); +const Square Square::F8 = Square(56 + 5); +const Square Square::G8 = Square(56 + 6); +const Square Square::H8 = Square(56 + 7); + +std::ostream& operator<<(std::ostream& os, const Square& square) { + (void)square; + return os; +} + +bool operator<(const Square& lhs, const Square& rhs) { + (void)lhs; + (void)rhs; + return false; +} + +bool operator==(const Square& lhs, const Square& rhs) { + (void)lhs; + (void)rhs; + return false; +} diff --git a/Square.hpp b/Square.hpp new file mode 100644 index 0000000..c7f1f57 --- /dev/null +++ b/Square.hpp @@ -0,0 +1,43 @@ +#ifndef CHESS_ENGINE_SQUARE_HPP +#define CHESS_ENGINE_SQUARE_HPP + +#include +#include +#include + +class Square { +public: + + using Coordinate = unsigned; + using Index = unsigned; + using Optional = std::optional; + + static Optional fromCoordinates(Coordinate file, Coordinate rank); + static Optional fromIndex(Index index); + static Optional fromName(const std::string& name); + + Coordinate file() const; + Coordinate rank() const; + Index index() const; + + static const Square A1, B1, C1, D1, E1, F1, G1, H1; + static const Square A2, B2, C2, D2, E2, F2, G2, H2; + static const Square A3, B3, C3, D3, E3, F3, G3, H3; + static const Square A4, B4, C4, D4, E4, F4, G4, H4; + static const Square A5, B5, C5, D5, E5, F5, G5, H5; + static const Square A6, B6, C6, D6, E6, F6, G6, H6; + static const Square A7, B7, C7, D7, E7, F7, G7, H7; + static const Square A8, B8, C8, D8, E8, F8, G8, H8; + +private: + + Square(Index index); +}; + +std::ostream& operator<<(std::ostream& os, const Square& square); + +// Necessary to support Square as the key in std::map. +bool operator<(const Square& lhs, const Square& rhs); +bool operator==(const Square& lhs, const Square& rhs); + +#endif diff --git a/Tests/BoardTests.cpp b/Tests/BoardTests.cpp new file mode 100644 index 0000000..b488f50 --- /dev/null +++ b/Tests/BoardTests.cpp @@ -0,0 +1,945 @@ +#include "catch2/catch.hpp" + +#include "TestUtils.hpp" + +#include "Board.hpp" +#include "Square.hpp" +#include "Fen.hpp" + +#include +#include +#include +#include + +TEST_CASE("A default-constructed board is empty", "[Board][Fundamental]") { + auto board = Board(); + + for (auto i = 0; i < 64; ++i) { + auto optSquare = Square::fromIndex(i); + REQUIRE(optSquare.has_value()); + + auto square = optSquare.value(); + auto optPiece = board.piece(square); + REQUIRE_FALSE(optPiece.has_value()); + } +} + +TEST_CASE("Pieces can be set on a board", "[Board][Fundamental]") { + auto board = Board(); + + auto optSquare = Square::fromIndex(41); + REQUIRE(optSquare.has_value()); + + auto square = optSquare.value(); + auto piece = Piece(PieceColor::White, PieceType::Rook); + board.setPiece(square, piece); + + auto optSetPiece = board.piece(square); + REQUIRE(optSetPiece.has_value()); + + auto setPiece = optSetPiece.value(); + REQUIRE(setPiece == piece); + + SECTION("Setting a piece on an occupied square overrides") { + auto newPiece = Piece(PieceColor::Black, PieceType::King); + board.setPiece(square, newPiece); + + auto optNewSetPiece = board.piece(square); + REQUIRE(optNewSetPiece.has_value()); + + auto newSetPiece = optNewSetPiece.value(); + REQUIRE(newSetPiece == newPiece); + } +} + +TEST_CASE("The turn can be set on a board", "[Board][Fundamental]") { + auto color = GENERATE(PieceColor::Black, PieceColor::White); + + auto board = Board(); + board.setTurn(color); + REQUIRE(board.turn() == color); +} + +static void testPseudoLegalMoves( + const char* fen, + const std::string& fromName, + const std::vector& expectedTargets) +{ + auto optBoard = Fen::createBoard(fen); + REQUIRE(optBoard.has_value()); + + auto from = Square::Optional(); + + if (!fromName.empty()) { + from = Square::fromName(fromName); + REQUIRE(from.has_value()); + } + + auto board = optBoard.value(); + + using MoveSet = std::set; + auto expectedMoves = MoveSet(); + + for (auto targetName : expectedTargets) { + if (targetName.length() == 2) { + // targetName is a square + auto optTarget = Square::fromName(targetName); + REQUIRE(optTarget.has_value()); + REQUIRE(from.has_value()); + + auto target = optTarget.value(); + auto move = Move(from.value(), target); + expectedMoves.insert(move); + } else { + // targetName is a move + auto optMove = Move::fromUci(targetName); + REQUIRE(optMove.has_value()); + + expectedMoves.insert(optMove.value()); + } + } + + auto generatedMovesVec = Board::MoveVec(); + + if (from.has_value()) { + board.pseudoLegalMovesFrom(from.value(), generatedMovesVec); + } else { + board.pseudoLegalMoves(generatedMovesVec); + } + + auto generatedMoves = MoveSet(generatedMovesVec.begin(), + generatedMovesVec.end()); + + auto unexpectedMoves = MoveSet(); + auto notGeneratedMoves = MoveSet(); + + std::set_difference( + generatedMoves.begin(), generatedMoves.end(), + expectedMoves.begin(), expectedMoves.end(), + std::inserter(unexpectedMoves, unexpectedMoves.begin()) + ); + + std::set_difference( + expectedMoves.begin(), expectedMoves.end(), + generatedMoves.begin(), generatedMoves.end(), + std::inserter(notGeneratedMoves, notGeneratedMoves.begin()) + ); + + CAPTURE(fen, from, unexpectedMoves, notGeneratedMoves); + REQUIRE(generatedMoves == expectedMoves); +} + +#define TEST_CASE_PSEUDO_MOVES(name, tag) \ + TEST_CASE(name, "[Board][MoveGen]" tag) + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal knight moves, empty board", "[Knight]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3N4/8/8/8_w_-_-_0_1 + "8/8/8/8/3N4/8/8/8 w - - 0 1", + "d4", + { + "c6", "e6", + "f5", "f3", + "c2", "e2", + "b3", "b5" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal knight moves, side of board", "[Knight]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/n7/8_b_-_-_0_1 + "8/8/8/8/8/8/n7/8 b - - 0 1", + "a2", + { + "b4", + "c3", "c1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal knight moves, captures", "[Knight]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/2p1P3/8/2PN4/2Pp4/8/8_w_-_-_0_1 + "8/8/2p1P3/8/2PN4/2Pp4/8/8 w - - 0 1", + "d4", + { + "c6", + "f5", "f3", + "c2", "e2", + "b3", "b5" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal king moves, empty board", "[King]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3K4/8/8/8_w_-_-_0_1 + "8/8/8/8/3K4/8/8/8 w - - 0 1", + "d4", + { + "c5", "d5", "e5", + "c4", "e4", + "c3", "d3", "e3", + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal king moves, side of board", "[King]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/8/K7_w_-_-_0_1 + "8/8/8/8/8/8/8/K7 w - - 0 1", + "a1", + { + "a2", "b2", + "b1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal king moves, captures", "[King]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/2PK4/3p4/8/8_w_-_-_0_1 + "8/8/8/8/2PK4/3p4/8/8 w - - 0 1", + "d4", + { + "c5", "d5", "e5", + "e4", + "c3", "d3", "e3", + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal bishop moves, empty board", "[Bishop]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3B4/8/8/8_w_-_-_0_1 + "8/8/8/8/3B4/8/8/8 w - - 0 1", + "d4", + { + "c5", "b6", "a7", + "e5", "f6", "g7", "h8", + "c3", "b2", "a1", + "e3", "f2", "g1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal bishop moves, captures", "[Bishop]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/1p3P2/8/3B4/8/8/8_w_-_-_0_1 + "8/8/1p3P2/8/3B4/8/8/8 w - - 0 1", + "d4", + { + "c5", "b6", + "e5", + "c3", "b2", "a1", + "e3", "f2", "g1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal rook moves, empty board", "[Rook]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3R4/8/8/8_w_-_-_0_1 + "8/8/8/8/3R4/8/8/8 w - - 0 1", + "d4", + { + "d5", "d6", "d7", "d8", + "d3", "d2", "d1", + "c4", "b4", "a4", + "e4", "f4", "g4", "h4" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal rook moves, captures", "[Rook]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/3p4/8/3R1P2/8/8/8_w_-_-_0_1 + "8/8/3p4/8/3R1P2/8/8/8 w - - 0 1", + "d4", + { + "d5", "d6", + "d3", "d2", "d1", + "c4", "b4", "a4", + "e4" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal queen moves, empty board", "[Queen]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3Q4/8/8/8_w_-_-_0_1 + "8/8/8/8/3Q4/8/8/8 w - - 0 1", + "d4", + { + "c5", "b6", "a7", + "e5", "f6", "g7", "h8", + "c3", "b2", "a1", + "e3", "f2", "g1", + "d5", "d6", "d7", "d8", + "d3", "d2", "d1", + "c4", "b4", "a4", + "e4", "f4", "g4", "h4" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal queen moves, captures", "[Queen]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/1p3P2/8/1P1Q4/8/3p4/8_w_-_-_0_1 + "8/8/1p3P2/8/1P1Q4/8/3p4/8 w - - 0 1", + "d4", + { + "c5", "b6", + "e5", + "c3", "b2", "a1", + "e3", "f2", "g1", + "d5", "d6", "d7", "d8", + "d3", "d2", + "c4", + "e4", "f4", "g4", "h4" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, step, empty board", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3P4/8/8/8_w_-_-_0_1 + "8/8/8/8/3P4/8/8/8 w - - 0 1", + "d4", + { + "d5" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, step, opponent blocked", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/3p4/3P4/8/8/8_w_-_-_0_1 + "8/8/8/3p4/3P4/8/8/8 w - - 0 1", + "d4", + {} + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, step, self blocked", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/3p4/3b4/8/8/8_b_-_-_0_1 + "8/8/8/3p4/3b4/8/8/8 b - - 0 1", + "d5", + {} + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, capture left", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/2ppP3/3P4/8/8/8_w_-_-_0_1 + "8/8/8/2ppP3/3P4/8/8/8 w - - 0 1", + "d4", + { + "c5" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, capture both", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/3p4/2PRQ3/8/8/8_b_-_-_0_1 + "8/8/8/3p4/2PRQ3/8/8/8 b - - 0 1", + "d5", + { + "c4", "e4" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, base step, empty board", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/3p4/8/8/8/8/8/8_b_-_-_0_1 + "8/3p4/8/8/8/8/8/8 b - - 0 1", + "d7", + { + "d6", "d5" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, base step, blocked 1", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/3p4/3P4/8/8/8/8/8_b_-_-_0_1 + "8/3p4/3P4/8/8/8/8/8 b - - 0 1", + "d7", + {} + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, base step, blocked 2", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/7p/8/7P/8_w_-_-_0_1 + "8/8/8/8/7p/8/7P/8 w - - 0 1", + "h2", + { + "h3" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, black left", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3Pp3/8/8/8_b_-_d3_0_1 + "8/8/8/8/3Pp3/8/8/8 b - d3 0 1", + "e4", + { + "e3", "d3" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, black right", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/2pP4/2P5/8/8_b_-_d3_0_1 + "8/8/8/8/2pP4/2P5/8/8 b - d3 0 1", + "c4", + { + "d3" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, white left", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/2n5/pP6/8/8/8/8_w_-_a6_0_1 + "8/8/2n5/pP6/8/8/8/8 w - a6 0 1", + "b5", + { + "a6", "b6", "c6" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, white right", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/n7/Pp6/8/8/8/8_w_-_b6_0_1 + "8/8/n7/Pp6/8/8/8/8 w - b6 0 1", + "a5", + { + "b6" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, black both", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/5pPp/7P/8/8_b_-_g3_0_1 + "8/8/8/8/5pPp/7P/8/8 b - g3 0 1", + "", + { + "f4f3", "f4g3", + "h4g3" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, white both", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/2r2n2/2PpP3/8/8/8/8_w_-_d6_0_1 + "8/8/2r2n2/2PpP3/8/8/8/8 w - d6 0 1", + "", + { + "c5d6", + "e5d6", "e5e6", "e5f6" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, en passant, wrong square", "[Pawn][EnPassant]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/3p1P2/8/8/8_b_-_f3_0_1 + "8/8/8/8/3p1P2/8/8/8 b - f3 0 1", + "d4", + { + "d3" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, visual en passant, no square", "[Pawn]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/1pPp4/8/8/8/8_w_-_-_0_1 + "8/8/8/1pPp4/8/8/8/8 w - - 0 1", + "c5", + { + "c6" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, white promotions", "[Pawn][Promotion]") { + testPseudoLegalMoves( + // https://lichess.org/editor/5r2/4P3/8/8/8/8/8/8_w_-_-_0_1 + "5r2/4P3/8/8/8/8/8/8 w - - 0 1", + "e7", + { + "e7e8q", "e7e8r", "e7e8b", "e7e8n", + "e7f8q", "e7f8r", "e7f8b", "e7f8n" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal pawn moves, black promotions", "[Pawn][Promotion]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/1p6/2Q5_b_-_-_0_1 + "8/8/8/8/8/8/1p6/2Q5 b - - 0 1", + "b2", + { + "b2b1q", "b2b1r", "b2b1b", "b2b1n", + "b2c1q", "b2c1r", "b2c1b", "b2c1n" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, white kingside", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/3PPP2/3QK2R_w_K_-_0_1 + "8/8/8/8/8/8/3PPP2/3QK2R w K - 0 1", + "e1", + { + "f1", "g1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, white queenside", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/3PPP2/R3KB2_w_Q_-_0_1 + "8/8/8/8/8/8/3PPP2/R3KB2 w Q - 0 1", + "e1", + { + "d1", "c1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, black kingside", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/3qk2r/3ppp2/8/8/8/8/8/8_b_k_-_0_1 + "3qk2r/3ppp2/8/8/8/8/8/8 b k - 0 1", + "e8", + { + "f8", "g8" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, black queenside", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/r3kb2/3ppp2/8/8/8/8/8/8_b_q_-_0_1 + "r3kb2/3ppp2/8/8/8/8/8/8 b q - 0 1", + "e8", + { + "d8", "c8" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, kingside blocked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/3PPP2/3QK1NR_w_K_-_0_1 + "8/8/8/8/8/8/3PPP2/3QK1NR w K - 0 1", + "e1", + { + "f1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, queenside blocked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/rn2kb2/3ppp2/8/8/8/8/8/8_b_q_-_0_1 + "rn2kb2/3ppp2/8/8/8/8/8/8 b q - 0 1", + "e8", + { + "d8" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, kingside attacked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/3qk2r/3ppp2/5N2/8/8/8/8/8_b_k_-_0_1 + "3qk2r/3ppp2/5N2/8/8/8/8/8 b k - 0 1", + "e8", + { + "f8" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, queenside attacked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/5n2/3PPP2/R3KB2_w_Q_-_0_1 + "8/8/8/8/8/5n2/3PPP2/R3KB2 w Q - 0 1", + "e1", + { + "d1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, queenside rook attacked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/r3kb2/3ppp2/8/8/8/5B2/8/8_b_q_-_0_1 + "r3kb2/3ppp2/8/8/8/5B2/8/8 b q - 0 1", + "e8", + { + "d8", "c8" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, kingside rook attacked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/2q5/8/8/8/3PPP2/3QK2R_w_K_-_0_1 + "8/8/2q5/8/8/8/3PPP2/3QK2R w K - 0 1", + "e1", + { + "f1", "g1" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, queenside b-square attacked", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/r3kb2/3ppp2/8/8/8/8/8/1R6_b_q_-_0_1 + "r3kb2/3ppp2/8/8/8/8/8/1R6 b q - 0 1", + "e8", + { + "d8", "c8" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal castling moves, no rights", "[Castling]") { + testPseudoLegalMoves( + // https://lichess.org/editor/r3k2r/8/8/8/8/8/3PPP2/R3K2R_w_Kkq_-_0_1 + "r3k2r/8/8/8/8/8/3PPP2/R3K2R w Kkq - 0 1", + "e1", + { + "f1", "g1", + "d1" + } + ); +} + +static void testMakeMove(const char* fen, const Move& move) { + auto board = Fen::createBoard(fen); + REQUIRE(board.has_value()); + + auto piece = board->piece(move.from()); + REQUIRE(piece.has_value()); + + auto turn = board->turn(); + board->makeMove(move); + REQUIRE(board->turn() == !turn); + + REQUIRE_FALSE(board->piece(move.from()).has_value()); + + auto toPiece = board->piece(move.to()); + REQUIRE(toPiece.has_value()); + REQUIRE(piece == toPiece.value()); +} + +static void testMakeMove(const char* fen, const Move& move, + const std::map& expectedBoard) { + CAPTURE(fen, move); + + auto board = Fen::createBoard(fen); + REQUIRE(board.has_value()); + + auto turn = board->turn(); + board->makeMove(move); + REQUIRE(board->turn() == !turn); + + for (auto [square, expectedPiece] : expectedBoard) { + auto piece = board->piece(square); + INFO("Expected piece not on board"); + CAPTURE(square, piece, expectedPiece); + REQUIRE(piece.has_value()); + REQUIRE(piece.value() == expectedPiece); + } + + for (auto i = 0; i < 64; ++i) { + auto square = Square::fromIndex(i); + REQUIRE(square.has_value()); + + auto piece = board->piece(square.value()); + CAPTURE(square.value(), piece); + auto expectedPieceIt = expectedBoard.find(square.value()); + + if (piece.has_value()) { + INFO("Unexpected piece on board"); + REQUIRE(expectedPieceIt != expectedBoard.end()); + CAPTURE(expectedPieceIt->second); + REQUIRE(piece.value() == expectedPieceIt->second); + } + } +} + +static void testMakeMove(const char* fen, const Move& move, CastlingRights cr) { + auto board = Fen::createBoard(fen); + REQUIRE(board.has_value()); + + auto turn = board->turn(); + board->makeMove(move); + REQUIRE(board->turn() == !turn); + + REQUIRE(board->castlingRights() == cr); +} + +static void testMakeMove(const char* fen, const Move& move, + const Square::Optional& epSquare) { + auto board = Fen::createBoard(fen); + REQUIRE(board.has_value()); + + auto turn = board->turn(); + board->makeMove(move); + REQUIRE(board->turn() == !turn); + + REQUIRE(board->enPassantSquare() == epSquare); +} + +#define TEST_CASE_MAKE_MOVE(name, tag) \ + TEST_CASE(name, "[Board][MoveMaking]" tag) + +TEST_CASE_MAKE_MOVE("Move making moves a piece on the board", "") { + // https://lichess.org/editor/8/8/8/8/8/8/8/2Q5_w_-_-_0_1 + testMakeMove( + "8/8/8/8/8/8/8/2Q5 w - - 0 1", + Move(Square::C1, Square::F4) + ); +} + +TEST_CASE_MAKE_MOVE("Move making supports captures", "") { + // https://lichess.org/editor/8/8/8/2n5/8/3P4/8/8_b_-_-_0_1 + testMakeMove( + "8/8/8/2n5/8/3P4/8/8 b - - 0 1", + Move(Square::C5, Square::D3) + ); +} + +TEST_CASE_MAKE_MOVE("Move making, white promotion", "[Promotion]") { + // https://lichess.org/editor/8/4P3/8/8/8/8/8/8_w_-_-_0_1 + testMakeMove( + "8/4P3/8/8/8/8/8/8 w - - 0 1", + Move(Square::E7, Square::E8, PieceType::Queen), + { + {Square::E8, Piece(PieceColor::White, PieceType::Queen)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, black promotion", "[Promotion]") { + // https://lichess.org/editor/8/8/8/8/8/8/p7/8_b_-_-_0_1 + testMakeMove( + "8/8/8/8/8/8/p7/8 b - - 0 1", + Move(Square::A2, Square::A1, PieceType::Knight), + { + {Square::A1, Piece(PieceColor::Black, PieceType::Knight)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling white kingside", "[Castling]") { + // https://lichess.org/editor/8/8/8/8/8/8/8/4K2R_w_K_-_0_1 + testMakeMove( + "8/8/8/8/8/8/8/4K2R w K - 0 1", + Move(Square::E1, Square::G1), + { + {Square::G1, Piece(PieceColor::White, PieceType::King)}, + {Square::F1, Piece(PieceColor::White, PieceType::Rook)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling white queenside", "[Castling]") { + // https://lichess.org/editor/8/8/8/8/8/8/8/R3K3_w_Q_-_0_1 + testMakeMove( + "8/8/8/8/8/8/8/R3K3 w Q - 0 1", + Move(Square::E1, Square::C1), + { + {Square::C1, Piece(PieceColor::White, PieceType::King)}, + {Square::D1, Piece(PieceColor::White, PieceType::Rook)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling black kingside", "[Castling]") { + // https://lichess.org/editor/4k2r/8/8/8/8/8/8/8_b_k_-_0_1 + testMakeMove( + "4k2r/8/8/8/8/8/8/8 b k - 0 1", + Move(Square::E8, Square::G8), + { + {Square::G8, Piece(PieceColor::Black, PieceType::King)}, + {Square::F8, Piece(PieceColor::Black, PieceType::Rook)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling black queenside", "[Castling]") { + // https://lichess.org/editor/r3k3/8/8/8/8/8/8/8_b_q_-_0_1 + testMakeMove( + "r3k3/8/8/8/8/8/8/8 b q - 0 1", + Move(Square::E8, Square::C8), + { + {Square::C8, Piece(PieceColor::Black, PieceType::King)}, + {Square::D8, Piece(PieceColor::Black, PieceType::Rook)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling rights king move", "[Castling]") { + // https://lichess.org/editor/r3k2r/8/8/8/8/8/8/R3K2R_w_KQkq_-_0_1 + testMakeMove( + "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1", + Move(Square::E1, Square::D1), + CastlingRights::Black + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling rights rook move", "[Castling]") { + // https://lichess.org/editor/r3k2r/8/8/8/8/8/8/R3K2R_b_KQkq_-_0_1 + testMakeMove( + "r3k2r/8/8/8/8/8/8/R3K2R b KQkq - 0 1", + Move(Square::H8, Square::G8), + CastlingRights::White | CastlingRights::BlackQueenside + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling rights rook capture", "[Castling]") { + // https://lichess.org/editor/r3k2r/8/8/8/8/8/8/R3K2R_w_KQkq_-_0_1 + testMakeMove( + "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1", + Move(Square::A1, Square::A8), + CastlingRights::WhiteKingside | CastlingRights::BlackKingside + ); +} + +TEST_CASE_MAKE_MOVE("Move making, castling rights king back to base", "[Castling]") { + // https://lichess.org/editor/r3k2r/8/8/8/8/8/8/R2K3R_w_kq_-_0_1 + testMakeMove( + "r3k2r/8/8/8/8/8/8/R2K3R w kq - 0 1", + Move(Square::D1, Square::E1), + CastlingRights::Black + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant, black left", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/8/5Pp1/8/8/8_b_-_f3_0_1 + testMakeMove( + "8/8/8/8/5Pp1/8/8/8 b - f3 0 1", + Move(Square::G4, Square::F3), + { + {Square::F3, Piece(PieceColor::Black, PieceType::Pawn)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant, black right", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/3P4/1PpPP3/2P5/8/8_b_-_d3_0_1 + testMakeMove( + "8/8/8/3P4/1PpPP3/2P5/8/8 b - d3 0 1", + Move(Square::C4, Square::D3), + { + {Square::D3, Piece(PieceColor::Black, PieceType::Pawn)}, + {Square::B4, Piece(PieceColor::White, PieceType::Pawn)}, + {Square::C3, Piece(PieceColor::White, PieceType::Pawn)}, + {Square::D5, Piece(PieceColor::White, PieceType::Pawn)}, + {Square::E4, Piece(PieceColor::White, PieceType::Pawn)}, + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant, white left", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/3pPp2/8/8/8/8_w_-_d6_0_1 + testMakeMove( + "8/8/8/3pPp2/8/8/8/8 w - d6 0 1", + Move(Square::E5, Square::D6), + { + {Square::D6, Piece(PieceColor::White, PieceType::Pawn)}, + {Square::F5, Piece(PieceColor::Black, PieceType::Pawn)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant, white right", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/1Pp5/8/8/8/8_w_-_c6_0_1 + testMakeMove( + "8/8/8/1Pp5/8/8/8/8 w - c6 0 1", + Move(Square::B5, Square::C6), + { + {Square::C6, Piece(PieceColor::White, PieceType::Pawn)} + } + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant square, white double step update", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/8/3p4/8/4P3/8_w_-_-_0_1 + testMakeMove( + "8/8/8/8/3p4/8/4P3/8 w - - 0 1", + Move(Square::E2, Square::E4), + Square::E3 + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant square, black double step update", "[EnPassant]") { + // https://lichess.org/editor/8/p7/8/1P6/8/8/8/8_b_-_-_0_1 + testMakeMove( + "8/p7/8/1P6/8/8/8/8 b - - 0 1", + Move(Square::A7, Square::A5), + Square::A6 + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant square, remove after capture", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/8/Pp6/8/8/8_b_-_a3_0_1 + testMakeMove( + "8/8/8/8/Pp6/8/8/8 b - a3 0 1", + Move(Square::B4, Square::A3), + std::nullopt + ); +} + +TEST_CASE_MAKE_MOVE("Move making, en passant square, remove after move", "[EnPassant]") { + // https://lichess.org/editor/8/8/8/4pP2/8/8/1N6/8_w_-_e6_0_1 + testMakeMove( + "8/8/8/4pP2/8/8/1N6/8 w - e6 0 1", + Move(Square::B2, Square::C4), + std::nullopt + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal moves, multiple pieces, white", "") { + testPseudoLegalMoves( + // https://lichess.org/editor/8/8/8/8/8/8/2P5/NB6_w_-_-_0_1 + "8/8/8/8/8/8/2P5/NB6 w - - 0 1", + "", + { + "a1b3", + "b1a2", + "c2c3", "c2c4" + } + ); +} + +TEST_CASE_PSEUDO_MOVES("Pseudo-legal moves, multiple pieces, black", "") { + testPseudoLegalMoves( + // https://lichess.org/editor/7n/7b/6p1/8/8/8/8/8_b_-_-_0_1 + "7n/7b/6p1/8/8/8/8/8 b - - 0 1", + "", + { + "h8f7", + "h7g8", + "g6g5" + } + ); +} diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt new file mode 100644 index 0000000..9a231f7 --- /dev/null +++ b/Tests/CMakeLists.txt @@ -0,0 +1,16 @@ +add_subdirectory(Catch2/) + +add_executable(tests + Main.cpp + SquareTests.cpp + MoveTests.cpp + PieceTests.cpp + BoardTests.cpp + FenTests.cpp + EngineTests.cpp +) + +target_link_libraries(tests cplchess_lib Catch2::Catch2) + +include(Catch2/contrib/Catch.cmake) +catch_discover_tests(tests) diff --git a/Tests/Catch2 b/Tests/Catch2 new file mode 160000 index 0000000..c4e3767 --- /dev/null +++ b/Tests/Catch2 @@ -0,0 +1 @@ +Subproject commit c4e3767e265808590986d5db6ca1b5532a7f3d13 diff --git a/Tests/EngineTests.cpp b/Tests/EngineTests.cpp new file mode 100644 index 0000000..a62ac57 --- /dev/null +++ b/Tests/EngineTests.cpp @@ -0,0 +1,48 @@ +#include "catch2/catch.hpp" + +#include "TestUtils.hpp" + +#include "EngineFactory.hpp" +#include "Engine.hpp" +#include "Fen.hpp" +#include "Board.hpp" + +static std::unique_ptr createEngine() { + return EngineFactory::createEngine(); +} + +static void testGameEnd(const char* fen, bool isMate) { + auto engine = createEngine(); + REQUIRE(engine != nullptr); + + auto board = Fen::createBoard(fen); + REQUIRE(board.has_value()); + + auto pv = engine->pv(board.value()); + + REQUIRE(pv.isMate() == isMate); + REQUIRE(pv.score() == 0); + REQUIRE(pv.length() == 0); +} + +TEST_CASE("Engine detects checkmate", "[Engine][Checkmate]") { + auto fen = GENERATE( + // https://lichess.org/editor/4R2k/6pp/8/8/8/8/8/K7_b_-_-_0_1 + "4R2k/6pp/8/8/8/8/8/K7 b - - 0 1", + // https://lichess.org/editor/7k/8/8/8/1bb5/8/r7/3BK3_w_-_-_0_1 + "7k/8/8/8/1bb5/8/r7/3BK3 w - - 0 1" + ); + + testGameEnd(fen, true); +} + +TEST_CASE("Engine detects stalemate", "[Engine][Stalemate]") { + auto fen = GENERATE( + // https://lichess.org/editor/k7/p7/P7/8/8/8/8/KR6_b_-_-_0_1 + "k7/p7/P7/8/8/8/8/KR6 b - - 0 1", + // https://lichess.org/editor/k7/8/8/6n1/r7/4K3/2q5/8_w_-_-_0_1 + "k7/8/8/6n1/r7/4K3/2q5/8 w - - 0 1" + ); + + testGameEnd(fen, false); +} diff --git a/Tests/FenTests.cpp b/Tests/FenTests.cpp new file mode 100644 index 0000000..f44d5c4 --- /dev/null +++ b/Tests/FenTests.cpp @@ -0,0 +1,117 @@ +#include "catch2/catch.hpp" + +#include "TestUtils.hpp" + +#include "Fen.hpp" + +#include + +TEST_CASE("Legal placements are correctly parsed", "[Fen]") { + // https://lichess.org/editor/k7/2B5/7p/1q3P2/8/3N4/8/5r2_w_-_-_0_1 + auto fen = "k7/2B5/7p/1q3P2/8/3N4/8/5r2 w - - 0 1"; + + auto optBoard = Fen::createBoard(fen); + REQUIRE(optBoard.has_value()); + + auto board = optBoard.value(); + + auto expectedPlacement = std::map{ + {Square::F1, Piece(PieceColor::Black, PieceType::Rook)}, + {Square::D3, Piece(PieceColor::White, PieceType::Knight)}, + {Square::B5, Piece(PieceColor::Black, PieceType::Queen)}, + {Square::F5, Piece(PieceColor::White, PieceType::Pawn)}, + {Square::H6, Piece(PieceColor::Black, PieceType::Pawn)}, + {Square::C7, Piece(PieceColor::White, PieceType::Bishop)}, + {Square::A8, Piece(PieceColor::Black, PieceType::King)} + }; + + for (auto index = 0; index < 64; ++index) { + auto optSquare = Square::fromIndex(index); + REQUIRE(optSquare.has_value()); + + auto square = optSquare.value(); + INFO("Checking square " << square); + + auto expectedPieceIt = expectedPlacement.find(square); + auto optActualPiece = board.piece(square); + + if (expectedPieceIt == expectedPlacement.end()) { + INFO("Expecting empty") + REQUIRE_FALSE(optActualPiece.has_value()); + } else { + auto expectedPiece = expectedPieceIt->second; + INFO("Expecting " << expectedPiece); + + REQUIRE(optActualPiece.has_value()); + + auto actualPiece = optActualPiece.value(); + REQUIRE(actualPiece == expectedPiece); + } + } +} + +TEST_CASE("Legal turns are correctly parsed", "[Fen]") { + auto [fen, turn] = GENERATE(table({ + // https://lichess.org/editor/8/8/8/8/8/8/8/8_w_-_-_0_1 + {"8/8/8/8/8/8/8/8 w - - 0 1", PieceColor::White}, + // https://lichess.org/editor/8/8/8/8/8/8/8/8_b_-_-_0_1 + {"8/8/8/8/8/8/8/8 b - - 0 1", PieceColor::Black} + })); + + auto optBoard = Fen::createBoard(fen); + REQUIRE(optBoard.has_value()); + + auto board = optBoard.value(); + REQUIRE(board.turn() == turn); +} + +TEST_CASE("Castling rights are correctly parsed", "[Fen]") { + auto [fen, cr] = GENERATE(table({ + // https://lichess.org/editor/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR_w_KQkq_-_0_1 + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + CastlingRights::All + }, + // https://lichess.org/editor/8/8/8/8/8/8/8/8_b_-_-_0_1 + { + "8/8/8/8/8/8/8/8 b - - 0 1", + CastlingRights::None + }, + // https://lichess.org/editor/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR_w_Qk_-_0_1 + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w Qk - 0 1", + CastlingRights::WhiteQueenside | CastlingRights::BlackKingside + } + })); + + CAPTURE(fen, cr); + + auto optBoard = Fen::createBoard(fen); + REQUIRE(optBoard.has_value()); + + auto board = optBoard.value(); + REQUIRE(board.castlingRights() == cr); +} + +TEST_CASE("En passant square is correctly parsed", "[Fen][EnPassant]") { + auto [fen, ep] = GENERATE(table({ + // https://lichess.org/editor/rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR_w_KQkq_-_0_1 + { + "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", + std::nullopt + }, + // https://lichess.org/editor/rnbqkbnr/ppppp1pp/8/8/2PPPp2/8/PP3PPP/RNBQKBNR_b_KQkq_e3_0_3 + { + "rnbqkbnr/ppppp1pp/8/8/2PPPp2/8/PP3PPP/RNBQKBNR b KQkq e3 0 3", + Square::E3 + } + })); + + CAPTURE(fen, ep); + + auto optBoard = Fen::createBoard(fen); + REQUIRE(optBoard.has_value()); + + auto board = optBoard.value(); + REQUIRE(board.enPassantSquare() == ep); +} diff --git a/Tests/Main.cpp b/Tests/Main.cpp new file mode 100644 index 0000000..62bf747 --- /dev/null +++ b/Tests/Main.cpp @@ -0,0 +1,2 @@ +#define CATCH_CONFIG_MAIN +#include "catch2/catch.hpp" diff --git a/Tests/MoveTests.cpp b/Tests/MoveTests.cpp new file mode 100644 index 0000000..9f30abe --- /dev/null +++ b/Tests/MoveTests.cpp @@ -0,0 +1,111 @@ +#include "catch2/catch.hpp" + +#include "TestUtils.hpp" + +#include "Move.hpp" + +#include + +TEST_CASE("Moves store the squares they are constructed with", "[Move][Fundamental]") { + auto from = Square::E2; + auto to = Square::G6; + auto move = Move(from, to); + REQUIRE(move.from() == from); + REQUIRE(move.to() == to); + REQUIRE_FALSE(move.promotion().has_value()); +} + +TEST_CASE("Moves store the promotion they are constructed with", "[Move][Promotion]") { + auto from = Square::A7; + auto to = Square::A8; + auto promotion = PieceType::Knight; + auto move = Move(from, to, promotion); + REQUIRE(move.from() == from); + REQUIRE(move.to() == to); + REQUIRE(move.promotion().has_value()); + REQUIRE(move.promotion().value() == promotion); +} + +TEST_CASE("Moves support equality checks", "[Move][Fundamental]") { + auto move1 = Move(Square::D4, Square::H7); + auto move2 = Move(Square::D4, Square::G3); + auto move3 = Move(Square::A3, Square::H7); + auto move1Copy = move1; + REQUIRE(move1 == move1Copy); + REQUIRE_FALSE(move1 == move2); + REQUIRE_FALSE(move1 == move3); +} + +TEST_CASE("Moves with promotions support equality checks", "[Move][Promotion]") { + auto move1 = Move(Square::B2, Square::B1, PieceType::Queen); + auto move2 = Move(Square::B2, Square::B1); + auto move3 = Move(Square::C2, Square::C1, PieceType::Queen); + auto move1Copy = move1; + REQUIRE(move1 == move1Copy); + REQUIRE_FALSE(move1 == move2); + REQUIRE_FALSE(move1 == move3); +} + +TEST_CASE("Moves stream their UCI correctly", "[Move][Fundamental]") { + auto [from, to, uci] = GENERATE(table({ + {Square::A1, Square::D7, "a1d7"}, + {Square::C8, Square::F2, "c8f2"}, + {Square::H6, Square::B1, "h6b1"} + })); + + auto move = Move(from, to); + auto stream = std::stringstream(); + stream << move; + REQUIRE(stream.str() == uci); +} + +TEST_CASE("Moves with promotions stream their UCI correctly", "[Move][Promotion]") { + auto [from, to, promotion, uci] = GENERATE(table({ + {Square::H7, Square::H8, PieceType::Knight, "h7h8n"}, + {Square::C2, Square::C1, PieceType::Rook, "c2c1r"}, + {Square::E7, Square::E8, PieceType::Queen, "e7e8q"} + })); + + auto move = Move(from, to, promotion); + auto stream = std::stringstream(); + stream << move; + REQUIRE(stream.str() == uci); +} + +TEST_CASE("Moves can be created from valid UCI notation", "[Move][Fundamental]") { + auto [uci, from, to] = GENERATE(table({ + {"a1d7", Square::A1, Square::D7}, + {"c8f2", Square::C8, Square::F2}, + {"h6b1", Square::H6, Square::B1} + })); + + auto move = Move::fromUci(uci); + auto expectedMove = Move(from, to); + + CAPTURE(uci, expectedMove, move); + REQUIRE(move.has_value()); + REQUIRE(move.value() == expectedMove); +} + +TEST_CASE("Moves can be created from valid UCI notation with promotion", "[Move][Promotion]") { + auto [uci, from, to, promotion] = GENERATE(table({ + {"a7a8n", Square::A7, Square::A8, PieceType::Knight}, + {"c2c1b", Square::C2, Square::C1, PieceType::Bishop}, + {"h7h8r", Square::H7, Square::H8, PieceType::Rook} + })); + + auto move = Move::fromUci(uci); + auto expectedMove = Move(from, to, promotion); + + CAPTURE(uci, expectedMove, move); + REQUIRE(move.has_value()); + REQUIRE(move.value() == expectedMove); +} + +TEST_CASE("Moves are not created from invalid UCI notation", "[Move][Fundamental]") { + auto uci = GENERATE("a1d7x", "a1d", "a1d7123", "a1", "", "a9d1", "a1c0"); + auto move = Move::fromUci(uci); + + CAPTURE(uci, move); + REQUIRE_FALSE(move.has_value()); +} diff --git a/Tests/PieceTests.cpp b/Tests/PieceTests.cpp new file mode 100644 index 0000000..8b9eede --- /dev/null +++ b/Tests/PieceTests.cpp @@ -0,0 +1,74 @@ +#include "catch2/catch.hpp" + +#include "Piece.hpp" + +#include +#include + +TEST_CASE("Pieces store color and type correctly", "[Piece][Fundamental]") { + auto color = PieceColor::Black; + auto type = PieceType::Queen; + auto piece = Piece(color, type); + REQUIRE(piece.color() == color); + REQUIRE(piece.type() == type); +} + +TEST_CASE("Pieces can be created from valid symbols", "[Piece][Fundamental]") { + auto [blackSymbol, pieceType] = GENERATE(table({ + {'p', PieceType::Pawn}, + {'n', PieceType::Knight}, + {'b', PieceType::Bishop}, + {'r', PieceType::Rook}, + {'q', PieceType::Queen}, + {'k', PieceType::King} + })); + + auto optBlackPiece = Piece::fromSymbol(blackSymbol); + REQUIRE(optBlackPiece.has_value()); + + auto createdBlackPiece = optBlackPiece.value(); + REQUIRE(createdBlackPiece == Piece(PieceColor::Black, pieceType)); + + auto whiteSymbol = static_cast(std::toupper(blackSymbol)); + auto optWhitePiece = Piece::fromSymbol(whiteSymbol); + REQUIRE(optWhitePiece.has_value()); + + auto createdWhitePiece = optWhitePiece.value(); + REQUIRE(createdWhitePiece == Piece(PieceColor::White, pieceType)); +} + +TEST_CASE("Pieces are not created from invalid symbols", "[Piece][Fundamental]") { + auto symbol = GENERATE('x', 'Z'); + + auto optPiece = Piece::fromSymbol(symbol); + REQUIRE_FALSE(optPiece.has_value()); +} + +TEST_CASE("Pieces stream their symbol correctly", "[Piece][Fundamental]") { + auto [blackSymbol, pieceType] = GENERATE(table({ + {'p', PieceType::Pawn}, + {'n', PieceType::Knight}, + {'b', PieceType::Bishop}, + {'r', PieceType::Rook}, + {'q', PieceType::Queen}, + {'k', PieceType::King} + })); + + auto blackStream = std::stringstream(); + auto blackPiece = Piece(PieceColor::Black, pieceType); + blackStream << blackPiece; + auto blackSymbolStr = std::string(1, blackSymbol); + REQUIRE(blackStream.str() == blackSymbolStr); + + auto whiteStream = std::stringstream(); + auto whitePiece = Piece(PieceColor::White, pieceType); + whiteStream << whitePiece; + auto whiteSymbol = static_cast(std::toupper(blackSymbol)); + auto whiteSymbolStr = std::string(1, whiteSymbol); + REQUIRE(whiteStream.str() == whiteSymbolStr); +} + +TEST_CASE("PieceColor can be inverted correctly", "[Piece][Fundamental]") { + REQUIRE((!PieceColor::White) == PieceColor::Black); + REQUIRE((!PieceColor::Black) == PieceColor::White); +} diff --git a/Tests/Puzzles/crushing_castling.csv b/Tests/Puzzles/crushing_castling.csv new file mode 100644 index 0000000..1d0233a --- /dev/null +++ b/Tests/Puzzles/crushing_castling.csv @@ -0,0 +1,20 @@ +1nIpz,4r3/pppkn3/8/3p2R1/3P4/8/PPP2P1b/R3K3 w Q - 1 21,e1c1 h2f4 c1b1 f4g5,816,95,90,588,crushing endgame fork short,https://lichess.org/hNgUKDs9#41 +Mu4H8,2r1k2r/1p3ppp/p1nqp3/3N4/6P1/3Q3P/PPP2P2/R3R1K1 b k - 0 18,e8g8 d5f6 g7f6 d3d6,1242,76,93,2547,crushing discoveredAttack middlegame short,https://lichess.org/4TidVneQ/black#36 +tXqKo,r1b1k2r/1pB2ppp/p7/n2q4/8/3B1N2/P5PP/RN1Q1K2 b kq - 0 19,e8g8 d3h7 g8h7 d1d5,1119,83,94,746,crushing discoveredAttack kingsideAttack opening short,https://lichess.org/7uOa49aA/black#38 +w24YE,r3k1r1/pbp1qp2/1pn2bp1/1N2p3/4Q3/PP2P1P1/RBPP2B1/4K2R b Kq - 0 20,e8c8 b5a7 c6a7 e4b7,1418,76,96,4838,crushing middlegame queensideAttack short,https://lichess.org/Xg4JUKxt/black#40 +3FThf,2kr1r2/pp3ppp/2n2nq1/4p3/8/2PP1Q2/PP2NPPP/RN2K2R w KQ - 2 16,e1g1 d8d3 f3d3 g6d3,1468,76,85,526,crushing middlegame short trappedPiece,https://lichess.org/IsNuAAN9#31 +jJ2Y6,r3kb1r/ppp2ppp/1nb5/6q1/3Q4/2P2P2/PP1NN1PP/R1B1K2R w KQkq - 1 12,e1g1 f8c5 d2b3 c5d4,1251,79,92,1234,crushing opening pin short,https://lichess.org/jXD6blSd#23 +RgLWD,r4r1k/1pp1q1pp/p2b1pn1/8/P2P4/1PQ1PN2/1B3PPP/R3K2R w KQ - 3 17,d4d5 d6b4 e1g1 b4c3,1061,76,91,782,crushing middlegame pin short,https://lichess.org/5BMsYBNs#33 +Oy4HD,4r1k1/1ppnqpb1/p5pp/3p4/1n1P2PP/PPQ2P2/1B1NN3/R3K2R w KQ - 1 20,e1c1 b4a2 c1b1 a2c3,1022,75,95,2356,crushing fork middlegame short,https://lichess.org/rLlXAUHf#39 +mYVLz,r3k2r/pp1q1pp1/2n4p/3Nb3/2P1pN2/P7/R4PPP/3Q1RK1 b kq - 0 16,e8g8 d5f6 g7f6 d1d7,1228,75,96,4950,crushing discoveredAttack middlegame short,https://lichess.org/4Td7Ylh7/black#32 +Xru4M,r1b1k2r/ppp2ppp/8/2bq4/8/3B4/PPPQ1PPP/R1B2RK1 b kq - 1 11,e8g8 d3h7 g8h7 d2d5,832,80,98,741,crushing discoveredAttack kingsideAttack middlegame short,https://lichess.org/SUQJEod2/black#22 +O3mf8,r1bqk2r/pp3pp1/2nb3p/2pQ4/3P4/2P2N2/PP3PPP/RNB1K2R w KQkq - 0 11,e1g1 d6h2 f3h2 d8d5,992,77,98,503,crushing discoveredAttack kingsideAttack opening short,https://lichess.org/25COtgWc#21 +YZphi,1rb2rk1/p4p1p/4p1p1/q1ppb3/6PP/1PN5/P1PQBB2/R3K2R w KQ - 0 17,e1c1 e5c3 d2c3 a5c3,1455,76,73,794,crushing master middlegame short,https://lichess.org/YUCwTutk#33 +Xp0Fu,rnbqk2r/pp1p1ppp/2p1p3/3n4/1b1P4/1P1BPN2/P1P2PPP/RNBQK2R w KQkq - 1 6,b1d2 d5c3 e1g1 c3d1,1291,74,98,1101,crushing opening short trappedPiece,https://lichess.org/ZLMwfhTb#11 +gSuce,r3k2r/ppp2p1p/2q3p1/3N1b2/4P2P/2bQ4/P1P2PP1/1R1K1B1R b kq - 0 15,e8c8 d5e7 c8b8 e7c6,978,93,97,502,crushing fork middlegame pin short,https://lichess.org/s986T8cn/black#30 +ojyVA,r4rk1/1pp2ppp/p7/8/3nq3/P3N1Q1/BPP2PP1/R3KR2 w Q - 5 19,e1c1 d4e2 c1b1 e2g3,1467,75,99,1838,crushing fork middlegame short,https://lichess.org/9QGJ7rZV#37 +JRXLt,2q1r1k1/5pbp/r4np1/8/1B2P1b1/Pp3PN1/1P1Q3P/R2NK2R w KQ - 0 19,f3g4 f6e4 e1g1 e4d2,1400,75,93,653,crushing master middlegame short,https://lichess.org/uOcxibZc#37 +Mqwig,r3k3/pp1q4/3p3Q/2p1p3/2Pn4/2NP2P1/PP1K2BP/8 b q - 0 22,e8c8 g2h3 d7h3 h6h3,1459,74,95,7529,crushing endgame pin short,https://lichess.org/eMX64gmi/black#44 +or8tr,r1bq3r/pp2kppp/3bpn2/1B6/3Q4/2N5/PPP2PPP/R1B1K2R w KQ - 3 10,e1g1 d6h2 g1h2 d8d4,1251,75,97,2368,crushing discoveredAttack kingsideAttack opening short,https://lichess.org/aTyyc4D6#19 +ufB48,r3k2r/pp2pp2/n4b1p/1Npq2p1/8/3B1P2/2P2P1P/1R1Q1RK1 b kq - 0 16,e8g8 d3h7 g8h7 d1d5,1044,76,100,578,crushing discoveredAttack master middlegame short,https://lichess.org/4O7cI2vX/black#32 +PPhgy,2r1k2r/pp1b1ppp/4p2n/3pP3/1b1q1P2/2NB4/PP1Q2PP/R1B2R1K b k - 5 13,e8g8 d3h7 g8h7 d2d4,1120,79,89,831,crushing discoveredAttack kingsideAttack middlegame short,https://lichess.org/NDtiGLde/black#26 diff --git a/Tests/Puzzles/crushing_enPassant.csv b/Tests/Puzzles/crushing_enPassant.csv new file mode 100644 index 0000000..44f3611 --- /dev/null +++ b/Tests/Puzzles/crushing_enPassant.csv @@ -0,0 +1,20 @@ +LjCmb,1k1r3r/pp4Rp/1n2Bp2/2pPq3/1Q2N3/P6P/1P6/K2R4 w - c6 0 29,d5c6 d8d1 a1a2 e5e6,1177,76,94,1259,crushing hangingPiece middlegame short,https://lichess.org/Yk2zG0UY#57 +04bSF,8/8/4kp2/2pPp1p1/1p4P1/1P1PK3/r1P1RP2/8 b - - 0 40,e6d5 c2c4 b4c3 e2a2,1055,78,94,722,crushing discoveredAttack endgame rookEndgame short,https://lichess.org/Ecn7TQaC/black#80 +AF9UO,2kr2nr/ppp3pp/2bb1p2/5q2/3Pp3/1BP3P1/PP3P1P/RNBQR1K1 b - d3 0 13,e4d3 b3e6 f5e6 e1e6,976,78,99,1079,crushing fork opening short,https://lichess.org/fVTwPwnn/black#26 +DeL19,2b2rk1/p4ppp/2p1p3/2qpP3/5B1P/3B1PPK/1r6/R2Q3R w - d6 0 24,e5d6 e6e5 g3g4 e5f4,1338,76,93,539,crushing discoveredAttack middlegame short,https://lichess.org/xu3NKiH3#47 +m2n9Q,2r5/B2q2bp/2Np4/1Q1Ppkp1/2P2pP1/5P2/P6P/6K1 b - g3 0 33,f4g3 c6d4 e5d4 b5d7,1426,75,97,7352,crushing discoveredAttack endgame short,https://lichess.org/mYaK9ddh/black#66 +r2rqH,1B6/7p/2p1k1p1/1pKpnpP1/8/8/PPP2P2/8 w - f6 0 33,g5f6 e5d7 c5c6 d7b8,1329,77,91,528,crushing endgame fork short,https://lichess.org/3jItgkvn#65 +t18DC,8/7p/3k2p1/8/1p1p2P1/1P1P1K2/r1P2R2/8 b - - 2 44,d6d5 c2c4 d4c3 f2a2,1060,83,83,516,crushing discoveredAttack endgame rookEndgame short,https://lichess.org/RIDIKGiy/black#88 +Gc8wi,4rk2/1p5p/2p1q1pP/1p1pPp2/rP1P2P1/P1R1QPK1/8/4R3 w - f6 0 34,e5f6 e6d6 e3e5 e8e5,1417,74,95,6861,crushing endgame short,https://lichess.org/YiVM7jB1#67 +RCtAF,r1bq1rk1/1p3pp1/p1np4/2p1pPPp/P1B1P1n1/3P4/1PP3P1/RNBQ1RK1 w - h6 0 12,g5h6 d8h4 d1g4 h4g4,1372,76,91,547,crushing middlegame short,https://lichess.org/rXkJ3qWC#23 +yLkJ8,2r2rk1/3q1ppp/3b4/2pP3b/3QP3/2N5/PP3PPP/R1B2RK1 w - c6 0 17,d5c6 d6h2 g1h2 d7d4,1278,74,90,1312,crushing discoveredAttack kingsideAttack middlegame short,https://lichess.org/zu4W2Xjc#33 +hQiNO,8/2r2p1R/pk2p3/4P3/1P1K1P2/1P6/8/8 w - - 5 45,d4e4 f7f5 e5f6 c7h7,1226,75,98,3368,crushing discoveredAttack endgame rookEndgame short,https://lichess.org/kiGIgx3X#89 +XcSfQ,8/R4pr1/2p3k1/1p4Pp/3P1P1P/P4K2/1P6/8 w - - 1 34,f3e4 f7f5 g5f6 g7a7,1435,74,95,7150,crushing discoveredAttack endgame rookEndgame short,https://lichess.org/H2FyMIZK#67 +mqUlP,r3r1k1/bb1q1pp1/p6p/2p5/1PPp4/P2BnP2/3QNBPP/R3R2K b - c3 0 22,d4c3 d3h7 g8h7 d2d7,1389,75,97,7843,crushing discoveredAttack middlegame short,https://lichess.org/2GkHylJl/black#44 +BlgSP,3r1k2/p2r1p1R/1pn1p1p1/2p3P1/2P1KP2/P1RPP3/5N2/8 w - - 1 34,f2g4 f7f5 g5f6 d7h7,1414,75,97,8105,crushing discoveredAttack endgame fork master short,https://lichess.org/SVDzwDrX#67 +8Wf37,2r3k1/8/1B1Qp1r1/8/4PPp1/P5Pp/1P5P/2q2RK1 b - f3 0 28,g4f3 f1c1 c8c1 g1f2,1131,77,93,2318,crushing endgame short,https://lichess.org/MgBKmMyw/black#56 +x1ST3,8/5p1r/r1pkp1p1/p5P1/P2PKP2/1RR4P/8/8 w - - 6 44,b3b7 f7f5 g5f6 h7b7,1206,75,88,120,crushing discoveredAttack endgame rookEndgame short,https://lichess.org/VKjN7j2C#87 +8zW22,1R6/1P6/5p2/5k2/5Pp1/1r4P1/6K1/8 b - f3 0 67,g4f3 g2f2 b3b2 f2f3,1125,77,93,2162,crushing defensiveMove endgame rookEndgame short,https://lichess.org/IGmSHmFL/black#134 +GNTiD,r1bq1rk1/2p4p/1p1p2p1/p2Pp1N1/P1P1p3/6P1/1P3P1P/R2Q1RK1 w - e6 0 17,d5e6 d8g5 d1d5 g5d5,1172,77,93,560,crushing hangingPiece middlegame short,https://lichess.org/Dvdu67gt#33 +DyB55,8/kpr2p1R/p3p1p1/2b1P1P1/4pP2/1PP2K2/3B4/8 w - - 0 34,f3e4 f7f5 e5f6 c7h7,988,76,95,633,crushing discoveredAttack endgame short,https://lichess.org/E6h2U3g9#67 +a3jh8,4r1k1/p2n2b1/1p1Br1pp/2p1Pp2/P3R3/5N1P/1PP2PP1/4R1K1 w - f6 0 28,e5f6 e6e4 e1e4 e8e4,1156,77,95,3041,crushing middlegame short,https://lichess.org/PMYUPzpW#55 diff --git a/Tests/Puzzles/crushing_simple.csv b/Tests/Puzzles/crushing_simple.csv new file mode 100644 index 0000000..6ed5d4d --- /dev/null +++ b/Tests/Puzzles/crushing_simple.csv @@ -0,0 +1,18 @@ +gjB0I,r1r3k1/p3nppp/5q2/3pp3/3nQ1P1/P1P2N1P/P2PPPB1/R1B1KR2 w Q - 0 15,e4e5 d4f3 g2f3 f6e5,1441,77,96,7017,crushing intermezzo middlegame short,https://lichess.org/NNlhThCX#29 +DjF9F,7k/1q4p1/PP2r2p/8/2Pp4/3p3P/4QPK1/RR6 w - - 0 37,e2f3 e6g6 g2h2 b7f3,1026,79,96,936,crushing deflection endgame pin short,https://lichess.org/ZRdpTIc7#73 +sdK1f,7R/N3p3/4k1b1/6p1/5bP1/1P3P1P/PKP2n2/8 w - - 5 32,h3h4 f4e5 b2b1 e5h8,1350,75,94,4319,crushing endgame fork short,https://lichess.org/jRPMtIwq#63 +c4Q2K,6k1/2pp4/1p1b2p1/p2n1rNp/7P/1P1Q3K/P4P2/5R2 w - - 2 40,g5e4 d5f4 h3h2 f4d3,964,75,100,633,crushing discoveredAttack endgame fork short,https://lichess.org/Wr2XI4kL#79 +JMggH,r1b1k1nr/pp2bBpp/1np5/8/3P4/3q1N2/PP1N1KPP/R1BQ3R b kq - 0 13,e8f7 f3e5 f7f8 e5d3,1062,76,98,531,crushing fork opening short,https://lichess.org/cV3k9SJS/black#26 +UkI37,8/7p/1b4k1/pB6/P2p1BP1/4nK2/8/8 b - - 3 48,e3c2 b5d3,1186,76,94,1406,crushing endgame oneMove,https://lichess.org/6Zy6lEOJ/black#96 +u2uUc,4r3/pp3qk1/2p2n2/3pQP1p/8/2NB4/PPP2bPP/2K2R2 w - - 2 24,e5f4 f2e3 f4e3 e8e3,1382,74,97,6750,crushing fork middlegame short,https://lichess.org/SA2OAz7V#47 +95aih,r1b1r1k1/pp3ppp/3p4/4n3/3p2B1/1P6/P1P2PPP/RN2R1K1 w - - 0 17,g4c8 e5f3 g2f3 e8e1,1180,78,94,4398,crushing discoveredAttack kingsideAttack middlegame short,https://lichess.org/OFPiuoKw#33 +TGtYY,2r2rk1/ppqb1pp1/7p/4p3/4Qn2/1P1B4/PBPP1PPP/2KR3R w - - 6 19,b2e5 f4d3 e4d3 c7e5,1257,75,94,975,crushing middlegame pin queensideAttack short,https://lichess.org/VKOdc5WH#37 +UFDsq,2R5/p2rr1pk/1p2b2p/8/5R1P/4B1P1/P4P2/6K1 w - - 11 30,a2a4 d7d1 g1h2 e6c8,1021,75,98,882,crushing discoveredAttack endgame short,https://lichess.org/6WDvsr9F#59 +EhBCM,6k1/4bp2/p3b1p1/1pp1P3/5P1p/1P2BK1P/P1B3P1/8 w - - 0 28,f3e4 e6f5 e4d5 f5c2,1106,75,99,1353,bishopEndgame crushing endgame master short skewer,https://lichess.org/kSGgZyH6#55 +1udMI,7k/1pp3p1/3p4/p7/6P1/2P1Pr1P/1P2K3/7R b - - 1 29,f3g3 e2f2 g3g4 h3g4,1165,76,90,743,crushing discoveredAttack endgame rookEndgame short trappedPiece,https://lichess.org/BwjVuezV/black#58 +TpwxP,2kr4/R1pq1ppp/8/8/8/2PPr3/P1P3P1/R2QK3 w - - 0 23,e1d2 e3d3 c2d3 d7d3,1300,75,96,3003,crushing endgame sacrifice short,https://lichess.org/wRRxYzZB#45 +v4SMo,3r2k1/5p1p/p3p1p1/2P1P1P1/5P2/P7/4p2P/2R3K1 w - - 0 31,g1f2 d8d1 f2e2 d1c1,1231,74,99,2872,crushing endgame rookEndgame short,https://lichess.org/80LcrMO8#61 +pv5iO,2kr1bnr/pp1n1p1p/2pp1q2/6p1/4Pp2/1PNP3P/1PP3PN/R1BQ1RK1 w - - 0 12,a1a7 f6d4 g1h1 d4a7,1164,75,98,835,crushing fork middlegame short,https://lichess.org/JeND0wKx#23 +IFFS9,3q2k1/3brppp/8/p1pp2N1/3P4/N7/4QPPP/1R4K1 w - - 0 25,e2e7 d8e7 b1b8 d7e8,994,75,98,970,crushing defensiveMove endgame hangingPiece short,https://lichess.org/L4E4AcIg#49 +2x4l0,5r1k/3b1pp1/p2qp2p/3p4/P1n5/2NBP1P1/1r1P1P1P/R1Q2RK1 b - - 5 24,b2d2 d3c4 d5c4 c3e4,1448,74,93,1998,crushing middlegame short,https://lichess.org/3YGuCQvX/black#48 +E9fXH,2kr4/3n1p2/2p2qp1/1p6/6P1/1Bb1P1Br/2P1QP2/1R1R2K1 w - - 2 23,d1d6 h3g3 f2g3 f6d6,1484,74,98,7693,crushing middlegame short,https://lichess.org/OhNQYiqs#45 diff --git a/Tests/Puzzles/mateIn1_castling.csv b/Tests/Puzzles/mateIn1_castling.csv new file mode 100644 index 0000000..c5bc33b --- /dev/null +++ b/Tests/Puzzles/mateIn1_castling.csv @@ -0,0 +1,20 @@ +WW5uk,r3kb1r/2p1ppp1/p1b2n1p/3qN3/3P4/4P3/PP3PPP/RNBQK2R w KQkq - 4 11,e1g1 d5g2,760,100,91,649,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/oBiCLIGA#21 +zjfWz,rnbqk2r/ppp1bppp/8/3p2P1/5PP1/3Q4/PPPP4/RNB1K1NR b KQkq - 0 10,e8g8 d3h7,1210,79,90,285,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/4kj3hdb1/black#20 +swBXg,r1b1k1nr/1p3ppp/p2bp3/4q3/8/2NBB3/PPP2PPP/R2QK2R w KQkq - 2 12,e1g1 e5h2,836,85,71,126,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/lhIYvgoM#23 +E1wln,Qn2kbnr/p1q1ppp1/2p5/5bp1/4p3/2N5/PPPPBPPP/R1B1K2R w KQk - 3 10,e1g1 c7h2,1350,77,75,146,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/mB3slcX0#19 +hIf6g,rn2k2r/2qbpp1p/2Np2p1/pB1P4/1p1Q4/7P/PPP2PP1/2KR3R b kq - 0 15,e8g8 c6e7,1020,163,82,11,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/IlQIKE1K/black#30 +PKlaU,r1bk3N/1p1pqBpQ/p1p5/2b1p3/3n4/8/PPP2nPP/RNB1K2R w KQ - 0 12,e1g1 d4e2,1427,131,86,61,mate mateIn1 middlegame oneMove,https://lichess.org/KStAa02H#23 +HgNow,r3k2r/pppn1p2/4pq1p/8/3P1Q2/P1P1P1B1/5P1P/R3K1R1 b Qkq - 2 18,e8c8 f4c7,931,76,100,195,mate mateIn1 middlegame oneMove queensideAttack,https://lichess.org/kzE6a9h5/black#36 +6b7Wt,r3kbnr/ppp4p/2p1bqp1/4Q3/3PPB2/8/PPP2PPP/RN2K2R b KQkq - 2 9,e8c8 e5c7,915,75,99,526,mate mateIn1 oneMove opening queensideAttack,https://lichess.org/rjh2BgbH/black#18 +nAfqs,r3k2r/2pq2p1/1b3p2/p6p/P1pP4/2P1PQP1/6P1/R3NRK1 b kq - 0 26,e8c8 f3a8,849,94,87,421,mate mateIn1 middlegame oneMove,https://lichess.org/65g7kSa0/black#52 +0sw4O,r3k2r/ppB2ppp/2n1pq2/6b1/4Q1P1/2PB4/PP5P/3K3R b kq - 4 19,e8g8 e4h7,1074,80,71,71,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/g1if65nb/black#38 +6kzIH,r1b1k2r/ppp2ppp/2n1pq2/8/3PQn2/P1PB1N2/1BP2PPP/R4RK1 b kq - 8 11,e8g8 e4h7,1154,93,68,44,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/NaDYEX6v/black#22 +42Bal,2r1k2r/ppq2ppp/1n2b3/4p3/2P5/P3P1P1/1KQN1PP1/3R1B1R b k - 0 19,e8g8 c2h7,835,83,85,44,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/A2xv3ghS/black#38 +AWZQ7,r2qk2r/2p1bppp/p3p3/n7/Pp1P4/3QPPP1/1PB2P2/RN3RK1 b kq - 0 15,e8g8 d3h7,602,96,60,48,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/m6RqcUwC/black#30 +P2snV,r3k2r/1p3ppp/2p1bq2/p2n2N1/PbB1Q2P/8/1P3PP1/R1B2K1R b kq - 0 16,e8g8 e4h7,1260,158,82,10,kingsideAttack master mate mateIn1 middlegame oneMove,https://lichess.org/6s9dBTUL/black#32 +DLdAn,r3k2r/p2q1p1p/b3pp2/1Nb5/P1Bp4/5Q2/1PP2PPP/R4RK1 b kq - 3 16,e8c8 f3a8,1287,76,94,3968,mate mateIn1 middlegame oneMove,https://lichess.org/l2DeuPYc/black#32 +p3Q6M,r3k2r/1b4R1/p1n1p2p/1p1q1p2/3P1Q1P/P2B1N2/1PP5/2K5 b kq - 3 21,e8c8 f4c7,1193,76,90,517,mate mateIn1 middlegame oneMove,https://lichess.org/heIdcTW0/black#42 +RsYIC,rnb1k2r/pp3ppp/4p3/q2p2N1/1b1P4/2N1P3/PPQ2PPP/2R1KB1R b Kkq - 0 10,e8g8 c2h7,847,103,89,164,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/YgEdGJ9v/black#20 +aozJ2,3bk2r/p3npp1/2q4p/4Q3/4P3/1P6/PBP2PPP/5RK1 b k - 2 19,e8g8 e5g7,846,91,70,103,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/LNWojJtd/black#38 +6PekE,r2qk2r/ppp2ppp/2n5/3p1Q2/3P3b/3B4/P2B2PP/R4K2 b kq - 0 16,e8g8 f5h7,754,82,97,188,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/Ncwq1lnM/black#32 +nfUZQ,r2qk2r/pp1n1ppp/8/3Pn3/4Qp2/2N2Bb1/PPP3P1/R1B2K1R b kq - 4 14,e8g8 e4h7,1232,92,69,45,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/04AZISKa/black#28 diff --git a/Tests/Puzzles/mateIn1_enPassant.csv b/Tests/Puzzles/mateIn1_enPassant.csv new file mode 100644 index 0000000..4ea15df --- /dev/null +++ b/Tests/Puzzles/mateIn1_enPassant.csv @@ -0,0 +1,20 @@ +HDgzc,2r2r1q/5p1k/p3p2B/1p2P1Q1/4B3/P7/2p3P1/5R1K b - - 0 31,f7f5 e5f6,1990,81,65,148,discoveredAttack enPassant mate mateIn1 middlegame oneMove,https://lichess.org/ol00Tz8q/black#62 +1j5dV,7k/3rN2p/7P/p1pp3n/5pP1/5R2/PPP5/2K5 b - g3 0 38,f4g3 f3f8,890,86,67,64,endgame mate mateIn1 oneMove,https://lichess.org/zjlGPoxK/black#76 +QKy7Z,r2q1r2/1p1b1p1R/pnn1p1k1/3pP1P1/3P1P2/P2Q2P1/1P6/R1B2KN1 b - - 1 20,f7f5 e5f6,1840,77,94,5888,discoveredAttack enPassant mate mateIn1 middlegame oneMove,https://lichess.org/gowIv2fI/black#40 +4588b,r1b4k/1pq2p1p/2pp1N2/p1nPr3/2P1pP2/P3P2P/1PQ1B1P1/2KR3R b - f3 0 19,e4f3 c2h7,1051,76,82,237,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/6u9WW5eB/black#38 +1ymyA,4brk1/ppq5/4p1pQ/4PpRp/2P4r/3BR3/P1P3PP/7K w - f6 0 32,e5f6 c7h2,973,76,100,270,kingsideAttack master masterVsMaster mate mateIn1 middlegame oneMove,https://lichess.org/QVy1wixN#63 +SsgM3,r4rk1/1bq3pp/p3p3/1pb1PpN1/6n1/2NB4/PPP1Q1PP/R4R1K w - f6 0 18,e5f6 c7h2,986,75,100,597,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/j8MpBLcv#35 +YnOUq,1k1r1b1r/1pq1n3/1n2p3/1P1pPp2/3P2B1/2P5/R4PPB/1N1Q1RK1 w - f6 0 22,e5f6 c7h2,1169,205,83,8,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/uSZx2YbU#43 +94PYq,7k/pb4pp/5p2/8/2PqpP2/1P5P/P3Q1PK/8 b - f3 0 26,e4f3 e2e8,600,95,69,68,backRankMate endgame mate mateIn1 oneMove,https://lichess.org/B1rpV4bq/black#52 +iKfCM,2b1r1k1/p4p1p/B2N1b2/6pP/3P1K2/2P2P2/P5r1/RN5R w - g6 0 25,h5g6 f6g5,1434,76,85,278,mate mateIn1 middlegame oneMove,https://lichess.org/hLbfMhxx#49 +Fyn9D,2r2rk1/2b3p1/4p2p/pp2Pp2/2pPQ1Pq/P1P1P3/1P1B4/R4RK1 w - f6 0 21,e5f6 h4h2,1324,75,98,3566,mate mateIn1 middlegame oneMove,https://lichess.org/RqPUtwYt#41 +GAWcc,r4rk1/p1q3p1/bpp1p2p/4Pp2/1bpPB1nB/2N1P3/PP2Q1PP/R4RK1 w - f6 0 16,e5f6 c7h2,1098,80,92,453,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/T2TeS6dB#31 +GAhR6,r3r1k1/p7/2p4p/1p2Ppp1/6R1/1P6/PP3PPP/4R1K1 w - f6 0 22,e5f6 e8e1,1010,167,67,10,backRankMate endgame hangingPiece mate mateIn1 oneMove rookEndgame,https://lichess.org/QSZy78Od#43 +aA1q8,r7/8/p1R5/1p1P1kp1/1PnP1pP1/5K2/2P1N3/8 b - g3 0 40,f4g3 e2g3,970,78,98,633,endgame mate mateIn1 oneMove,https://lichess.org/dTA4VEdI/black#80 +4fBpP,1q6/pp6/4r1pk/2p1PpRn/3p2Q1/1P1P4/1PP3P1/4R2K w - f6 0 35,e5f6 e6e1,761,80,99,539,endgame hangingPiece mate mateIn1 oneMove,https://lichess.org/UA8Kpwh5#69 +CX1BO,8/p2n1p2/1p3b1p/3P1bk1/3pPN2/1P3KPP/PB4B1/8 b - e3 0 31,d4e3 h3h4,1177,74,97,3570,endgame mate mateIn1 oneMove,https://lichess.org/oyn66GTz/black#62 +ZQj16,r3kb1r/ppqn2p1/2p1p3/3pPpp1/N2P4/3QN3/PPP2PPP/R4RK1 w kq f6 0 14,e5f6 c7h2,1225,77,73,405,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/NaUlTKhY#27 +TfjSn,r2qk2r/ppp2pb1/3pn2p/6pn/2N1PpP1/2PB3P/PP3Q2/R1B2RK1 b kq g3 0 15,f4g3 f2f7,1033,86,88,455,attackingF2F7 mate mateIn1 oneMove opening,https://lichess.org/puM6gguU/black#30 +0Px2t,8/1b6/p4Q2/1p2Pppk/1P5p/3B3P/P1PN2PK/4q3 w - - 2 36,g2g4 h4g3,1851,78,71,230,enPassant endgame mate mateIn1 oneMove,https://lichess.org/qyXVm3Xe#71 +FSGUl,5r1k/8/3p3p/5RpP/5KP1/3Q1P2/8/4q3 w - g6 0 56,h5g6 e1e5,1541,93,84,217579,endgame mate mateIn1 oneMove pin,https://lichess.org/wFoaCTu7#111 +akDOj,r1b2rk1/p1q3pp/2p1p3/4Pp2/4Q1n1/2NB4/PPP3PP/R4RK1 w - f6 0 17,e5f6 c7h2,1244,77,41,211,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/rC5UCaJ0#33 diff --git a/Tests/Puzzles/mateIn1_simple.csv b/Tests/Puzzles/mateIn1_simple.csv new file mode 100644 index 0000000..a7057fa --- /dev/null +++ b/Tests/Puzzles/mateIn1_simple.csv @@ -0,0 +1,20 @@ +jrJls,2bQ1k1r/1p3p2/p4b1p/4pNp1/2q5/8/PPP2PPP/1K1RR3 b - - 3 23,f6d8 d1d8,779,82,97,239,mate mateIn1 middlegame oneMove,https://lichess.org/gQa01loO/black#46 +rZoKr,5rk1/ppp5/2n4p/3b2p1/6R1/P1B1P2P/1Pq5/R4QK1 w - - 2 29,f1d1 c2f2,1467,79,81,132,mate mateIn1 middlegame oneMove,https://lichess.org/haKJxadR#57 +0urF2,2kr4/ppp2ppp/2n5/8/1P1PrP2/q4bP1/P3N2P/1R1Q1RBK w - - 1 18,f1f3 a3f3,922,80,96,245,hangingPiece kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/Ea0aHiBh#35 +2HNcF,r3rk2/5ppQ/b1ppnq1p/p7/P1P5/2N5/1PB2PP1/R3R1K1 b - - 5 23,e6g5 h7h8,1031,76,92,123,mate mateIn1 middlegame oneMove,https://lichess.org/TQl97w12/black#46 +qFJW4,r5k1/ppp1nrpp/1b2Q3/8/6n1/B1N5/P5PP/R4RqK w - - 4 21,f1g1 g4f2,935,77,98,647,mate mateIn1 middlegame oneMove smotheredMate,https://lichess.org/4IhbR7Z4#41 +07FBh,r7/1Rrk1p1P/1R2bQn1/3pP3/3P1p2/p1P3PK/5q2/3B4 w - - 0 46,b6e6 f2g3,1378,75,80,255,master masterVsMaster mate mateIn1 middlegame oneMove,https://lichess.org/N6cvrT1h#91 +BOh5N,r1b1k2r/ppp2pp1/2p5/2b1q1Bp/4P1n1/2PP4/PP2BPPP/RN1Q1RK1 w kq - 1 10,d3d4 e5h2,1110,79,87,307,kingsideAttack mate mateIn1 oneMove opening,https://lichess.org/hBADdo6I#19 +8K7dF,5Nr1/1p5k/3p1Q2/p2Pn3/4q3/1P6/P5RP/7K b - - 3 38,g8f8 f6g7,1374,78,85,113,endgame master mate mateIn1 oneMove,https://lichess.org/dVBFepDr/black#76 +pfcY2,1rq4r/pk3ppp/Q1p5/8/R7/5P2/PPP4P/2KR4 b - - 14 24,b7a8 a6a7,969,76,100,457,endgame mate mateIn1 oneMove queensideAttack,https://lichess.org/iq1SsukA/black#48 +1bg7G,r4rk1/2q4p/2p5/P1bp1pp1/4pNn1/2B1P2P/P1P2PP1/R2Q1RK1 w - - 0 18,f4d5 c7h2,1313,77,85,102,mate mateIn1 middlegame oneMove,https://lichess.org/6jomSlbK#35 +Rsm4I,3r1Q1k/6p1/7p/1p2q3/8/1P1B1R2/2P3PP/7K b - - 0 33,d8f8 f3f8,600,90,68,137,endgame mate mateIn1 oneMove,https://lichess.org/cBl2WADX/black#66 +lq6Om,5Q1k/1b2q1pp/p3B3/1pp1P3/8/2N5/PP4PP/5R1K b - - 0 28,e7f8 f1f8,629,104,84,148,backRankMate endgame mate mateIn1 oneMove,https://lichess.org/Hcocq7AO/black#56 +WOhqq,8/1b4k1/p1q2rp1/8/2PQ4/1P1p3P/1P3RP1/7K w - - 2 41,f2f6 c6g2,1000,79,81,84,endgame mate mateIn1 oneMove,https://lichess.org/s4VKQPNb#81 +1ZxkV,r1bqkb1r/p1p2ppp/2p5/3pN3/4n3/5Q2/PPPP1PPP/RNB1K2R b KQkq - 1 7,f8d6 f3f7,1102,79,100,66,attackingF2F7 mate mateIn1 oneMove opening,https://lichess.org/8mudJHJO/black#14 +ozS9D,6k1/ppp2pp1/1nn2q1p/3p4/5B2/1PPB1P2/P1P3PP/4Q1K1 b - - 0 21,f6f4 e1e8,653,89,60,146,endgame master mate mateIn1 oneMove,https://lichess.org/r5n4dpAN/black#42 +8xWfh,r4rk1/2p3pp/p2p4/1p1P1b2/5Q2/P1P1P2P/BP4q1/2KR3R w - - 0 19,h1g1 g2c2,1323,149,55,14,mate mateIn1 middlegame oneMove,https://lichess.org/nMPORUnV#37 +GLtpk,r6r/p1p1kpQ1/3p2p1/2p5/4PP2/5Pq1/PPP1R3/R1B3K1 w - - 2 22,e2g2 g3e1,1195,75,94,1852,mate mateIn1 middlegame oneMove,https://lichess.org/fGjY3KEU#43 +kOz0U,8/Q4kp1/2qB1p2/pp2p2p/n3P2P/5P2/2P3PK/8 b - - 5 36,f7e6 a7e7,805,75,100,281,endgame master mate mateIn1 oneMove,https://lichess.org/jS8203Ng/black#72 +BEUkI,r1bq1r1k/pp3pR1/1b1p3p/2p2P2/2BBP2n/2N5/PPPQ2PP/R5K1 b - - 0 15,c5d4 d2h6,1080,84,63,46,kingsideAttack mate mateIn1 middlegame oneMove,https://lichess.org/qUYVfwWj/black#30 +EAnMf,3R4/5r1k/6p1/4B3/3P1PK1/4r3/8/8 b - - 1 42,e3e1 d8h8,959,118,33,20,endgame master mate mateIn1 oneMove,https://lichess.org/7KoQ0ISo/black#84 diff --git a/Tests/Puzzles/mateIn2_castling.csv b/Tests/Puzzles/mateIn2_castling.csv new file mode 100644 index 0000000..e204416 --- /dev/null +++ b/Tests/Puzzles/mateIn2_castling.csv @@ -0,0 +1,20 @@ +C3LDZ,N2k1bnr/pp3ppp/8/5b2/1n1p1B2/8/PP2PPPP/R3KBNR w KQ - 3 10,e1c1 b4a2 c1d2 f8b4,946,76,98,512,mate mateIn2 opening queensideAttack short,https://lichess.org/TEvwwNPR#19 +9LFa6,N4b1r/p1p1kp2/4pp1p/1B6/4b3/8/PP3P1P/RNB1K2R w KQ - 0 17,e1g1 h8g8 c1g5 g8g5,1184,81,88,300,mate mateIn2 middlegame short,https://lichess.org/F32R0fa0#33 +1pIrM,r3k1r1/p1p1qp2/1p2b1p1/n6p/4Q3/2PB4/2P2PPP/3RR1K1 b q - 3 21,e8c8 e4a8 c8d7 d3b5,1862,77,86,105,doubleCheck mate mateIn2 middlegame queensideAttack short,https://lichess.org/pSb6NXeF/black#42 +zr2fX,r2qk2r/ppp2ppp/3p4/2b1p2n/2BnPP2/2NP3P/PPP3P1/R1BQK2R w KQkq - 3 10,e1g1 d4f3 g1h1 h5g3,1815,75,95,5210,doubleCheck kingsideAttack mate mateIn2 opening short,https://lichess.org/kjhbiYdz#19 +YpQqh,r1b1k2r/ppb2ppp/2p1p3/5Nq1/3PQ3/2P4P/PPB2PP1/3RR1K1 b kq - 6 20,e8g8 f5e7 g8h8 e4h7,1226,74,97,2181,discoveredAttack kingsideAttack mate mateIn2 middlegame short,https://lichess.org/oqBsdf2S/black#40 +DvrCz,r3k3/p1p3p1/1pP3p1/2b5/Q2NP3/4K3/PP3Pq1/R7 b q - 0 24,e8c8 a4a6 c8b8 a6b7,1169,77,93,1757,endgame mate mateIn2 queensideAttack short,https://lichess.org/Foi1kNan/black#48 +XtR9Q,r3k2r/pp1n1p2/2p1p2p/5bp1/3P4/4P1B1/P2KBPPP/2R1R3 b kq - 3 17,e8c8 c1c6 b7c6 e2a6,1189,117,70,776,bodenMate mate mateIn2 middlegame queensideAttack sacrifice short,https://lichess.org/ALOKPySz/black#34 +ELwWj,r3k3/pp1nqpb1/2p3p1/8/5BP1/5Q2/PPP1B3/2K4R b q - 2 21,e8c8 f3c6 b7c6 e2a6,1180,158,38,144,bodenMate master mate mateIn2 middlegame queensideAttack sacrifice short,https://lichess.org/MkCYHnmC/black#42 +c1OJQ,Qn2k2r/p4pp1/1q1bp1p1/8/3P4/3BP3/P2B1PPP/R3K2R w KQk - 2 18,e1c1 d6a3 c1c2 b6b2,1170,76,88,402,mate mateIn2 middlegame queensideAttack short,https://lichess.org/ze00fCw3#35 +0NplY,3r1q1r/pkp2p1p/2p2p2/7R/3bN3/1P2NQ2/P1PP2P1/R3K3 w Q - 1 19,e1c1 f8a3 c1b1 a3b2,1095,75,91,481,mate mateIn2 middlegame queensideAttack short,https://lichess.org/iqeYu7au#37 +zaghF,r3k1r1/pp1nbp1p/2p1pn2/8/P2q4/2NB1QBP/1PP2PP1/R3R1K1 b q - 1 17,e8c8 f3c6 b7c6 d3a6,1180,114,72,618,bodenMate mate mateIn2 middlegame queensideAttack sacrifice short,https://lichess.org/2QzZrb7U/black#34 +micYX,r3k2r/pp1n1pp1/2p2np1/q3p3/3P4/b1P1BQNP/PP3PP1/R3KB1R w KQkq - 1 13,e1c1 a5c3 c1b1 c3b2,1554,80,74,42,mate mateIn2 middlegame pin queensideAttack short,https://lichess.org/6fCoAKep#25 +LjS86,r1bqk2r/p3bppp/2p1n3/1p2pQ2/4N3/2PB4/P1PP1PPP/R1B2RK1 b kq - 4 12,e8g8 e4f6 g8h8 f5h7,1122,79,72,358,kingsideAttack mate mateIn2 opening short,https://lichess.org/O55hxxVw/black#24 +0uCn9,r5kr/pp2p1b1/1q1p4/2pPnbP1/4N3/5P2/PPPBQ2P/R3KBNR w KQ - 5 16,e1c1 e5d3 c1b1 b6b2,1522,76,92,1510,mate mateIn2 middlegame queensideAttack short,https://lichess.org/fpIzh0mk#31 +MLdUA,r3k2r/1p3p1p/p2p1B1b/4pP2/1PP5/q7/2K2PPP/3Q1B1R b kq - 3 21,e8g8 d1g4 h6g7 g4g7,1365,81,40,68,kingsideAttack mate mateIn2 middlegame short,https://lichess.org/eBsszmz6/black#42 +AhG2i,rn2k2r/2q2pp1/p3p1p1/2Np2N1/1P1P1n2/P7/2PQ1PPP/R3K2R w KQkq - 2 18,e1g1 f4e2 g1h1 c7h2,1345,76,85,185,discoveredAttack kingsideAttack mate mateIn2 middlegame short,https://lichess.org/eNauwPCr#35 +vnoDi,r3k3/p3q1p1/2p2n2/2P2p2/1P3Br1/5Q2/P4PPP/RN4K1 b q - 1 21,e8c8 f3c6 e7c7 c6c7,1222,79,92,1655,mate mateIn2 middlegame queensideAttack short,https://lichess.org/nq7O55Iv/black#42 +KCfTj,r1b1k2r/pppp1Npp/2n5/4p3/2B4q/5Q2/PPPP1b1P/RNB4K b kq - 0 10,e8g8 f7h6 g8h8 f3f8,1092,78,98,2011,deflection discoveredAttack doubleCheck kingsideAttack mate mateIn2 middlegame short,https://lichess.org/5aGYiZI2/black#20 +W6DEV,r3kb1r/2p2p2/p1P1p3/4P1p1/Pp1q4/8/1P1NQ1PP/R3K2R b KQkq - 1 20,e8c8 e2a6 c8b8 a6b7,1100,80,95,536,mate mateIn2 middlegame queensideAttack short,https://lichess.org/GEq3AYMj/black#40 +lo4i9,r3k2r/pp1nqppp/2p1pn2/5b2/1bB2B2/3P1QPP/PPP1NP2/2KR3R b kq - 4 13,e8c8 f3c6 b7c6 c4a6,1061,95,85,1074,bodenMate mate mateIn2 middlegame queensideAttack sacrifice short,https://lichess.org/LmkZcypC/black#26 diff --git a/Tests/Puzzles/mateIn2_enPassant.csv b/Tests/Puzzles/mateIn2_enPassant.csv new file mode 100644 index 0000000..3eba2e6 --- /dev/null +++ b/Tests/Puzzles/mateIn2_enPassant.csv @@ -0,0 +1,20 @@ +4ealO,7R/1r6/4npp1/3p2k1/4p1PN/4P1K1/5P2/8 b - - 1 41,e6g7 f2f4 e4f3 h4f3,1570,74,93,541,endgame mate mateIn2 short,https://lichess.org/Ake1xrvX/black#82 +0jFaq,3r3R/p1r1kpp1/2pb4/6P1/3p1P2/1PnP4/PB4B1/K3R3 b - - 8 31,e7d7 g2h3 f7f5 g5f6,1473,75,95,8904,discoveredAttack enPassant mate mateIn2 middlegame short,https://lichess.org/PTxvhx4O/black#62 +IzNuV,rnbq1bkr/pppp2pp/8/4P3/8/1Q6/PPP2nPP/RNB1K1NR b KQ - 3 7,d7d5 e5d6 c8e6 b3e6,2101,76,90,597,discoveredAttack enPassant kingsideAttack mate mateIn2 opening short,https://lichess.org/jeruntSh/black#14 +pnp1m,6k1/2p2p2/7p/p4Pp1/PpP4K/1N1P3P/1PB1b1r1/R7 w - g6 0 36,f5g6 f7g6 d3d4 g6g5,1332,74,98,5203,endgame mate mateIn2 short,https://lichess.org/RqiEClbR#71 +C8vzb,7r/6R1/5P1p/3p3k/b1pP3p/4K3/r4PPP/7R b - - 1 31,h8b8 g2g4 h4g3 h2g3,1734,75,93,284,discoveredAttack endgame mate mateIn2 short,https://lichess.org/dc3gg8Ux/black#62 +m3UQp,r1bq1bkr/pppp2pp/2n5/4P3/6n1/1QN2N2/PP3PPP/R1B1K2R b KQ - 2 9,d7d5 e5d6 c8e6 b3e6,2256,77,99,139,discoveredAttack enPassant kingsideAttack mate mateIn2 opening short,https://lichess.org/VE0cPfvX/black#18 +jkW7a,r1bq1bkr/pppp2pp/2n5/4P1N1/2Q5/8/Pp3PPP/RNB1K2R b KQ - 1 10,d7d5 e5d6 c8e6 c4e6,2008,85,100,60,discoveredAttack enPassant kingsideAttack mate mateIn2 opening short,https://lichess.org/R6aKczaF/black#20 +VkFyK,3Q4/1k6/1n1b2p1/1B1p1pP1/P2Pp1K1/4q3/4N3/2R5 w - f6 0 46,g5f6 e3f3 g4g5 f3h5,2129,116,74,30,mate mateIn2 middlegame short,https://lichess.org/gcWm81m8#91 +7dd9h,6k1/1bQ2p2/p3p1p1/6Pp/2P3K1/1P2PnP1/P6r/R4R2 w - h6 0 30,g5h6 f7f5 g4f4 g6g5,1817,75,96,4785,endgame mate mateIn2 short,https://lichess.org/URt1dC1A#59 +GJfk2,5nk1/R4pp1/4p2p/2BpP2P/3P2P1/1r3K2/8/2R5 w - - 4 44,f3f4 g7g5 h5g6 f8g6,1637,74,86,400,endgame mate mateIn2 short,https://lichess.org/Rs0Ce6wy#87 +By846,6k1/5rpp/2pB2b1/2P5/4pP2/2P4P/3r2P1/2R1R1K1 b - f3 0 28,e4f3 e1e8 f7f8 e8f8,687,107,78,374,endgame mate mateIn2 short,https://lichess.org/oVDK3Vtd/black#56 +aCX4c,7k/1R2B1b1/p5pp/5p2/4p1P1/P2bP2P/5PB1/2r3K1 w - - 1 31,g1h2 g7e5 f2f4 e4f3,1611,74,95,6485,discoveredAttack enPassant endgame mate mateIn2 short,https://lichess.org/dVTQ6KQK#61 +Ksv3B,6k1/7p/R5p1/5p2/4p3/1RP1B1PP/2P2PK1/3r1r2 w - - 0 35,f2f4 e4f3 g2h2 f1h1,1923,76,92,558,enPassant endgame mate mateIn2 short,https://lichess.org/wi3kg3x1#69 +3DJKJ,8/4ppk1/3p2p1/1pnP3p/4PK1P/r4PP1/PR6/5B2 w - - 9 43,f1b5 e7e5 d5e6 c5e6,1962,74,96,4418,endgame mate mateIn2 short,https://lichess.org/RR03JsOK#85 +n4CSG,1r2r1k1/p3qpbp/1pp3p1/3pP3/5BPn/2N3Q1/PPP2P1P/3RR1K1 w - d6 0 21,e5d6 e7e1 d1e1 e8e1,742,95,47,49,kingsideAttack mate mateIn2 middlegame short,https://lichess.org/B4cIJuuo#41 +12YGT,r2qkbnr/1p2p3/2p2p2/2Pp1bp1/p2P3p/PN2BN1P/RP1KPPP1/3Q1B1R w kq - 0 15,b3c1 d8a5 b2b4 a4b3,1583,76,88,1028,discoveredAttack enPassant mate mateIn2 middlegame short,https://lichess.org/ggbXxMB9#29 +BOPKK,1r5r/p1p2pk1/4p2p/1BNnP3/6R1/P7/1P4PP/7K b - - 2 23,g7h7 b5d3 f7f5 e5f6,1723,94,85,37,discoveredAttack enPassant endgame mate mateIn2 short,https://lichess.org/9He2SHa7/black#46 +wU03v,5nk1/p4pp1/4p2p/4Pq1P/1p3P2/6PK/P1R3Q1/8 w - - 4 32,h3h4 g7g5 h5g6 f8g6,1602,75,95,386,endgame mate mateIn2 short,https://lichess.org/adh9umHR#63 +gLCiH,3r2k1/p2r1ppp/2p5/1pP5/8/1R2P3/P4PPP/1R4K1 w - b6 0 24,c5b6 d7d1 b1d1 d8d1,829,114,90,117,backRankMate endgame mate mateIn2 rookEndgame short,https://lichess.org/EPK4b73V#47 +5UCLk,8/8/p3RB2/kp3p1p/1Pp2K1P/2P5/6r1/8 b - b3 0 59,c4b3 f6d8 a5a4 e6a6,1610,75,85,304,deflection endgame mate mateIn2 short,https://lichess.org/Z6jDY4v4/black#118 diff --git a/Tests/Puzzles/mateIn2_simple.csv b/Tests/Puzzles/mateIn2_simple.csv new file mode 100644 index 0000000..b2f6dc3 --- /dev/null +++ b/Tests/Puzzles/mateIn2_simple.csv @@ -0,0 +1,20 @@ +uj7Uv,6k1/r4p2/6p1/4B3/p4P2/5r1p/K1R5/8 b - - 5 43,a7b7 c2c8 g8h7 c8h8,840,100,67,43,endgame mate mateIn2 short,https://lichess.org/Z3FRw1Fn/black#86 +GWKLu,3r2qr/pp2Q3/2p2P1p/7k/6R1/1P6/1PP3PP/R1B4K w - - 5 23,g4g8 d8d1 e7e1 d1e1,1038,85,96,471,backRankMate endgame mate mateIn2 short,https://lichess.org/1kIpfnCX#45 +ay0e7,1rr3k1/Q4pp1/4p2p/8/1PqPp3/P3P3/5PPP/R4RK1 w - - 3 27,f1c1 c4c1 a1c1 c8c1,630,95,69,168,backRankMate endgame mate mateIn2 short,https://lichess.org/x83fY9GE#53 +f2HsU,8/2q1kp2/4p3/p7/4Q3/7P/P1Pr1PP1/1R4K1 w - - 0 32,b1b7 d2d1 e4e1 d1e1,941,78,76,128,endgame mate mateIn2 short,https://lichess.org/NxooUJUL#63 +gEBEu,r3r2k/ppp3pp/3b4/5p2/1PQ1n2q/PnN1PB2/1B3PPP/R3K2R w KQ - 4 18,c4b3 h4f2 e1d1 f2d2,1124,80,94,862,attackingF2F7 mate mateIn2 middlegame short,https://lichess.org/prTiL04V#35 +Lk2iz,r5k1/p1p2p1Q/2pq1Bp1/3p1b2/5R2/1P1Pr3/P1P3PP/R5K1 b - - 0 21,g8h7 f4h4 h7g8 h4h8,741,91,88,90,master mate mateIn2 middlegame short,https://lichess.org/i2Wi7l4e/black#42 +t4DNf,2r3k1/5ppp/8/8/4pP2/4P2P/2Q3P1/1R3K2 b - - 0 43,c8c2 b1b8 c2c8 b8c8,655,107,82,33,backRankMate endgame mate mateIn2 rookEndgame short,https://lichess.org/r5W2eFui/black#86 +xrekH,8/p1P5/8/PP6/8/5rkp/8/6K1 w - - 2 59,c7c8q h3h2 g1h1 f3f1,990,93,96,295,advancedPawn endgame mate mateIn2 queenRookEndgame short,https://lichess.org/dQGDwh3G#117 +fBhqm,8/3R2kp/p5p1/8/5KP1/2p5/P1P4r/8 b - - 2 39,g7h6 g4g5 h6h5 d7h7,988,75,100,750,deflection endgame mate mateIn2 rookEndgame short,https://lichess.org/rfc0Wh0t/black#78 +tn9U3,8/4Q3/p2pN3/2pP4/2p1k3/8/1PPK1q2/8 w - - 1 36,d2c3 f2e1 c3c4 e1b4,1917,75,96,2210,endgame mate mateIn2 short,https://lichess.org/js3slxFD#71 +doGMx,5rk1/pp1R1pp1/4p3/8/6QP/3bB1N1/Pqr2PP1/3K3R w - - 0 22,d7d3 b2b1 e3c1 b1c1,1243,76,90,222,mate mateIn2 middlegame short,https://lichess.org/jYHnS2gv#43 +TVLHT,r1bqr1k1/p1pnbp2/1p2pn1Q/6N1/3P4/2NB4/PPP3PP/R4RK1 b - - 2 13,e7f8 d3h7 f6h7 h6h7,1274,76,94,1181,kingsideAttack mate mateIn2 middlegame short,https://lichess.org/3luk8Lyr/black#26 +EfOGc,8/5pk1/7q/3Pr3/8/3Q3P/4KRP1/8 w - - 1 45,e2f1 h6c1 d3d1 c1d1,992,76,95,561,endgame mate mateIn2 short,https://lichess.org/oWIOQKUU#89 +I7xRS,8/pp1r4/2p5/5b1p/5N2/5kP1/P4P1P/Q5K1 w - - 1 34,a1e5 d7d1 e5e1 d1e1,600,84,83,169,endgame mate mateIn2 short,https://lichess.org/D6tGkCvf#67 +CnqaD,r1bqk2r/1p6/p1n1pp1p/3p2p1/1QpPNB1P/P3P3/1PP1BPP1/R3K2R b KQkq - 0 14,g5f4 e2h5 e8d7 b4d6,1799,75,95,4247,mate mateIn2 middlegame short,https://lichess.org/gVyTK6U8/black#28 +yuqa6,4k2r/p2nppb1/6pp/1p1NP3/1P5N/K5P1/P1b2rBP/3R3R b k - 3 25,d7e5 d5c7 e8f8 d1d8,1098,78,77,313,backRankMate mate mateIn2 middlegame short,https://lichess.org/HCiRNU2E/black#50 +koa5w,r5k1/pbp1qpp1/1p2p1n1/3r2NQ/3P3P/5P2/PP4P1/R2R2K1 b - - 0 20,g6f4 h5h7 g8f8 h7h8,944,76,100,195,kingsideAttack mate mateIn2 middlegame short,https://lichess.org/TXpDhxAx/black#40 +6HZF3,2rq3r/5k2/p3p3/1pn2Np1/4P3/P1Q2P2/6b1/1KBR2N1 b - - 0 29,d8d1 c3g7 f7e8 g7e7,1524,74,98,7690,mate mateIn2 middlegame short,https://lichess.org/f4k6roov/black#58 +85ZUB,6k1/2p2pp1/p6p/3pPp2/P7/r3P3/5PPP/3R2K1 w - - 2 27,d1d5 a3a1 d5d1 a1d1,681,104,76,365,backRankMate endgame mate mateIn2 rookEndgame short,https://lichess.org/pBZZo8XP#53 +YHYZd,8/pp1R4/6pk/8/6PP/5K2/PP1prP2/4r3 b - - 0 36,d2d1q g4g5 h6h5 d7h7,1239,77,90,369,endgame mate mateIn2 queenRookEndgame short,https://lichess.org/E8UBGJ5F/black#72 diff --git a/Tests/SquareTests.cpp b/Tests/SquareTests.cpp new file mode 100644 index 0000000..869ec15 --- /dev/null +++ b/Tests/SquareTests.cpp @@ -0,0 +1,80 @@ +#include "catch2/catch.hpp" + +#include "TestUtils.hpp" + +#include "Square.hpp" + +#include + +TEST_CASE("Squares can be created from valid coordinates", "[Square][Fundamental]") { + auto [file, rank] = GENERATE(table({ + {4, 2}, {0, 0}, {7, 7}, {1, 7} + })); + + auto optSquare = Square::fromCoordinates(file, rank); + REQUIRE(optSquare.has_value()); + + auto square = optSquare.value(); + REQUIRE(square.file() == file); + REQUIRE(square.rank() == rank); + REQUIRE(square.index() == rank * 8 + file); +} + +TEST_CASE("Squares are not created from invalid coordinates", "[Square][Fundamental]") { + auto [file, rank] = GENERATE(table({ + {4, 8}, {8, 3}, {12, 45} + })); + + auto optSquare = Square::fromCoordinates(file, rank); + REQUIRE_FALSE(optSquare.has_value()); +} + +TEST_CASE("Squares can be created from valid indices", "[Square][Fundamental]") { + auto index = GENERATE(as{}, 0, 63, 12, 41); + + auto optSquare = Square::fromIndex(index); + REQUIRE(optSquare.has_value()); + + auto square = optSquare.value(); + REQUIRE(square.file() == index % 8); + REQUIRE(square.rank() == index / 8); + REQUIRE(square.index() == index); +} + +TEST_CASE("Squares are not created from invalid indices", "[Square][Fundamental]") { + auto index = GENERATE(as{}, 64, 1024); + + auto optSquare = Square::fromIndex(index); + REQUIRE_FALSE(optSquare.has_value()); +} + +TEST_CASE("Squares can be created from valid names", "[Square][Fundamental]") { + auto [square, name] = GENERATE(table({ + {Square::A1, "a1"}, {Square::H8, "h8"}, {Square::D5, "d5"} + })); + + auto optCreatedSquare = Square::fromName(name); + CAPTURE(square, name, optCreatedSquare); + REQUIRE(optCreatedSquare.has_value()); + + auto createdSquare = optCreatedSquare.value(); + REQUIRE(createdSquare == square); +} + +TEST_CASE("Squares are not created from invalid names", "[Square][Fundamental]") { + auto name = GENERATE("", "a", "a12", "1a", "xyz"); + + auto optSquare = Square::fromName(name); + CAPTURE(name, optSquare); + REQUIRE_FALSE(optSquare.has_value()); +} + +TEST_CASE("Squares stream their name correctly", "[Square][Fundamental]") { + auto [square, name] = GENERATE(table({ + {Square::A1, "a1"}, {Square::H8, "h8"}, {Square::E4, "e4"} + })); + + auto stream = std::stringstream(); + stream << square; + REQUIRE(stream.str() == name); +} diff --git a/Tests/TestUtils.hpp b/Tests/TestUtils.hpp new file mode 100644 index 0000000..585e796 --- /dev/null +++ b/Tests/TestUtils.hpp @@ -0,0 +1,26 @@ +#ifndef CHESS_ENGINE_TESTS_TESTUTILS_HPP +#define CHESS_ENGINE_TESTS_TESTUTILS_HPP + +#include +#include +#include + +template +inline std::ostream& operator<<(std::ostream& os, const std::optional& opt) { + if (opt.has_value()) { + return os << opt.value(); + } else { + return os << "nullopt"; + } +} + +template +inline std::ostream& operator<<(std::ostream& os, const std::unique_ptr& ptr) { + if (ptr == nullptr) { + return os << "nullptr"; + } else { + return os << ptr.get(); + } +} + +#endif diff --git a/Tests/puzzledb.py b/Tests/puzzledb.py new file mode 100755 index 0000000..e81894f --- /dev/null +++ b/Tests/puzzledb.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 + +import csv +from pathlib import Path +import argparse +import sys + +import chess + + +class Puzzle: + def __init__(self, puzzle_id, fen, moves, rating, rd, popularity, + num_plays, tags, game_url): + self.puzzle_id = puzzle_id + self.fen = fen + self.moves = moves + self.rating = rating + self.rd = rd + self.popularity = popularity + self.num_plays = num_plays + self.tags = tags + self.game_url = game_url + + @staticmethod + def from_csv(row): + puzzle_id, fen, moves, rating, rd, popularity, \ + num_plays, tags, game_url = row[:9] + moves = [chess.Move.from_uci(m) for m in moves.split()] + return Puzzle(puzzle_id, fen, moves, int(rating), int(rd), + int(popularity), int(num_plays), tags.split(), game_url) + + def to_csv(self): + moves = ' '.join(m.uci() for m in self.moves) + tags = ' '.join(self.tags) + + return ( + self.puzzle_id, self.fen, moves, self.rating, self.rd, + self.popularity, self.num_plays, tags, self.game_url + ) + + def has_tags(self, tags): + return all(tag in self.tags for tag in tags) + + def has_not_tags(self, tags): + return not any(tag in self.tags for tag in tags) + + def _has_move(self, predicate): + board = chess.Board(self.fen) + + for move in self.moves: + if predicate(board, move): + return True + + board.push(move) + + return False + + def has_castling(self): + return self._has_move(chess.Board.is_castling) + + def has_en_passant(self): + return self._has_move(chess.Board.is_en_passant) + + def num_plies(self): + return len(self.moves) - 1 + + +class PuzzleDb: + def __init__(self, puzzles): + self._puzzles = puzzles + + @staticmethod + def from_csv(file): + def generate_puzzles(): + csv_reader = csv.reader(file) + + for row in csv_reader: + puzzle = Puzzle.from_csv(row) + yield puzzle + + return PuzzleDb(generate_puzzles()) + + def to_csv(self, file): + csv_writer = csv.writer(file) + + for puzzle in self._puzzles: + csv_writer.writerow(puzzle.to_csv()) + + def filter(self, predicate): + return PuzzleDb(filter(predicate, self)) + + def collect(self): + return list(self) + + def sorted(self, key=None): + return PuzzleDb(sorted(self._puzzles, key=key)) + + def __iter__(self): + yield from self._puzzles + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('puzzle_db', type=Path) + parser.add_argument('--tag', action='append', dest='tags') + parser.add_argument('--not-tag', action='append', dest='not_tags') + parser.add_argument('--castling', choices=['yes', 'no', 'only'], + default='yes') + parser.add_argument('--en-passant', choices=['yes', 'no', 'only'], + default='yes') + parser.add_argument('--min-plies', type=int, default=0) + parser.add_argument('--max-plies', type=int, default=1000) + parser.add_argument('--min-rating', type=int, default=0) + parser.add_argument('--max-rating', type=int, default=4000) + parser.add_argument('--min-num-plays', type=int, default=0) + args = parser.parse_args() + + with args.puzzle_db.open(newline='') as f: + puzzles = PuzzleDb.from_csv(f) + + if args.tags is not None: + puzzles = puzzles.filter(lambda p: p.has_tags(args.tags)) + + if args.not_tags is not None: + puzzles = puzzles.filter(lambda p: p.has_not_tags(args.not_tags)) + + if args.castling == 'no': + puzzles = puzzles.filter(lambda p: not p.has_castling()) + elif args.castling == 'only': + puzzles = puzzles.filter(lambda p: p.has_castling()) + + if args.en_passant == 'no': + puzzles = puzzles.filter(lambda p: not p.has_en_passant()) + elif args.en_passant == 'only': + puzzles = puzzles.filter(lambda p: p.has_en_passant()) + + puzzles = puzzles.filter( + lambda p: args.min_plies <= p.num_plies() <= args.max_plies + ) + + puzzles = puzzles.filter( + lambda p: args.min_rating <= p.rating <= args.max_rating + ) + + puzzles = puzzles.filter(lambda p: args.min_num_plays <= p.num_plays) + + puzzles.to_csv(sys.stdout) + + +if __name__ == '__main__': + main() diff --git a/Tests/puzzlerating.py b/Tests/puzzlerating.py new file mode 100755 index 0000000..6343e6f --- /dev/null +++ b/Tests/puzzlerating.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +from puzzledb import PuzzleDb +from puzzlerunner import run_puzzle + +import argparse +from pathlib import Path +from random import Random + +import glicko2 + + +def select_puzzles(player, puzzle_db, num_puzzles, played_puzzles, random): + rd = player.rd + candidates = [] + + while len(candidates) < num_puzzles: + candidates = puzzle_db \ + .filter(lambda p: p.rating >= player.rating - 2 * rd) \ + .filter(lambda p: p.rating <= player.rating + 2 * rd) \ + .filter(lambda p: p not in played_puzzles) \ + .collect() + + rd += 50 + + return sorted(random.sample(candidates, num_puzzles), + key=lambda p: p.rating) + + +def play_round(player, puzzle_db, num_puzzles, engine, + timeout, played_puzzles, random): + puzzles = select_puzzles(player, puzzle_db, num_puzzles, + played_puzzles, random) + played_puzzles.update(puzzles) + outcomes = [] + + for puzzle in puzzles: + print(f'Running puzzle {puzzle.puzzle_id} ' + f'with rating {puzzle.rating}... ', + end='', flush=True) + + result = run_puzzle(puzzle, engine, timeout) + success = result.is_success() + outcomes.append(success) + + if success: + print('OK') + else: + print('FAIL') + + ratings = [p.rating for p in puzzles] + rds = [p.rd for p in puzzles] + player.update_player(ratings, rds, outcomes) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--engine', type=Path, required=True) + parser.add_argument('--timeout', type=float, default=60) + parser.add_argument('--min-plays', type=int, default=500) + parser.add_argument('--min-popularity', type=int, default=50) + parser.add_argument('--rounds', type=int, default=20) + parser.add_argument('--puzzles-per-round', type=int, default=5) + parser.add_argument('--random-seed', type=int, default=0) + parser.add_argument('--puzzle-db', type=Path, required=True) + args = parser.parse_args() + + with args.puzzle_db.open(newline='') as f: + puzzle_db = PuzzleDb.from_csv(f) \ + .filter(lambda p: p.num_plays >= args.min_plays) \ + .filter(lambda p: p.popularity >= args.min_popularity) \ + .sorted(key=lambda p: p.rating) + + player = glicko2.Player() + played_puzzles = set() + random = Random(args.random_seed) + + for current_round in range(1, args.rounds + 1): + print(f'=== Round {current_round}/{args.rounds}, ' + f'current rating: {round(player.rating)} ' + f'(rd: {round(player.rd)}) ===') + play_round(player, puzzle_db, args.puzzles_per_round, + args.engine, args.timeout, played_puzzles, random) + + print(f'Final rating: {round(player.rating)} (rd: {round(player.rd)})') + + +if __name__ == '__main__': + main() diff --git a/Tests/puzzlerunner.py b/Tests/puzzlerunner.py new file mode 100755 index 0000000..857ee6e --- /dev/null +++ b/Tests/puzzlerunner.py @@ -0,0 +1,260 @@ +#!/usr/bin/env python3 + +from puzzledb import Puzzle, PuzzleDb + +from pathlib import Path +import argparse +from abc import ABC, abstractmethod +from collections import defaultdict +import sys +import time +import asyncio +import contextlib + +import chess +import chess.engine + +import junit_xml +from junit_xml import TestSuite, TestCase + + +class PuzzleRunResult(ABC): + def __init__(self, puzzle): + self.puzzle = puzzle + self.puzzle_type = None + self.duration_sec = None + + @abstractmethod + def is_success(self): + pass + + def to_junit_test_case(self): + return TestCase( + name=f'Puzzle {self.puzzle.puzzle_id}', + status='run', + classname=self.puzzle_type, + elapsed_sec=self.duration_sec + ) + + +class PuzzleRunSuccess(PuzzleRunResult): + def __init__(self, puzzle): + super().__init__(puzzle) + + def is_success(self): + return True + + +class PuzzleRunFailure(PuzzleRunResult): + def __init__(self, puzzle, reason, info): + super().__init__(puzzle) + self.reason = reason + url = f'https://lichess.org/training/{self.puzzle.puzzle_id}' + self.info = f'URL: {url}\n{info}' + + def is_success(self): + return False + + def __str__(self): + return f'Failure reason: {self.reason}\n{self.info}' + + def to_junit_test_case(self): + test_case = super().to_junit_test_case() + test_case.add_failure_info( + message=self.reason, + output=self.info + ) + + return test_case + + +class PuzzleRunWrongMove(PuzzleRunFailure): + def __init__(self, puzzle, position, move, expected_move): + reason = 'unexpected move' + info = f'position={position}\n' \ + f'move={move.uci()}\n' \ + f'expected move={expected_move.uci()}' + + super().__init__(puzzle, reason, info) + + +class PuzzleRunTimeout(PuzzleRunFailure): + def __init__(self, puzzle, timeout): + reason = 'timeout' + info = f'Puzzle timed out after {timeout} seconds' + super().__init__(puzzle, reason, info) + + +class PuzzleRunException(PuzzleRunFailure): + def __init__(self, puzzle, exception): + reason = 'exception' + info = exception + + super().__init__(puzzle, reason, info) + + +@contextlib.asynccontextmanager +async def create_engine(engine_path): + transport, engine = \ + await chess.engine.popen_uci(engine_path) + + try: + yield engine + finally: + try: + await asyncio.wait_for(engine.quit(), timeout=1) + except asyncio.TimeoutError: + try: + transport.kill() + transport.close() + except Exception: + pass + + +async def _run_puzzle(puzzle, engine_path, total_time): + start_time = time.time() + + moves = puzzle.moves[:] + assert len(moves) >= 2 + assert len(moves) % 2 == 0 + + board = chess.Board(puzzle.fen) + + time_limit = chess.engine.Limit() + use_limit = total_time is not None + + if use_limit: + time_limit.white_clock = total_time + time_limit.black_clock = total_time + time_limit.white_inc = 0 + time_limit.black_inc = 0 + time_limit.remaining_moves = next_multiple(puzzle.num_plies() // 2, 5) + time_left = total_time + else: + time_left = None + + async with create_engine(engine_path) as engine: + while len(moves) > 0: + board.push(moves.pop(0)) + + try: + result = await asyncio.wait_for(engine.play(board, time_limit), + timeout=time_left) + except asyncio.TimeoutError: + return PuzzleRunTimeout(puzzle, total_time) + + board.push(result.move) + expected_move = moves.pop(0) + + if result.move != expected_move: + if len(moves) == 0 and board.is_checkmate(): + break + else: + board.pop() + return PuzzleRunWrongMove(puzzle, board.fen(), + result.move, expected_move) + + if use_limit: + time_limit.remaining_moves -= 1 + current_time = time.time() + elapsed_time = current_time - start_time + time_left = total_time - elapsed_time + + if time_left < 0: + return PuzzleRunTimeout(puzzle, total_time) + + # The last move we made on the board was for the engine. So the + # current turn is for the engine's opponent. We only update the + # engine's clock to reflect the time limit. + if board.turn == chess.WHITE: + time_limit.black_clock = time_left + else: + time_limit.white_clock = time_left + + return PuzzleRunSuccess(puzzle) + + +def run_puzzle(puzzle, engine_path, timeout): + async def run(): + try: + return await _run_puzzle(puzzle, engine_path, timeout) + except Exception as e: + return PuzzleRunException(puzzle, e) + + start_time = time.time() + result = asyncio.run(run()) + end_time = time.time() + result.duration_sec = end_time - start_time + return result + + +def format_duration(duration): + return f'{duration:.3f}s' + + +def next_multiple(n, multiple): + return n + (multiple - n % multiple) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument('--engine', type=Path, required=True) + parser.add_argument('--timeout', type=float, + help='Timeout in seconds per puzzle') + parser.add_argument('--junit', type=Path) + parser.add_argument('puzzle_dbs', type=Path, nargs='+') + args = parser.parse_args() + + results = defaultdict(list) + num_fails = 0 + total_duration = 0 + + for puzzle_db_path in args.puzzle_dbs: + print(f'=== Running puzzles from {puzzle_db_path.resolve()} ===') + + with puzzle_db_path.open(newline='') as f: + puzzles = PuzzleDb.from_csv(f) + + for puzzle in puzzles: + print(f'Running puzzle {puzzle.puzzle_id} ... ', + end='', flush=True) + result = run_puzzle(puzzle, args.engine, args.timeout) + total_duration += result.duration_sec + duration_msg = f'({format_duration(result.duration_sec)})' + + if result.is_success(): + print(f'OK {duration_msg}') + else: + num_fails += 1 + print(f'FAIL {duration_msg}') + print(f'===\n{result}\n===') + + results[puzzle_db_path].append(result) + + if args.junit is not None: + test_suites = [] + + for db_path, db_results in results.items(): + name = f'puzzles.{db_path.stem}' + + def create_test_case(result): + result.puzzle_type = name + return result.to_junit_test_case() + + test_cases = [create_test_case(r) for r in db_results] + test_suite = TestSuite(name, test_cases) + test_suites.append(test_suite) + + xml = junit_xml.to_xml_report_string(test_suites) + args.junit.write_text(xml) + + print(f'Total time: {format_duration(total_duration)}') + + if num_fails > 0: + sys.exit(f'{num_fails} tests failed') + else: + print('All tests passed') + + +if __name__ == '__main__': + main() diff --git a/TimeInfo.hpp b/TimeInfo.hpp new file mode 100644 index 0000000..09b73a9 --- /dev/null +++ b/TimeInfo.hpp @@ -0,0 +1,20 @@ +#ifndef CHESS_ENGINE_TIMEINFO_HPP +#define CHESS_ENGINE_TIMEINFO_HPP + +#include +#include + +struct PlayerTimeInfo { + std::chrono::milliseconds timeLeft; + std::chrono::milliseconds increment; +}; + +struct TimeInfo { + using Optional = std::optional; + + PlayerTimeInfo white; + PlayerTimeInfo black; + std::optional movesToGo; +}; + +#endif diff --git a/Uci.cpp b/Uci.cpp new file mode 100644 index 0000000..2506ceb --- /dev/null +++ b/Uci.cpp @@ -0,0 +1,389 @@ +#include "Uci.hpp" + +#include "Engine.hpp" +#include "Fen.hpp" + +#include +#include +#include +#include +#include +#include + +class UciOptionBase { +public: + + virtual ~UciOptionBase() = default; + + virtual std::string name() const = 0; + virtual std::string type() const = 0; + virtual void streamOptionCommand(std::ostream& stream) const = 0; + virtual bool setValue(Engine& engine, std::istream& stream) const = 0; +}; + +template +class UciOption : public UciOptionBase { +public: + + using Value = T; + using OptionalValue = std::optional; + + virtual OptionalValue default_() const { + return std::nullopt; + } + + virtual OptionalValue min() const { + return std::nullopt; + } + + virtual OptionalValue max() const { + return std::nullopt; + } + + virtual std::vector vars() const { + return {}; + } + + void streamOptionCommand(std::ostream& stream) const override { + stream << "option name " << name() << " type " << type(); + streamIfHasValue(stream, "default", default_()); + streamIfHasValue(stream, "min", min()); + streamIfHasValue(stream, "max", max()); + + for (auto var : vars()) { + stream << ' ' << "var " << var; + } + } + + bool setValue(Engine& engine, std::istream& stream) const override { + if (Value value; stream >> value) { + return setValue(engine, value); + } else { + return false; + } + } + + virtual bool setValue(Engine& engine, Value value) const = 0; + +private: + + void streamIfHasValue(std::ostream& stream, + const char* name, + OptionalValue info) const { + if (info.has_value()) { + stream << ' ' << name << ' ' << info.value(); + } + } + +}; + +template +class UciSpinOption : public UciOption { +public: + + std::string type() const override { + return "spin"; + } +}; + +class UciHashOption : public UciSpinOption { +public: + + UciHashOption(const HashInfo& hashInfo) : hashInfo_(hashInfo) {} + + std::string name() const override { + return "Hash"; + } + + OptionalValue default_() const override { + return hashInfo_.defaultSize; + } + + OptionalValue min() const override { + return hashInfo_.minSize; + } + + OptionalValue max() const override { + return hashInfo_.maxSize; + } + + bool setValue(Engine& engine, Value value) const override { + if (value >= hashInfo_.minSize && value <= hashInfo_.maxSize) { + engine.setHashSize(value); + return true; + } else { + return false; + } + } + +private: + + HashInfo hashInfo_; +}; + +Uci::Uci(std::unique_ptr engine, + std::istream& cmdIn, + std::ostream& cmdOut, + std::ostream& log +) : engine_(std::move(engine)), cmdIn_(cmdIn), cmdOut_(cmdOut), log_(log) { + if (auto hashInfo = engine_->hashInfo(); hashInfo) { + auto hashOption = std::make_unique(*hashInfo); + options_[hashOption->name()] = std::move(hashOption); + } +} + +// Needed here because Engine is only forward-declared in Uci.hpp causing an +// error when compiling the destructor of std::unique_ptr. +Uci::~Uci() = default; + +void Uci::run() { + log_ << "UCI engine started" << std::endl; + + while (!cmdIn_.eof()) { + std::string line; + std::getline(cmdIn_, line); + runCommand(line); + } +} + +void Uci::runCommand(const std::string& line) { + log_ << "> " << line << std::endl; + + auto stream = std::stringstream(line); + auto command = std::string(); + stream >> command; + + if (command == "uci") { + uciCommand(stream); + } else if (command == "setoption") { + setoptionCommand(stream); + } else if (command == "isready") { + isreadyCommand(stream); + } else if (command == "ucinewgame") { + ucinewgameCommand(stream); + } else if (command == "position") { + positionCommand(stream); + } else if (command == "go") { + goCommand(stream); + } else if (command == "quit") { + quitCommand(stream); + } +} + +void Uci::uciCommand(std::istream&) { + std::stringstream nameCommand; + nameCommand << "id name " << engine_->name() << " " << engine_->version(); + sendCommand(nameCommand.str()); + + std::stringstream authorCommand; + authorCommand << "id author " << engine_->author(); + sendCommand(authorCommand.str()); + + sendOptions(); + sendCommand("uciok"); +} + +void Uci::isreadyCommand(std::istream&) { + sendCommand("readyok"); +} + +void Uci::ucinewgameCommand(std::istream&) { + engine_->newGame(); +} + +void Uci::positionCommand(std::istream& stream) { + auto type = std::string(); + stream >> type; + + auto newBoard = Board::Optional(); + + if (type == "startpos") { + newBoard = Fen::createBoard(Fen::StartingPos); + } else if (type == "fen") { + newBoard = Fen::createBoard(stream); + } else { + error("Illegal position type " + type); + return; + } + + if (!newBoard.has_value()) { + error("Illegal FEN"); + return; + } + + board_ = newBoard.value(); + + auto moves = std::string(); + stream >> moves; + + if (moves == "moves") { + while (!stream.eof()) { + auto uciMove = std::string(); + stream >> uciMove; + + if (uciMove.empty()) { + continue; + } + + auto optMove = Move::fromUci(uciMove); + + if (!optMove.has_value()) { + error("Illegal move " + uciMove); + return; + } + + board_.makeMove(optMove.value()); + } + } + + log_ << board_ << std::endl; +} + +template +static std::optional readValue(std::istream& stream) { + if (T value; stream >> value) { + return value; + } else { + return std::nullopt; + } +} + +TimeInfo::Optional Uci::readTimeInfo(std::istream& stream) { + std::optional wtime, winc, btime, binc, movestogo; + + for (std::string command; stream >> command;) { + auto value = readValue(stream); + + if (command == "wtime") { + wtime = value; + } else if (command == "winc") { + winc = value; + } else if (command == "btime") { + btime = value; + } else if (command == "binc") { + binc = value; + } else if (command == "movestogo") { + movestogo = value; + } else if (command == "infinite") { + error("go infinite not supported"); + return std::nullopt; + } + } + + if (wtime.has_value() && btime.has_value()) { + PlayerTimeInfo whiteTime, blackTime; + whiteTime.timeLeft = std::chrono::milliseconds(wtime.value()); + whiteTime.increment = std::chrono::milliseconds(winc.value_or(0)); + blackTime.timeLeft = std::chrono::milliseconds(btime.value()); + blackTime.increment = std::chrono::milliseconds(binc.value_or(0)); + + TimeInfo timeInfo; + timeInfo.white = whiteTime; + timeInfo.black = blackTime; + timeInfo.movesToGo = movestogo; + return timeInfo; + } else { + return std::nullopt; + } +} + +void Uci::goCommand(std::istream& stream) { + auto timeInfo = readTimeInfo(stream); + auto pv = engine_->pv(board_, timeInfo); + + if (pv.length() == 0) { + error("Engine returned no PV"); + return; + } + + log_ << "PV: " << pv << std::endl; + sendPvInfo(pv); + + auto bestMove = *pv.begin(); + board_.makeMove(bestMove); + log_ << board_ << std::endl; + + auto bestMoveCmd = std::stringstream(); + bestMoveCmd << "bestmove " << bestMove; + sendCommand(bestMoveCmd.str()); +} + +void Uci::quitCommand(std::istream&) { + std::exit(EXIT_SUCCESS); +} + +void Uci::setoptionCommand(std::istream& stream) { + std::string nameCommand; + stream >> nameCommand; + + if (nameCommand != "name") { + error("Illegal setoption: did not start with 'name'"); + return; + } + + std::string name; + stream >> name; + + for (std::string nextPart; stream >> nextPart;) { + if (nextPart == "value") { + break; + } + + name += ' ' + nextPart; + } + + // We could get here without a "value" command. This is by design because + // "button" types don't have one. + auto optionIt = options_.find(name); + + if (optionIt == options_.end()) { + error("Illegal option: " + name); + return; + } + + if (!optionIt->second->setValue(*engine_, stream)) { + error("Illegal option value"); + return; + } +} + +void Uci::sendPvInfo(const PrincipalVariation& pv) { + auto stream = std::stringstream(); + stream << "info score "; + + auto score = pv.score(); + + if (pv.isMate()) { + auto numMoves = + score < 0 ? std::floor(score / 2.0) : std::ceil(score / 2.0); + stream << "mate " << numMoves; + } else { + stream << "cp " << score; + } + + stream << " pv"; + + for (auto move : pv) { + stream << ' ' << move; + } + + sendCommand(stream.str()); +} + +void Uci::sendOptions() { + for (const auto& [name, option] : options_) { + std::stringstream cmd; + option->streamOptionCommand(cmd); + sendCommand(cmd.str()); + } +} + +void Uci::sendCommand(const std::string& command) { + log_ << "< " << command << std::endl; + cmdOut_ << command << std::endl; +} + +void Uci::error(const std::string& msg) { + log_ << "UCI error: " << msg << std::endl; + std::exit(EXIT_FAILURE); +} diff --git a/Uci.hpp b/Uci.hpp new file mode 100644 index 0000000..d338a45 --- /dev/null +++ b/Uci.hpp @@ -0,0 +1,51 @@ +#ifndef CHESS_ENGINE_UCI_HPP +#define CHESS_ENGINE_UCI_HPP + +#include "Board.hpp" +#include "TimeInfo.hpp" + +#include +#include +#include +#include + +class Engine; +class PrincipalVariation; +class UciOptionBase; + +class Uci { +public: + + Uci(std::unique_ptr engine, + std::istream& cmdIn, + std::ostream& cmdOut, + std::ostream& log); + ~Uci(); + + void run(); + +private: + + void runCommand(const std::string& line); + void uciCommand(std::istream& stream); + void isreadyCommand(std::istream& stream); + void ucinewgameCommand(std::istream& stream); + void positionCommand(std::istream& stream); + void goCommand(std::istream& stream); + void quitCommand(std::istream& stream); + void setoptionCommand(std::istream& stream); + TimeInfo::Optional readTimeInfo(std::istream& stream); + void sendPvInfo(const PrincipalVariation& pv); + void sendOptions(); + void sendCommand(const std::string& line); + void error(const std::string& msg); + + std::unique_ptr engine_; + Board board_; + std::istream& cmdIn_; + std::ostream& cmdOut_; + std::ostream& log_; + std::map> options_; +}; + +#endif