kokopu
Version:
A JavaScript/TypeScript library implementing the chess game rules and providing tools to read/write the standard chess file formats.
451 lines • 22.4 kB
JavaScript
"use strict";
/*!
* -------------------------------------------------------------------------- *
* *
* Kokopu - A JavaScript/TypeScript chess library. *
* <https://www.npmjs.com/package/kokopu> *
* Copyright (C) 2018-2025 Yoann Le Montagner <yo35 -at- melix.net> *
* *
* Kokopu is free software: you can redistribute it and/or *
* modify it under the terms of the GNU Lesser General Public License *
* as published by the Free Software Foundation, either version 3 of *
* the License, or (at your option) any later version. *
* *
* Kokopu is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU Lesser General Public License for more details. *
* *
* You should have received a copy of the GNU Lesser General *
* Public License along with this program. If not, see *
* <http://www.gnu.org/licenses/>. *
* *
* -------------------------------------------------------------------------- */
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNotation = getNotation;
exports.parseNotation = parseNotation;
const attacks_1 = require("./attacks");
const base_types_impl_1 = require("./base_types_impl");
const fen_1 = require("./fen");
const impl_1 = require("./impl");
const legality_1 = require("./legality");
const move_descriptor_impl_1 = require("./move_descriptor_impl");
const move_generation_1 = require("./move_generation");
const exception_1 = require("../exception");
const i18n_1 = require("../i18n");
/**
* Convert the given move descriptor to standard algebraic notation.
*/
function getNotation(position, descriptor, pieceStyle) {
let result = '';
// Castling move
if (descriptor.isCastling()) {
result = descriptor._to % 16 === 6 ? 'O-O' : 'O-O-O';
}
// Pawn move
else if (Math.trunc(descriptor._movingColoredPiece / 2) === 5 /* PieceImpl.PAWN */) {
if (descriptor.isCapture()) {
result += (0, base_types_impl_1.fileToString)(descriptor._from % 16) + 'x';
}
result += (0, base_types_impl_1.squareToString)(descriptor._to);
if (descriptor.isPromotion()) {
result += '=' + getPieceSymbol(descriptor._finalColoredPiece, pieceStyle);
}
}
// Non-pawn move
else {
result += getPieceSymbol(descriptor._movingColoredPiece, pieceStyle);
result += getDisambiguationSymbol(position, descriptor._from, descriptor._to);
if (descriptor.isCapture()) {
result += 'x';
}
result += (0, base_types_impl_1.squareToString)(descriptor._to);
}
// Check/checkmate detection and final result.
result += getCheckCheckmateSymbol(position, descriptor);
return result;
}
/**
* Return a string representing the given chess piece according to the given style.
*/
function getPieceSymbol(coloredPiece, pieceStyle) {
switch (pieceStyle) {
case 'figurine':
return (0, base_types_impl_1.figurineToString)(coloredPiece);
case 'standard':
return (0, base_types_impl_1.pieceToString)(Math.trunc(coloredPiece / 2)).toUpperCase();
}
}
/**
* Return the check/checkmate symbol to use for a move.
*/
function getCheckCheckmateSymbol(position, descriptor) {
const nextPosition = (0, impl_1.makeCopy)(position);
(0, move_generation_1.play)(nextPosition, descriptor);
return (0, move_generation_1.isCheckmate)(nextPosition) ? '#' : (0, move_generation_1.isCheck)(nextPosition) ? '+' : '';
}
/**
* Return the disambiguation symbol to use for a move from `from` to `to`.
*/
function getDisambiguationSymbol(position, from, to) {
const attackers = (0, attacks_1.getAttacks)(position, to, position.turn).filter(sq => position.board[sq] === position.board[from]);
// Disambiguation is not necessary if there less than 2 attackers.
if (attackers.length < 2) {
return '';
}
let foundNotPined = false;
let foundOnSameRank = false;
let foundOnSameFile = false;
const rankFrom = Math.trunc(from / 16);
const fileFrom = from % 16;
for (const sq of attackers) {
if (sq === from || isPinned(position, sq, to)) {
continue;
}
foundNotPined = true;
if (rankFrom === Math.trunc(sq / 16)) {
foundOnSameRank = true;
}
if (fileFrom === sq % 16) {
foundOnSameFile = true;
}
}
if (foundOnSameFile) {
return foundOnSameRank ? (0, base_types_impl_1.squareToString)(from) : (0, base_types_impl_1.rankToString)(rankFrom);
}
else {
return foundNotPined ? (0, base_types_impl_1.fileToString)(fileFrom) : '';
}
}
/**
* Whether the piece on the given square is pinned or not.
*/
function isPinned(position, sq, aimingAtSq) {
const kingSquare = position.king[position.turn];
if (kingSquare < 0) {
return false;
}
const vector = Math.abs(kingSquare - sq);
const aimingAtVector = Math.abs(aimingAtSq - sq);
const pinnerQueen = 1 /* PieceImpl.QUEEN */ * 2 + 1 - position.turn;
const pinnerRook = 2 /* PieceImpl.ROOK */ * 2 + 1 - position.turn;
const pinnerBishop = 3 /* PieceImpl.BISHOP */ * 2 + 1 - position.turn;
// Potential pinning on file or rank.
if (vector < 8) {
return aimingAtVector >= 8 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 1 : -1, pinnerRook, pinnerQueen);
}
else if (vector % 16 === 0) {
return aimingAtVector % 16 !== 0 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 16 : -16, pinnerRook, pinnerQueen);
}
// Potential pinning on diagonal.
else if (vector % 15 === 0) {
return aimingAtVector % 15 !== 0 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 15 : -15, pinnerBishop, pinnerQueen);
}
else if (vector % 17 === 0) {
return aimingAtVector % 17 !== 0 && pinningLoockup(position, kingSquare, sq, kingSquare < sq ? 17 : -17, pinnerBishop, pinnerQueen);
}
// No pinning for sure.
else {
return false;
}
}
function pinningLoockup(position, kingSquare, targetSquare, direction, pinnerColoredPiece1, pinnerColoredPiece2) {
for (let sq = kingSquare + direction; sq !== targetSquare; sq += direction) {
if (position.board[sq] !== -1 /* SpI.EMPTY */) {
return false;
}
}
for (let sq = targetSquare + direction; (sq & 0x88) === 0; sq += direction) {
if (position.board[sq] !== -1 /* SpI.EMPTY */) {
return position.board[sq] === pinnerColoredPiece1 || position.board[sq] === pinnerColoredPiece2;
}
}
return false;
}
/**
* Parse a move notation for the given position.
*/
function parseNotation(position, notation, strict, pieceStyle) {
// General syntax
const m = /^(?:(O-O-O|0-0-0)|(O-O|0-0)|([A-Z\u2654-\u265f])([a-h])?([1-8])?(x)?([a-h][1-8])|(?:([a-h])(x)?)?([a-h][1-8])(?:(=)?([A-Z\u2654-\u265f]))?)([+#])?$/.exec(notation);
if (m === null) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_MOVE_NOTATION_SYNTAX);
}
// Ensure that the position is legal.
if (!(0, legality_1.isLegal)(position)) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.ILLEGAL_POSITION);
}
// CASTLING
// m[1] -> O-O-O
// m[2] -> O-O
// NON-PAWN MOVE
// m[3] -> moving piece
// m[4] -> file disambiguation
// m[5] -> rank disambiguation
// m[6] -> x (capture symbol)
// m[7] -> to
// PAWN MOVE
// m[ 8] -> from column (only for captures)
// m[ 9] -> x (capture symbol)
// m[10] -> to
// m[11] -> = (promotion symbol)
// m[12] -> promoted piece
// OTHER
// m[13] -> +/# (check/checkmate symbol)
let descriptor = false;
// Parse castling moves
if (m[1] !== undefined || m[2] !== undefined) {
descriptor = parseCastlingNotation(position, notation, strict, m[1], m[2]);
}
// Non-pawn move
else if (m[3] !== undefined) {
descriptor = parseNonPawnNotation(position, notation, strict, pieceStyle, m[3], m[4], m[5], m[7]);
}
// Pawn move
else {
descriptor = parsePawnMoveNotation(position, notation, strict, pieceStyle, m[8], m[10], m[11], m[12]);
}
// STRICT MODE
if (strict) {
const observedIsCapture = m[6] !== undefined || m[9] !== undefined;
if (descriptor.isCapture() !== observedIsCapture) {
const message = descriptor.isCapture() ? i18n_1.i18n.MISSING_CAPTURE_SYMBOL : i18n_1.i18n.INVALID_CAPTURE_SYMBOL;
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message);
}
const expectedCCS = getCheckCheckmateSymbol(position, descriptor);
const observedCCS = m[13] ?? '';
if (expectedCCS !== observedCCS) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.WRONG_CHECK_CHECKMATE_SYMBOL, expectedCCS, observedCCS);
}
}
// Final result
return descriptor;
}
/**
* Delegate function that computes the move descriptor corresponding to a castling move (corresponding notation: "O-O" or "O-O-O").
*/
function parseCastlingNotation(position, notation, strict, queenSideCastlingSymbol, kingSideCastlingSymbol) {
const from = position.king[position.turn];
if (from < 0) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.ILLEGAL_NO_KING_CASTLING);
}
(0, legality_1.refreshEffectiveCastling)(position);
const isKingSideCastling = kingSideCastlingSymbol !== undefined;
const toFile = getCastlingDestinationFile(position, isKingSideCastling);
const descriptor = toFile >= 0 ? (0, move_generation_1.isCastlingMoveLegal)(position, from, toFile + 112 * position.turn) : false;
if (!descriptor) {
const message = isKingSideCastling ? i18n_1.i18n.ILLEGAL_KING_SIDE_CASTLING : i18n_1.i18n.ILLEGAL_QUEEN_SIDE_CASTLING;
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message);
}
// STRICT-MODE -> ensure that upper-case O is used instead of digit 0.
if (strict) {
const firstChar = (isKingSideCastling ? kingSideCastlingSymbol : queenSideCastlingSymbol).charAt(0);
if (firstChar === '0') {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.CASTLING_MOVE_ENCODED_WITH_ZERO);
}
}
return descriptor;
}
/**
* Returns the file of a `to` square to take into account to check whether a castling move is legal or not.
*/
function getCastlingDestinationFile(position, isKingSideCastling) {
if (position.variant === 1 /* GameVariantImpl.CHESS960 */) {
if (position.effectiveCastling[position.turn] !== 0) {
const castlingKing = 0 /* PieceImpl.KING */ * 2 + position.turn;
for (let file = isKingSideCastling ? 7 : 0; position.board[file + 112 * position.turn] !== castlingKing; file += isKingSideCastling ? -1 : 1) {
if ((position.effectiveCastling[position.turn] & 1 << file) !== 0) {
return file;
}
}
}
return -1;
}
else {
return isKingSideCastling ? 6 : 2;
}
}
/**
* Delegate function that computes the move descriptor corresponding to a non-pawn move. Corresponding notation, for instance "Ne3xd5":
*
* - N: piece symbol
* - e: file disambiguation
* - 3: rank disambiguation
* - d5: destination square
*/
function parseNonPawnNotation(position, notation, strict, pieceStyle, pieceSymbol, fileDisambiguation, rankDisambiguation, destinationSquare) {
const movingColoredPiece = parsePieceSymbol(position, notation, pieceSymbol, strict, pieceStyle) * 2 + position.turn;
const to = (0, base_types_impl_1.squareFromString)(destinationSquare);
const toContent = position.board[to];
// Cannot take your own pieces!
if (toContent !== -1 /* SpI.EMPTY */ && toContent % 2 === position.turn) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.TRYING_TO_CAPTURE_YOUR_OWN_PIECES);
}
// Capture may be mandatory in some variants.
if (toContent === -1 /* SpI.EMPTY */ && (0, move_generation_1.isCaptureMandatory)(position)) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.CAPTURE_IS_MANDATORY);
}
// Find the "from"-square candidates
let attackers = (0, attacks_1.getAttacks)(position, to, position.turn).filter(sq => position.board[sq] === movingColoredPiece);
// Apply disambiguation
if (fileDisambiguation !== undefined) {
const fileFrom = (0, base_types_impl_1.fileFromString)(fileDisambiguation);
attackers = attackers.filter(sq => sq % 16 === fileFrom);
}
if (rankDisambiguation !== undefined) {
const rankFrom = (0, base_types_impl_1.rankFromString)(rankDisambiguation);
attackers = attackers.filter(sq => Math.trunc(sq / 16) === rankFrom);
}
if (attackers.length === 0) {
const message = fileDisambiguation === undefined && rankDisambiguation === undefined ? i18n_1.i18n.NO_PIECE_CAN_MOVE_TO : i18n_1.i18n.NO_PIECE_CAN_MOVE_TO_DISAMBIGUATION;
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message, pieceSymbol, destinationSquare);
}
// Compute the move descriptor for each remaining "from"-square candidate
let descriptor = false;
for (const sq of attackers) {
if ((0, legality_1.isKingSafeAfterMove)(position, sq, to)) {
if (descriptor) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.REQUIRE_DISAMBIGUATION, pieceSymbol, destinationSquare);
}
descriptor = move_descriptor_impl_1.MoveDescriptorImpl.make(sq, to, movingColoredPiece, toContent);
}
}
if (!descriptor) {
const message = position.turn === 0 /* ColorImpl.WHITE */ ? i18n_1.i18n.NOT_SAFE_FOR_WHITE_KING : i18n_1.i18n.NOT_SAFE_FOR_BLACK_KING;
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message);
}
// STRICT-MODE -> check the disambiguation symbol.
if (strict) {
const expectedDS = getDisambiguationSymbol(position, descriptor._from, to);
const observedDS = (fileDisambiguation ?? '') + (rankDisambiguation ?? '');
if (expectedDS !== observedDS) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.WRONG_DISAMBIGUATION_SYMBOL, expectedDS, observedDS);
}
}
return descriptor;
}
/**
* Delegate function that computes the move descriptor corresponding to a pawn move. Corresponding notation, for instance "fxg8=Q":
*
* - f: origin file
* - g8: destination square
* - =: promotion symbol
* - Q: promoted piece
*/
function parsePawnMoveNotation(position, notation, strict, pieceStyle, originFile, destinationSquare, promotionSymbol, promotedPiece) {
const coloredPawn = 5 /* PieceImpl.PAWN */ * 2 + position.turn;
const to = (0, base_types_impl_1.squareFromString)(destinationSquare);
const toContent = position.board[to];
const vector = 16 - position.turn * 32;
let from = to - vector;
let enPassantSquare = -1;
if (originFile !== undefined) { // Capturing pawn move
// Ensure that `to` is not on the 1st row.
if ((from & 0x88) !== 0) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE);
}
// Compute the "from"-square.
const columnFrom = (0, base_types_impl_1.fileFromString)(originFile);
const columnTo = to % 16;
if (columnTo - columnFrom === 1) {
from -= 1;
}
else if (columnTo - columnFrom === -1) {
from += 1;
}
else {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE);
}
// Check the content of the "from"-square
if (position.board[from] !== coloredPawn) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE);
}
// Check the content of the "to"-square
if (toContent === -1 /* SpI.EMPTY */) { // Look for en-passant captures
(0, legality_1.refreshEffectiveEnPassant)(position);
if (to !== (5 - position.turn * 3) * 16 + position.effectiveEnPassant) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE);
}
enPassantSquare = (4 - position.turn) * 16 + position.effectiveEnPassant;
}
else if (toContent % 2 === position.turn) { // detecting regular captures
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_CAPTURING_PAWN_MOVE);
}
}
else if ((0, move_generation_1.isCaptureMandatory)(position)) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.CAPTURE_IS_MANDATORY);
}
else { // Non-capturing pawn move
// Ensure that `to` is not on the 1st row.
if ((from & 0x88) !== 0) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE);
}
// Check the content of the "to"-square
if (toContent !== -1 /* SpI.EMPTY */) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE);
}
// Check the content of the "from"-square
if (position.board[from] === -1 /* SpI.EMPTY */) { // Look for two-square pawn moves
from -= vector;
const firstSquareOfArea = position.turn * 96; // a1 for white, a7 for black (2-square pawn move is allowed from 1st row at horde chess)
if (from < firstSquareOfArea || from >= firstSquareOfArea + 24 || position.board[from] !== coloredPawn) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE);
}
}
else if (position.board[from] !== coloredPawn) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_NON_CAPTURING_PAWN_MOVE);
}
}
// Ensure that the pawn move do not let a king in check.
if (!(0, legality_1.isKingSafeAfterMove)(position, from, to, enPassantSquare)) {
const message = position.turn === 0 /* ColorImpl.WHITE */ ? i18n_1.i18n.NOT_SAFE_FOR_WHITE_KING : i18n_1.i18n.NOT_SAFE_FOR_BLACK_KING;
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, message);
}
// Promotions
if (to < 8 || to >= 112) {
if (promotedPiece === undefined) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.MISSING_PROMOTION);
}
const promotion = parsePieceSymbol(position, notation, promotedPiece, strict, pieceStyle);
if (promotion === 5 /* PieceImpl.PAWN */ || (promotion === 0 /* PieceImpl.KING */ && position.variant !== 5 /* GameVariantImpl.ANTICHESS */)) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PROMOTED_PIECE, promotedPiece);
}
// STRICT MODE -> do not forget the `=` character!
if (strict && promotionSymbol === undefined) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.MISSING_PROMOTION_SYMBOL);
}
return move_descriptor_impl_1.MoveDescriptorImpl.makePromotion(from, to, position.turn, toContent, promotion);
}
// Non-promotion moves
else {
if (promotedPiece !== undefined) { // Detect illegal promotion attempts!
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.ILLEGAL_PROMOTION);
}
return enPassantSquare >= 0 ? move_descriptor_impl_1.MoveDescriptorImpl.makeEnPassant(from, to, enPassantSquare, position.turn) : move_descriptor_impl_1.MoveDescriptorImpl.make(from, to, coloredPawn, toContent);
}
}
/**
* Delegate function for piece symbol parsing.
*/
function parsePieceSymbol(position, notation, pieceSymbol, strict, pieceStyle) {
switch (pieceStyle) {
case 'figurine': {
const coloredPieceCode = (0, base_types_impl_1.figurineFromString)(pieceSymbol);
if (coloredPieceCode < 0) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PIECE_SYMBOL, pieceSymbol);
}
if (strict && coloredPieceCode % 2 !== position.turn) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PIECE_SYMBOL_COLOR, pieceSymbol);
}
return Math.trunc(coloredPieceCode / 2);
}
case 'standard': {
const pieceCode = (0, base_types_impl_1.pieceFromString)(pieceSymbol.toLowerCase());
if (pieceCode < 0) {
throw new exception_1.InvalidNotation((0, fen_1.getFEN)(position), notation, i18n_1.i18n.INVALID_PIECE_SYMBOL, pieceSymbol);
}
return pieceCode;
}
}
}
//# sourceMappingURL=notation.js.map