@mateola/cm-pgn
Version:
Module for parsing and rendering of PGNs (Portable Game Notation)
234 lines (222 loc) • 8.1 kB
JavaScript
/**
* Author and copyright: Stefan Haack (https://shaack.com)
* Repository: https://github.com/shaack/cm-pgn
* License: MIT, see file 'LICENSE'
*/
import {pgnParser} from "./parser/pgnParser.js"
import {Chess} from "chess.js"
function IllegalMoveException(fen, notation) {
this.fen = fen
this.notation = notation
this.toString = function () {
return "IllegalMoveException: " + fen + " => " + notation
}
}
export class History {
constructor(historyString = null, setUpFen = null, sloppy = false) {
if (!historyString) {
this.clear()
} else {
const parsedMoves = pgnParser.parse(historyString
.replace(/\s\s+/g, " ")
.replace(/\n/g, " ")
)
this.moves = this.traverse(parsedMoves[0], setUpFen, null, 1, sloppy)
}
this.setUpFen = setUpFen
}
clear() {
this.moves = []
}
traverse(parsedMoves, fen, parent = null, ply = 1, sloppy = false) {
const chess = fen ? new Chess(fen) : new Chess() // chess.js must be included in HTML
const moves = []
let previousMove = parent
for (let parsedMove of parsedMoves) {
if (parsedMove.notation) {
const notation = parsedMove.notation.notation
const move = chess.move(notation, {sloppy: sloppy})
if (move) {
if (previousMove) {
if (!move.previous) {
move.previous = previousMove
}
if (!previousMove.next) {
previousMove.next = move
}
} else {
move.previous = null
}
move.ply = ply
this.fillMoveFromChessState(move, chess)
if (parsedMove.nag) {
move.nag = parsedMove.nag[0]
}
if (parsedMove.commentBefore) {
move.commentBefore = parsedMove.commentBefore
}
if (parsedMove.commentMove) {
move.commentMove = parsedMove.commentMove
}
if (parsedMove.commentAfter) {
move.commentAfter = parsedMove.commentAfter
}
move.variations = []
const parsedVariations = parsedMove.variations
if (parsedVariations.length > 0) {
const lastFen = moves.length > 0 ? moves[moves.length - 1].fen : fen
for (let parsedVariation of parsedVariations) {
move.variations.push(this.traverse(parsedVariation, lastFen, previousMove, ply, sloppy))
}
}
move.variation = moves
moves.push(move)
previousMove = move
} else {
throw new IllegalMoveException(chess.fen(), notation)
}
}
ply++
}
return moves
}
fillMoveFromChessState(move, chess) {
move.fen = chess.fen()
move.uci = move.from + move.to + (move.promotion ? move.promotion : "")
move.variations = []
if (chess.gameOver()) {
move.gameOver = true
if (chess.isDraw()) {
move.inDraw = true
}
if (chess.isStalemate()) {
move.inStalemate = true
}
if (chess.isInsufficientMaterial()) {
move.insufficientMaterial = true
}
if (chess.isThreefoldRepetition()) {
move.inThreefoldRepetition = true
}
if (chess.isCheckmate()) {
move.inCheckmate = true
}
}
if (chess.isCheck()) {
move.inCheck = true
}
}
/**
* @param move
* @return the history to the move which may be in a variation
*/
historyToMove(move) {
const moves = []
let pointer = move
moves.push(pointer)
while (pointer.previous) {
moves.push(pointer.previous)
pointer = pointer.previous
}
return moves.reverse()
}
/**
* Don't add the move, just validate, if it would be correct
* @param notation
* @param previous
* @param sloppy
* @returns {[]|{}}
*/
validateMove(notation, previous = null, sloppy = true) {
if (!previous) {
if (this.moves.length > 0) {
previous = this.moves[this.moves.length - 1]
}
}
const chess = new Chess(this.setUpFen ? this.setUpFen : undefined)
if (previous) {
const historyToMove = this.historyToMove(previous)
for (const moveInHistory of historyToMove) {
chess.move(moveInHistory)
}
}
const move = chess.move(notation, {sloppy: sloppy})
if (move) {
this.fillMoveFromChessState(move, chess)
}
return move
}
addMove(notation, previous = null, sloppy = true) {
if (!previous) {
if (this.moves.length > 0) {
previous = this.moves[this.moves.length - 1]
}
}
const move = this.validateMove(notation, previous, sloppy)
if (!move) {
throw new Error("invalid move")
}
move.previous = previous
if (previous) {
move.ply = previous.ply + 1
move.uci = move.from + move.to + (move.promotion ? move.promotion : "")
if (previous.next) {
previous.next.variations.push([])
move.variation = previous.next.variations[previous.next.variations.length - 1]
move.variation.push(move)
} else {
previous.next = move
move.variation = previous.variation
previous.variation.push(move)
}
} else {
move.variation = this.moves
move.ply = 1
this.moves.push(move)
}
return move
}
render(renderComments = true, renderNags = true) {
const renderVariation = (variation, needReminder = false) => {
let result = ""
for (let move of variation) {
if (move.ply % 2 === 1) {
result += Math.floor(move.ply / 2) + 1 + ". "
} else if (result.length === 0 || needReminder) {
result += move.ply / 2 + "... "
}
needReminder = false
if (renderNags && move.nag) {
result += "$" + move.nag + " "
}
if (renderComments && move.commentBefore) {
result += "{" + move.commentBefore + "} "
needReminder = true
}
result += move.san + " "
if (renderComments && move.commentMove) {
result += "{" + move.commentMove + "} "
needReminder = true
}
if (renderComments && move.commentAfter) {
result += "{" + move.commentAfter + "} "
needReminder = true
}
if (move.variations.length > 0) {
for (let variation of move.variations) {
result += "(" + renderVariation(variation) + ") "
needReminder = true
}
}
result += " "
}
return result
}
let ret = renderVariation(this.moves)
// remove spaces before brackets
ret = ret.replace(/\s+\)/g, ')')
// remove double spaces
ret = ret.replace(/\s\s+/g, ' ').trim()
return ret
}
}