pgn.js
Version:
Chess PGN, Portable Game Notation, Javascript Library
449 lines (382 loc) • 9.99 kB
JavaScript
import { Chess } from 'chess.js';
import { Util } from './util.js';
export const VAR = {
replace: 'replace', // replace old move with new one, no variation
main: 'main', // becomes the mainline
next: 'next', // added as the next variation
last: 'last' // (default) added as the last variation
}
// some essential tags placed at the beginning
/**
* @private
*/
const STDTAGS = [
// Standard "Seven Tag Roster"
"Event",
"Site",
"Date",
"Round",
"White",
"Black",
"Result",
"Steup",
"FEN",
];
/**
* @private
*/
const QUICKNAG = {
'$1': '!',
'$2': '?',
'$3': '!!',
'$4': '??',
'$5': '!?',
'$6': '?!',
'!': '!',
'?': '?',
'!!': '!!',
'??': '??',
'!?': '!?',
'?!': '?!',
};
/**
* @private
*/
const NAGSTR = {
'$18': '+-',
'$19': '-+',
};
export class Game {
constructor() {
/** @type {Tag[]} */
this.tags = [];
/** @type {Move[]} */
this.moves = [];
/** @type {string} */
this.gtm = null; // game termination mark
/** @type {Move} */
this.prev = null; // successfully added previous move
/** @type {VAR} */
this.var = VAR.last; // default variation mode
}
/**
* get setup fen if there is
* @return {string | undefined} fen
*/
setupFen() {
return this.tags.find(tag => tag.name.toUpperCase() == 'FEN')?.value;
}
/**
* set fen tag with setup tag
* @param {string} fen
* @return {void}
*/
setFen(fen) {
this.delTag('FEN');
this.delTag('SetUp');
this.tags.push({name: 'FEN', value: fen});
this.tags.push({name: 'SetUp', value: '1'});
}
/**
* delete fen and setup tag
* @return {void}
*/
delFen() {
this.delTag('FEN');
this.delTag('Setup');
}
/**
* set a tag
* @param {string} name
* @param {string} value
* @return {void}
*/
setTag(name, value) {
this.delTag(name);
this.tags.push({name: name, value: value});
}
/**
* delete a tag, name is case insensitive
* @param {string} name
* @return {void}
*/
delTag(name) {
const i = this.tags.findIndex(tag => {return tag.name.toUpperCase()==name.toUpperCase();});
if(i>=0)
this.tags.splice(i,1);
}
/**
* return pgn string of the game
* @return {void}
*/
pgn() {
let ctx = { out: '', line: '', _indent: 0,
at$: function() { return (this.line.length<=this._indent); },
nl: function() {
if(ctx.line!=' '.repeat(ctx._indent)) {
ctx.out += ctx.line + '\n';
ctx.line = ' '.repeat(ctx._indent);
}
},
indent: function() {
if(ctx.line!=' '.repeat(ctx._indent))
ctx.out += ctx.line + '\n';
ctx._indent += 2;
ctx.line = ' '.repeat(ctx._indent);
},
unindent: function() {
ctx.out += ctx.line + '\n';
ctx._indent -= 2;
ctx.line = ' '.repeat(ctx._indent);
},
add: function(delim, s) {
if(!this.at$())
ctx.line += delim;
ctx.line += s;
if(ctx.line.length > 75)
nl();
},
};
STDTAGS.forEach(stdtag => {
let value = this.tags.find(item => {item.name == stdtag})?.value;
if(value)
ctx.out += `[${stdtag} "${value}"]\n`
});
this.tags.forEach(tag => {
if(!STDTAGS.find(item => {item == tag.name}))
ctx.out += `[${tag.name} "${tag.value}"]\n`;
});
if(this.tags.length==0) {
// default tag
ctx.out += '[Event "?"]\n';
}
ctx.out += '\n';
this._moves_to_pgn(ctx, this.moves);
// game termination mark
let endmark = this.gtm;
if(!endmark) {
endmark = '*';
let last_move = this.moves.length?this.moves[this.moves.length-1]:null;
if(last_move?.over) {
if(last_move.over.mate) {
endmark = last_move.color=='w'?'1-0':'0-1';
}
else {
endmark = '1/2-1/2';
}
}
}
ctx.add(' ', endmark);
ctx.nl();
return ctx.out;
}
/**
* @private
*/
_moves_to_pgn(ctx, moves) {
let firstmove = true;
let white_had_comment = false;
let white_had_variation = false;
moves.forEach(move => {
if(move.comment&&move.comment.pre) {
ctx.nl();
ctx.add('', `{ ${move.comment.pre} }`);
ctx.nl();
}
if(move.color=='w' || firstmove ||
(move.color=='b'&&move.comment&&(move.comment.pre||move.comment.before))||
(move.color=='b'&&white_had_comment) ||
(move.color=='b'&&white_had_variation)) {
ctx.add(' ', (move.color=='w'?`${move.num}.`:`${move.num}...`));
if(move.comment&&move.comment.before) {
ctx.add(' ', `{ ${move.comment.before} } `);
}
}
else {
if(move.comment&&move.comment.before) {
ctx.add(' ', `{ ${move.comment.before} }`);
}
ctx.add(' ', '');
}
ctx.add('', move.san);
if(move.nags) {
move.nags.forEach((nag,idx) =>{
if(QUICKNAG[nag]) {
ctx.add(idx>0?' ':'', `${QUICKNAG[nag]}`);
}
else {
ctx.add(' ', `${NAGSTR[nag]?NAGSTR[nag]:nag}`);
}
});
}
if(move.comment&&move.comment.after) {
ctx.add(' ', `{ ${move.comment.after} }`);
}
if(move.vars.length) {
move.vars.forEach(rav => {
ctx.indent();
ctx.line += '(';
this._moves_to_pgn(ctx, rav);
ctx.add(' ', ')');
ctx.unindent();
});
}
white_had_comment = (move.color=='w'&&move.comment&&move.comment.after);
white_had_variation = (move.color=='w'&&move.vars?.length>0);
firstmove = false;
});
}
/**
* add a move, this.prev will be updated if successful
* @param {string} san // short algebraic notation
* @param {Move=} prev // add after this move
* // if undefined, next to the prevously added move
* // if null, as the game's first move
* @param {VAR=} varmode // variation mode, 'this.var' if null
*/
add(san, prev, varmode) {
let prev_move = prev===undefined?this.prev:prev;
let line = prev_move?prev_move.line:this.moves;
let iprev = prev_move?line.indexOf(prev_move):-1;
let oldmove = (iprev+1)<line.length?line[iprev+1]:null;
let move;
if(oldmove) {
let vm = varmode?varmode:this.var;
// variation
switch(vm) {
case VAR.replace: move = this._add_replace(san, prev_move); break;
case VAR.main: move = this._add_mainvar(san, prev_move); break;
case VAR.next: move = this._add_nextvar(san, prev_move); break;
default:
case VAR.last: move = this._add_lastvar(san, prev_move); break;
}
}
else {
// append to the line
move = this._add_to_line(san, line);
}
if(move)
this.prev = move;
return move;
}
/**
* @private
* @param {string} san
* @param {Move[]} line to add
* @return {Move | null}
*/
_add_to_line(san, line) {
let last_move = line.length?line[line.length-1]:null;
let move = this._make_move(san, last_move);
if(!move)
return null;
// fill extends
move.line = line;
line.push(move);
return move;
}
/**
* as the last variation
* @private
* @param {string} san
* @param {Move | null} prev first move if null
* @return {Move | null}
*/
_add_lastvar(san, prev) {
let line = prev?prev.line:this.moves;
let iprev = prev?line.indexOf(prev):-1;
let oldmove = line[iprev+1];
let move = this._make_move(san, prev);
if(!move)
return null;
if(!oldmove.vars)
oldmove.vars = [];
let rav = [];
oldmove.vars.push(rav);
move.line = rav;
rav.push(move);
return move;
}
/**
* make others as variations
* @private
* @param {string} san
* @param {Move | null} prev
* @return {Move | null}
*/
_add_mainvar(san, prev) {
let line = prev?prev.line:this.moves;
let iprev = prev?line.indexOf(prev):-1;
let move = this._make_move(san, prev);
if(!move)
return null;
let rav = line.splice(iprev+1);
rav.forEach(move => {move.line = rav;});
move.vars=[];
move.vars.push(rav);
// move all varations
while(rav[0].vars?.length)
move.vars.push(rav[0].vars.shift());
move.line = line;
line.push(move);
return move;
}
/**
* as the very next variation
* @private
* @param {string} san
* @param {Move | null} prev first move if null
* @return {Move | null}
*/
_add_nextvar(san, prev) {
let line =prev?prev.line:this.moves;
let iprev = prev?line.indexOf(prev):-1;
let oldmove = line[iprev+1];
let move = this._make_move(san, prev);
if(!move)
return null;
if(!oldmove.vars)
oldmove.vars = [];
let rav = [];
oldmove.vars.unshift(rav);
move.line = rav;
rav.push(move);
return move;
}
/**
* no variation, overwrite
* @private
* @param {string} san
* @param {Move | null} prev first move if null
* @return {Move | null}
*/
_add_replace(san, prev) {
let line = prev?prev.line:this.moves;
let iprev = prev?line.indexOf(prev):-1;
line.splice(iprev+1);
return this.add(san, prev);
}
/**
* @private
* @param {string} san
* @param {Move?} prev first move if null
* @return {Move | null}
*/
_make_move(san, prev) {
let fen = prev?prev.fen:this.setupFen();
const chess = new Chess(fen);
let move = chess.move(san, {sloppy: true});
if(!move)
return null;
// fill extends
move.ply = prev?prev.ply+1:1;
move.prev = prev;
Util.fill_move(move, chess);
if(prev&&prev.num)
move.num = prev.num+(move.color=='w'?1:0);
else {
move.num = Util.move_num_from_fen(fen);
}
return move;
}
};