✨ Simple PGN parser
This commit is contained in:
parent
80e9a0d890
commit
5025d29d9e
6 changed files with 1064 additions and 0 deletions
24
pkg/pgn/doc.go
Normal file
24
pkg/pgn/doc.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
// Package pgn implments an importer and exporter for the Portable Game Notation(PGN).
|
||||
// It provides an interface for efficiently reading and writing PGN files using buffered IO.
|
||||
//
|
||||
// PGN is the defacto standard format for storing and exchanging chess games. It is a cleartext
|
||||
// format that is both human- and machine-readable. Most chess libraries and frameworks have some
|
||||
// kind of built-in support for reading and/or writing PGN files.
|
||||
//
|
||||
// Spec: https://www.chessclub.com/help/pgn-spec
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// f, _ := os.Open("file.pgn")
|
||||
// parser := pgn.NewParser(bufio.NewReader(f))
|
||||
// for {
|
||||
// game, err := parser.Next()
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Println(game)
|
||||
// if game == nil {
|
||||
// return
|
||||
// }
|
||||
// }
|
||||
package pgn
|
44
pkg/pgn/game.go
Normal file
44
pkg/pgn/game.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package pgn
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.c-base.org/gochess/libchess/pkg/board"
|
||||
"code.c-base.org/gochess/libchess/pkg/game"
|
||||
"code.c-base.org/gochess/libchess/pkg/pgn/move"
|
||||
)
|
||||
|
||||
// Game represents a PGN game.
|
||||
type Game struct {
|
||||
Tags []game.Tag
|
||||
Moves []string
|
||||
}
|
||||
|
||||
func newGame() *Game {
|
||||
return &Game{
|
||||
Tags: []game.Tag{},
|
||||
Moves: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
// Game returns a *game.Game representation of this PGN game.
|
||||
func (g *Game) Game() (*game.Game, error) {
|
||||
// parse all moves
|
||||
moves := make([]*board.Move, len(g.Moves))
|
||||
for i, _m := range g.Moves {
|
||||
m, err := move.NewParser(_m).Move()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
moves[i] = m
|
||||
}
|
||||
// return initialized *game.Game
|
||||
return &game.Game{
|
||||
Tags: g.Tags,
|
||||
Moves: moves,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (g Game) String() string {
|
||||
return fmt.Sprintf("<Game(Tags: %q, Moves: %q)>", g.Tags, g.Moves)
|
||||
}
|
332
pkg/pgn/lexer.go
Normal file
332
pkg/pgn/lexer.go
Normal file
|
@ -0,0 +1,332 @@
|
|||
package pgn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EOF signals end of input
|
||||
const EOF = -1
|
||||
|
||||
// TokenType defines the type of a token
|
||||
type TokenType uint64
|
||||
|
||||
// The following TokenTypes exist:
|
||||
const (
|
||||
TokenInvalid TokenType = iota
|
||||
TokenEOF
|
||||
TokenDiv
|
||||
TokenNewline
|
||||
TokenWhitespace
|
||||
TokenComment
|
||||
TokenString
|
||||
|
||||
TokenBracketLeft
|
||||
TokenBracketRight
|
||||
TokenParenthesisLeft
|
||||
TokenParenthesisRight
|
||||
TokenAngleLeft
|
||||
TokenAngleRight
|
||||
TokenSymbol
|
||||
|
||||
TokenEscapeMechanism
|
||||
)
|
||||
|
||||
var tokenName = map[TokenType]string{
|
||||
TokenInvalid: "INVALID",
|
||||
TokenEOF: "EOF",
|
||||
TokenDiv: "Div",
|
||||
TokenNewline: "Newline",
|
||||
TokenWhitespace: "Whitespace",
|
||||
TokenComment: "Comment",
|
||||
TokenString: "String",
|
||||
TokenBracketLeft: "BracketLeft",
|
||||
TokenBracketRight: "BracketRight",
|
||||
TokenParenthesisLeft: "ParenthesisLeft",
|
||||
TokenParenthesisRight: "ParenthesisRight",
|
||||
TokenAngleLeft: "AngleLeft",
|
||||
TokenAngleRight: "AngleRight",
|
||||
TokenSymbol: "Symbol",
|
||||
}
|
||||
|
||||
// Token represents a PGN token.
|
||||
type Token struct {
|
||||
Line int
|
||||
Col int
|
||||
Type TokenType
|
||||
Value string
|
||||
}
|
||||
|
||||
func (t Token) String() string {
|
||||
return fmt.Sprintf(
|
||||
"<Token%s(Line: %d, Col: %d, Value: %q)>",
|
||||
tokenName[t.Type],
|
||||
t.Line,
|
||||
t.Col,
|
||||
t.Value,
|
||||
)
|
||||
}
|
||||
|
||||
// LexFn defines the signature of a lexer function.
|
||||
type LexFn func(*Lexer) LexFn
|
||||
|
||||
// Lexer implements a PGN tokenizer.
|
||||
type Lexer struct {
|
||||
input *bufio.Reader
|
||||
output chan *Token
|
||||
err chan error
|
||||
line int
|
||||
start int
|
||||
pos int
|
||||
}
|
||||
|
||||
// NewLexer returns an initialized Lexer.
|
||||
func NewLexer(input *bufio.Reader) *Lexer {
|
||||
l := &Lexer{
|
||||
input: input,
|
||||
output: make(chan *Token, 1),
|
||||
err: make(chan error, 1),
|
||||
line: 1,
|
||||
start: 1,
|
||||
pos: 1,
|
||||
}
|
||||
go l.run()
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Lexer) run() *Lexer {
|
||||
go func() {
|
||||
defer close(l.output)
|
||||
defer close(l.err)
|
||||
for fn := lexMain; fn != nil; {
|
||||
fn = fn(l)
|
||||
}
|
||||
}()
|
||||
return l
|
||||
}
|
||||
|
||||
// Next returns the next Token from the input stream or EOF once the input stream has ended.
|
||||
func (l *Lexer) Next() (*Token, error) {
|
||||
select {
|
||||
case err := <-l.err:
|
||||
return nil, err
|
||||
case t := <-l.output:
|
||||
return t, nil
|
||||
}
|
||||
}
|
||||
|
||||
// All returns all parsed tokens as []*Token.
|
||||
func (l *Lexer) All() ([]*Token, error) {
|
||||
out := []*Token{}
|
||||
for {
|
||||
t, err := l.Next()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
if t == nil || t.Type == TokenEOF {
|
||||
return out, nil
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) next() rune {
|
||||
r, _, err := l.input.ReadRune()
|
||||
if err != nil {
|
||||
return EOF
|
||||
}
|
||||
l.pos++
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *Lexer) undo() {
|
||||
l.input.UnreadRune()
|
||||
l.pos--
|
||||
}
|
||||
|
||||
func (l *Lexer) peek() rune {
|
||||
defer l.undo()
|
||||
return l.next()
|
||||
}
|
||||
|
||||
func (l *Lexer) newToken(t TokenType, v string) *Token {
|
||||
return &Token{
|
||||
Line: l.line,
|
||||
Col: l.start,
|
||||
Type: t,
|
||||
Value: v,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Lexer) emit(t *Token) {
|
||||
l.output <- t
|
||||
l.start = l.pos
|
||||
}
|
||||
|
||||
func (l *Lexer) emitUnexpected(r rune) LexFn {
|
||||
l.err <- fmt.Errorf(
|
||||
"unexpected character in line %d at col %d: %v",
|
||||
l.line,
|
||||
l.pos,
|
||||
r,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
////////////////
|
||||
//// LEXERS ////
|
||||
////////////////
|
||||
|
||||
func lexMain(l *Lexer) LexFn {
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case EOF:
|
||||
l.emit(l.newToken(TokenEOF, "EOF"))
|
||||
return nil
|
||||
case '\n':
|
||||
return lexNewline
|
||||
case ' ':
|
||||
return lexWhitespace
|
||||
case '%':
|
||||
if l.pos == 2 {
|
||||
return lexEscape
|
||||
}
|
||||
return l.emitUnexpected(r)
|
||||
case ';':
|
||||
return lexCommentUntilNewline
|
||||
case '{':
|
||||
return lexComment
|
||||
case '[':
|
||||
l.emit(l.newToken(TokenBracketLeft, "["))
|
||||
case ']':
|
||||
l.emit(l.newToken(TokenBracketRight, "]"))
|
||||
case '"':
|
||||
return lexString
|
||||
default:
|
||||
l.undo()
|
||||
return lexSymbol
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexNewline(l *Lexer) LexFn {
|
||||
out := bytes.NewBuffer(make([]byte, 0, 255))
|
||||
out.WriteRune('\n')
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case '\n':
|
||||
out.WriteRune('\n')
|
||||
default:
|
||||
l.undo()
|
||||
l.emit(l.newToken(TokenNewline, out.String()))
|
||||
l.line += out.Len()
|
||||
l.start = 1
|
||||
l.pos = 1
|
||||
return lexMain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexWhitespace(l *Lexer) LexFn {
|
||||
out := bytes.NewBuffer(make([]byte, 0, 255))
|
||||
out.WriteRune(' ')
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case ' ':
|
||||
out.WriteRune(' ')
|
||||
default:
|
||||
l.undo()
|
||||
l.emit(l.newToken(TokenWhitespace, out.String()))
|
||||
return lexMain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexEscape(l *Lexer) LexFn {
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case EOF, '\n':
|
||||
return lexMain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexCommentUntilNewline(l *Lexer) LexFn {
|
||||
out := bytes.NewBuffer(make([]byte, 0, 8192))
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case EOF, '\n':
|
||||
if out.Len() > 0 {
|
||||
l.emit(l.newToken(TokenComment, out.String()))
|
||||
}
|
||||
return lexMain
|
||||
default:
|
||||
_, err := out.WriteRune(r)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexComment(l *Lexer) LexFn {
|
||||
out := bytes.NewBuffer(make([]byte, 0, 8192))
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case EOF:
|
||||
l.emit(l.newToken(TokenComment, out.String()))
|
||||
return lexMain
|
||||
case '\\':
|
||||
out.WriteRune(l.next())
|
||||
case '}':
|
||||
l.emit(l.newToken(TokenComment, out.String()))
|
||||
return lexMain
|
||||
default:
|
||||
out.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexString(l *Lexer) LexFn {
|
||||
out := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case EOF:
|
||||
return l.emitUnexpected(r)
|
||||
case '\\':
|
||||
out.WriteRune(l.next())
|
||||
case '"':
|
||||
l.emit(l.newToken(TokenString, out.String()))
|
||||
return lexMain
|
||||
default:
|
||||
out.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexSymbol(l *Lexer) LexFn {
|
||||
out := bytes.NewBuffer(make([]byte, 0, 255))
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case EOF:
|
||||
l.emit(l.newToken(TokenSymbol, out.String()))
|
||||
l.undo()
|
||||
return lexMain
|
||||
case '\n', ' ', '"':
|
||||
l.undo()
|
||||
l.emit(l.newToken(TokenSymbol, out.String()))
|
||||
return lexMain
|
||||
default:
|
||||
out.WriteRune(r)
|
||||
}
|
||||
}
|
||||
}
|
199
pkg/pgn/move/lexer.go
Normal file
199
pkg/pgn/move/lexer.go
Normal file
|
@ -0,0 +1,199 @@
|
|||
package move
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.c-base.org/gochess/libchess/pkg/board"
|
||||
)
|
||||
|
||||
var tokenFactory = make(chan *Token, 128)
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
for {
|
||||
tokenFactory <- &Token{}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// TokenType defines the type of a token
|
||||
type TokenType uint8
|
||||
|
||||
// The following TokenTypes exist
|
||||
const (
|
||||
TokenError TokenType = iota
|
||||
TokenEOF
|
||||
TokenPiece
|
||||
TokenFile
|
||||
TokenRank
|
||||
TokenCapture
|
||||
TokenSquare
|
||||
TokenCheck
|
||||
TokenMate
|
||||
TokenCastles
|
||||
)
|
||||
|
||||
// eof signals the end of a move
|
||||
const eof = -1
|
||||
|
||||
// Token represents a move token.
|
||||
type Token struct {
|
||||
Pos int // character column of this token
|
||||
Type TokenType // type (see above)
|
||||
Value string // literal value
|
||||
}
|
||||
|
||||
// Lexer implements a lexer for tokenizing PGN formatted moves.
|
||||
type Lexer struct {
|
||||
input *bufio.Reader // buffered io for streaming the input
|
||||
tokens chan Token // output channel
|
||||
start int // starting position of the current token
|
||||
pos int // current scanning position
|
||||
buf *Token
|
||||
}
|
||||
|
||||
// NewLexer returns an initialized Lexer.
|
||||
func NewLexer(input string) *Lexer {
|
||||
l := &Lexer{
|
||||
input: bufio.NewReader(strings.NewReader(input)),
|
||||
start: 1,
|
||||
pos: 1,
|
||||
tokens: make(chan Token, 1),
|
||||
}
|
||||
go l.scan()
|
||||
return l
|
||||
}
|
||||
|
||||
// NextToken returns the next token from the input string.
|
||||
func (l *Lexer) NextToken() Token {
|
||||
return <-l.tokens
|
||||
}
|
||||
|
||||
// emit emits the given token to the output channel.
|
||||
func (l *Lexer) emit(t Token) {
|
||||
// When encountering a token of type TokenFile *[a-h]*, it needs to be buffered and compared to
|
||||
// the next token, which may be of type TokenRank *[1-8]* combining them into a token of type
|
||||
// TokenSquare.
|
||||
if l.buf == nil {
|
||||
// check for TokenFile and buffer it
|
||||
if t.Type == TokenFile {
|
||||
l.buf = &t
|
||||
} else {
|
||||
l.tokens <- t
|
||||
l.start = l.pos
|
||||
}
|
||||
} else {
|
||||
// grab the last token off the buffer
|
||||
prev := l.buf
|
||||
l.buf = nil
|
||||
// TokenFile followed by TokenRank combines to TokenSquare
|
||||
if t.Type == TokenRank {
|
||||
strSq := fmt.Sprintf("%s%s", prev.Value, t.Value)
|
||||
_, ok := board.StrToSquareMap[strSq]
|
||||
if !ok {
|
||||
// technically this should not be reached, but I'm handling it anyways, just in case
|
||||
l.tokens <- *prev
|
||||
l.tokens <- t
|
||||
} else {
|
||||
// emit TokenSquare instead of individual TokenFile & TokenRank
|
||||
l.tokens <- Token{
|
||||
Pos: l.start,
|
||||
Type: TokenSquare,
|
||||
Value: strSq,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// next reads the next rune from the buffered input stream
|
||||
func (l *Lexer) next() rune {
|
||||
r, _, err := l.input.ReadRune()
|
||||
if err != nil {
|
||||
return eof
|
||||
}
|
||||
l.pos++
|
||||
return r
|
||||
}
|
||||
|
||||
func (l *Lexer) undo() {
|
||||
l.input.UnreadRune()
|
||||
l.pos--
|
||||
}
|
||||
|
||||
// newToken is a helper for easily initializing Tokens with the correct values.
|
||||
func (l *Lexer) newToken(tokType TokenType, v string) Token {
|
||||
t := <-tokenFactory
|
||||
t.Pos = l.start
|
||||
t.Type = tokType
|
||||
t.Value = v
|
||||
return *t
|
||||
}
|
||||
|
||||
// scan scans for tokens and emits them to the output channel until the end of the input stream is
|
||||
// reached.
|
||||
func (l *Lexer) scan() {
|
||||
defer close(l.tokens)
|
||||
for {
|
||||
r := l.next()
|
||||
switch r {
|
||||
case eof:
|
||||
l.emit(l.newToken(TokenEOF, "eof"))
|
||||
return
|
||||
case 'O', '0':
|
||||
l.undo()
|
||||
m := lexCastles(l)
|
||||
if m == "" {
|
||||
l.emit(l.newToken(TokenError, m))
|
||||
} else {
|
||||
l.emit(l.newToken(TokenCastles, m))
|
||||
}
|
||||
case 'K', 'Q', 'B', 'N', 'R':
|
||||
l.emit(l.newToken(TokenPiece, string(r)))
|
||||
case 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h':
|
||||
l.emit(l.newToken(TokenFile, string(r)))
|
||||
case '1', '2', '3', '4', '5', '6', '7', '8':
|
||||
l.emit(l.newToken(TokenRank, string(r)))
|
||||
case '+':
|
||||
l.emit(l.newToken(TokenCheck, string(r)))
|
||||
case '#':
|
||||
l.emit(l.newToken(TokenMate, string(r)))
|
||||
case 'x':
|
||||
l.emit(l.newToken(TokenCapture, string(r)))
|
||||
case '=':
|
||||
// noop
|
||||
default:
|
||||
l.emit(l.newToken(TokenError, string(r)))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func lexCastles(l *Lexer) string {
|
||||
var (
|
||||
buf = make([]byte, 0, 5)
|
||||
out = bytes.NewBuffer(buf)
|
||||
c = 0
|
||||
)
|
||||
for {
|
||||
r := l.next()
|
||||
switch {
|
||||
case c == 5:
|
||||
m := out.String()
|
||||
switch m {
|
||||
case "O-O", "0-0":
|
||||
return "O-O"
|
||||
case "O-O-O", "0-0-0":
|
||||
return "O-O-O"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
case r == 'O', r == '-':
|
||||
out.WriteRune(r)
|
||||
}
|
||||
c++
|
||||
}
|
||||
}
|
224
pkg/pgn/move/parser.go
Normal file
224
pkg/pgn/move/parser.go
Normal file
|
@ -0,0 +1,224 @@
|
|||
package move
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"code.c-base.org/gochess/libchess/pkg/board"
|
||||
)
|
||||
|
||||
// Parser implements a parser for PGN moves.
|
||||
type Parser struct {
|
||||
lexer *Lexer
|
||||
}
|
||||
|
||||
// NewParser returns an initialized parser for the given move.
|
||||
func NewParser(m string) *Parser {
|
||||
return &Parser{
|
||||
lexer: NewLexer(m),
|
||||
}
|
||||
}
|
||||
|
||||
// Move parses the move and returns it or an error.
|
||||
func (p *Parser) Move() (*board.Move, error) {
|
||||
var (
|
||||
stateCastles bool
|
||||
statePiece bool
|
||||
stateDisambiguity bool
|
||||
stateCaptures bool
|
||||
stateSquare bool
|
||||
stateCheck bool
|
||||
move = &board.Move{}
|
||||
)
|
||||
parsing:
|
||||
for {
|
||||
t := p.lexer.NextToken()
|
||||
if t.Type == TokenEOF {
|
||||
if move.To == board.NoSquare {
|
||||
if !move.HasProp(board.KingSideCastle) && !move.HasProp(board.QueenSideCastle) {
|
||||
return nil, p.throwToken(t)
|
||||
}
|
||||
}
|
||||
return move, nil
|
||||
}
|
||||
|
||||
if !stateCastles {
|
||||
stateCastles = true
|
||||
if parseCastles(t, move) {
|
||||
continue parsing
|
||||
}
|
||||
}
|
||||
|
||||
if !statePiece {
|
||||
statePiece = true
|
||||
if parsePiece(t, move) {
|
||||
continue parsing
|
||||
}
|
||||
}
|
||||
|
||||
if !stateDisambiguity {
|
||||
stateDisambiguity = true
|
||||
if parseDisambiguity(t, move) {
|
||||
continue parsing
|
||||
}
|
||||
}
|
||||
|
||||
if !stateCaptures {
|
||||
stateCaptures = true
|
||||
if parseCaptures(t, move) {
|
||||
continue parsing
|
||||
}
|
||||
}
|
||||
|
||||
if !stateSquare {
|
||||
stateSquare = true
|
||||
if parseSquare(t, move) {
|
||||
continue parsing
|
||||
}
|
||||
}
|
||||
|
||||
if !stateCheck {
|
||||
stateCheck = true
|
||||
if parseCheckMate(t, move) {
|
||||
continue parsing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p Parser) throwToken(t Token) error {
|
||||
return fmt.Errorf("invalid token at pos %d: %s", t.Pos, t.Value)
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
//// PARSE CASTLES ////
|
||||
///////////////////////
|
||||
|
||||
func parseCastles(t Token, m *board.Move) bool {
|
||||
if t.Type == TokenCastles {
|
||||
switch t.Value {
|
||||
case "O-O", "0-0":
|
||||
m.AddProp(board.KingSideCastle)
|
||||
return true
|
||||
case "O-O-O", "0-0-0":
|
||||
m.AddProp(board.QueenSideCastle)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
//// PARSE PIECE ////
|
||||
/////////////////////
|
||||
|
||||
var legalPieces = map[string]board.PieceType{
|
||||
"K": board.King,
|
||||
"Q": board.Queen,
|
||||
"B": board.Bishop,
|
||||
"N": board.Knight,
|
||||
"R": board.Rook,
|
||||
}
|
||||
|
||||
func parsePiece(t Token, m *board.Move) bool {
|
||||
if t.Type != TokenPiece {
|
||||
return false
|
||||
}
|
||||
p, ok := legalPieces[t.Value]
|
||||
if ok {
|
||||
m.Piece = p
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
//// PARSE SQUARES ////
|
||||
///////////////////////
|
||||
|
||||
var (
|
||||
legalFiles = map[string]board.File{
|
||||
"a": board.FileA,
|
||||
"b": board.FileB,
|
||||
"c": board.FileC,
|
||||
"d": board.FileD,
|
||||
"e": board.FileE,
|
||||
"f": board.FileF,
|
||||
"g": board.FileG,
|
||||
"h": board.FileH,
|
||||
}
|
||||
legalRanks = map[string]board.Rank{
|
||||
"1": board.Rank1,
|
||||
"2": board.Rank2,
|
||||
"3": board.Rank3,
|
||||
"4": board.Rank4,
|
||||
"5": board.Rank5,
|
||||
"6": board.Rank6,
|
||||
"7": board.Rank7,
|
||||
"8": board.Rank8,
|
||||
}
|
||||
)
|
||||
|
||||
func parseDisambiguity(t Token, m *board.Move) bool {
|
||||
if t.Type == TokenFile {
|
||||
f, ok := legalFiles[t.Value]
|
||||
if ok {
|
||||
m.FromFile = &f
|
||||
return true
|
||||
}
|
||||
}
|
||||
if t.Type == TokenRank {
|
||||
r, ok := legalRanks[t.Value]
|
||||
if ok {
|
||||
m.FromRank = &r
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func parseSquare(t Token, m *board.Move) bool {
|
||||
if t.Type == TokenSquare {
|
||||
m.To = board.StrToSquareMap[t.Value]
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
///////////////////////
|
||||
//// PARSE CAPTURE ////
|
||||
///////////////////////
|
||||
|
||||
var legalCapture = map[string]struct{}{
|
||||
"x": {},
|
||||
}
|
||||
|
||||
func parseCaptures(t Token, m *board.Move) bool {
|
||||
if t.Type == TokenCapture {
|
||||
_, ok := legalCapture[t.Value]
|
||||
if ok {
|
||||
m.AddProp(board.Capture)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
//////////////////////////
|
||||
//// PARSE CHECK/MATE ////
|
||||
//////////////////////////
|
||||
|
||||
func parseCheckMate(t Token, m *board.Move) bool {
|
||||
if t.Type == TokenCheck {
|
||||
if t.Value == "+" {
|
||||
m.AddProp(board.Check)
|
||||
return true
|
||||
}
|
||||
}
|
||||
if t.Type == TokenMate {
|
||||
if t.Value == "#" {
|
||||
m.AddProp(board.Mate)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
241
pkg/pgn/parser.go
Normal file
241
pkg/pgn/parser.go
Normal file
|
@ -0,0 +1,241 @@
|
|||
package pgn
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"code.c-base.org/gochess/libchess/pkg/game"
|
||||
)
|
||||
|
||||
// PoolParsers defines how may parsers are prenitialized.
|
||||
const PoolParsers = 8
|
||||
|
||||
// ParseFn defines the signature of a parser function.
|
||||
type ParseFn func(*Parser) ParseFn
|
||||
|
||||
// Parser implements a PGN parser.
|
||||
type Parser struct {
|
||||
lexer *Lexer
|
||||
errors chan error
|
||||
games chan *Game
|
||||
game *Game
|
||||
|
||||
tokenBuf *Token
|
||||
useBuf bool
|
||||
}
|
||||
|
||||
var parserFactory = make(chan *Parser, PoolParsers)
|
||||
|
||||
func init() {
|
||||
go func() {
|
||||
for {
|
||||
parserFactory <- &Parser{
|
||||
errors: make(chan error),
|
||||
games: make(chan *Game),
|
||||
game: newGame(),
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// NewParser returns an initialized parser
|
||||
func NewParser(input *bufio.Reader) *Parser {
|
||||
p := <-parserFactory
|
||||
p.lexer = NewLexer(input)
|
||||
go p.run()
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *Parser) run() {
|
||||
defer close(p.errors)
|
||||
defer close(p.games)
|
||||
for fn := parseTagSection; fn != nil; {
|
||||
fn = fn(p)
|
||||
}
|
||||
}
|
||||
|
||||
// Next returns the next parsed game from the input stream or an error.
|
||||
func (p *Parser) Next() (*Game, error) {
|
||||
select {
|
||||
case err := <-p.errors:
|
||||
return nil, err
|
||||
case g := <-p.games:
|
||||
return g, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Parser) next() (*Token, error) {
|
||||
if p.useBuf {
|
||||
p.useBuf = false
|
||||
return p.tokenBuf, nil
|
||||
}
|
||||
t, err := p.lexer.Next()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.tokenBuf = t
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (p *Parser) undo() {
|
||||
p.useBuf = true
|
||||
}
|
||||
|
||||
func (p *Parser) throwUnexpected(t *Token) {
|
||||
p.errors <- fmt.Errorf(
|
||||
"parsing error: unexpected token in line %d at %d: %q",
|
||||
t.Line,
|
||||
t.Col,
|
||||
t.Value,
|
||||
)
|
||||
}
|
||||
|
||||
func throwUnexpectedEOF(p *Parser) ParseFn {
|
||||
p.errors <- fmt.Errorf(
|
||||
"parsing error: unexpected EOF",
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Parser) emit() {
|
||||
p.games <- p.game
|
||||
p.game = newGame()
|
||||
}
|
||||
|
||||
func parseTagSection(p *Parser) ParseFn {
|
||||
for {
|
||||
// grab next token
|
||||
t, err := p.next()
|
||||
// bail out on error
|
||||
if err != nil {
|
||||
p.errors <- err
|
||||
return nil
|
||||
}
|
||||
// handle for EOF
|
||||
if t == nil || t.Type == TokenEOF {
|
||||
p.emit()
|
||||
return nil
|
||||
}
|
||||
switch t.Type {
|
||||
case TokenNewline, TokenWhitespace:
|
||||
// noop
|
||||
case TokenBracketLeft:
|
||||
return parseTag
|
||||
case TokenSymbol:
|
||||
p.undo()
|
||||
return parseMovetext
|
||||
default:
|
||||
p.throwUnexpected(t)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseTag(p *Parser) ParseFn {
|
||||
tag := game.Tag{}
|
||||
findSymbol:
|
||||
for {
|
||||
t, err := p.next()
|
||||
if err != nil {
|
||||
p.errors <- err
|
||||
return nil
|
||||
}
|
||||
if t == nil || t.Type == TokenEOF {
|
||||
return throwUnexpectedEOF
|
||||
}
|
||||
switch t.Type {
|
||||
case TokenNewline, TokenWhitespace, TokenComment:
|
||||
// noop
|
||||
case TokenSymbol:
|
||||
tag.Key = t.Value
|
||||
break findSymbol
|
||||
default:
|
||||
p.throwUnexpected(t)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
findValue:
|
||||
for {
|
||||
t, err := p.next()
|
||||
if err != nil {
|
||||
p.errors <- err
|
||||
return nil
|
||||
}
|
||||
if t == nil || t.Type == TokenEOF {
|
||||
return throwUnexpectedEOF
|
||||
}
|
||||
switch t.Type {
|
||||
case TokenNewline, TokenWhitespace, TokenComment:
|
||||
// noop
|
||||
case TokenString:
|
||||
tag.Value = t.Value
|
||||
break findValue
|
||||
default:
|
||||
p.throwUnexpected(t)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
for {
|
||||
t, err := p.next()
|
||||
if err != nil {
|
||||
p.errors <- err
|
||||
return nil
|
||||
}
|
||||
if t == nil || t.Type == TokenEOF {
|
||||
return throwUnexpectedEOF
|
||||
}
|
||||
switch t.Type {
|
||||
case TokenNewline, TokenWhitespace, TokenComment:
|
||||
// noop
|
||||
case TokenBracketRight:
|
||||
p.game.Tags = append(p.game.Tags, tag)
|
||||
return parseTagSection
|
||||
default:
|
||||
p.throwUnexpected(t)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseMovetext(p *Parser) ParseFn {
|
||||
isTermination := func(s string) bool {
|
||||
switch s {
|
||||
case "0-1", "1-0", "1/2", "1/2-1/2", "*":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
for {
|
||||
t, err := p.next()
|
||||
if err != nil {
|
||||
p.errors <- err
|
||||
return nil
|
||||
}
|
||||
if t == nil || t.Type == TokenEOF {
|
||||
p.undo()
|
||||
return parseTagSection
|
||||
}
|
||||
switch t.Type {
|
||||
case TokenNewline, TokenWhitespace, TokenComment:
|
||||
// noop
|
||||
case TokenSymbol:
|
||||
if strings.Contains(t.Value, ".") {
|
||||
continue
|
||||
}
|
||||
if !isTermination(t.Value) {
|
||||
p.game.Moves = append(p.game.Moves, t.Value)
|
||||
}
|
||||
case TokenBracketLeft:
|
||||
p.emit()
|
||||
p.undo()
|
||||
return parseTagSection
|
||||
default:
|
||||
p.throwUnexpected(t)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue