Chessalyzer

Parsing PGN files

Turn PGN files into plain JavaScript objects — no analysis attached.

Sometimes you don't want statistics at all — you just want the games. The chessalyzer/pgn module reads a PGN file and hands you structured data: the tag-pair headers, the moves, the result. No board replay, no trackers, no setup.

Both examples below run on the three-game games.pgn from the quickstart.

parsePGN — all games at once

import { parsePGN } from 'chessalyzer/pgn';

const games = await parsePGN('games.pgn', { headers: true });

console.log(games.length); // 3
console.log(games[0].headers?.White); // 'alice'
console.log(games[0].moves[0].san); // 'e4'

You get back an array of ParsedGame — one object per game, in file order. This is the real thing for our example file (games two and three trimmed for brevity, they look the same):

[
    {
        moves: [
            { san: 'e4' },
            { san: 'e5' },
            { san: 'Bc4' },
            { san: 'Nc6' },
            { san: 'Qh5' },
            { san: 'Nf6' },
            { san: 'Qxf7' },
        ],
        result: '1-0',
        headers: {
            Event: 'Casual Blitz game',
            Site: 'https://lichess.org/a1b2c3d4',
            Date: '2024.05.12',
            White: 'alice',
            Black: 'blunderbuss',
            Result: '1-0',
            WhiteElo: '2114',
            BlackElo: '1892',
            ECO: 'C23',
        },
    },
    // …two more games
]

A few things worth knowing about this shape:

  • moves holds mainline SAN strings, one { san } object per half-move. Check and mate suffixes are normalized away — the file's Qxf7# comes out as "Qxf7".
  • result is '1-0', '0-1', '1/2-1/2', or '*' (unfinished).
  • headers is only present with headers: true. Without it you still get result (from the movetext), just not the tag pairs:
[{ moves: [{ san: 'e4' }, { san: 'e5' } /* … */], result: '1-0' }]

streamParsePGN — one game at a time

Same data, delivered as an async iterator. The whole file never sits in memory at once:

import { streamParsePGN } from 'chessalyzer/pgn';

for await (const game of streamParsePGN('games.pgn', { headers: true })) {
    console.log(
        game.headers?.White,
        'vs',
        game.headers?.Black,
        '—',
        game.moves.length,
        'half-moves',
    );
}
alice vs blunderbuss — 7 half-moves
blunderbuss vs cleopatra — 4 half-moves
alice vs cleopatra — 8 half-moves

Which one should I pick?

Rule of thumb: if the file fits comfortably in memory and you want random access (games[42]), take parsePGN. If the file is huge — database dumps can be gigabytes — or you process each game and then forget it, take streamParsePGN.

Options

OptionWhat it doesDefault
headersParse tag pairs (White, BlackElo, ECO, …) into headersfalse
maxGamesStop after this many gamesno limit

Parser or analyzer?

parsePGN / streamParsePGNanalyzePGN
What you getThe games themselvesStatistics via trackers
Moves replayed on a boardNoYes, when your trackers need it
Typical useInspect, convert, or export game dataAnswer questions about many games

If you find yourself writing for loops that count things over parsed games, that's the smell — trackers do exactly that, in parallel, and How Chessalyzer works explains what else the full pipeline buys you.

On this page