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.

167 lines
3.8 KiB

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)]
}
}