Skip to content

Commit

Permalink
Improved opening logic to handle variations
Browse files Browse the repository at this point in the history
  • Loading branch information
jean-bovet committed May 6, 2021
1 parent b846d9a commit ef56606
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 158 deletions.
6 changes: 0 additions & 6 deletions BChess/Openings.pgn
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,6 @@

1. e4 c5

[ECO "C20"]
[Name "King's pawn game"]
[Score "57"]

1. e4 e5

[Score "56"]

1. d4
Expand Down
92 changes: 49 additions & 43 deletions BChessTests/OpeningsTests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<Move> 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<Move> 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);
Expand Down
9 changes: 4 additions & 5 deletions Shared/Engine/Engine/ChessEngine.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
}
Expand Down
16 changes: 16 additions & 0 deletions Shared/Engine/Engine/ChessGame.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<Move> moves, NodeCallback callback) {
if (cursor < moves.size()) {
for (int vindex=0; vindex<variations.size(); vindex++) {
if (variations[vindex].move == moves[cursor]) {
return variations[vindex].matches(cursor+1, moves, callback);
}
}
return false;
} else {
callback(*this);
return true;
}
}

void clear() {
variations.clear();
}
Expand Down
88 changes: 46 additions & 42 deletions Shared/Engine/Engine/ChessOpenings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,59 +14,63 @@ ChessOpenings::ChessOpenings() {
}

bool ChessOpenings::load(std::string pgn) {
std::vector<ChessGame> 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<Move> moves, OpeningTreeNode::NodeCallback callback) {
return root.lookup(moves, callback);
bool ChessOpenings::lookup(std::vector<Move> moves, OpeningCallback callback) {
return lookupAll(moves, [&callback](std::vector<OpeningMove> 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<Move> moves, OpeningTreeNode::NodeCallback callback) {
bool ChessOpenings::lookupAll(std::vector<Move> moves, OpeningsCallback callback) {
std::vector<OpeningMove> possibleMoves;
for (int gIndex=0; gIndex<games.size(); gIndex++) {
auto & game = games[gIndex];
game.getRoot().matches(0, moves, [&game, &callback, &possibleMoves](auto &node) {
auto opening = OpeningMove();
opening.move = node.move;
opening.name = game.tags["Name"];
opening.score = 0;
auto scoreString = game.tags["Score"];
if (scoreString.length() > 0) {
opening.score = integer(scoreString);
}
for (int vindex=0; vindex<node.variations.size(); vindex++) {
opening.nextMoves.push_back(node.variations[vindex].move);
}
possibleMoves.push_back(opening);
});
}

if (possibleMoves.empty()) {
return false;
} else {
// Sort and return the move that corresponds to the opening
// with the highest SCORE
std::sort(possibleMoves.begin(), possibleMoves.end(), openingMoveComparison);
callback(possibleMoves);
return true;
}
}

bool ChessOpenings::best(std::vector<Move> 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<OpeningTreeNode> 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;
}
});
Expand Down
Loading

0 comments on commit ef56606

Please sign in to comment.