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