Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

En-passant erroneously rejected as invalid when specified via SAN #54

Open
minor-fixes opened this issue Apr 22, 2021 · 3 comments
Open

Comments

@minor-fixes
Copy link

With 3.2.0, the following case prints test successful:

use chess::{Board, BoardStatus, ChessMove, Square};
let mut board = Board::default();

assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "e4").expect("invalid: e4"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "e5").expect("invalid: e5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "d4").expect("invalid: d4"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "h5").expect("invalid: h5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "d5").expect("invalid: d5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "c5").expect("invalid: c5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::new(Square::D5, Square::C6, None)); // en passant
assert_eq!(board.status(), BoardStatus::Ongoing);

println!("test successful");

whereas this case panics with invalid: dxc6: InvalidSanMove:

use chess::{Board, BoardStatus, ChessMove, Square};
let mut board = Board::default();

assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "e4").expect("invalid: e4"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "e5").expect("invalid: e5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "d4").expect("invalid: d4"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "h5").expect("invalid: h5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "d5").expect("invalid: d5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "c5").expect("invalid: c5"));
assert_eq!(board.status(), BoardStatus::Ongoing);
board = board.make_move_new(ChessMove::from_san(&board, "dxc6").expect("invalid: dxc6")); // same en passant!
assert_eq!(board.status(), BoardStatus::Ongoing);

println!("test successful");

As far as I can tell, this is the same move, and valid SAN? (SAN comes from the beginning of https://www.chess.com/game/live/9695070671)

@minor-fixes
Copy link
Author

Upon further testing, it looks like the first case works because moves generated via ChessMove::new() are not validated to be legal, while ones from SAN are.

The SAN en passant move maybe is failing because the SAN does not have the e.p. suffix: https://docs.rs/chess/3.2.0/src/chess/chess_move.rs.html#310 and thus it is not recognized as en passant. Though, maybe it should, since Wikipedia says this suffix is optional: https://en.wikipedia.org/wiki/Algebraic_notation_(chess)#Notation_for_moves

@minor-fixes
Copy link
Author

Examining this loop a bit more, where the parsed SAN is being checked against the full list of legal moves - if the SAN is assumed to be valid (say, SAN from another system that is being played back, which happens to be what I'm trying to do) then checking for takes here and here is redundant, since the list of legal moves with a given source and destination will either be a capture or non-capture; it's not possible to have both capture and non-capture returned as legal moves, so there's no need to do these checks to disambiguate between the two.

However, dropping these checks would make the parsing more permissive to nonsensical moves, such as Nxf3 being accepted and matching to Nf3 when there is no capture on f3; this sanitization is admittedly useful if the SAN is possibly invalid (say, allowing a user to input moves during a game).

Some possible solutions are:

  • drop the checks, and fail to reject SAN capture strings that are actually valid non-capture moves, in order to handle non-annotated en passant
  • have a second function that assumes valid PGN, that does fewer checks and can handle non-annotated en passant as a result
  • store whether a move is a capture or not in the ChessMove struct returned by the MoveGen, and compare this to whether the input SAN declared a capture. Possibly the most robust, but a breaking change for ChessMove?

minor-fixes added a commit to minorhacks/chess that referenced this issue Apr 23, 2021
This change modifies the from_san parsing to fix an issue where
un-annotated en passant (missing the `e.p.` suffix) is not found as a
valid move.

This bug stems from the fact that the SAN parser:

* records if a proposed move is a capture by looking for the `x`
  character
* matching the proposed move's capture status with whether each legal
  move is a capture, in a slightly buggy way - by detecting if the
  destination square holds a piece.

En passant is the only capture type during which the destination square
doesn't hold the captured piece. When the `e.p.` suffix is present in
the input SAN move, this check is skipped; however, the check is not
skipped when the suffix is not present, and such an en passant SAN input
will fail to match its corresponding legal move.

This change drops the check for takes. This will allow en passant SAN
inputs to match their corresponding legal move, but it will also alias
capture SAN inputs to a non-capture legal move (Nxf3 will be interpreted
as Nf3 if Nf3 happens to be legal, rather than being rejected) and vice
versa.

Since this crate will only be used when we assume the SAN is valid, we
don't care about this aliasing.

Fixes: jordanbray#54
@Philipp-Sc
Copy link

Philipp-Sc commented Sep 29, 2021

Chess.js generates the following moves:

c4 g6 e4 d6 Nc3 c5 Nge2 e5 Nd5 Bg7 Nec3 Nf6 d3 Nc6 Bg5 Nd4 Be2 Be6 O-O h6 Bh4 O-O a4 Bxd5 cxd5 a6 a5 Rb8 Ra2 b5 axb6

https://lichess.org/NlJw2ipp#31

axb6 is an en_passant move and this is the standard notation for it. It being rejected as invalid is definitely a bug.

EDIT: Chessjs provides a flag:
'e' - an en passant capture

this can be used to add the necessary suffix " e.p."

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants