diff --git a/BasicEngine.cpp b/BasicEngine.cpp index 4d42ff2..83e947a 100644 --- a/BasicEngine.cpp +++ b/BasicEngine.cpp @@ -1,4 +1,5 @@ #include "BasicEngine.hpp" +#include "Search.hpp" std::string BasicEngine::name() const { return mName; @@ -15,5 +16,5 @@ void BasicEngine::newGame() { PrincipalVariation BasicEngine::pv(const Board &board, const TimeInfo::Optional &timeInfo) { (void)board; (void)timeInfo; - return PrincipalVariation(); + return Search::start(board); } \ No newline at end of file diff --git a/BitBoard.cpp b/BitBoard.cpp index 72e285f..e2c66e6 100644 --- a/BitBoard.cpp +++ b/BitBoard.cpp @@ -1,4 +1,3 @@ -#include #include "BitBoard.hpp" BitBoard::BitBoard(uint64_t v) { @@ -67,11 +66,12 @@ BitBoard BitBoard::castlingMoves(BitBoard kings, BitBoard rooks, BitBoard empty) BitBoard BitBoard::bishopAttacks(BitBoard bishops, BitBoard empty) { BitBoard result = 0; - BitBoard diag1 = bishops; - BitBoard diag2 = bishops; empty ^= bishops; - for (int i = 0; i < 7; i++) { + BitBoard diag1 = (bishops.northWest() | bishops.southEast() | bishops) & empty; + BitBoard diag2 = (bishops.northEast() | bishops.southWest() | bishops) & empty; + + for (int i = 0; i < 6; i++) { result |= (diag1 | diag2); diag1 = (diag1.northWest() | diag1.southEast()) & empty; diag2 = (diag2.northEast() | diag2.southWest()) & empty; @@ -82,17 +82,18 @@ BitBoard BitBoard::bishopAttacks(BitBoard bishops, BitBoard empty) { BitBoard BitBoard::rookAttacks(BitBoard rooks, BitBoard empty) { BitBoard result = 0; - BitBoard diag1 = rooks; - BitBoard diag2 = rooks; empty ^= rooks; - for (int i = 0; i < 7; i++) { - result |= (diag1 | diag2); - diag1 = (diag1.north() | diag1.south()) & empty; - diag2 = (diag2.east() | diag2.west()) & empty; + BitBoard dir1 = (rooks.north() | rooks.south() | rooks) & empty; + BitBoard dir2 = (rooks.east() | rooks.west() | rooks) & empty; + + for (int i = 0; i < 6; i++) { + result |= (dir1 | dir2); + dir1 = (dir1.north() | dir1.south()) & empty; + dir2 = (dir2.east() | dir2.west()) & empty; } - return (result | diag1.north() | diag1.south() | diag2.east() | diag2.west()) & ~rooks; + return (result | dir1.north() | dir1.south() | dir2.east() | dir2.west()) & ~rooks; } BitBoard BitBoard::queenAttacks(BitBoard queens, BitBoard empty) { diff --git a/BitBoard.hpp b/BitBoard.hpp index c67b3cc..21d0ce7 100644 --- a/BitBoard.hpp +++ b/BitBoard.hpp @@ -127,7 +127,7 @@ public: unsigned pop(); private: - U64 mBoard = {}; + U64 mBoard = 0; }; // Relational operators diff --git a/Board.cpp b/Board.cpp index 8c829fd..aa9d575 100644 --- a/Board.cpp +++ b/Board.cpp @@ -32,6 +32,45 @@ Piece::Optional Board::piece(const Square &square) const { return Piece(c, t); } +int Board::evaluate() const { + int score = evaluate(mTurn); + score -= evaluate(!mTurn); + + return score; +} + +int Board::evaluate(const PieceColor color) const { + // Pawns + int score = 0; + + BitBoard bb; + BitBoard colorMask = mPieceBBs[toIndex(color)]; + for (int i = 2; i < 8; i++) { + bb = mPieceBBs[i] & colorMask; + while (bb) { + bb.pop(); + score += mPieceValue[i]; + } + } + + return score; +} + +bool Board::isCheckMate() const { + auto moves = MoveVec(); + pseudoLegalMoves(moves); + + if (!moves.empty()) + return false; + + BoardState bs = BoardState(&mPieceBBs, mOccupiedBB, mTurn, mCR, mEPS); + auto attacked = MoveGenerator::generateAttackedSquares(bs, ~mOccupiedBB, !mTurn); + + return attacked & mPieceBBs[toIndex(PieceType::King)] & mPieceBBs[toIndex(mTurn)]; + + return moves.empty(); +} + void Board::setTurn(PieceColor turn) { mTurn = turn; } @@ -143,10 +182,11 @@ void Board::handlePawnDoubleAdvance(const Move &move, BitBoard bb, const Piece & } void Board::pseudoLegalMoves(MoveVec &moves) const { - auto occupied = mOccupiedBB; - while(occupied) { - auto to = Square(occupied.pop()); - pseudoLegalMovesFrom(to, moves); + auto pieces = mPieceBBs[toIndex(mTurn)]; + + while (pieces) { + auto sq = Square(pieces.pop()); + pseudoLegalMovesFrom(sq, moves); } } @@ -158,29 +198,21 @@ void Board::pseudoLegalMovesFrom(const Square &from, Board::MoveVec &moves) cons } BoardState bs = BoardState(&mPieceBBs, mOccupiedBB, mTurn, mCR, mEPS); - auto p = Piece(mTurn, pieceType(fromBB)); - BitBoard movesBB; - switch (p.type()) { + switch (pieceType(fromBB)) { case PieceType::Pawn: MoveGenerator::generatePawnMoves(bs, from, moves); - return; + break; case PieceType::Knight: MoveGenerator::generateKnightMoves(bs, from, moves); - return; + break; case PieceType::Bishop: MoveGenerator::generateBishopMoves(bs, from, moves); - return; + break; case PieceType::Rook: MoveGenerator::generateRookMoves(bs, from, moves); - return; + break; case PieceType::Queen: MoveGenerator::generateQueenMoves(bs, from, moves); - return; - + break; case PieceType::King: MoveGenerator::generateKingMoves(bs, from, moves); break; } - - while (movesBB) { - auto to = Square(movesBB.pop()); - moves.emplace_back(from, to, std::nullopt); - } } void Board::handleCastlingRights(const Piece &piece, const BitBoard &bb) { if (piece.type() != PieceType::King && piece.type() != PieceType::Rook) { diff --git a/Board.hpp b/Board.hpp index 91b0189..23e44ec 100644 --- a/Board.hpp +++ b/Board.hpp @@ -21,13 +21,11 @@ public: using MoveVec = std::vector; Board() = default; - Board(const Board &) = default; - Board(Board &&other) = default; - Board &operator=(const Board &) = default; - Board &operator=(Board &&) = default; void setPiece(const Square &square, const Piece::Optional &piece); Piece::Optional piece(const Square &square) const; + std::vector pieces(PieceColor color) const; + int evaluate() const; void setTurn(PieceColor turn); PieceColor turn() const; void setCastlingRights(CastlingRights cr); @@ -47,6 +45,9 @@ public: return static_cast(c); } + + bool isCheckMate() const; + private: BitBoard mPieceBBs[BB_NUM] = {}; @@ -56,6 +57,8 @@ private: CastlingRights mCR = CastlingRights::None; std::optional mEPS; + constexpr static const int mPieceValue[8] = {0,0, 100, 350, 350, 525, 1000, 10000}; + void handleCastlingRights(const Piece &piece, const BitBoard &bb); // Check if the move is castling without checking the rights or validity. @@ -83,6 +86,7 @@ private: void handlePawnDoubleAdvance(const Move &move, BitBoard bb, const Piece &movedPiece); void handleEnPassant(const Move &move, const Piece &movedPiece); + int evaluate(const PieceColor color) const; }; std::ostream &operator<<(std::ostream &os, const Board &board); diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bceaa0..b47345f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ else () endif () if (CMAKE_BUILD_TYPE STREQUAL "Release") - add_compile_options(-O3 -march=native) + add_compile_options(-O3 -march=native -funroll-loops) endif() add_library(cplchess_lib OBJECT diff --git a/Move.hpp b/Move.hpp index 48346ad..73de343 100644 --- a/Move.hpp +++ b/Move.hpp @@ -23,9 +23,9 @@ public: std::optional promotion() const; private: - const Square mFrom; - const Square mTo; - const std::optional mPromotion; + Square mFrom; + Square mTo; + std::optional mPromotion; }; std::ostream &operator<<(std::ostream &os, const Move &move); diff --git a/MoveGenerator.cpp b/MoveGenerator.cpp index 3cc181a..9626a33 100644 --- a/MoveGenerator.cpp +++ b/MoveGenerator.cpp @@ -1,3 +1,4 @@ +#include #include "MoveGenerator.hpp" #include "Board.hpp" #include "BoardState.hpp" @@ -28,9 +29,9 @@ void MoveGenerator::generatePawnMoves(const BoardState &bs, const Square &from, bool isPromotion = movesBB & (Rank1 | Rank8); if (isPromotion) { - generateMovesWithPromotion(from, movesBB, moves); + generateMovesWithPromotion(bs, from, movesBB, moves); } else { - generateMoves(from, movesBB, moves); + generateMoves(bs, from, movesBB, moves); } } @@ -38,19 +39,29 @@ void MoveGenerator::generateBishopMoves(const BoardState &bs, const Square &from auto fromBB = BitBoard::fromIndex(from.index()); auto movesBB = BitBoard::bishopAttacks(fromBB, ~bs.occupiedBB) & ~(*bs.pieceBBs)[Board::toIndex(bs.turn)]; - generateMoves(from, movesBB, moves); + generateMoves(bs, from, movesBB, moves); } -void MoveGenerator::generateMoves(const Square &from, BitBoard movesBB, MoveVec &moves) { +void MoveGenerator::generateMoves(const BoardState &bs, const Square &from, BitBoard movesBB, MoveVec &moves) { + auto fromBB = BitBoard::fromIndex(from.index()); while (movesBB) { auto to = Square(movesBB.pop()); - moves.emplace_back(from, to, std::nullopt); + if (isCheck(bs, fromBB, to)) { + continue; + } + + moves.emplace_back(from, to); } } -void MoveGenerator::generateMovesWithPromotion(const Square &from, BitBoard movesBB, MoveVec &moves) { +void MoveGenerator::generateMovesWithPromotion(const BoardState &bs, const Square &from, BitBoard movesBB, MoveVec &moves) { + auto fromBB = BitBoard::fromIndex(from.index()); while (movesBB) { auto to = Square(movesBB.pop()); + if (isCheck(bs, fromBB, to)) { + continue; + } + for (const auto &kItem : Piece::PromotionTypes) { moves.emplace_back(from, to, static_cast(kItem)); } @@ -74,7 +85,7 @@ void MoveGenerator::generateKingMoves(const BoardState &bs, const Square &from, if (checkCR != CastlingRights::None) { // Generate attacked squares BitBoard target = CastlingRanks | bs.occupiedBB; - auto attacked = generateAttackedSquares(bs, target, !bs.turn); + auto attacked = generateAttackedSquares(bs, ~target, !bs.turn); movesBB |= BitBoard::castlingMoves(fromBB & ~attacked, (*bs.pieceBBs)[Board::toIndex(PieceType::Rook)], @@ -89,28 +100,28 @@ void MoveGenerator::generateKingMoves(const BoardState &bs, const Square &from, } } - generateMoves(from, movesBB, moves); + generateMoves(bs, from, movesBB, moves); } void MoveGenerator::generateRookMoves(const BoardState &bs, const Square &from, MoveVec &moves) { auto fromBB = BitBoard::fromIndex(from.index()); auto movesBB = BitBoard::rookAttacks(fromBB, ~bs.occupiedBB) & ~(*bs.pieceBBs)[Board::toIndex(bs.turn)]; - generateMoves(from, movesBB, moves); + generateMoves(bs, from, movesBB, moves); } void MoveGenerator::generateQueenMoves(const BoardState &bs, const Square &from, MoveVec &moves) { auto fromBB = BitBoard::fromIndex(from.index()); auto movesBB = BitBoard::queenAttacks(fromBB, ~bs.occupiedBB) & ~(*bs.pieceBBs)[Board::toIndex(bs.turn)]; - generateMoves(from, movesBB, moves); + generateMoves(bs, from, movesBB, moves); } void MoveGenerator::generateKnightMoves(const BoardState &bs, const Square &from, MoveVec &moves) { auto fromBB = BitBoard::fromIndex(from.index()); auto movesBB = BitBoard::knightMoves(fromBB) & ~(*bs.pieceBBs)[Board::toIndex(bs.turn)]; - generateMoves(from, movesBB, moves); + generateMoves(bs, from, movesBB, moves); } BitBoard MoveGenerator::generateAttackedSquares(const BoardState &bs, BitBoard target, PieceColor opColor) { @@ -125,7 +136,7 @@ BitBoard MoveGenerator::generateAttackedSquares(const BoardState &bs, BitBoard t } // knights - attacked |= BitBoard::knightMoves((*bs.pieceBBs)[Board::toIndex(PieceType::Knight)] & opponentBB) & target; + attacked |= BitBoard::knightMoves((*bs.pieceBBs)[Board::toIndex(PieceType::Knight)] & opponentBB) & ~target; // Bishop attacked |= BitBoard::bishopAttacks((*bs.pieceBBs)[Board::toIndex(PieceType::Bishop)] & opponentBB, target); @@ -137,7 +148,20 @@ BitBoard MoveGenerator::generateAttackedSquares(const BoardState &bs, BitBoard t attacked |= BitBoard::queenAttacks((*bs.pieceBBs)[Board::toIndex(PieceType::Queen)] & opponentBB, target); // King - attacked |= BitBoard::kingMoves((*bs.pieceBBs)[Board::toIndex(PieceType::Queen)] & opponentBB) & target; + attacked |= BitBoard::kingMoves((*bs.pieceBBs)[Board::toIndex(PieceType::Queen)] & opponentBB) & ~target; return attacked; } +inline bool MoveGenerator::isCheck(const BoardState &bs, const BitBoard &fromBB, const Square &to) { + auto kingBB = (*bs.pieceBBs)[Board::toIndex(bs.turn)] & (*bs.pieceBBs)[Board::toIndex(PieceType::King)]; + auto toBB = BitBoard::fromIndex(to.index()); + auto changeBB = (fromBB ^ toBB); + auto target = bs.occupiedBB ^ changeBB; + auto attacked = generateAttackedSquares(bs, ~target, !bs.turn); + + if (kingBB & fromBB) { + kingBB ^= changeBB; + } + + return attacked & kingBB; +} diff --git a/MoveGenerator.hpp b/MoveGenerator.hpp index 38e89e9..0101cd6 100644 --- a/MoveGenerator.hpp +++ b/MoveGenerator.hpp @@ -23,13 +23,15 @@ public: static void generateRookMoves(const BoardState &bs, const Square &from, MoveVec &moves); static void generateQueenMoves(const BoardState &bs, const Square &from, MoveVec &moves); static void generateKnightMoves(const BoardState &bs, const Square &from, MoveVec &moves); + static BitBoard generateAttackedSquares(const BoardState &bs, BitBoard target, PieceColor opColor); private: static void generatePawnMoves(const BoardState &bs, const Square &from, BitBoard targets, MoveVec &moves); - static void generateMoves(const Square &from, BitBoard movesBB, MoveVec &moves); - static void generateMovesWithPromotion(const Square &from, BitBoard movesBB, MoveVec &moves); - static BitBoard generateAttackedSquares(const BoardState &bs, BitBoard target, PieceColor opColor); + static void generateMoves(const BoardState &bs, const Square &from, BitBoard movesBB, MoveVec &moves); + static void generateMovesWithPromotion(const BoardState &bs, const Square &from, BitBoard movesBB, MoveVec &moves); + + inline static bool isCheck(const BoardState &bs, const BitBoard &fromBB, const Square &to); }; #endif //CHESS_ENGINE_MOVEGENERATOR_HPP diff --git a/PrincipalVariation.cpp b/PrincipalVariation.cpp index 4b8857c..89628be 100644 --- a/PrincipalVariation.cpp +++ b/PrincipalVariation.cpp @@ -1,29 +1,53 @@ #include "PrincipalVariation.hpp" #include +#include + + +PrincipalVariation::PrincipalVariation(const std::vector &v, bool isMate): mMoves(v), mIsMate(isMate) { + +} + +PrincipalVariation::PrincipalVariation(const std::vector &v, Board &board): mMoves(v) { + for (const auto &kMove : v) { + board.makeMove(kMove); + } + + mIsMate = board.isCheckMate(); + if (mIsMate || v.empty()) { + mScore = static_cast(v.size()); + } else { + mScore = board.evaluate(); + } +} bool PrincipalVariation::isMate() const { - return false; + return mIsMate; } int PrincipalVariation::score() const { - return 0; + return mScore; } std::size_t PrincipalVariation::length() const { - return 0; + return mMoves.size(); } PrincipalVariation::MoveIter PrincipalVariation::begin() const { - return nullptr; + return mMoves.begin(); } PrincipalVariation::MoveIter PrincipalVariation::end() const { - return nullptr; + return mMoves.end(); } - -std::ostream& operator<<(std::ostream& os, const PrincipalVariation& pv) { - (void)pv; +std::ostream &operator<<(std::ostream &os, const PrincipalVariation &pv) { + os << "isMate=" << pv.isMate(); + os << " score=" << pv.score(); + os << " length=" << pv.length() << std::endl; + os << "Moves:" << std::endl; + for (const auto &item : pv) { + os << item << std::endl; + } return os; } diff --git a/PrincipalVariation.hpp b/PrincipalVariation.hpp index 5dc30ee..755821d 100644 --- a/PrincipalVariation.hpp +++ b/PrincipalVariation.hpp @@ -3,23 +3,33 @@ #include "Move.hpp" #include "Piece.hpp" +#include "Board.hpp" #include #include +#include class PrincipalVariation { public: - using MoveIter = Move*; + using MoveIter = std::vector::const_iterator; + PrincipalVariation() = default; + PrincipalVariation(const std::vector &v, bool isMate); + PrincipalVariation(const std::vector &v, Board &board); bool isMate() const; int score() const; std::size_t length() const; MoveIter begin() const; MoveIter end() const; + +private: + std::vector mMoves; + bool mIsMate = false; + int mScore = 0; }; -std::ostream& operator<<(std::ostream& os, const PrincipalVariation& pv); +std::ostream &operator<<(std::ostream &os, const PrincipalVariation &pv); #endif diff --git a/Search.cpp b/Search.cpp index 1b8cceb..fac0b58 100644 --- a/Search.cpp +++ b/Search.cpp @@ -1,3 +1,75 @@ #include "Search.hpp" +#include "Move.hpp" +#include "Board.hpp" +int Search::search(Node *node, const Board &board, unsigned depth) { + if (depth == 0) { + return evaluate(board); + } + + auto moves = Board::MoveVec(); + board.pseudoLegalMoves(moves); + if (moves.empty()) { + return evaluate(board); + } + + int maxValue = kNegInfinity; + for (auto &kMove: moves) { + Board nodeBoard = board; + nodeBoard.makeMove(kMove); + + Node child(kMove); + child.value = search(&child, nodeBoard, depth - 1); + + auto value = -child.value; + if (value > maxValue) { + maxValue = value; + node->next = std::make_unique(child); + } + } + + node->value = maxValue; + + return maxValue; +} + +int Search::evaluate(const Board &board) { + + return board.evaluate(); +} + +PrincipalVariation Search::start(const Board &board) { + auto rootMoves = Board::MoveVec(); + board.pseudoLegalMoves(rootMoves); + + if (rootMoves.size() <= 1) { + auto b = board; + return PrincipalVariation(rootMoves, b); + } + + std::unique_ptr best = std::make_unique(); + for (const auto &kMove: rootMoves) { + + auto node = Node(kMove); + + Board nodeBoard = board; + nodeBoard.makeMove(kMove); + + auto value = -search(&node, nodeBoard, 4); + if (value > best->value) { + best->child = std::make_unique(node); + best->board = nodeBoard; + best->value = value; + } + } + + auto moves = std::vector(); + auto current = std::move(best->child); + do { + moves.push_back(current->move); + current = std::move(current->next); + } while (current); + + return PrincipalVariation(moves, best->board); +} diff --git a/Search.hpp b/Search.hpp index 40f3d56..eba1c9f 100644 --- a/Search.hpp +++ b/Search.hpp @@ -1,8 +1,38 @@ #ifndef CHESS_ENGINE_SEARCH_HPP #define CHESS_ENGINE_SEARCH_HPP -class Search { +#include "PrincipalVariation.hpp" +#include "Board.hpp" +#include +#include + +namespace Search { + +const int kNegInfinity = std::numeric_limits::min(); + +struct Node { + explicit Node(const Move move) : move(move) { + } + + Node(Node &node) : move(node.move) { + next = std::move(node.next); + value = node.value + 10; + } + std::unique_ptr next; + Move move; + int value = kNegInfinity; }; +struct RootNode { + std::unique_ptr child; + Board board; + int value = kNegInfinity; +}; + +int search(Node *node, const Board &board, unsigned depth); +PrincipalVariation start(const Board &board); +int evaluate(const Board &board); +} + #endif //CHESS_ENGINE_SEARCH_HPP diff --git a/Tests/BoardTests.cpp b/Tests/BoardTests.cpp index b488f50..5cc9532 100644 --- a/Tests/BoardTests.cpp +++ b/Tests/BoardTests.cpp @@ -224,6 +224,18 @@ TEST_CASE_PSEUDO_MOVES("Pseudo-legal bishop moves, empty board", "[Bishop]") { ); } +TEST_CASE_PSEUDO_MOVES("Pseudo-legal bishop moves, empty board side", "[Bishop]") { + testPseudoLegalMoves( + // https://lichess.org/editor/B7/8/8/8/8/8/8/8_w_-_-_0_1 + "B7/8/8/8/8/8/8/8 w - - 0 1", + "a8", + { + "b7", "c6", "d5", + "e4", "f3", "g2", "h1" + } + ); +} + 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 @@ -252,6 +264,41 @@ TEST_CASE_PSEUDO_MOVES("Pseudo-legal rook moves, empty board", "[Rook]") { ); } +TEST_CASE_PSEUDO_MOVES("Pseudo-legal rook moves, empty board side", "[Rook]") { + testPseudoLegalMoves( + // https://lichess.org/editor/3R4/8/8/8/8/8/8/8_w_-_-_0_1 + "3R4/8/8/8/8/8/8/8 w - - 0 1", + "d8", + { + "d1", "d2", "d3", "d4", "d5", "d6", "d7", + "a8", "b8", "c8", + "e8", "f8", "g8", + "h8" + } + ); +} + + + +TEST_CASE_PSEUDO_MOVES("legal rook moves, king check", "[Rook]") { + testPseudoLegalMoves( +// https://lichess.org/editor/5R1k/6pp/8/8/8/8/8/K7_b_-_-_0_1 + "5R1k/6pp/8/8/8/8/8/K7 b - - 0 1", + "g7", + {} + ); +} + +TEST_CASE_PSEUDO_MOVES("legal king moves, king stale", "[Rook]") { + testPseudoLegalMoves( +// 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", + "e3", + {} + ); +} + + 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 diff --git a/Tests/EngineTests.cpp b/Tests/EngineTests.cpp index a62ac57..aeafb96 100644 --- a/Tests/EngineTests.cpp +++ b/Tests/EngineTests.cpp @@ -25,6 +25,22 @@ static void testGameEnd(const char* fen, bool isMate) { REQUIRE(pv.length() == 0); } +static void testMove(const char *fen, const std::vector &expectedMoves) { + auto engine = createEngine(); + REQUIRE(engine != nullptr); + + auto board = Fen::createBoard(fen); + REQUIRE(board.has_value()); + + for (const auto &moveName : expectedMoves) { + auto pv = engine->pv(board.value()); + Move move = *pv.begin(); + REQUIRE(move == Move::fromUci(moveName)); + board->makeMove(move); + } + +} + 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 @@ -46,3 +62,11 @@ TEST_CASE("Engine detects stalemate", "[Engine][Stalemate]") { testGameEnd(fen, false); } + +TEST_CASE("Puzzles mateIn1_simple", "[Engine][Puzzle]") { + auto [fen, moves] = GENERATE(table>({ + {"2bQ1k1r/1p3p2/p4b1p/4pNp1/2q5/8/PPP2PPP/1K1RR3 b - - 3 23",{"f6d8", "d1d8"}}, // https://lichess.org/gQa01loO/black#46 + {"5rk1/ppp5/2n4p/3b2p1/6R1/P1B1P2P/1Pq5/R2Q2K1 b - - 3 29",{"c2f2"}}, // https://lichess.org/haKJxadR#57 + })); + testMove(fen, moves); +}