kokopu
Version:
A JavaScript/TypeScript library implementing the chess game rules and providing tools to read/write the standard chess file formats.
359 lines • 15.5 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.ascii = ascii;
exports.getFEN = getFEN;
exports.parseFEN = parseFEN;
const base_types_impl_1 = require("./base_types_impl");
const impl_1 = require("./impl");
const legality_1 = require("./legality");
const exception_1 = require("../exception");
const i18n_1 = require("../i18n");
const FEN_PIECE_SYMBOL = [...'KkQqRrBbNnPp'];
const EN_PASSANT_RANK = ['6', '3'];
/**
* Return a human-readable string representing the position. This string is multi-line,
* and is intended to be displayed in a fixed-width font (similarly to an ASCII-art picture).
*/
function ascii(position, { flipped = false, prefix = '', coordinateVisible = false }) {
// Board scanning
const lineSeparator = prefix + (coordinateVisible ? ' ' : '') + '+---+---+---+---+---+---+---+---+\n';
const rows = flipped ? [0, 1, 2, 3, 4, 5, 6, 7] : [7, 6, 5, 4, 3, 2, 1, 0];
const columns = flipped ? [7, 6, 5, 4, 3, 2, 1, 0] : [0, 1, 2, 3, 4, 5, 6, 7];
let result = lineSeparator;
for (const r of rows) {
result += prefix;
if (coordinateVisible) {
result += (r + 1) + ' ';
}
for (const c of columns) {
const cp = position.board[r * 16 + c];
result += '| ' + (cp === -1 /* SpI.EMPTY */ ? ' ' : FEN_PIECE_SYMBOL[cp]) + ' ';
}
result += '|\n';
result += lineSeparator;
}
if (coordinateVisible) {
result += prefix + (flipped ? ' h g f e d c b a\n' : ' a b c d e f g h\n');
}
// Flags
result += prefix + (0, base_types_impl_1.colorToString)(position.turn) + ' ' + castlingToString(position) + ' ' + enPassantToString(position);
if (position.variant !== 0 /* GameVariantImpl.REGULAR_CHESS */) {
result += ' (' + (0, base_types_impl_1.variantToString)(position.variant) + ')';
}
return result;
}
function getFEN(position, fiftyMoveClock = 0, fullMoveNumber = 1, regularFENIfPossible = false) {
let result = '';
// Board scanning
for (let r = 7; r >= 0; --r) {
let emptyCount = 0;
for (let c = 0; c < 8; ++c) {
const cp = position.board[r * 16 + c];
if (cp === -1 /* SpI.EMPTY */) {
++emptyCount;
}
else {
if (emptyCount > 0) {
result += emptyCount;
emptyCount = 0;
}
result += FEN_PIECE_SYMBOL[cp];
}
}
if (emptyCount > 0) {
result += emptyCount;
}
if (r > 0) {
result += '/';
}
}
// Flags + additional move counters
result += ' ' + (0, base_types_impl_1.colorToString)(position.turn) + ' ' + castlingToString(position, regularFENIfPossible) + ' ' + enPassantToString(position);
result += ' ' + fiftyMoveClock + ' ' + fullMoveNumber;
return result;
}
/**
* @param regularFENIfPossible - For Chess960, if `true`, format the flags as `KQkq` (regular FEN style) if possible
* (instead of `AB...Hab...h` which is used by default, i.e. X-FEN style).
* For the other variants, this flag has no effect, as regulary FEN style is always used.
*/
function castlingToString(position, regularFENIfPossible = false) {
(0, legality_1.refreshEffectiveCastling)(position);
if (position.variant === 1 /* GameVariantImpl.CHESS960 */) {
if (regularFENIfPossible) {
const whiteRegularFlags = regularFENCaslingFlagIfPossible(position, 0 /* ColorImpl.WHITE */);
const blackRegularFlags = regularFENCaslingFlagIfPossible(position, 1 /* ColorImpl.BLACK */);
if (whiteRegularFlags !== false && blackRegularFlags !== false) {
return whiteRegularFlags === '' && blackRegularFlags === '' ? '-' : whiteRegularFlags.toUpperCase() + blackRegularFlags;
}
}
let whiteFlags = '';
let blackFlags = '';
for (let file = 0; file < 8; ++file) {
if (position.effectiveCastling[0 /* ColorImpl.WHITE */] & 1 << file) {
whiteFlags += (0, base_types_impl_1.fileToString)(file);
}
if (position.effectiveCastling[1 /* ColorImpl.BLACK */] & 1 << file) {
blackFlags += (0, base_types_impl_1.fileToString)(file);
}
}
return whiteFlags === '' && blackFlags === '' ? '-' : whiteFlags.toUpperCase() + blackFlags;
}
else {
let result = '';
if (position.effectiveCastling[0 /* ColorImpl.WHITE */] & 0x80) {
result += 'K';
}
if (position.effectiveCastling[0 /* ColorImpl.WHITE */] & 0x01) {
result += 'Q';
}
if (position.effectiveCastling[1 /* ColorImpl.BLACK */] & 0x80) {
result += 'k';
}
if (position.effectiveCastling[1 /* ColorImpl.BLACK */] & 0x01) {
result += 'q';
}
return result === '' ? '-' : result;
}
}
function regularFENCaslingFlagIfPossible(position, color) {
// Decompose the castling flags into:
//
// +---------------+---+--------------+
// | queenSideMask | 0 | kingSideMask |
// +---------------+---+--------------+
// ^ ^ ^
// File a King file File h
//
const kingFileMask = 1 << (position.king[color] % 16);
const kingSideMask = position.effectiveCastling[color] & ~(kingFileMask | (kingFileMask - 1));
const queenSideMask = position.effectiveCastling[color] & (kingFileMask - 1);
let fenFlag = '';
const firstSquare = 112 * color;
const lastSquare = 112 * color + 7;
const targetRook = 2 /* PieceImpl.ROOK */ * 2 + color;
// Search for the rooks on king-side.
if (kingSideMask !== 0) {
let rookFound = false;
for (let sq = position.king[color] + 1; sq <= lastSquare; ++sq) {
if (position.board[sq] === targetRook) {
if (rookFound) { // Ensure there is only 1 rook on the king side.
return false;
}
else {
rookFound = true;
}
}
}
fenFlag += 'k';
}
// Search for the rooks on queen-side.
if (queenSideMask !== 0) {
let rookFound = false;
for (let sq = position.king[color] - 1; sq >= firstSquare; --sq) {
if (position.board[sq] === targetRook) {
if (rookFound) { // Ensure there is only 1 rook on the queen side.
return false;
}
else {
rookFound = true;
}
}
}
fenFlag += 'q';
}
return fenFlag;
}
function enPassantToString(position) {
(0, legality_1.refreshEffectiveEnPassant)(position);
return position.effectiveEnPassant < 0 ? '-' : (0, base_types_impl_1.fileToString)(position.effectiveEnPassant) + EN_PASSANT_RANK[position.turn];
}
function parseFEN(variant, fen, strict) {
// Trim the input string and split it into 6 fields.
const fields = strict ? fen.split(' ') : fen.replace(/^\s+|\s+$/g, '').split(/\s+/);
if (fields.length !== 6) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.WRONG_NUMBER_OF_FEN_FIELDS);
}
// The first field (that represents the board) is split in 8 sub-fields.
const rankFields = fields[0].split('/');
if (rankFields.length !== 8) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.WRONG_NUMBER_OF_SUBFIELDS_IN_BOARD_FIELD);
}
// Initialize the position
const position = (0, impl_1.makeEmpty)(variant);
position.legal = null;
position.effectiveCastling = null;
position.effectiveEnPassant = null;
// Board parsing
for (let r = 7; r >= 0; --r) {
const rankField = rankFields[7 - r];
let i = 0;
let c = 0;
while (i < rankField.length && c < 8) {
const s = rankField[i];
const cp = FEN_PIECE_SYMBOL.indexOf(s);
// The current character is in the range [1-8] -> skip the corresponding number of squares.
if (/^[1-8]$/.test(s)) {
c += parseInt(s, 10);
}
// The current character corresponds to a colored piece symbol -> set the current square accordingly.
else if (cp >= 0) {
position.board[r * 16 + c] = cp;
++c;
}
// Otherwise -> parsing error.
else {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.UNEXPECTED_CHARACTER_IN_BOARD_FIELD, s);
}
// Increment the character counter.
++i;
}
// Ensure that the current sub-field deals with all the squares of the current rank.
if (i !== rankField.length || c !== 8) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.UNEXPECTED_END_OF_SUBFIELD_IN_BOARD_FIELD, 8 - r);
}
}
// Turn parsing
position.turn = (0, base_types_impl_1.colorFromString)(fields[1]);
if (position.turn < 0) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_TURN_FIELD);
}
// Castling rights parsing
const castling = variant === 1 /* GameVariantImpl.CHESS960 */ ? castlingFromStringXFEN(fields[2], strict, position.board) : castlingFromStringFEN(fields[2], strict);
if (castling === null) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_CASTLING_FIELD);
}
else {
position.castling = castling;
}
// En-passant rights parsing
const enPassantField = fields[3];
if (enPassantField !== '-') {
if (!/^[a-h][36]$/.test(enPassantField)) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_EN_PASSANT_FIELD);
}
position.enPassant = (0, base_types_impl_1.fileFromString)(enPassantField[0]);
if (strict) {
if (enPassantField[1] !== EN_PASSANT_RANK[position.turn]) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.WRONG_RANK_IN_EN_PASSANT_FIELD);
}
(0, legality_1.refreshEffectiveEnPassant)(position);
if (position.enPassant !== position.effectiveEnPassant) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INEFFECTIVE_EN_PASSANT_FIELD, (0, base_types_impl_1.fileToString)(position.enPassant));
}
}
}
// Move counting flags parsing
const moveCountingRegex = strict ? /^(?:0|[1-9][0-9]*)$/ : /^[0-9]+$/;
if (!moveCountingRegex.test(fields[4])) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_HALF_MOVE_COUNT_FIELD);
}
if (!moveCountingRegex.test(fields[5])) {
throw new exception_1.InvalidFEN(fen, i18n_1.i18n.INVALID_MOVE_NUMBER_FIELD);
}
return { position: position, fiftyMoveClock: parseInt(fields[4], 10), fullMoveNumber: parseInt(fields[5], 10) };
}
function castlingFromStringFEN(castling, strict) {
const result = [0, 0];
if (castling === '-') {
return result;
}
if (!(strict ? /^K?Q?k?q?$/ : /^[KQkq]*$/).test(castling)) {
return null;
}
if (castling.indexOf('K') >= 0) {
result[0 /* ColorImpl.WHITE */] |= 1 << 7;
}
if (castling.indexOf('Q') >= 0) {
result[0 /* ColorImpl.WHITE */] |= 1 << 0;
}
if (castling.indexOf('k') >= 0) {
result[1 /* ColorImpl.BLACK */] |= 1 << 7;
}
if (castling.indexOf('q') >= 0) {
result[1 /* ColorImpl.BLACK */] |= 1 << 0;
}
return result;
}
function castlingFromStringXFEN(castling, strict, board) {
const result = [0, 0];
if (castling === '-') {
return result;
}
if (!(strict ? /^[A-H]{0,2}[a-h]{0,2}$/ : /^[A-Ha-h]*|[KQkq]*$/).test(castling)) {
return null;
}
function searchQueenSideRook(color) {
const targetRook = 2 /* PieceImpl.ROOK */ * 2 + color;
const targetKing = 0 /* PieceImpl.KING */ * 2 + color;
for (let sq = 112 * color; sq < 112 * color + 8; ++sq) {
if (board[sq] === targetRook) {
return sq % 8;
}
else if (board[sq] === targetKing) {
break;
}
}
return 0;
}
function searchKingSideRook(color) {
const targetRook = 2 /* PieceImpl.ROOK */ * 2 + color;
const targetKing = 0 /* PieceImpl.KING */ * 2 + color;
for (let sq = 112 * color + 7; sq >= 112 * color; --sq) {
if (board[sq] === targetRook) {
return sq % 8;
}
else if (board[sq] === targetKing) {
break;
}
}
return 7;
}
if (!strict) {
if (castling.indexOf('K') >= 0) {
result[0 /* ColorImpl.WHITE */] |= 1 << searchKingSideRook(0 /* ColorImpl.WHITE */);
}
if (castling.indexOf('Q') >= 0) {
result[0 /* ColorImpl.WHITE */] |= 1 << searchQueenSideRook(0 /* ColorImpl.WHITE */);
}
if (castling.indexOf('k') >= 0) {
result[1 /* ColorImpl.BLACK */] |= 1 << searchKingSideRook(1 /* ColorImpl.BLACK */);
}
if (castling.indexOf('q') >= 0) {
result[1 /* ColorImpl.BLACK */] |= 1 << searchQueenSideRook(1 /* ColorImpl.BLACK */);
}
}
for (let file = 0; file < 8; ++file) {
const s = (0, base_types_impl_1.fileToString)(file);
if (castling.indexOf(s.toUpperCase()) >= 0) {
result[0 /* ColorImpl.WHITE */] |= 1 << file;
}
if (castling.indexOf(s) >= 0) {
result[1 /* ColorImpl.BLACK */] |= 1 << file;
}
}
return result;
}
//# sourceMappingURL=fen.js.map