bhlangonijr / chesslib

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

SAN for Moves #37

Closed dlbbld closed 3 years ago

dlbbld commented 4 years ago

I find it a big problem that when I create a new move as below, I cannot find the SAN for the move:

final Board board = new Board();
final Move pawnMove = new Move(Square.E2, Square.E4);
board.doMove(pawnMove);
System.out.println(pawnMove.getSan());

For example pawnMove.getSan() above is null. This attribute is only set, if a game was loaded from a PGN. But I find it essential to have the SAN for a move constructed as above. Is it anywhere? MoveList offers some methods for SAN generation, but I could not look through.

Also is there a method which supports specifying a move by the SAN? Move(String move, Side side) does not accept SAN, also not LAN, only some sort of "improvised" notation (e.g. e2e4, e7e5 is ok, but then Qe2 or Qd1e2 is not supported. Am I missing something?

bhlangonijr commented 4 years ago

Move#getSan() and Move#setSan() function as a sort of caching when used together with the MoveList - after calculating the SAN. This field is not really helpful if used without the MoveList. Ideally it should not be visible in the aforementioned use case - yes, it's misleading having it visible when it doesn't do what it appears to do. SAN is a particular notation that is dependent on contextual information. Individual moves like the one you mentioned doesn't make a lot of sense if expressed in SAN notation. The very example you mentioned, Qe2 assumes one has to have some previous knowledge about the game and which square the queen is currently sitting at. Based on this previous knowledge you can build the complete algebraic coordinate for moving that piece: "Queen moves from d1 to e2".

That said, you can specify a move by the SAN but it requires having a MoveList which encodes that previous knowledge about the game. E.g.:

    String san = "e4 Nc6 d4 Nf6 d5 Ne5 Nf3 d6 Nxe5 dxe5 Bb5+ Bd7 Bxd7+ Qxd7 Nc3 e6 O-O";
    MoveList list = new MoveList();
    list.loadFromSan(san); // Load the SAN moves into the list

    list.addSanMove("exd5"); // parse and add a move from SAN format and sets the `san` field in that move

    Move last = list.getLast(); // get the exd5 Move 
    System.out.println(last.getSan()); // should print exd5
    System.out.println(list.toSan()); // should print e4 Nc6 d4 Nf6 d5 Ne5 Nf3 d6 Nxe5 dxe5 Bb5+ Bd7 Bxd7+ Qxd7 Nc3 e6 O-O exd5

Another example, you could also build the MoveList from a FEN string:

    String fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1";
    MoveList list = new MoveList(fen);

    list.addSanMove("e6"); // parse and add a move from SAN format and sets the `san` field in that move

    Move last = list.getLast(); // get the e6 Move 
    System.out.println(last.getSan()); // should print e6

Try it out and let me know if it suffices for your use case.

dlbbld commented 4 years ago

Thanks for the examples. I got a rather "scary" result below. Can you please check. I can specify an illegal SAN and there is no exception. Then Black executes an impossible pawn non capturing move e6d5.

final MoveList list = new MoveList();
list.loadFromSan("1. d4 e6 2. e3");

list.addSanMove("exd5", false, true); // invalid move but no exception thrown

System.out.println(list.getLast().getSan()); // exd5
System.out.println(list.getLast()); // e6d5
System.out.println(list.getFen()); // rnbqkbnr/pppp1ppp/8/3p4/3P4/4P3/PPP2PPP/RNBQKBNR w KQkq - 0 3
bhlangonijr commented 4 years ago

Hey @dlbbld ,

This is expected behaviour: The Board, Move and MoveList don't enforce legal moves to be played. You can play any moves as you wish using a sort of free style chessboard set. This is useful, for example, when you are building a chess GUI which has an Edit feature where the user can drag and drop pieces around and setup an arbitrary position. The fullValidation argument is only validating if the resulting chess position is a legal one - not if the move itself is a legal move. If you want to make sure only legal moves are played and not pseudo-legal ones, you should use the MoveGenerator to give you a list of only legal moves, then you check them against the move the user has entered:

        // in the example below legalMoves are populated with a list of Legal chess moves for the board position passed as parameter, not a sequence of moves of the game playing
        final MoveList list = new MoveList();
        list.loadFromSan("1. d4 e6 2. e3");

        Board board = new Board();
        board.loadFromFen(list.getFen()); // you could also load the position replaying the moves on the Board using a list iteration and playing the move using `Board#doMove(move.get(moveIndex))
        MoveList legalMoves = MoveGenerator.generateLegalMoves(board); // generate the legal moves for the position
        // this can be used in a GUI to highlight the legal target squares for the piece dragged, for example.
        list.addSanMove("exd5");

        if (!legalMoves.contains(list.getLast())) {
            list.removeLast();
            System.out.println("Hey, you've played an illegal move. Try one of the following: ");
            for (Move move: legalMoves) {
                list.add(move);
                System.out.println(" Legal variation: " + list.toSan());
                System.out.println("Legal Move: " + list.toSanArray()[list.size() - 1]);
                list.removeLast();
            }
        }
dlbbld commented 4 years ago

I get the idea. Thanks for the explanation.