Parsing a Simple PGN
Parse PGN text into structured game objects.import { parsePgn } from 'chessops/pgn';
const pgnText = `
[Event "Casual Game"]
[White "Alice"]
[Black "Bob"]
[Result "1-0"]
1. e4 e5 2. Nf3 Nc6 3. Bc4 Bc5 4. b4 Bxb4 5. c3 Ba5 6. d4 exd4 7. O-O d3 8. Qb3 Qf6 9. e5 Qg6 10. Re1 Nge7 11. Ba3 b5 12. Qxb5 Rb8 13. Qa4 Bb6 14. Nbd2 Bb7 15. Ne4 Qf5 16. Bxd3 Qh5 17. Nf6+ gxf6 18. exf6 Rg8 19. Rad1 Qxf3 20. Rxe7+ Nxe7 21. Qxd7+ Kxd7 22. Bf5+ Ke8 23. Bd7+ Kf8 24. Bxe7# 1-0
`;
const games = parsePgn(pgnText);
console.log(`Parsed ${games.length} game(s)`);
const game = games[0];
console.log('Event:', game.headers.get('Event'));
console.log('White:', game.headers.get('White'));
console.log('Black:', game.headers.get('Black'));
console.log('Result:', game.headers.get('Result'));
Walking Through Moves
Traverse the move tree and replay the game.import { parsePgn, startingPosition, walk } from 'chessops/pgn';
import { parseSan } from 'chessops/san';
import { makeFen } from 'chessops/fen';
const pgn = '1. e4 e5 2. Nf3 Nc6 3. Bb5 a6';
const game = parsePgn(pgn)[0];
// Get starting position
const pos = startingPosition(game.headers).unwrap();
// Walk through all moves
walk(game.moves, pos, (position, node) => {
const move = parseSan(position, node.san);
if (!move) {
console.error(`Invalid move: ${node.san}`);
return false; // Stop walking
}
position.play(move);
console.log(`${node.san}: ${makeFen(position.toSetup())}`);
return true; // Continue walking
});
Working with Headers
Access and manipulate PGN headers.import { parsePgn, emptyHeaders } from 'chessops/pgn';
const pgn = `
[Event "World Championship"]
[Site "London"]
[Date "2023.04.15"]
[Round "1"]
[White "Carlsen, Magnus"]
[Black "Nepomniachtchi, Ian"]
[Result "1-0"]
[ECO "C84"]
[WhiteElo "2865"]
[BlackElo "2795"]
1. e4 e5 2. Nf3 Nc6 1-0
`;
const game = parsePgn(pgn)[0];
// Access standard headers
console.log('Event:', game.headers.get('Event'));
console.log('Date:', game.headers.get('Date'));
console.log('White ELO:', game.headers.get('WhiteElo'));
console.log('Black ELO:', game.headers.get('BlackElo'));
console.log('Opening:', game.headers.get('ECO'));
// Iterate through all headers
for (const [key, value] of game.headers) {
console.log(`${key}: ${value}`);
}
// Create custom headers
const customHeaders = emptyHeaders();
customHeaders.set('Event', 'My Tournament');
customHeaders.set('White', 'Player 1');
customHeaders.set('Black', 'Player 2');
Streaming Large PGN Files
Efficiently parse large PGN databases without loading everything into memory.import { createReadStream } from 'fs';
import { PgnParser } from 'chessops/pgn';
import { startingPosition, walk } from 'chessops/pgn';
import { parseSan } from 'chessops/san';
const status = {
games: 0,
errors: 0,
moves: 0,
};
const stream = createReadStream('games.pgn', { encoding: 'utf-8' });
const parser = new PgnParser((game, err) => {
status.games++;
if (err) {
console.error('Parse error:', err);
status.errors++;
return;
}
// Process each game as it's parsed
startingPosition(game.headers).unwrap(
pos => {
walk(game.moves, pos, (position, node) => {
const move = parseSan(position, node.san);
if (!move) {
console.error(`Invalid move in game ${status.games}:`, node.san);
status.errors++;
return false;
}
position.play(move);
status.moves++;
return true;
});
},
err => {
console.error('Invalid starting position:', err);
status.errors++;
}
);
// Progress indicator
if (status.games % 1000 === 0) {
console.log(`Processed ${status.games} games, ${status.moves} moves`);
}
});
await new Promise<void>(resolve => {
stream
.on('data', (chunk: string) => {
parser.parse(chunk, { stream: true });
})
.on('close', () => {
parser.parse(''); // Flush remaining data
console.log('Final stats:', status);
resolve();
});
});
Working with Variations
Handle games with alternative move sequences.import { parsePgn } from 'chessops/pgn';
const pgnWithVariations = `
1. e4 e5 2. Nf3 Nc6 3. Bb5 a6
(3... Nf6 4. O-O Nxe4 5. d4 Be7)
4. Ba4 Nf6 5. O-O Be7
(5... Nxe4 6. d4 b5 7. Bb3 d5 8. dxe5 Be6)
6. Re1 b5 7. Bb3 d6 8. c3 O-O
`;
const game = parsePgn(pgnWithVariations)[0];
function printTree(node: any, depth = 0) {
const indent = ' '.repeat(depth);
for (const child of node.children) {
console.log(`${indent}${child.san}`);
// Print all variations
if (child.children.length > 1) {
console.log(`${indent}(Variations:)`);
for (let i = 1; i < child.children.length; i++) {
console.log(`${indent} Variation ${i}:`);
printTree({ children: [child.children[i]] }, depth + 2);
}
// Continue with main line
if (child.children.length > 0) {
printTree({ children: [child.children[0]] }, depth);
}
return;
}
printTree(child, depth);
}
}
printTree(game.moves);
Parsing Comments and Annotations
Extract comments, evaluations, clocks, and drawing annotations.import { parsePgn, parseComment } from 'chessops/pgn';
const pgnWithComments = `
1. e4 { [%eval 0.3] [%clk 1:30:00] } e5
2. Nf3 { A classical opening. [%cal Gf3e5] [%csl Ge5] } Nc6
3. Bb5 { [%eval 0.25,18] The Ruy Lopez! } a6
`;
const game = parsePgn(pgnWithComments)[0];
for (const node of game.moves.mainline()) {
console.log(`Move: ${node.san}`);
if (node.comments) {
for (const comment of node.comments) {
const parsed = parseComment(comment);
if (parsed.text) {
console.log(` Comment: ${parsed.text}`);
}
if (parsed.evaluation) {
if ('pawns' in parsed.evaluation) {
console.log(` Evaluation: ${parsed.evaluation.pawns} pawns`);
if (parsed.evaluation.depth) {
console.log(` (depth ${parsed.evaluation.depth})`);
}
} else if ('mate' in parsed.evaluation) {
console.log(` Mate in: ${parsed.evaluation.mate}`);
}
}
if (parsed.clock) {
console.log(` Clock: ${parsed.clock}s`);
}
if (parsed.emt) {
console.log(` Time spent: ${parsed.emt}s`);
}
if (parsed.shapes && parsed.shapes.length > 0) {
console.log(` Shapes: ${parsed.shapes.length} annotations`);
}
}
}
}
Creating PGN from Games
Generate PGN text from game objects.import { makePgn, emptyHeaders, Node, ChildNode } from 'chessops/pgn';
// Build a game tree
const root = new Node();
const e4 = new ChildNode({ san: 'e4' });
const e5 = new ChildNode({ san: 'e5' });
const nf3 = new ChildNode({ san: 'Nf3' });
const nc6 = new ChildNode({ san: 'Nc6' });
root.children.push(e4);
e4.children.push(e5);
e5.children.push(nf3);
nf3.children.push(nc6);
// Add a variation
const nf6 = new ChildNode({
san: 'Nf6',
comments: ['Petrov Defense']
});
e5.children.push(nf6); // Alternative to Nf3
// Create headers
const headers = emptyHeaders();
headers.set('Event', 'Example Game');
headers.set('White', 'Alice');
headers.set('Black', 'Bob');
headers.set('Result', '*');
// Generate PGN
const pgn = makePgn({ headers, moves: root });
console.log(pgn);
// Output:
// [Event "Example Game"]
// [White "Alice"]
// [Black "Bob"]
// [Result "*"]
//
// 1. e4 e5 2. Nf3 ( 2... Nf6 { Petrov Defense } ) 2... Nc6 *
Transforming Game Trees
Augment game tree nodes with custom data (e.g., positions, evaluations).import { parsePgn, startingPosition, transform } from 'chessops/pgn';
import { parseSan } from 'chessops/san';
import { makeFen } from 'chessops/fen';
interface ExtendedNodeData {
san: string;
fen: string;
legalMoves?: number;
}
const pgn = '1. e4 e5 2. Nf3 (2. f4 exf4) 2... Nc6';
const game = parsePgn(pgn)[0];
const transformedTree = transform(
game.moves,
startingPosition(game.headers).unwrap(),
(pos, data) => {
const move = parseSan(pos, data.san);
if (!move) return;
pos.play(move);
// Count legal moves in resulting position
let legalMoves = 0;
for (const [_from, dests] of pos.allDests()) {
legalMoves += dests.size();
}
return {
san: data.san,
fen: makeFen(pos.toSetup()),
legalMoves,
};
}
);
// Walk the transformed tree
for (const node of transformedTree.mainline()) {
console.log(`${node.data.san}:`);
console.log(` FEN: ${node.data.fen}`);
console.log(` Legal moves: ${node.data.legalMoves}`);
}
Handling NAGs (Numeric Annotation Glyphs)
Work with move annotations like !!, ?, !?, etc.import { parsePgn } from 'chessops/pgn';
const pgnWithNags = `
1. e4! e5 2. Nf3!! Nc6?! 3. Bb5? a6! 4. Ba4?? Nf6
`;
const game = parsePgn(pgnWithNags)[0];
const nagDescriptions: Record<number, string> = {
1: 'good move (!)',
2: 'poor move (?)',
3: 'brilliant move (!!)',
4: 'blunder (??)',
5: 'interesting move (!?)',
6: 'dubious move (?!)',
};
for (const node of game.moves.mainline()) {
let annotation = '';
if (node.nags && node.nags.length > 0) {
const nag = node.nags[0];
annotation = ` ${nagDescriptions[nag] || `NAG ${nag}`}`;
}
console.log(`${node.san}${annotation}`);
}
Next Steps
Basic Usage
Learn the fundamentals of position creation and moves
Position Analysis
Analyze positions for checks, checkmate, and material