From ef566069e425be3cf428858a3594b74807dac4f0 Mon Sep 17 00:00:00 2001 From: Jean Bovet Date: Wed, 5 May 2021 22:21:32 -0700 Subject: [PATCH] Improved opening logic to handle variations --- BChess/Openings.pgn | 6 -- BChessTests/OpeningsTests.cpp | 92 ++++++++++++------------ Shared/Engine/Engine/ChessEngine.hpp | 9 ++- Shared/Engine/Engine/ChessGame.hpp | 16 +++++ Shared/Engine/Engine/ChessOpenings.cpp | 88 ++++++++++++----------- Shared/Engine/Engine/ChessOpenings.hpp | 96 +++++++++----------------- 6 files changed, 149 insertions(+), 158 deletions(-) diff --git a/BChess/Openings.pgn b/BChess/Openings.pgn index d9bc5c1..92ce0f3 100644 --- a/BChess/Openings.pgn +++ b/BChess/Openings.pgn @@ -5,12 +5,6 @@ 1. e4 c5 -[ECO "C20"] -[Name "King's pawn game"] -[Score "57"] - -1. e4 e5 - [Score "56"] 1. d4 diff --git a/BChessTests/OpeningsTests.cpp b/BChessTests/OpeningsTests.cpp index 3b33e3a..bc10188 100644 --- a/BChessTests/OpeningsTests.cpp +++ b/BChessTests/OpeningsTests.cpp @@ -24,45 +24,31 @@ class OpeningsTests: public ::testing::Test { void SetUp() { ChessEngine::initialize(); - loadOpenings(); } - void loadOpenings() { - openings.root.push({ createMove(e2, e4, WHITE, PAWN) }, [](auto & node) { - node.score = 55; - }); - - openings.root.push({ createMove(e2, e4, WHITE, PAWN), createMove(c7, c5, BLACK, PAWN) }, [](auto & node) { - node.score = 54; - node.name = "Sicilian defense"; - node.eco = "B20"; - }); - - openings.root.push({ createMove(e2, e4, WHITE, PAWN), createMove(e7, e5, BLACK, PAWN) }, [](auto & node) { - node.score = 56; - node.name = "King's pawn game"; - node.eco = "C20"; - }); - - openings.root.push({ createMove(d2, d4, WHITE, PAWN) }, [](auto & node) { - node.score = 56; - }); - } - - bool lookupOpeningNode(ChessOpenings &openings, std::string pgn, OpeningTreeNode &outNode) { + bool lookupOpeningNode(ChessOpenings &openings, std::string pgn, ChessOpenings::OpeningMove &outNode) { ChessGame temp; if (!FPGN::setGame(pgn, temp)) { return false; } - - OpeningTreeNode node; - bool result = openings.lookup(temp.allMoves(), [&](auto & node) { + + ChessGame::MoveNode node; + bool result = openings.lookup(temp.allMoves(), [&](auto node) { outNode = node; }); return result; } + void initializeDefaultOpenings() { + auto path = UnitTestHelper::pathToResources; + auto pathToFile = path + "/Openings.pgn"; + + auto pgn = readFromFile(pathToFile); + ASSERT_FALSE(pgn.empty()); + ASSERT_TRUE(openings.load(pgn)); + } + std::string readFromFile(std::string file) { std::ifstream input(file); std::stringstream sstr; @@ -75,56 +61,76 @@ class OpeningsTests: public ::testing::Test { }; TEST_F(OpeningsTests, Loading) { - auto path = UnitTestHelper::pathToResources; - auto pathToFile = path + "/Openings.pgn"; - - auto pgn = readFromFile(pathToFile); - ASSERT_FALSE(pgn.empty()); - ASSERT_TRUE(openings.load(pgn)); + initializeDefaultOpenings(); + + ChessOpenings::OpeningMove node; - OpeningTreeNode node; + // Play openings that exist in the book ASSERT_TRUE(lookupOpeningNode(openings, "1. e4 c5", node)); - ASSERT_EQ(54, node.score); ASSERT_EQ("Sicilian defense", node.name); - ASSERT_EQ("B20", node.eco); + + ASSERT_TRUE(lookupOpeningNode(openings, "1. e4 e5", node)); + ASSERT_EQ(58, node.score); + ASSERT_EQ("Ruy López Opening: Morphy Defense, Columbus Variation, 4...Nf6", node.name); + + // Play an opening that does not exist + ASSERT_FALSE(lookupOpeningNode(openings, "1. e4 e6", node)); +} + +TEST_F(OpeningsTests, OpeningWithVariation) { + ChessOpenings::OpeningMove node; + + ASSERT_TRUE(openings.load("1. e4 e5")); + + ASSERT_TRUE(lookupOpeningNode(openings, "1. e4 e5", node)); + ASSERT_FALSE(lookupOpeningNode(openings, "1. e4 c5", node)); + + ASSERT_TRUE(openings.load("1. e4 e5 (1... c5)")); + + ASSERT_TRUE(lookupOpeningNode(openings, "1. e4 e5", node)); + ASSERT_TRUE(lookupOpeningNode(openings, "1. e4 c5", node)); } TEST_F(OpeningsTests, KingsPawnOpening) { + initializeDefaultOpenings(); + Move m; std::vector moves; moves.push_back(m = createMove(e2, e4, WHITE, PAWN)); - bool result = openings.lookup(moves, [&](auto & node) { + bool result = openings.lookup(moves, [&](auto node) { ASSERT_EQ(m, node.move); }); ASSERT_TRUE(result); moves.push_back(m = createMove(e7, e5, BLACK, PAWN)); - result = openings.lookup(moves, [&](auto & node) { + result = openings.lookup(moves, [&](auto node) { ASSERT_EQ(m, node.move); - ASSERT_EQ("King's pawn game", node.name); + ASSERT_EQ("Ruy López Opening: Morphy Defense, Columbus Variation, 4...Nf6", node.name); }); ASSERT_TRUE(result); moves.push_back(createMove(e2, e4, WHITE, PAWN)); - result = openings.lookup(moves, [&](auto & node) { + result = openings.lookup(moves, [&](auto node) { FAIL(); }); ASSERT_FALSE(result); } TEST_F(OpeningsTests, BestMove) { + initializeDefaultOpenings(); + std::vector moves; - bool result = openings.best(moves, [&](auto & node) { - ASSERT_EQ(createMove(d2, d4, WHITE, PAWN), node.move); + bool result = openings.best(moves, [&](auto node) { + ASSERT_EQ(createMove(e2, e4, WHITE, PAWN), node.move); }); ASSERT_TRUE(result); moves.push_back(createMove(e2, e4, WHITE, PAWN)); - result = openings.best(moves, [&](auto & node) { + result = openings.best(moves, [&](auto node) { ASSERT_EQ(createMove(e7, e5, BLACK, PAWN), node.move); }); ASSERT_TRUE(result); diff --git a/Shared/Engine/Engine/ChessEngine.hpp b/Shared/Engine/Engine/ChessEngine.hpp index dd36d4e..709d9a5 100644 --- a/Shared/Engine/Engine/ChessEngine.hpp +++ b/Shared/Engine/Engine/ChessEngine.hpp @@ -128,9 +128,8 @@ class ChessEngine { bool isValidOpeningMoves(std::string &name) { name = ""; if (game.getNumberOfMoves() > 0) { - bool result = openings.lookup(game.allMoves(), [&](auto & node) { - // no-op - name = node.name; + bool result = openings.lookup(game.allMoves(), [&](auto opening) { + name = opening.name; }); return result; } else { @@ -146,8 +145,8 @@ class ChessEngine { // any openings. return false; } - bool result = openings.best(game.allMoves(), [&evaluation](OpeningTreeNode & node) { - evaluation.line.push(node.move); + bool result = openings.best(game.allMoves(), [&evaluation](auto opening) { + evaluation.line.push(opening.move); }); return result; } diff --git a/Shared/Engine/Engine/ChessGame.hpp b/Shared/Engine/Engine/ChessGame.hpp index 9016c7e..fa91ae1 100644 --- a/Shared/Engine/Engine/ChessGame.hpp +++ b/Shared/Engine/Engine/ChessGame.hpp @@ -73,6 +73,22 @@ class ChessGame { } } + // This function takes an array of moves and returns true if and only if + // all the moves match at least one variation starting with this node. + bool matches(int cursor, std::vector moves, NodeCallback callback) { + if (cursor < moves.size()) { + for (int vindex=0; vindex games; - if (FPGN::setGames(pgn, games)) { - root.clear(); - - for (auto game : games) { - auto scoreString = game.tags["Score"]; - auto score = 0; - if (scoreString.length() > 0) { - score = integer(scoreString); - } - auto name = game.tags["Name"]; - auto eco = game.tags["ECO"]; - - root.push(game.allMoves(), [&](auto & node) { - // Make sure the node always contain - // the best score so that branch is always - // taken upon lookup. - if (node.score < score) { - node.score = score; - node.name = name; - node.eco = eco; - } - }); - } - return true; - } else { - return false; - } + return FPGN::setGames(pgn, games); } -bool ChessOpenings::lookup(std::vector moves, OpeningTreeNode::NodeCallback callback) { - return root.lookup(moves, callback); +bool ChessOpenings::lookup(std::vector moves, OpeningCallback callback) { + return lookupAll(moves, [&callback](std::vector moves) { + callback(moves[0]); + }); } -bool nodeComparison(OpeningTreeNode i, OpeningTreeNode j) { +bool openingMoveComparison(ChessOpenings::OpeningMove i, ChessOpenings::OpeningMove j) { return i.score > j.score; } -bool ChessOpenings::best(std::vector moves, OpeningTreeNode::NodeCallback callback) { +bool ChessOpenings::lookupAll(std::vector moves, OpeningsCallback callback) { + std::vector possibleMoves; + for (int gIndex=0; gIndex 0) { + opening.score = integer(scoreString); + } + for (int vindex=0; vindex moves, OpeningCallback callback) { bool bestFound = false; - bool result = lookup(moves, [&](auto & node) { + bool result = lookup(moves, [&](OpeningMove opening) { // Check if the opening book has some more moves for that particular node - if (node.children.empty()) { + if (opening.nextMoves.empty()) { bestFound = false; } else { - // Pick the child with the best score - std::vector sortedChildren; - for (auto entry : node.children) { - sortedChildren.push_back(entry.second); - } - std::sort(sortedChildren.begin(), sortedChildren.end(), nodeComparison); - auto & best = sortedChildren.front(); - callback(best); + // Always pick the main line variation for the opening + auto bestOpening = OpeningMove(); + bestOpening.move = opening.nextMoves[0]; + bestOpening.name = opening.name; + bestOpening.score = opening.score; + callback(bestOpening); bestFound = true; } }); diff --git a/Shared/Engine/Engine/ChessOpenings.hpp b/Shared/Engine/Engine/ChessOpenings.hpp index 4578aed..26e8b93 100644 --- a/Shared/Engine/Engine/ChessOpenings.hpp +++ b/Shared/Engine/Engine/ChessOpenings.hpp @@ -12,75 +12,47 @@ #include #include -#include "MoveList.hpp" - -struct OpeningTreeNode { - Move move; - int score = 0; - std::string name; - std::string eco; - - std::map children; - - typedef std::function NodeCallback; - - void clear() { - children.clear(); - } - - void push(std::vector moves, NodeCallback callback) { - MoveList moveList; - for (auto move : moves) { - moveList.push(move); - } - push(moveList, 0, callback); - } - - void push(MoveList moves, int moveIndex, NodeCallback callback) { - callback(*this); - if (moveIndex < moves.count) { - auto key = moves[moveIndex]; - auto & child = children[key]; - child.move = key; - child.push(moves, moveIndex+1, callback); - } - } - - bool lookup(std::vector moves, NodeCallback callback) { - MoveList moveList; - for (auto move : moves) { - moveList.push(move); - } - return lookup(moveList, 0, callback); - } - - bool lookup(MoveList moves, int moveIndex, NodeCallback callback) { - if (moveIndex < moves.count) { - Move move = moves[moveIndex]; - if (children.count(move) == 0) { - return false; - } else { - auto & child = children[move]; - return child.lookup(moves, moveIndex+1, callback); - } - } else { - callback(*this); - return true; - } - } - -}; +#include "ChessGame.hpp" class ChessOpenings { +private: + std::vector games; + public: - OpeningTreeNode root; ChessOpenings(); bool load(std::string pgn); - bool lookup(std::vector moves, OpeningTreeNode::NodeCallback callback); - - bool best(std::vector moves, OpeningTreeNode::NodeCallback callback); + // Structure containing the information about a specific move in an opening + struct OpeningMove { + // The move itself + Move move; + // The name of the opening containing this move + std::string name; + // The score of the opening containing this move + int score; + // The array of moves after this one as defined by the opening. + // The first element in the array is always the main line. + std::vector nextMoves; + }; + + typedef std::function OpeningCallback; + typedef std::function moves)> OpeningsCallback; + + // This function looks up the best opening corresponding to + // the specified list of moves (which can be empty). + // Returns false if no opening is found + bool lookup(std::vector moves, OpeningCallback callback); + + // This function looks up all the openins corresponding to + // the specified list of moves (which can be empty). + // Returns false if no opening is found + bool lookupAll(std::vector moves, OpeningsCallback callback); + + // This function looks up the best next move given by the opening + // that matches the list of moves (which can be empty). + // Returns false if no opening is found + bool best(std::vector moves, OpeningCallback callback); };