Skip to content
This repository has been archived by the owner on Jan 25, 2025. It is now read-only.

notation: remove extraneous disambiguators when decoding (L)AR #84

Merged
merged 2 commits into from
Dec 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion fixtures/valid_notation_tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
62 changes: 49 additions & 13 deletions notation.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package chess

import (
"fmt"
"regexp"
"strings"
)

Expand Down Expand Up @@ -121,15 +122,58 @@ 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{}

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
}
}
}
return nil, fmt.Errorf("chess: could not decode algebraic notation %s for position %s", s, pos.String())
}
Expand Down Expand Up @@ -170,15 +214,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 {
Expand Down
6 changes: 6 additions & 0 deletions notation_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
}
)

Expand Down