Skip to content

shaack/cm-pgn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

279 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

cm-pgn

Parser for PGNs (Portable Game Notation)

This is as ES6 Module for parsing and rendering of PGNs (Portable Game Notation).

The API is similar to history() of chess.js, but this module supports variations, nags and comments in the pgn.

I used the grammar file from PgnViewerJS of mliebelt to create the parser.

Install

npm install cm-pgn

Usage

Use the Pgn class as JS Module:

<script type="module">
    import {Pgn} from "./PATH/TO/cm-pgn/src/Pgn.js"
    // parse pgn
    const pgn = new Pgn(`[Site "Berlin"]
[Date "1989.07.02"]
[White "Haack, Stefan"]
[Black "Maier, Karsten"]

1. e4 e5 (e6) 2. Nf3 $1 {Great move!} Nc6 *`)
</script>

Pgn constructor

constructor(pgnString = "", props = {})

Supported props:

  • sloppy (default false) — accept non-standard move notations like e2e4 or e2-e4. See .move(move, options) from chess.js.
  • chess960 (default false) — parse Chess960 / Fischer Random games, including non-standard castling notation. Also auto-enabled when the header contains [Variant "Chess960"], "Fischerandom" or "Freestyle".

If the header contains [SetUp "1"] together with [FEN "…"], the game is replayed from that FEN instead of the standard starting position.

Rendering a PGN

pgn.render(renderHeader = true, renderComments = true, renderNags = true)

Re-serializes pgn.header and pgn.history back to a PGN string, word-wrapped at 80 columns. pgn.header.render() and pgn.history.render(renderComments, renderNags) are available if you only need one of the two blocks.

Header tags

pgn.header.tags is a plain object mapping tag names to string values. A TAGS constant with the well-known PGN tag names is exported from src/Header.js for safer lookups:

import {TAGS} from "./PATH/TO/cm-pgn/src/Header.js"
pgn.header.tags[TAGS.White] // "Haack, Stefan"

Data structure

The pgn has a pgn.header and a pgn.history.

pgn.header

The header holds the PGN header elements in the key value object tags.

pgn.header.tags = {
    Site: "Berlin",
    Date: "1989.07.02",
    White: "Haack, Stefan",
    Black: "Maier, Karsten"
}

pgn.history

The moves are stored in an array. Every element of that array has the following structure

pgn.history.moves[i] = {
    color: "w", // the moving color
    fen: "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", // the fen after that move
    flags: "b", // the flags, like described below
    from: "e2", // the square from
    next: {color: "b", from: "e7", to: "e6", flags: "n", piece: "p", /*…*/}, // a pointer to the next move 
    piece: "p", // the piece type 
    ply: 1, // the ply number
    previous: undefined, // a pointer to the previous move
    san: "e4", // the move in SAN notation
    to: "e4", // the square to
    uci: "e2e4", // the move in UCI notation
    variation: (4) [{/*…*/}, {/*…*/}, {/*…*/}, {/*…*/}], // a pointer to the begin of the current variation
    variations: [] // all variations starting with that move
}

pgn.history.moves[i].flags

  • 'n' - a non-capture
  • 'b' - a pawn push of two squares
  • 'e' - an en passant capture
  • 'c' - a standard capture
  • 'p' - a promotion
  • 'k' - kingside castling
  • 'q' - queenside castling

pgn.history.moves[i].piece

  • 'p' - pawn
  • 'n' - knight
  • 'b' - bishop
  • 'r' - rook
  • 'q' - queen
  • 'k' - king

Optional fields on pgn.history.moves[i]

  • nag — the NAG as string, e.g. "$1"
  • commentMove, commentBefore, commentAfter — PGN { ... } comments around the move; newlines are preserved
  • gameOver, inCheck, inCheckmate, inDraw, inStalemate, insufficientMaterial, inThreefoldRepetition — set to true when the corresponding chess.js predicate holds after the move

Examples

const history = pgn.history
assert.equal(4, history.moves.length)
assert.equal(history.moves[0].san, "e4")
assert.equal(history.moves[1].variations.length, 1)
assert.equal(history.moves[1].variations[0][0].san, "e6")
assert.equal(history.moves[2].nag, "$1")
assert.equal(history.moves[2].commentAfter, "Great move!")
assert.equal(history.moves[2].fen, "rnbqkbnr/pppp1ppp/8/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R b KQkq - 1 2")
assert.equal(history.moves[3].from, "b8")
assert.equal(history.moves[3].to, "c6")
assert.equal(history.moves[3].uci, "b8c6")
assert.equal(history.moves[3].san, "Nc6")
assert.equal(history.moves[3].previous.san, "Nf3")
assert.equal(history.moves[3].previous.next.san, "Nc6")

Building a history programmatically

pgn.history also exposes a small mutation API:

// validate without appending — returns a move object or null
pgn.history.validateMove("Nf3")

// append a move to the main line (or to a variation, via `previous`)
const move = pgn.history.addMove("Nf3")

// walk the linked list back to the starting position
pgn.history.historyToMove(move) // => move[]

addMove(notation, previous = null, sloppy = true) appends to the end of the main line by default. Passing an existing move as previous appends after that move; if previous already has a next, the new move is attached as a variation instead.

To add a move at the very start of the game — either as the first main-line move or as an alternative to an existing first move (e.g. 1. e4 (1. d4) 1... e5) — use addMoveAtStart:

history.addMove("e4")
history.addMoveAtStart("d4") // => 1. e4 (1. d4)
// equivalent shorthand through addMove:
history.addMove("d4", "start")

The move is validated from the starting position (or setUpFen). If the main line is empty, it simply becomes moves[0]; otherwise it is pushed onto moves[0].variations. The returned move can be extended with further addMove(..., previousVariationMove) calls.

Parsing multi-game PGN databases

Use PgnList to parse a string (or file) containing several games:

import {PgnList} from "./PATH/TO/cm-pgn/src/PgnList.js"

// from a string
const pgns = PgnList.parse(multiGamePgnString) // => Pgn[]
pgns[0].history.moves

// or keep the raw game strings
const list = new PgnList(multiGamePgnString)
list.pgns // => string[]

// or load from a URL (browser)
const list2 = new PgnList()
await list2.fetch("./games.pgn")
list2.pgns // => string[]

Notes on comments

Line breaks inside { ... } comments are preserved as-is in commentMove / commentBefore / commentAfter. Multiple consecutive comments at the same position (e.g. { a } { b }) are supported and their texts are joined with a single space.

Development

This module uses PEG.js for parser generation. The parser (pgnParser.js) in src/parser/ is generated from the grammar file src/grammar/pgn.pegjs.

To recreate the parser after modification of src/grammar/pgn.pegjs, run ./generate-parser.sh in the repository root.

Testing

Run the unit tests

External Links

About

Parse and create PGNs (Portable Game Notation for chess games)

Topics

Resources

License

Stars

Watchers

Forks

Contributors