diff --git a/BChessTests/Helper/GoogleTests.mm b/BChessTests/Helper/GoogleTests.mm index 628292c..92fc5d0 100755 --- a/BChessTests/Helper/GoogleTests.mm +++ b/BChessTests/Helper/GoogleTests.mm @@ -181,8 +181,6 @@ + (void)load { UnitTestHelper::pathToResources = std::string([[bundle resourcePath] cStringUsingEncoding:NSASCIIStringEncoding]); [self registerTestClasses]; } -// if ([notification.userInfo[@"NSLoadedClasses"] count] > 0) { -// } }]; } diff --git a/BChessTests/OpeningsTests.cpp b/BChessTests/OpeningsTests.cpp index 2e10ff0..3b33e3a 100644 --- a/BChessTests/OpeningsTests.cpp +++ b/BChessTests/OpeningsTests.cpp @@ -56,10 +56,10 @@ class OpeningsTests: public ::testing::Test { } OpeningTreeNode node; - bool result = openings.lookup(temp.moves, [&](auto & node) { + bool result = openings.lookup(temp.allMoves(), [&](auto & node) { outNode = node; }); - + return result; } @@ -92,22 +92,22 @@ TEST_F(OpeningsTests, Loading) { TEST_F(OpeningsTests, KingsPawnOpening) { Move m; - MoveList moves; - moves.push(m = createMove(e2, e4, WHITE, PAWN)); + std::vector moves; + moves.push_back(m = createMove(e2, e4, WHITE, PAWN)); bool result = openings.lookup(moves, [&](auto & node) { ASSERT_EQ(m, node.move); }); ASSERT_TRUE(result); - moves.push(m = createMove(e7, e5, BLACK, PAWN)); + moves.push_back(m = createMove(e7, e5, BLACK, PAWN)); result = openings.lookup(moves, [&](auto & node) { ASSERT_EQ(m, node.move); ASSERT_EQ("King's pawn game", node.name); }); ASSERT_TRUE(result); - moves.push(createMove(e2, e4, WHITE, PAWN)); + moves.push_back(createMove(e2, e4, WHITE, PAWN)); result = openings.lookup(moves, [&](auto & node) { FAIL(); }); @@ -115,14 +115,14 @@ TEST_F(OpeningsTests, KingsPawnOpening) { } TEST_F(OpeningsTests, BestMove) { - MoveList moves; - + std::vector moves; + bool result = openings.best(moves, [&](auto & node) { ASSERT_EQ(createMove(d2, d4, WHITE, PAWN), node.move); }); ASSERT_TRUE(result); - moves.push(createMove(e2, e4, WHITE, PAWN)); + moves.push_back(createMove(e2, e4, WHITE, PAWN)); result = openings.best(moves, [&](auto & node) { ASSERT_EQ(createMove(e7, e5, BLACK, PAWN), node.move); diff --git a/BChessTests/PGNTests.cpp b/BChessTests/PGNTests.cpp index cd8800d..04d1690 100644 --- a/BChessTests/PGNTests.cpp +++ b/BChessTests/PGNTests.cpp @@ -57,7 +57,14 @@ static void assertMovetextSingle(std::string pgn, std::string expectedMove) { } } -TEST(PGN, SingleMove) { +class PGN : public testing::Test { +protected: + virtual void SetUp() { + ChessEngine::initialize(); + } +}; + +TEST_F(PGN, SingleMove) { ChessGame game; unsigned cursor = 0; bool end = false; @@ -66,7 +73,7 @@ TEST(PGN, SingleMove) { EXPECT_TRUE(end); } -TEST(PGN, SingleMoveWithComments) { +TEST_F(PGN, SingleMoveWithComments) { ChessGame game; unsigned cursor = 0; bool end = false; @@ -75,7 +82,7 @@ TEST(PGN, SingleMoveWithComments) { EXPECT_TRUE(end); } -TEST(PGN, SingleMoveForBlack) { +TEST_F(PGN, SingleMoveForBlack) { ChessGame game; EXPECT_TRUE(FFEN::setFEN("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq - 0 1", game.board)); unsigned cursor = 0; @@ -85,7 +92,7 @@ TEST(PGN, SingleMoveForBlack) { EXPECT_TRUE(end); } -TEST(PGN, Moves) { +TEST_F(PGN, Moves) { assertMovetextSingle("e4", "e2e4"); assertMovetextSingle("e5", ""); @@ -125,7 +132,7 @@ constexpr const static char *allMoves = "Nf2 42. g4 Bd3 43. Re6 1/2-1/2" ; -TEST(PGN, GameInput) { +TEST_F(PGN, GameInput) { assertPGNGame(move1to3, "r1bqkbnr/1ppp1ppp/p1n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4"); assertPGNGame(move1to10, "r1bq1rk1/2pnbppp/p2p1n2/1p2p3/3PP3/1BP2N1P/PP3PP1/RNBQR1K1 w - - 1 11"); @@ -135,7 +142,7 @@ TEST(PGN, GameInput) { assertPGNGame(allMoves, "8/8/4R1p1/2k3p1/1p4P1/1P1b1P2/3K1n2/8 b - - 2 43"); } -TEST(PGN, GameOutput) { +TEST_F(PGN, GameOutput) { assertOutputPGN(move1to3, "r1bqkbnr/1ppp1ppp/p1n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R w KQkq - 0 4", "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 *"); assertOutputPGN(move1to10, "r1bq1rk1/2pnbppp/p2p1n2/1p2p3/3PP3/1BP2N1P/PP3PP1/RNBQR1K1 w - - 1 11", "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7 *"); @@ -145,7 +152,7 @@ TEST(PGN, GameOutput) { assertOutputPGN(allMoves, "8/8/4R1p1/2k3p1/1p4P1/1P1b1P2/3K1n2/8 b - - 2 43", "1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Nb8 10. d4 Nbd7 11. c4 c6 12. cxb5 axb5 13. Nc3 Bb7 14. Bg5 b4 15. Nb1 h6 16. Bh4 c5 17. dxe5 Nxe4 18. Bxe7 Qxe7 19. exd6 Qf6 20. Nbd2 Nxd6 21. Nc4 Nxc4 22. Bxc4 Nb6 23. Ne5 Rae8 24. Bxf7+ Rxf7 25. Nxf7 Rxe1+ 26. Qxe1 Kxf7 27. Qe3 Qg5 28. Qxg5 hxg5 29. b3 Ke6 30. a3 Kd6 31. axb4 cxb4 32. Ra5 Nd5 33. f3 Bc8 34. Kf2 Bf5 35. Ra7 g6 36. Ra6+ Kc5 37. Ke1 Nf4 38. g3 Nxh3 39. Kd2 Kb5 40. Rd6 Kc5 41. Ra6 Nf2 42. g4 Bd3 43. Re6 1/2-1/2"); } -TEST(PGN, GameWithBlackFromFEN) { +TEST_F(PGN, GameWithBlackFromFEN) { ChessGame game; game.setFEN("1K1k4/1P6/8/8/8/8/r7/2R5 w - - 0 1"); game.move("c1", "d1"); @@ -154,7 +161,7 @@ TEST(PGN, GameWithBlackFromFEN) { ASSERT_EQ(pgn, "[FEN \"1K1k4/1P6/8/8/8/8/r7/2R5 w - - 0 1\"]\n[Setup \"1\"]\n1. Rd1+ *"); } -TEST(PGN, GameWithBlackPromotion) { +TEST_F(PGN, GameWithBlackPromotion) { ChessGame game; ASSERT_TRUE(FPGN::setGame("1.e4 Nf6 2.Nc3 Nxe4 3.Nxe4 d5 4.Nc3 Qd6 5.Nf3 h5 6.d4 Qd8 7.Bb5+ c6 8.Ba4 b5 9.Bb3 a5 10.a4 b4 11.Na2 Bg4 12.Qd3 Bxf3 13.Qxf3 h4 14.h3 Qd6 15.Bf4 Qe6+ 16.Be3 Qd6 17.O-O g6 18.c4 bxc3 19.bxc3 Nd7 20.c4 dxc4 21.Bxc4 Qf6 22.Qg4 e5 23.Rfe1 Qg7 24.dxe5 Nc5 25.Bxc5 Bxc5 26.e6 Bd4 27.exf7+ Kf8 28.Qe6 Qf6 29.Rad1 Qxe6 30.Bxe6 c5 31.Bd5 Rb8 32.Nc1 Rh5 33.Bc4 Rf5 34.Re2 Rf4 35.Rde1 Bxf2+ 36.Rxf2 Rxc4 37.Nd3 Kg7 38.Re7 Kf8 39.Re6 Rxa4 40.Nxc5 Ra1+ 41.Kh2 Rd1 42.Ne4 Kg7 43.Ng5 Rf8 44.Rfe2 Rxf7 45.Nxf7 Kxf7 46.R6e4 Ra1 47.Rxh4 Kg8 48.Rf2 Kg7 49.Rhf4 a4 50.Rf7+ Kh6 51.R2f4 a3 52.Rg4 a2 53.Rf6 Rh1+ 54.Kxh1", game)); @@ -165,7 +172,7 @@ TEST(PGN, GameWithBlackPromotion) { ASSERT_EQ(pgn, "1. e4 Nf6 2. Nc3 Nxe4 3. Nxe4 d5 4. Nc3 Qd6 5. Nf3 h5 6. d4 Qd8 7. Bb5+ c6 8. Ba4 b5 9. Bb3 a5 10. a4 b4 11. Na2 Bg4 12. Qd3 Bxf3 13. Qxf3 h4 14. h3 Qd6 15. Bf4 Qe6+ 16. Be3 Qd6 17. O-O g6 18. c4 bxc3 19. bxc3 Nd7 20. c4 dxc4 21. Bxc4 Qf6 22. Qg4 e5 23. Rfe1 Qg7 24. dxe5 Nc5 25. Bxc5 Bxc5 26. e6 Bd4 27. exf7+ Kf8 28. Qe6 Qf6 29. Rad1 Qxe6 30. Bxe6 c5 31. Bd5 Rb8 32. Nc1 Rh5 33. Bc4 Rf5 34. Re2 Rf4 35. Rde1 Bxf2+ 36. Rxf2 Rxc4 37. Nd3 Kg7 38. Re7 Kf8 39. Re6 Rxa4 40. Nxc5 Ra1+ 41. Kh2 Rd1 42. Ne4 Kg7 43. Ng5 Rf8 44. Rfe2 Rxf7 45. Nxf7 Kxf7 46. Re6e4 Ra1 47. Rxh4 Kg8 48. Rf2 Kg7 49. Rhf4 a4 50. Rf7+ Kh6 51. Rf2f4 a3 52. Rg4 a2 53. Rf6 Rh1+ 54. Kxh1 a1=Q+ *"); } -TEST(PGN, OutputFromInitialPosition) { +TEST_F(PGN, OutputFromInitialPosition) { ChessGame game; ASSERT_EQ(StartFEN, game.initialFEN); @@ -176,7 +183,7 @@ TEST(PGN, OutputFromInitialPosition) { ASSERT_EQ("1. e4 *", FPGN::getGame(game)); } -TEST(PGN, InputFromInitialPosition) { +TEST_F(PGN, InputFromInitialPosition) { ChessGame game; FPGN::setGame("*", game); ASSERT_EQ(StartFEN, game.initialFEN); @@ -186,7 +193,7 @@ TEST(PGN, InputFromInitialPosition) { ASSERT_EQ("1. e4 *", FPGN::getGame(game)); } -TEST(PGN, OutputFromPosition) { +TEST_F(PGN, OutputFromPosition) { std::string fen = "r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3"; ChessGame game; @@ -197,19 +204,36 @@ TEST(PGN, OutputFromPosition) { ASSERT_EQ("[FEN \"r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3\"]\n[Setup \"1\"]\n*", pgn); } -TEST(PGN, InputFromPosition) { +TEST_F(PGN, InputFromPosition) { ChessGame game; FPGN::setGame("[FEN \"r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3\"]\n*", game); ASSERT_EQ("r1bqkbnr/ppp1pppp/2n5/3p4/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3", game.initialFEN); - ASSERT_EQ(0, game.moveCursor); + ASSERT_EQ(0, game.getNumberOfMoves()); } -TEST(PGN, InputMultipleGames) { +TEST_F(PGN, InputMultipleGames) { std::string pgn = "[Title \"a\"] 1. e4 e5 [Title \"b\"] 1. d4 d5"; std::vector games; - FPGN::setGames(pgn, games); + ASSERT_TRUE(FPGN::setGames(pgn, games)); ASSERT_EQ(2, games.size()); } +TEST_F(PGN, PGNWithVariations) { +// std::string pgn = "1. e4 e5 2. Nc3 Nf6 3. f4 d5 4. fxe5 Nxe4 5. Qf3 Nxc3 {We want them to take\ +// this knight so we will recapture with the b2 pawn and go for d2-d4, with a\ +// large center, and Bd3/Ne2 setup.} (5... Nc6 {A tricky and challenging move. We\ +// must prevent Nxe5.}) 6. bxc3 Be7"; + std::string pgn = "1. e4 (1. d4)"; + + ChessGame game; + ASSERT_TRUE(FPGN::setGame(pgn, game)); + + ASSERT_EQ(1, game.getNumberOfMoves()); + ASSERT_EQ(2, game.getRoot().variations.size()); + + // TODO: generate PGN with variation included + auto pgnAgain = FPGN::getGame(game); + ASSERT_EQ("1. e4 *", pgnAgain); +} diff --git a/Shared/Bridge/FEngineInfo.mm b/Shared/Bridge/FEngineInfo.mm index 8241b89..45b671b 100644 --- a/Shared/Bridge/FEngineInfo.mm +++ b/Shared/Bridge/FEngineInfo.mm @@ -98,7 +98,7 @@ - (NSString*)bestLine:(BOOL)uci { } else { // Remember the number of moves in the game // so we only display the PGN for the best line. - int cursor = (int)self.game.moveCursor; + int cursor = (int)self.game.getNumberOfMoves(); // Copy the current game and play the best line. ChessGame lineGame = self.game; diff --git a/Shared/Engine/Engine/ChessEngine.hpp b/Shared/Engine/Engine/ChessEngine.hpp index 074dd79..b137ed2 100644 --- a/Shared/Engine/Engine/ChessEngine.hpp +++ b/Shared/Engine/Engine/ChessEngine.hpp @@ -127,8 +127,8 @@ class ChessEngine { // by the openings loaded with loadOpening() bool isValidOpeningMoves(std::string &name) { name = ""; - if (game.moves.count > 0) { - bool result = openings.lookup(game.moves, [&](auto & node) { + if (game.getNumberOfMoves() > 0) { + bool result = openings.lookup(game.allMoves(), [&](auto & node) { // no-op name = node.name; }); @@ -139,14 +139,14 @@ class ChessEngine { } bool lookupOpeningMove(ChessEvaluation & evaluation) { - if (game.moves.count == 0 && game.board.fullMoveCount > 1) { + if (game.getNumberOfMoves() == 0 && game.board.fullMoveCount > 1) { // If the game is not at the starting position, that is, // there are no moves recorded yet but the fullMoveCount is greater // than one (meaning the game has one or more move already), don't use // any openings. return false; } - bool result = openings.best(game.moves, [&evaluation](OpeningTreeNode & node) { + bool result = openings.best(game.allMoves(), [&evaluation](OpeningTreeNode & node) { evaluation.line.push(node.move); }); return result; diff --git a/Shared/Engine/Engine/ChessGame.cpp b/Shared/Engine/Engine/ChessGame.cpp index 350f2c0..a381026 100644 --- a/Shared/Engine/Engine/ChessGame.cpp +++ b/Shared/Engine/Engine/ChessGame.cpp @@ -19,8 +19,7 @@ ChessGame::ChessGame() { } void ChessGame::reset() { - moveCursor = 0; - moves.count = 0; + moveIndexes.reset(); history->clear(); board.reset(); outcome = Outcome::in_progress; @@ -52,20 +51,45 @@ std::vector ChessGame::movesAt(File file, Rank rank) { } std::vector ChessGame::allMoves() { - return moves.allMoves(); + std::vector all; + MoveNode currentNode = root; + for (int index=0; indexclear(); - } - - moves.push(move); - moveCursor = moves.count; + // Lookup the node representing the last move by `moveIndexes` + root.lookupNode(0, moveIndexes.moveCursor, moveIndexes, [&move, this](auto & node) { + // First try to match the move with an existing move for that node. + // This happens, for example, when a PGN has been loaded and + // the player is playing a series of move that correspond to that PGN. + bool found = false; + for (int index=0; index 0; + return moveIndexes.moveCursor > 0; } bool ChessGame::canRedoMove() { - return moveCursor < moves.count; + return moveIndexes.moveCursor < getNumberOfMoves(); } void ChessGame::undoMove() { assert(canUndoMove()); - moveCursor--; + moveIndexes.moveCursor--; replayMoves(); } void ChessGame::redoMove() { assert(canRedoMove()); - moveCursor++; + moveIndexes.moveCursor++; replayMoves(); } @@ -119,7 +143,8 @@ void ChessGame::replayMoves() { board.reset(); auto result = FFEN::setFEN(initialFEN, board); assert(result); - for (int index=0; index class ChessGame { -private: - void replayMoves(); - public: enum class Outcome { white_wins, black_wins, draw, in_progress }; + // This structure identifies the current moves of the game, + // taking into account the exact variation taken when appropriate. + struct MoveIndexes { + // Current move path identifier + // [0, 0, 1] means there are 3 moves, the first 2 in the main line + // and the third one using the first variation. + std::vector moves; + + // The current index in the index of moves above + int moveCursor = 0; + + void reset() { + moveCursor = 0; + moves.clear(); + } + }; + + // This structure represents a particular move in the tree of moves + // that a game represents. A node has a move associated with it, including + // the next move node (or move nodes if there is more than one variation). + struct MoveNode { + // Representation of this node's move + Move move; + + // List representing the next node following this one. + // In other words, this list contains the moves that can be + // played against this node's move, including any variation. + // Note: the main line is always at index 0. + std::vector variations; + + typedef std::function NodeCallback; + + // Find the node that represents the move at `atIndex`. Note that `atIndex==0` represents + // the "root" node, which is not a move. The first move is actually at `atIndex==1`. + // cursor: the cursor used to keep track of the current index while recursively looking up + void lookupNode(int cursor, int atIndex, MoveIndexes indexes, NodeCallback callback) { + if (cursor == atIndex) { + callback(*this); + } else if (cursor < indexes.moveCursor) { + int varIndex = indexes.moves[cursor]; + variations[varIndex].lookupNode(cursor+1, atIndex, indexes, callback); + } + } + }; + ChessGame(); // The initial representation of the game in FEN. @@ -32,9 +74,39 @@ class ChessGame { ChessBoard board; HistoryPtr history; + + MoveIndexes getMoveIndexes() { + return moveIndexes; + } - MoveList moves; - int moveCursor = 0; + void setMoveIndexes(MoveIndexes indexes) { + moveIndexes = indexes; + + // Always replay the moves to update the internal + // board state when the move indexes change, + // because that mean we are starting another + // location in the tree of moves. + replayMoves(); + } + + int getNumberOfMoves() { + return moveIndexes.moveCursor; + } + + Move getMoveAtIndex(int indexMove) { + Move move = INVALID_MOVE; + // Note: use indexMove+1 because the node at index 0 is the root node + // which does not represent the first move (the first move is attached + // to the root node in the variations list). + root.lookupNode(0, indexMove+1, moveIndexes, [&move](auto & node) { + move = node.move; + }); + return move; + } + + MoveNode getRoot() { + return root; + } Outcome outcome = Outcome::in_progress; @@ -60,5 +132,11 @@ class ChessGame { void redoMove(); std::string getState(); + +private: + MoveNode root; + MoveIndexes moveIndexes; + + void replayMoves(); }; diff --git a/Shared/Engine/Engine/ChessOpenings.cpp b/Shared/Engine/Engine/ChessOpenings.cpp index 0085901..073105b 100644 --- a/Shared/Engine/Engine/ChessOpenings.cpp +++ b/Shared/Engine/Engine/ChessOpenings.cpp @@ -27,7 +27,7 @@ bool ChessOpenings::load(std::string pgn) { auto name = game.tags["Name"]; auto eco = game.tags["ECO"]; - root.push(game.moves, 0, [&](auto & node) { + root.push(game.allMoves(), [&](auto & node) { // Make sure the node always contain // the best score so that branch is always // taken upon lookup. @@ -44,15 +44,15 @@ bool ChessOpenings::load(std::string pgn) { } } -bool ChessOpenings::lookup(MoveList moves, OpeningTreeNode::NodeCallback callback) { - return root.lookup(moves, 0, callback); +bool ChessOpenings::lookup(std::vector moves, OpeningTreeNode::NodeCallback callback) { + return root.lookup(moves, callback); } bool nodeComparison(OpeningTreeNode i, OpeningTreeNode j) { return i.score > j.score; } -bool ChessOpenings::best(MoveList moves, OpeningTreeNode::NodeCallback callback) { +bool ChessOpenings::best(std::vector moves, OpeningTreeNode::NodeCallback callback) { bool bestFound = false; bool result = lookup(moves, [&](auto & node) { // Check if the opening book has some more moves for that particular node diff --git a/Shared/Engine/Engine/ChessOpenings.hpp b/Shared/Engine/Engine/ChessOpenings.hpp index 2d8ae7b..4578aed 100644 --- a/Shared/Engine/Engine/ChessOpenings.hpp +++ b/Shared/Engine/Engine/ChessOpenings.hpp @@ -45,6 +45,14 @@ struct OpeningTreeNode { 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) { @@ -71,8 +79,8 @@ class ChessOpenings { bool load(std::string pgn); - bool lookup(MoveList moves, OpeningTreeNode::NodeCallback callback); + bool lookup(std::vector moves, OpeningTreeNode::NodeCallback callback); - bool best(MoveList moves, OpeningTreeNode::NodeCallback callback); + bool best(std::vector moves, OpeningTreeNode::NodeCallback callback); }; diff --git a/Shared/Engine/Helpers/FPGN.cpp b/Shared/Engine/Helpers/FPGN.cpp index b67a7b9..e20cb67 100644 --- a/Shared/Engine/Helpers/FPGN.cpp +++ b/Shared/Engine/Helpers/FPGN.cpp @@ -380,7 +380,7 @@ bool FPGN::parseMove(std::string pgn, unsigned &cursor, ChessGame &game, Move &m fromRank = getRank(pgn[cursor++]); toFile = getFile(pgn[cursor++]); toRank = getRank(pgn[cursor++]); - } else if (isFile(pgn[cursor]) && isRank(pgn[cursor+1]) && (isSpaceOrNewLine(pgn[cursor+2]) || isCheckOrMate(pgn[cursor+2]) || isPromotion(pgn[cursor+2]) || cursor+2 == pgn.size())) { + } else if (isFile(pgn[cursor]) && isRank(pgn[cursor+1])) { // e4 // Ba4 toFile = getFile(pgn[cursor++]); @@ -480,6 +480,55 @@ bool FPGN::parseMove(std::string pgn, unsigned &cursor, ChessGame &game, Move &m } } +// A variation is simply an alternative line to the move that has just been played +bool FPGN::parseVariation(std::string pgn, unsigned &cursor, ChessGame &game, bool &end) { + eatWhiteSpace(pgn, cursor); + + if (cursor >= pgn.length()) { + end = true; + return false; + } + + if (pgn[cursor] != '(') { + return false; + } + +// std::cout << "Before variation begins " << game.getMoveIndexes().moveCursor << std::endl; +// game.board.print(); + + // Remember the move indexes so we can restore it once + // we are done parsing the variation. + auto moveIndexes = game.getMoveIndexes(); + + // Restore the game as it was before the last move, so we + // apply correctly the variation. + moveIndexes.moveCursor--; + game.setMoveIndexes(moveIndexes); + moveIndexes.moveCursor++; + +// std::cout << "Backtracking one move for the variation " << game.getMoveIndexes().moveCursor << std::endl; +// game.board.print(); + + // Parse the variation (potentially recursively) + parseMoveText(pgn, cursor, game, end); + + // Restore the move indexes as it was before the variation + // so the internal game state is properly restored and + // ready to consume the next moves. + game.setMoveIndexes(moveIndexes); + +// std::cout << "After variation is explored " << game.getMoveIndexes().moveCursor << std::endl; +// game.board.print(); + + eatWhiteSpace(pgn, cursor); + if (cursor < pgn.length() && pgn[cursor] == ')') { + cursor++; + return true; + } else { + return false; + } +} + /** Parse a movetext block as defined in the PGN specifications. Not everything is yet supported. [move number] [move] [comment] [move] [comment] @@ -508,13 +557,18 @@ bool FPGN::parseMoveText(std::string pgn, unsigned &cursor, ChessGame &game, boo return false; } - // std::cout << MOVE_DESCRIPTION(whiteMove) << std::endl; - // game.board.print(); - game.move(whiteMove); +// std::cout << to_string(whiteMove, SANType::full) << std::endl; +// game.board.print(); + parseComment(pgn, cursor); + parseVariation(pgn, cursor, game, end); + if (end) { + return true; + } + // Did we reach the end now? if (cursor >= pgn.length()) { end = true; @@ -534,7 +588,14 @@ bool FPGN::parseMoveText(std::string pgn, unsigned &cursor, ChessGame &game, boo end = true; return true; } - + + if (pgn[cursor] == ')') { + // End of a variation after white move, + // so return early to avoid parsing + // the non-existent black move + return true; + } + Move blackMove = 0; if (!parseMove(pgn, cursor, game, blackMove, end)) { return true; @@ -552,6 +613,11 @@ bool FPGN::parseMoveText(std::string pgn, unsigned &cursor, ChessGame &game, boo parseComment(pgn, cursor); + parseVariation(pgn, cursor, game, end); + if (end) { + return true; + } + // Did we reach the end now? if (cursor >= pgn.length()) { end = true; @@ -635,8 +701,8 @@ std::string FPGN::getGame(ChessGame game, Formatting formatting, int fromIndex) std::string pgn; unsigned fullMoveIndex = 0; - for (int index=0; index