You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
168 lines
3.8 KiB
168 lines
3.8 KiB
4 years ago
|
package chess
|
||
|
|
||
|
import (
|
||
|
"bufio"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"log"
|
||
|
"regexp"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// GamesFromPGN returns all PGN decoding games from the
|
||
|
// reader. It is designed to be used decoding multiple PGNs
|
||
|
// in the same file. An error is returned if there is an
|
||
|
// issue parsing the PGNs.
|
||
|
func GamesFromPGN(r io.Reader) ([]*Game, error) {
|
||
|
games := []*Game{}
|
||
|
current := ""
|
||
|
count := 0
|
||
|
totalCount := 0
|
||
|
br := bufio.NewReader(r)
|
||
|
for {
|
||
|
line, err := br.ReadString('\n')
|
||
|
if err == io.EOF {
|
||
|
break
|
||
|
} else if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
if strings.TrimSpace(line) == "" {
|
||
|
count++
|
||
|
} else {
|
||
|
current += line
|
||
|
}
|
||
|
if count == 2 {
|
||
|
game, err := decodePGN(current)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
games = append(games, game)
|
||
|
count = 0
|
||
|
current = ""
|
||
|
totalCount++
|
||
|
log.Println("Processed game", totalCount)
|
||
|
}
|
||
|
}
|
||
|
return games, nil
|
||
|
}
|
||
|
|
||
|
func decodePGN(pgn string) (*Game, error) {
|
||
|
tagPairs := getTagPairs(pgn)
|
||
|
moveStrs, outcome := moveList(pgn)
|
||
|
gameFuncs := []func(*Game){}
|
||
|
for _, tp := range tagPairs {
|
||
|
if strings.ToLower(tp.Key) == "fen" {
|
||
|
fenFunc, err := FEN(tp.Value)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("chess: pgn decode error %s on tag %s", err.Error(), tp.Key)
|
||
|
}
|
||
|
gameFuncs = append(gameFuncs, fenFunc)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
gameFuncs = append(gameFuncs, TagPairs(tagPairs))
|
||
|
g := NewGame(gameFuncs...)
|
||
|
g.IgnoreAutomaticDraws = true
|
||
|
var notation Notation = AlgebraicNotation{}
|
||
|
if len(moveStrs) > 0 {
|
||
|
_, err := LongAlgebraicNotation{}.Decode(g.Position(), moveStrs[0])
|
||
|
if err == nil {
|
||
|
notation = LongAlgebraicNotation{}
|
||
|
}
|
||
|
}
|
||
|
for _, alg := range moveStrs {
|
||
|
m, err := notation.Decode(g.Position(), alg)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("chess: pgn decode error %s on move %d", err.Error(), g.Position().moveCount)
|
||
|
}
|
||
|
if err := g.Move(m); err != nil {
|
||
|
return nil, fmt.Errorf("chess: pgn invalid move error %s on move %d", err.Error(), g.Position().moveCount)
|
||
|
}
|
||
|
}
|
||
|
g.Outcome = outcome
|
||
|
return g, nil
|
||
|
}
|
||
|
|
||
|
func encodePGN(g *Game) string {
|
||
|
s := ""
|
||
|
for _, tag := range g.TagPairs {
|
||
|
s += fmt.Sprintf("[%s \"%s\"]\n", tag.Key, tag.Value)
|
||
|
}
|
||
|
s += "\n"
|
||
|
for i, move := range g.Moves {
|
||
|
pos := g.Positions[i]
|
||
|
txt := g.Notation.Encode(pos, move)
|
||
|
if i%2 == 0 {
|
||
|
s += fmt.Sprintf("%d.%s", (i/2)+1, txt)
|
||
|
} else {
|
||
|
s += fmt.Sprintf(" %s ", txt)
|
||
|
}
|
||
|
}
|
||
|
s += " " + string(g.Outcome)
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
tagPairRegex = regexp.MustCompile(`\[(.*)\s\"(.*)\"\]`)
|
||
|
)
|
||
|
|
||
|
func getTagPairs(pgn string) []*TagPair {
|
||
|
tagPairs := []*TagPair{}
|
||
|
matches := tagPairRegex.FindAllString(pgn, -1)
|
||
|
for _, m := range matches {
|
||
|
results := tagPairRegex.FindStringSubmatch(m)
|
||
|
if len(results) == 3 {
|
||
|
pair := &TagPair{
|
||
|
Key: results[1],
|
||
|
Value: results[2],
|
||
|
}
|
||
|
tagPairs = append(tagPairs, pair)
|
||
|
}
|
||
|
}
|
||
|
return tagPairs
|
||
|
}
|
||
|
|
||
|
var (
|
||
|
moveNumRegex = regexp.MustCompile(`(?:\d+\.+)?(.*)`)
|
||
|
)
|
||
|
|
||
|
func moveList(pgn string) ([]string, Outcome) {
|
||
|
// remove comments
|
||
|
text := removeSection("{", "}", pgn)
|
||
|
// remove variations
|
||
|
text = removeSection(`\(`, `\)`, text)
|
||
|
// remove tag pairs
|
||
|
text = removeSection(`\[`, `\]`, text)
|
||
|
// remove line breaks
|
||
|
text = strings.Replace(text, "\n", " ", -1)
|
||
|
|
||
|
list := strings.Split(text, " ")
|
||
|
filtered := []string{}
|
||
|
var outcome Outcome
|
||
|
for _, move := range list {
|
||
|
move = strings.TrimSpace(move)
|
||
|
switch move {
|
||
|
case string(NoOutcome), string(WhiteWon), string(BlackWon), string(Draw):
|
||
|
outcome = Outcome(move)
|
||
|
case "":
|
||
|
default:
|
||
|
results := moveNumRegex.FindStringSubmatch(move)
|
||
|
if len(results) == 2 && results[1] != "" {
|
||
|
filtered = append(filtered, results[1])
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return filtered, outcome
|
||
|
}
|
||
|
|
||
|
func removeSection(leftChar, rightChar, s string) string {
|
||
|
r := regexp.MustCompile(leftChar + ".*?" + rightChar)
|
||
|
for {
|
||
|
i := r.FindStringIndex(s)
|
||
|
if i == nil {
|
||
|
return s
|
||
|
}
|
||
|
s = s[0:i[0]] + s[i[1]:len(s)]
|
||
|
}
|
||
|
}
|