commit 9f05ab03c172b2f870b9405b8f35786eacd45cab Author: Job Noorman Date: Thu Oct 27 12:29:19 2022 +0200 Add assignment 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