The PGN module provides comprehensive support for parsing, manipulating, and generating PGN (Portable Game Notation) files. It includes both a simple parser for trusted input and a streaming parser for handling large files with DoS protection.
Core Types
Game
Represents a complete PGN game with headers and moves.
interface Game<T> {
headers: Map<string, string>;
comments?: string[];
moves: Node<T>;
}
Game headers (Event, Site, Date, Round, White, Black, Result, etc.)
Comments before the first move
Root node of the game tree
Node
Represents a node in the game tree (root node or move node).
class Node<T> {
children: ChildNode<T>[] = [];
mainlineNodes(): Iterable<ChildNode<T>>;
mainline(): Iterable<T>;
end(): Node<T>;
}
Iterates through the mainline nodes (first variation at each position)
Iterates through the mainline data (first variation at each position)
Returns the last node in the mainline
ChildNode
Represents a move node in the game tree.
class ChildNode<T> extends Node<T> {
constructor(public data: T);
}
The move data (typically PgnNodeData)
PgnNodeData
Standard data stored in PGN nodes.
interface PgnNodeData {
san: string;
startingComments?: string[];
comments?: string[];
nags?: number[];
}
Numeric Annotation Glyphs (NAGs): 1(!),2 (?), etc.
Parsing Functions
parsePgn
Parses a PGN string into an array of games.
import { parsePgn } from 'chessops/pgn';
const pgn = `
[Event "F/S Return Match"]
[Site "Belgrade, Serbia JUG"]
[Date "1992.11.04"]
[Round "29"]
[White "Fischer, Robert J."]
[Black "Spassky, Boris V."]
[Result "1/2-1/2"]
1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 1/2-1/2
`;
const games = parsePgn(pgn);
console.log(games.length); // 1
console.log(games[0].headers.get('White')); // 'Fischer, Robert J.'
for (const node of games[0].moves.mainline()) {
console.log(node.san); // 'e4', 'e5', 'Nf3', ...
}
Optional function to initialize headers. Defaults to defaultHeaders()
This parser is permissive and will skip invalid tokens. Use PgnParser for stricter control and DoS protection.
PgnParser
Streaming parser with DoS protection for parsing large PGN files.
import { createReadStream } from 'fs';
import { PgnParser } from 'chessops/pgn';
const stream = createReadStream('games.pgn', { encoding: 'utf-8' });
const parser = new PgnParser((game, err) => {
if (err) {
console.error('Budget exceeded:', err.message);
stream.destroy(err);
return;
}
// Process game
console.log(game.headers.get('White'), 'vs', game.headers.get('Black'));
});
await new Promise<void>(resolve =>
stream
.on('data', (chunk: string) => parser.parse(chunk, { stream: true }))
.on('close', () => {
parser.parse(''); // Flush remaining data
resolve();
})
);
emitGame
(game: Game<PgnNodeData>, err: PgnError | undefined) => void
required
Callback invoked for each parsed game or error
Optional function to initialize headers. Defaults to defaultHeaders()
Maximum complexity budget per game (default: 1,000,000)
parse
Parses a chunk of PGN data.
parser.parse(chunk: string, options?: ParseOptions): void
The chunk of PGN data to parse
stream?: boolean - If true, waits for more data before emitting final game
PgnError
Error class for PGN parsing failures.
class PgnError extends Error {}
Generation Functions
makePgn
Generates a PGN string from a game.
import { makePgn, defaultGame } from 'chessops/pgn';
const game = defaultGame();
game.headers.set('White', 'Carlsen, Magnus');
game.headers.set('Black', 'Nepomniachtchi, Ian');
const move1 = new ChildNode({ san: 'e4' });
const move2 = new ChildNode({ san: 'e5' });
move1.children.push(move2);
game.moves.children.push(move1);
const pgn = makePgn(game);
console.log(pgn);
game
Game<PgnNodeData>
required
The game to convert to PGN. Each node must have at least a san property.
The PGN string representation
Game Tree Functions
Transforms a game tree by applying a function to each node.
import { transform } from 'chessops/pgn';
import { parseSan, makeSanAndPlay } from 'chessops/san';
import { makeFen } from 'chessops/fen';
import { Box } from 'chessops/pgn';
const pos = startingPosition(game.headers).unwrap();
game.moves = transform(game.moves, new Box(pos), (box, node) => {
const move = parseSan(box.value, node.san);
if (!move) {
// Invalid move - cut off tree here
return;
}
const san = makeSanAndPlay(box.value, move);
return {
...node,
san, // Normalized SAN
fen: makeFen(box.value.toSetup()), // Add FEN annotation
};
});
The root node to transform
ctx
C extends { clone(): C }
required
Context object that will be cloned at variations. Use Box<T> for simple contexts.
f
(ctx: C, data: T, childIndex: number) => U | undefined
required
Transformation function. Return undefined to cut off the tree at this point.
The transformed game tree
walk
Walks through a game tree, visiting each node.
import { walk, Box } from 'chessops/pgn';
import { Chess } from 'chessops/chess';
import { parseSan } from 'chessops/san';
const pos = Chess.default();
walk(game.moves, new Box(pos), (box, node) => {
const move = parseSan(box.value, node.san);
if (!move) {
console.log('Illegal move:', node.san);
return false; // Stop walking this variation
}
box.value.play(move);
console.log(node.san, makeFen(box.value.toSetup()));
return true; // Continue walking
});
ctx
C extends { clone(): C }
required
Context object that will be cloned at variations
f
(ctx: C, data: T, childIndex: number) => boolean | void
required
Visitor function. Return false to stop walking this branch.
extend
Extends a node by appending moves to the mainline.
import { extend, ChildNode } from 'chessops/pgn';
const game = defaultGame();
const moves = [
{ san: 'e4' },
{ san: 'e5' },
{ san: 'Nf3' },
];
const endNode = extend(game.moves, moves);
console.log(endNode instanceof ChildNode); // true
Array of data to append as moves
The last node in the extended chain
isChildNode
Type guard to check if a node is a ChildNode (has data).
import { isChildNode } from 'chessops/pgn';
if (isChildNode(node)) {
console.log(node.data.san); // TypeScript knows node has data
}
True if the node is a ChildNode
Variant Functions
parseVariant
Parses a variant name into a Rules type.
import { parseVariant } from 'chessops/pgn';
console.log(parseVariant('Standard')); // 'chess'
console.log(parseVariant('Crazyhouse')); // 'crazyhouse'
console.log(parseVariant('King of the Hill')); // 'kingofthehill'
console.log(parseVariant('Unknown')); // undefined
variant
string | undefined
required
The variant name to parse
The parsed Rules, or undefined if unrecognized
makeVariant
Converts a Rules type to a variant name.
import { makeVariant } from 'chessops/pgn';
console.log(makeVariant('chess')); // undefined (standard chess, no variant header needed)
console.log(makeVariant('crazyhouse')); // 'Crazyhouse'
console.log(makeVariant('kingofthehill')); // 'King of the Hill'
The variant name, or undefined for standard chess
startingPosition
Creates a position from PGN headers.
import { startingPosition } from 'chessops/pgn';
const headers = new Map([
['Variant', 'Crazyhouse'],
['FEN', 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR[] w KQkq - 0 1'],
]);
const result = startingPosition(headers);
if (result.isOk) {
const pos = result.value;
console.log(pos.rules); // 'crazyhouse'
}
Game headers (checks Variant and FEN headers)
return
Result<Position, FenError | PositionError>
The starting position, or an error
setStartingPosition
Sets the variant and FEN headers based on a position.
import { setStartingPosition } from 'chessops/pgn';
import { Crazyhouse } from 'chessops/variant';
const headers = new Map();
const pos = Crazyhouse.default();
setStartingPosition(headers, pos);
console.log(headers.get('Variant')); // 'Crazyhouse'
console.log(headers.has('FEN')); // false (default position)
Parsed comment with embedded annotations.
interface Comment {
text: string;
shapes: CommentShape[];
clock?: number;
emt?: number;
evaluation?: Evaluation;
}
Arrow or circle annotation.
interface CommentShape {
color: 'green' | 'red' | 'yellow' | 'blue';
from: Square;
to: Square; // Same as from for circles
}
Parses a comment string, extracting annotations.
import { parseComment } from 'chessops/pgn';
const comment = parseComment(
'Great move! [%csl Gf3] [%cal Ra1h8] [%eval +1.5,18] [%clk 1:30:00]'
);
console.log(comment.text); // 'Great move!'
console.log(comment.shapes.length); // 2 (circle on f3, arrow a1-h8)
console.log(comment.evaluation); // { pawns: 1.5, depth: 18 }
console.log(comment.clock); // 5400 (seconds)
The comment string to parse
Parsed comment with extracted annotations
Generates a comment string with annotations.
import { makeComment } from 'chessops/pgn';
const comment = makeComment({
text: 'Brilliant move',
shapes: [
{ color: 'green', from: 21, to: 21 }, // Circle on f3
{ color: 'red', from: 0, to: 63 }, // Arrow a1 to h8
],
evaluation: { pawns: 2.5, depth: 20 },
clock: 1800, // 30 minutes
});
console.log(comment);
// 'Brilliant move [%csl Gf3] [%cal Ra1h8] [%eval 2.50,20] [%clk 0:30:00.000]'
The comment string with embedded annotations
Helper Functions
defaultGame
Creates a new game with default headers.
import { defaultGame } from 'chessops/pgn';
const game = defaultGame();
console.log(game.headers.get('Event')); // '?'
console.log(game.headers.get('Result')); // '*'
Creates default PGN headers.
import { defaultHeaders } from 'chessops/pgn';
const headers = defaultHeaders();
// Returns Map with: Event, Site, Date, Round, White, Black, Result
Creates an empty headers map.
import { emptyHeaders } from 'chessops/pgn';
const headers = emptyHeaders();
console.log(headers.size); // 0
makeOutcome
Converts an Outcome to PGN result notation.
import { makeOutcome } from 'chessops/pgn';
console.log(makeOutcome({ winner: 'white' })); // '1-0'
console.log(makeOutcome({ winner: 'black' })); // '0-1'
console.log(makeOutcome({ winner: undefined })); // '1/2-1/2'
console.log(makeOutcome(undefined)); // '*'
parseOutcome
Parses a PGN result string.
import { parseOutcome } from 'chessops/pgn';
console.log(parseOutcome('1-0')); // { winner: 'white' }
console.log(parseOutcome('0-1')); // { winner: 'black' }
console.log(parseOutcome('1/2-1/2')); // { winner: undefined }
console.log(parseOutcome('*')); // undefined
Box
Simple wrapper for cloneable context in transform/walk.
import { Box } from 'chessops/pgn';
const box = new Box(someValue);
const cloned = box.clone(); // Creates shallow copy
Usage Examples
Parsing and Validating a Game
import { parsePgn, startingPosition } from 'chessops/pgn';
import { parseSan } from 'chessops/san';
const games = parsePgn(pgnString);
for (const game of games) {
const pos = startingPosition(game.headers).unwrap();
for (const node of game.moves.mainline()) {
const move = parseSan(pos, node.san);
if (!move) {
console.log('Illegal move:', node.san);
break;
}
pos.play(move);
}
console.log('Final position:', makeFen(pos.toSetup()));
}
Adding Computer Analysis
import { transform, Box, makeComment } from 'chessops/pgn';
import { parseSan, makeSanAndPlay } from 'chessops/san';
// Add engine evaluation to each move
game.moves = transform(game.moves, new Box(pos), (box, node) => {
const move = parseSan(box.value, node.san);
if (!move) return;
makeSanAndPlay(box.value, move);
// Simulate engine evaluation
const evaluation = evaluatePosition(box.value);
const comment = makeComment({
text: node.comments?.[0] || '',
evaluation,
});
return {
...node,
comments: [comment],
};
});