From 5f0226abccb1481b13f230aa439c032b9ca7874f Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Sun, 12 Dec 2021 14:18:42 -0700 Subject: [PATCH 1/2] notation: remove extraneous disambiguators when decoding (L)AR --- fixtures/valid_notation_tests.json | 26 +++++++++++++++- notation.go | 50 ++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 14 deletions(-) diff --git a/fixtures/valid_notation_tests.json b/fixtures/valid_notation_tests.json index cdfc781..2daa2ae 100644 --- a/fixtures/valid_notation_tests.json +++ b/fixtures/valid_notation_tests.json @@ -158,5 +158,29 @@ "longAlgText" : "Rd1d2", "uciText" : "d1d2", "description" : "From Lichess DB https://lichess.org/editor/3r1rk1/1p1bqp2/p1pR1p1p/8/4P3/P4B2/1PP1QPP1/3R3K_w_-_-_2_22" - } + }, + { + "pos1": "r7/1R1nk3/2R1p3/p2n1p2/P5p1/4P3/1P2KPP1/8 b - - 1 35", + "pos2": "r7/1R1nk3/2R1pn2/p4p2/P5p1/4P3/1P2KPP1/8 w - - 2 36", + "algText": "Nf6", + "longAlgText": "Nd5f6", + "uciText": "d5f6", + "description" : "https://lichess.org/analysis/fromPosition/r7/1R1nk3/2R1p3/p2n1p2/P5p1/4P3/1P2KPP1/8_b_-_-_1_35" + }, + { + "pos1": "r7/1R1nk3/2R1p3/p2n1p2/P5p1/4P3/1P2KPP1/8 b - - 1 35", + "pos2": "r7/1R1nk3/2R1pn2/p4p2/P5p1/4P3/1P2KPP1/8 w - - 2 36", + "algText": "N5f6", + "longAlgText": "Nd5f6", + "uciText": "d5f6", + "description" : "https://lichess.org/analysis/fromPosition/r7/1R1nk3/2R1p3/p2n1p2/P5p1/4P3/1P2KPP1/8_b_-_-_1_35" + }, + { + "pos1": "r7/1R1nk3/2R1p3/p2n1p2/P5p1/4P3/1P2KPP1/8 b - - 1 35", + "pos2": "r7/1R1nk3/2R1pn2/p4p2/P5p1/4P3/1P2KPP1/8 w - - 2 36", + "algText": "Ndf6", + "longAlgText": "Nd5f6", + "uciText": "d5f6", + "description" : "https://lichess.org/analysis/fromPosition/r7/1R1nk3/2R1p3/p2n1p2/P5p1/4P3/1P2KPP1/8_b_-_-_1_35" + } ] diff --git a/notation.go b/notation.go index b570a07..7708134 100644 --- a/notation.go +++ b/notation.go @@ -2,6 +2,7 @@ package chess import ( "fmt" + "regexp" "strings" ) @@ -121,15 +122,46 @@ func (AlgebraicNotation) Encode(pos *Position, m *Move) string { return pChar + s1Str + capChar + m.s2.String() + promoText + checkChar } +var pgnRegex = regexp.MustCompile(`^(?:([RNBQKP]?)([abcdefgh]?)(\d?)(x?)([abcdefgh])(\d)(=Q)?|(O-O(?:-O)?))([+#!?]|e\.p\.)*$`) + +func algebraicNotationParts(s string) (string, string, string, string, string, string, string, string, error) { + submatches := pgnRegex.FindStringSubmatch(s) + if len(submatches) == 0 { + return "", "", "", "", "", "", "", "", fmt.Errorf("could not decode algebraic notation %s", s) + } + + return submatches[1], submatches[2], submatches[3], submatches[4], submatches[5], submatches[6], submatches[7], submatches[8], nil +} + // Decode implements the Decoder interface. func (AlgebraicNotation) Decode(pos *Position, s string) (*Move, error) { - s = removeSubstrings(s, "?", "!", "+", "#", "e.p.") + piece, originFile, originRank, capture, file, rank, promotes, castles, err := algebraicNotationParts(s) + if err != nil { + return nil, fmt.Errorf("chess: %+v for position %s", err, pos.String()) + } + for _, m := range pos.ValidMoves() { - str := AlgebraicNotation{}.Encode(pos, m) - str = removeSubstrings(str, "?", "!", "+", "#", "e.p.") - if str == s { + moveStr := AlgebraicNotation{}.Encode(pos, m) + moveSubmatches := pgnRegex.FindStringSubmatch(moveStr) + moveCleaned := strings.Join(moveSubmatches[1:9], "") + + cleaned := piece + originFile + originRank + capture + file + rank + promotes + castles + if cleaned == moveCleaned { return m, nil } + + // Try and remove the disambiguators and see if it parses. Sometimes they + // get extraneously added. + options := []string{ + piece + originRank + capture + file + rank + promotes + castles, // no origin file + piece + originFile + capture + file + rank + promotes + castles, // no origin rank + piece + capture + file + rank + promotes + castles, // no origin + } + for _, opt := range options { + if opt == moveCleaned { + return m, nil + } + } } return nil, fmt.Errorf("chess: could not decode algebraic notation %s for position %s", s, pos.String()) } @@ -170,15 +202,7 @@ func (LongAlgebraicNotation) Encode(pos *Position, m *Move) string { // Decode implements the Decoder interface. func (LongAlgebraicNotation) Decode(pos *Position, s string) (*Move, error) { - s = removeSubstrings(s, "?", "!", "+", "#", "e.p.") - for _, m := range pos.ValidMoves() { - str := LongAlgebraicNotation{}.Encode(pos, m) - str = removeSubstrings(str, "?", "!", "+", "#", "e.p.") - if str == s { - return m, nil - } - } - return nil, fmt.Errorf("chess: could not decode long algebraic notation %s for position %s", s, pos.String()) + return AlgebraicNotation{}.Decode(pos, s) } func getCheckChar(pos *Position, move *Move) string { From b2306061b363526639828523e05ba32d947a3731 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Mon, 13 Dec 2021 10:59:25 -0700 Subject: [PATCH 2/2] notation: improve parsing to handle pawn moves in (L)AN --- notation.go | 20 ++++++++++++++++---- notation_test.go | 6 ++++++ 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/notation.go b/notation.go index 7708134..8e64766 100644 --- a/notation.go +++ b/notation.go @@ -152,11 +152,23 @@ func (AlgebraicNotation) Decode(pos *Position, s string) (*Move, error) { // Try and remove the disambiguators and see if it parses. Sometimes they // get extraneously added. - options := []string{ - piece + originRank + capture + file + rank + promotes + castles, // no origin file - piece + originFile + capture + file + rank + promotes + castles, // no origin rank - piece + capture + file + rank + promotes + castles, // no origin + options := []string{} + + if piece != "" { + options = append(options, piece+capture+file+rank+promotes+castles) // no origin + options = append(options, piece+originRank+capture+file+rank+promotes+castles) // no origin file + options = append(options, piece+originFile+capture+file+rank+promotes+castles) // no origin rank + } else { + if capture != "" { + // Possibly a pawn capture. In order to parse things like d4xe5, we need + // to try parsing without the rank. + options = append(options, piece+originFile+capture+file+rank+promotes+castles) // no origin rank + } + if originFile != "" && originRank != "" { + options = append(options, piece+capture+file+rank+promotes+castles) // no origin + } } + for _, opt := range options { if opt == moveCleaned { return m, nil diff --git a/notation_test.go b/notation_test.go index bc802a1..c005904 100644 --- a/notation_test.go +++ b/notation_test.go @@ -100,6 +100,12 @@ var ( Pos: unsafeFEN("rnbqkbnr/pppp1ppp/8/4p3/4P3/8/PPPP1PPP/RNBQKBNR w KQkq e6 0 2"), Text: "nf3", }, + { + // disambiguation should not allow for this since it is not a capture + N: AlgebraicNotation{}, + Pos: unsafeFEN("rnbqkbnr/ppp1pppp/8/3p4/3P4/8/PPP1PPPP/RNBQKBNR w KQkq - 0 2"), + Text: "bf4", + }, } )