0.5.0 • Published 1 year ago
pgn.js v0.5.0
pgn.js
Chess PGN, Portable Game Notation, Javascript Library
Why Another?
- Multiple Games Parsing and Generation
- Commments and Variations Handling
- Sync for CLI use, Async for large file handling with callbacks
- Single dependency chess.js, Exported as well
Simple Parsing
import { Pgn } from 'pgn.js';
const pgn = new Pgn(
`[Event "Sample"]
[Site "Toronto"]
[Date "2022.11.17"]
{This is a sample game for Pgn.js}
1.d4 (1.e4 c5 (1...e5 Bc4) (1...e6)) d5 {move comment}
2.c4! +- (2.Nf3 {variation move comment} Nf6)
(2. {comment before a move} Bf4) 2...c6 Nf3`);
console.log(pgn.pgn());
Output
[Event "Sample"]
[Site "Toronto"]
[Date "2022.11.17"]
{ This is a sample game for Pgn.js }
1.d4
( 1.e4 c5
( 1...e5 2.Bc4 )
( 1...e6 )
)
1...d5 { move comment } 2.c4! +-
( 2.Nf3 { variation move comment } 2...Nf6 )
( 2. { comment before a move } Bf4 )
2...c6 3.Nf3 *
Composing a Game
import { Pgn } from 'pgn.js';
const pgn = new Pgn();
let game = pgn.newgame();
game.setTag('Event', 'Sample');
game.setFen('rn3rk1/ppp1b1pp/1n2p3/4N2Q/3qNR2/8/PPP3PP/R1B4K b - - 0 13');
// add a move
let mvQxe4 = game.add('Qxe4');
// 13...Qxe4
// add a move next to the previous one
game.add('Rxf8');
// 13...Qxe4 14.Rxf8+
// add a varation to the next to 'Qxe4'
game.add('Nd3', mvQxe4);
// 13...Qxe4 14.Rxf8+
// ( 14.Nd3 )
// add 2 variations at the beginning
// null == the begin of the game
let mvN8d7 = game.add('N8d7', null);
game.add('Nc6', null);
// 13...Qxe4
// ( 13...N8d7 )
// ( 13...Nc6 )
// 14.Rxf8+
// ( 14.Nd3 )
// add moves next to 'N8d7'
game.add('Nxd7', mvN8d7);
game.add('Qxe4'); // undefined == next to the previous
// 13...Qxe4
// ( 13...N8d7 14.Nxd7 Qxe4 )
// ( 13...Nc6 )
// 14.Rxf8+
// ( 14.Nd3 )
// add a variation next to' N8d7'
game.add('Nf7', mvN8d7);
// 13...Qxe4
// ( 13...N8d7 14.Nxd7
// ( 14.Nf7 )
// 14...Qxe4 )
// ( 13...Nc6 )
// 14.Rxf8+
// ( 14.Nd3 )
// add moves to the previous
game.add('Qd5');
game.add('Nh6');
console.log(game.pgn());
Output
[Event "Sample"]
[FEN "rn3rk1/ppp1b1pp/1n2p3/4N2Q/3qNR2/8/PPP3PP/R1B4K b - - 0 13"]
[SetUp "1"]
13...Qxe4
( 13...N8d7 14.Nxd7
( 14.Nf7 Qd5 15.Nh6+ )
14...Qxe4 )
( 13...Nc6 )
14.Rxf8+
( 14.Nd3 )
*
Add a Move
Game.add(san: string, prev?: Move | null, varmode?: VAR): Move | null;
// prev
// if 'undefined', next to the previously added move
// if 'null', as the first move
// otherwise, next to 'prev' move
//
// varmode
// if 'undefined' or 'null', Game.var, default==VAR.last
// VAR.last, as the last variation
// VAR.next, as the next variation
// VAR.main, as the main line
// VAR.replace, delete existing, no variation
Examples
// add a move
let mvQxe4 = game.add('Qxe4');
// 13...Qxe4
// add a move next to the previous move 'Qxe4'
game.add('Rxf8');
// 13...Qxe4 14.Rxf8+
game.var == VAR.last by default
game.add('Nd3', mvQxe4);
game.add('Bd2', mvQxe4);
game.add('Be3', mvQxe4);
// 13...Qxe4 14.Rxf8+
// ( 14.Nd3 )
// ( 14.Bd2 )
// ( 14.Be3 )
VAR.next, as the next variation
game.add('Nd3', mvQxe4, VAR.next);
game.add('Bd2', mvQxe4, VAR.next);
game.add('Be3', mvQxe4, VAR.next);
// 13...Qxe4 14.Rxf8+
// ( 14.Be3 )
// ( 14.Bd2 )
// ( 14.Nd3 )
VAR.main, as the mainline
game.add('Nd3', mvQxe4, VAR.main);
game.add('Bd2', mvQxe4, VAR.main);
game.add('Be3', mvQxe4, VAR.main);
// 13...Qxe4 14.Be3
// ( 14.Bd2 )
// ( 14.Nd3 )
// ( 14.Rxf8+ )
VAR.replace, no variation
game.add('Nd3', mvQxe4, VAR.replace);
game.add('Bd2', mvQxe4, VAR.replace);
game.add('Be3', mvQxe4, VAR.replace);
// 13...Qxe4 14.Be3
change default var mode
game.var = VAR.main;
game.add('Nd3', mvQxe4);
game.add('Bd2', mvQxe4);
game.add('Be3', mvQxe4);
// 13...Qxe4 14.Be3
// ( 14.Bd2 )
// ( 14.Nd3 )
// ( 14.Rxf8+ )
Async, big file handling
Without loading the whole file, parsing games as reading the input file.
async Pgn.load(path: string, opts?: Option): Pgn;
// stdin if path == ''
opts.onGame: (game: Game, error: Error) => void
opts.onFinish: () => void
Example
In this example, we write a parsed game to the output file as soon as it's parsed.
import { createWriteStream } from 'node:fs';
import { Pgn } from 'pgn.js';
var writer = createWriteStream('output.pgn');
let ngames = 0;
Pgn.load('./pgn/big.pgn', {onGame: (game) => {
ngames++;
writer.write(game.pgn() + '\n');
console.log(`${ngames} parsed`);
}});
Error Handling
Type
type Err = {
msg?: string | undefined;
fen?: string | undefined;
san?: string | undefined;
num?: number | undefined;
movetext?: string | undefined;
/**
* unexpected token
*/
found?: string | undefined;
location?: any | undefined;
start: {
offset: number;
line: number;
column: number;
};
end: {
offset: number;
line: number;
column: number;
};
};
Example
import { createWriteStream } from 'node:fs';
import { Pgn } from 'pgn.js';
var writer = createWriteStream('output.pgn');
let ngames = 0;
Pgn.load('./pgn/big2.pgn', {onGame: (game, err) => {
ngames++;
if(err) {
game.tags.push({name: 'pgn.js', value: 'parsing error encountered'});
if(err.location) {
console.log('found:', err.found, ' at ', err.location, ' with movetext: ', err.movetext);
} else {
console.log(err);
}
}
writer.write(game.pgn() + '\n');
console.log(`${ngames} parsed ${err?'- error!':''}`);
}});
Types
Options
type OnGame = (game: Game, error: Err) => void;
type OnFinish = () => void;
type Options = {
/**
* print error message
*/
verbose?: boolean | undefined;
/**
* called when a game is parsed
*/
onGame: OnGame;
onFinish: OnFinish;
};
Move
type Move = {
/**
* 'w', 'b'
*/
color: string;
/**
* square
*/
from: string;
/**
* square
*/
to: string;
/**
* '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
*/
flags: string;
/**
* p, b, n, r, q, k
*/
piece: string;
/**
* SAN notation
*/
san: string;
captured?: string | undefined;
/**
* extended by pgn.js
*/
promotion?: string | undefined;
/**
* move number, not ply
*/
num: number;
/**
* position fen
*/
fen: string;
/**
* uci long algerbraic notation
*/
uci: string;
comment: {
pre?: string | undefined;
before?: string | undefined;
after?: string | undefined;
};
nags: string[];
/**
* variations (RAV)
*/
vars: Move[][];
over: {
mate?: boolean | undefined;
draw?: string | undefined;
};
ply: number;
/**
* the line contaning this move
*/
line: Move[];
/**
* previous move
*/
prev: Move;
};
Tag
type Tag = {
name: string;
valuee: string;
};
Pgn
Data
pgn.games = Game[];
Functions
Pgn(pgn: string, opts: Options);
pgn.count() : Number // # of games
pgn.game(idx: Number): Game // access a game
pgn.newgame(): Game // create a new game
pgn.pgn(): void; // pgn string
static async Pgn.load(path: string, opts: Options): Pgn
// load from file, stdin if path == ''
Game
Data
game.tags: Tag[];
game.moves: Move[];
game.gtm : string; // game termintion mark
Functions
game.setupFen(): string | undefined // setup fen string or undefined if none
game.setFen(fen: string): void; // set setup fen string (and SetUp)
game.delFen(): void; // delete setup fen string (and SetUp)
game.setTag(name: string, value: string): void; // set a tag
game.delTag(_name: any): void; // delete a tag
game.add(san: string, prev?: Move | undefined, varmode?: {
replace: string;
main: string;
next: string;
last: string;
} | undefined): Move;
game.pgn(): void; // pgn string