bhlangonijr / chesslib

chess library for legal move generation, FEN/PGN parsing and more
Apache License 2.0
223 stars 78 forks source link

Add ability to load a game from a single string in PGN format. #27

Closed safield closed 4 years ago

safield commented 4 years ago

Currently you can only load pgn format from a file. It is also a valid scenario to have a single pgn string that represents a single match.

bhlangonijr commented 4 years ago

Hello @safield. Example in the unit test:

    @Test
    public void testPGNLoadFromString() throws Exception {

        String lines = "[Event \"CCT 13\"]\n" +
                "[Site \"FICS, San Jose, California US\"]\n" +
                "[Date \"2011.01.29\"]\n" +
                "[Round \"2\"]\n" +
                "[White \"Rookie\"]\n" +
                "[Black \"JabbaChess\"]\n" +
                "[Result \"1-0\"]\n" +
                "[ECO \"C00\"]\n" +
                "[WhiteElo \"2285\"]\n" +
                "[BlackElo \"1680\"]\n" +
                "[Annotator \"Albert Silver\"]\n" +
                "[PlyCount \"67\"]\n" +
                "[EventDate \"2011.??.??\"]\n" +
                "[TimeControl \"3000+3\"]\n" +
                "\n" +
                "1. e4 e6 2. d4 a6 3. Nf3 d5 4. exd5 exd5 5. Bd3 Nc6 6. O-O Nf6 7. Re1+ Be7 8.\n" +
                "c3 O-O 9. Nbd2 Re8 10. Ne5 Nxe5 11. dxe5 Nd7 12. Nb3 g6 13. Nd4 c5 14. Nf3 b5\n" +
                "15. Bh6 Bb7 16. h4 Bxh4 17. a4 b4 18. cxb4 cxb4 19. Qc1 Be7 20. Qf4 Nc5 21.\n" +
                "Rad1 Bc6 {#} 22. e6 $1 {A nice combination that takes apart Black's position\n" +
                "with exemplary precision. The variations are no less interesting than the game\n" +
                "continuation, as is typical.} f6 ({The engine obviously goes for the most\n" +
                "resistant defense, however the question is what would happen if Black were to\n" +
                "play a more fallible and human continuation such as} 22... Nxe6 $2 {The answer\n" +
                "is} 23. Rxe6 $1 fxe6 24. Ne5 $1 {The double attack between mate and the bishop\n" +
                "on c6 is decisive.} Rf8 25. Qg4 $1 {and JabbaChess would be forced to leave\n" +
                "the bishop since} Be8 {trying to protect g6, fails to} 26. Bxg6 hxg6 27. Nxg6\n" +
                "Kh7 28. Nxf8+ Bxf8 29. Bxf8 Bg6 30. Bxb4) 23. Nh4 Bxa4 $2 {This is certainly\n" +
                "one of the more suicidal ways to go down.} ({but even the better} 23... Bf8 {\n" +
                "would not hold.} 24. Nxg6 hxg6 25. Qg4 g5 (25... Bxh6 26. Qxg6+ Bg7 27. Qf7+\n" +
                "Kh8 28. Re3 f5 29. Rh3+) 26. Qh5 Qe7 27. Bg6 Nxe6 28. Bxf8 Rxf8 29. Rxe6 Qxe6\n" +
                "30. Qh7#) (23... g5 24. Bxh7+ Kh8 25. Qf5 Rg8 26. Bxg8 Qxg8 27. Ng6+ Kh7 28.\n" +
                "Nxe7+) 24. Nxg6 Nxe6 25. Rxe6 Bd6 26. Qg4 {As the Borg famously said,\n" +
                "\"Resistance is futile\". (Star Trek Next Gen for those who have no idea what I\n" +
                "am talking about).} Bh2+ 27. Kxh2 Qc7+ 28. Kg1 Qg7 {#} 29. Re7 {Typical\n" +
                "computer weirdness. Any normal person would take the queen, but then again,\n" +
                "any normal player would have resigned instead of playing Qg7...} Rxe7 ({Just\n" +
                "in case you wondered how White would finish off Black if the bishop was\n" +
                "captured with} 29... Qxh6 30. Qe6#) 30. Nxe7+ Kf7 31. Qxg7+ Ke8 32. Nxd5 Ra7\n" +
                "33. Bb5+ axb5 34. Nxf6# 1-0\n\n";

        PgnHolder pgn = new PgnHolder(null);
        pgn.loadPgn(lines);
        Game game = pgn.getGames().get(0);
        game.loadMoveText();
        // ...
safield commented 4 years ago

Thanks! I will give it a test spin soon.

safield commented 4 years ago

This did not work for me. The following example resulted in a Game with a null board.

` String pgnStr ="[Event \"CCT 13\"]\n" + "[Site \"FICS, San Jose, California US\"]\n" + "[Date \"2011.01.29\"]\n" + "[Round \"2\"]\n" + "[White \"Rookie\"]\n" + "[Black \"JabbaChess\"]\n" + "[Result \"*\"]\n" + "[ECO \"C00\"]\n" + "[WhiteElo \"2285\"]\n" + "[BlackElo \"1680\"]\n" + "[Annotator \"Albert Silver\"]\n" + "[PlyCount \"67\"]\n" + "[EventDate \"2011.??.??\"]\n" + "[TimeControl \"3000+3\"]\n" + "1. d4 d5";

        PgnHolder pgn = new PgnHolder(null);
        pgn.loadPgn(pgnStr);
        List<Game> games = pgn.getGames();
        Game game = games.get(0);
        game.loadMoveText();`

To give you some background what I am trying to do...

I have just the moves as listed in pgn format in a string (none of the other header data , maybe I will just stuff in generic header data), and I want to determine...

-who's move it is -the current state of the board -all the valid next moves

So ultimately I want to take a list of mvoes in pgn, and ultimately end up with a Board class.

bhlangonijr commented 4 years ago

ah okay. PgnHolder was initially intended as a container for completed games. You want to use the MoveList for that:

// You can load the moves in SAN ou LAN format:
String san = "e4 Nc6 d4 Nf6 d5 Ne5 Nf3 d6 Nxe5 dxe5 Bb5+ Bd7 Bxd7+ Qxd7 Nc3 e6 O-O exd5 ";
MoveList moves = new MoveList();
moves.loadFromSan(san);
// or 
String lan = "e2e4 b8c6 d2d4 g8f6 d4d5 c6e5 g1f3 d7d6 f3e5 d6e5 f1b5 c8d7 b5d7 d8d7 b1c3 e7e6 e1g1 e6d5";
MoveList moves = new MoveList();
moves.loadFromText(lan);

you can also build the MoveList assuming a initial FEN position (you don't know the previous moves), like:

MoveList moves = new MoveList("rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1");
// then
moves.add(move);
//or
moves.loadFrom...

Instantiate a new Board object and replay the moves there:

        Board board = new Board();
        board.loadFromFen(moves.getStartFen());  // no need if it starts from standard start position
        for (Move move: moves) {
            board.doMove(move); // replay each move
            System.out.println("Side to move: " + board.getSideToMove());
            // check whatever state you want in the board object
        }

        MoveList legalMoves = MoveGenerator.generateLegalMoves(board); // valid next moves after final state
        System.out.println("next moves: " + legalMoves);

        for (int i = 0; i < moves.size(); i++) {
            board.undoMove(); // undo each move -backwards - until get to initial board state
        }

Hope this helps. Let me know how it goes. Regards,

safield commented 4 years ago

Thanks for the info!

PGN format is intended to store in progress games as well. This is a valid use case. Could this be supported eventually? It would seem reasonable to want to take a pgn format and then end up with the board it represents.

The dart client side library I use returns pgn format move lists of format "1. d4 d5 2. Qd3 c5 3. e4". Can I recreate a board using this pgn format? Are pgn move lists the same as san but with numbers?

bhlangonijr commented 4 years ago

Yes @safield it should load games in progress but you need ending the moves line with a *, like in:

1. d4 d5 2. Qd3 c5 3. e4 *

PGN moves are in SAN format and using numbers for enumerating the chess moves. The numbers in it are just for human readability, it's totally superfluous from a technical perspective and chesslib just ignores it when parsing the moves. The unit tests in chesslib are using this PGN format with number with no problem.

safield commented 4 years ago

Thanks for the further clarification. Does MoveList.loadFromSan() also ignore any superfluous numbering like "1."?

bhlangonijr commented 4 years ago

Thanks for the further clarification. Does MoveList.loadFromSan() also ignore any superfluous numbering like "1."?

Yes indeed. Pgn parser in chess lib uses de MoveList itself to parse the moves section of the PGN file. The method that gets the moves after loading the game is actually returning a MoveList: Game#getHalfMoves() (as you may know each Move in chess terminology a half-move, each number in the SAN is therefore denoting a "full" move).