isepic-chess
Version:
Chess utility library written in JavaScript
1,425 lines • 110 kB
JavaScript
/*! Copyright (c) 2025 Ajax Isepic (ajax333221) Licensed MIT */
(function (windw, expts, defin) {
var Ic = (function (_WIN) {
const _VERSION = '9.0.0';
let _SILENT_MODE = true;
let _BOARDS = {};
const _EMPTY_SQR = 0;
const _PAWN_W = 1;
const _KNIGHT_W = 2;
const _BISHOP_W = 3;
const _ROOK_W = 4;
const _QUEEN_W = 5;
const _KING_W = 6;
const _PAWN_B = -1;
const _KNIGHT_B = -2;
const _BISHOP_B = -3;
const _ROOK_B = -4;
const _QUEEN_B = -5;
const _KING_B = -6;
const _DIRECTION_TOP = 1;
const _DIRECTION_TOP_RIGHT = 2;
const _DIRECTION_RIGHT = 3;
const _DIRECTION_BOTTOM_RIGHT = 4;
const _DIRECTION_BOTTOM = 5;
const _DIRECTION_BOTTOM_LEFT = 6;
const _DIRECTION_LEFT = 7;
const _DIRECTION_TOP_LEFT = 8;
const _SHORT_CASTLE = 1;
const _LONG_CASTLE = 2;
const _RESULT_ONGOING = '*';
const _RESULT_W_WINS = '1-0';
const _RESULT_B_WINS = '0-1';
const _RESULT_DRAW = '1/2-1/2';
const _DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
const _ALERT_LIGHT = 'light';
const _ALERT_DARK = 'dark';
const _ALERT_SUCCESS = 'success';
const _ALERT_WARNING = 'warning';
const _ALERT_ERROR = 'error';
const _TEST_COLLISION_OP_CANDIDATE_MOVES = 1;
const _TEST_COLLISION_OP_IS_ATTACKED = 2;
const _MUTABLE_KEYS = [
'w',
'b',
'activeColor',
'nonActiveColor',
'fen',
'enPassantBos',
'halfMove',
'fullMove',
'moveList',
'currentMove',
'isRotated',
'isPuzzleMode',
'checks',
'isCheck',
'isCheckmate',
'isStalemate',
'isThreefold',
'isInsufficientMaterial',
'isFiftyMove',
'inDraw',
'promoteTo',
'manualResult',
'isHidden',
'legalUci',
'legalUciTree',
'legalRevTree',
'squares',
];
//!---------------- helpers
function _promoteValHelper(pvqal) {
let rtn = _toInt(toAbsVal(pvqal) || _QUEEN_W, _KNIGHT_W, _QUEEN_W);
return rtn;
}
function _pgnResultHelper(str) {
let rtn = null;
str = String(str || '')
.replace(/\s/g, '')
.replace(/o/gi, '0')
.replace(/½/g, '1/2');
if (str === _RESULT_ONGOING || str === _RESULT_W_WINS || str === _RESULT_B_WINS || str === _RESULT_DRAW) {
rtn = str;
}
return rtn;
}
function _strToValHelper(str) {
let rtn = _EMPTY_SQR;
block: {
if (!str) {
break block;
}
if (!Number.isNaN(Number(str)) && _isIntOrStrInt(str)) {
let temp = _toInt(str, _KING_B, _KING_W);
rtn = temp;
break block;
}
str = _trimSpaces(str);
if (/^[pnbrqk]$/i.test(str)) {
let temp = str.toLowerCase();
let temp2 = ('pnbrqk'.indexOf(temp) + 1) * getSign(str === temp);
rtn = temp2;
break block;
}
let pc_exec = /^([wb])([pnbrqk])$/.exec(str.toLowerCase());
if (pc_exec) {
let temp = ('pnbrqk'.indexOf(pc_exec[2]) + 1) * getSign(pc_exec[1] === 'b');
rtn = temp;
break block;
}
}
return rtn;
}
function _strToBosHelper(str) {
let rtn = null;
str = _trimSpaces(str);
if (str && /^[a-h][1-8]$/i.test(str)) {
rtn = str.toLowerCase();
}
return rtn;
}
function _arrToPosHelper(arr) {
let rtn = null;
if (_isArray(arr) && arr.length === 2) {
let pre_rank_pos = _toInt(arr[0]);
let pre_file_pos = _toInt(arr[1]);
if (pre_rank_pos <= 7 && pre_rank_pos >= 0 && pre_file_pos <= 7 && pre_file_pos >= 0) {
let rank_pos = pre_rank_pos;
let file_pos = pre_file_pos;
rtn = [rank_pos, file_pos];
}
}
return rtn;
}
function _pgnParserHelper(str) {
let rtn = null;
block: {
if (!_isNonBlankStr(str)) {
break block;
}
let meta_tags = {};
let last_index = -1;
let rgxp = /\[\s*(\w+)\s+\"([^\"]*)\"\s*\]/g;
str = str.replace(/“|”/g, '"');
let mtch;
while ((mtch = rgxp.exec(str))) {
last_index = rgxp.lastIndex;
meta_tags[_trimSpaces(mtch[1])] = _trimSpaces(mtch[2]);
}
if (last_index === -1) {
last_index = 0;
}
let g = ' ' + _cleanSan(str.slice(last_index));
let move_list = [];
last_index = -1;
rgxp = /\s+([1-9][0-9]*)*\s*\.*\s*\.*\s*([^\s]+)/g;
let last_match = '';
while ((mtch = rgxp.exec(g))) {
last_index = rgxp.lastIndex;
last_match = mtch[0];
move_list.push(mtch[2]);
}
if (last_index === -1) {
break block;
}
let game_result = _RESULT_ONGOING;
let result_last_match = _pgnResultHelper(last_match);
if (result_last_match) {
move_list.pop();
game_result = result_last_match;
}
if (meta_tags.Result) {
let result_meta_tag = _pgnResultHelper(meta_tags.Result);
if (result_meta_tag) {
meta_tags.Result = result_meta_tag;
game_result = result_meta_tag;
}
}
rtn = {
tags: meta_tags,
sanMoves: move_list,
result: game_result,
};
}
return rtn;
}
function _uciParserHelper(str) {
let rtn = null;
block: {
if (!_isNonBlankStr(str)) {
break block;
}
str = _trimSpaces(str)
.replace(/[^a-h1-8 nrq]/gi, '')
.toLowerCase();
if (!str) {
break block;
}
rtn = str.split(' ');
}
return rtn;
}
function _uciWrapmoveHelper(mov) {
let rtn = null;
block: {
if (!_isNonBlankStr(mov)) {
break block;
}
let temp = _trimSpaces(String(mov));
if (temp.length !== 4 && temp.length !== 5) {
break block;
}
let pre_from_bos = _strToBosHelper(temp.slice(0, 2));
let pre_to_bos = _strToBosHelper(temp.slice(2, 4));
if (pre_from_bos === null || pre_to_bos === null) {
break block;
}
let from_bos = pre_from_bos;
let to_bos = pre_to_bos;
let from_to = [from_bos, to_bos];
let possible_promote = temp.charAt(4) || '';
rtn = [from_to, possible_promote];
}
return rtn;
}
function _joinedWrapmoveHelper(mov, p) {
let rtn = null;
p = _unreferenceP(p);
block: {
p.delimiter = _isNonEmptyStr(p.delimiter) ? p.delimiter.charAt(0) : '-';
if (!_isNonBlankStr(mov)) {
break block;
}
let temp = _trimSpaces(String(mov));
if (temp.length !== 5 || temp.charAt(2) !== p.delimiter) {
break block;
}
let temp2 = temp.split(p.delimiter);
let pre_from_bos = _strToBosHelper(temp2[0]);
let pre_to_bos = _strToBosHelper(temp2[1]);
if (pre_from_bos === null || pre_to_bos === null) {
break block;
}
let from_bos = pre_from_bos;
let to_bos = pre_to_bos;
let from_to = [from_bos, to_bos];
rtn = from_to;
}
return rtn;
}
function _fromToWrapmoveHelper(mov) {
let rtn = null;
block: {
if (!_isArray(mov)) {
break block;
}
if (mov.length !== 2) {
break block;
}
if (!isInsideBoard(mov[0]) || !isInsideBoard(mov[1])) {
break block;
}
let from_bos = toBos(mov[0]);
let to_bos = toBos(mov[1]);
let from_to = [from_bos, to_bos];
rtn = from_to;
}
return rtn;
}
function _moveWrapmoveHelper(mov) {
let rtn = null;
block: {
if (!_isMove(mov)) {
break block;
}
let possible_promote = mov.promotion || '';
rtn = [[mov.fromBos, mov.toBos], possible_promote];
}
return rtn;
}
function _unreferencedMoveHelper(obj) {
let rtn = {};
rtn.colorMoved = obj.colorMoved;
rtn.colorToPlay = obj.colorToPlay;
rtn.fen = obj.fen;
rtn.san = obj.san;
rtn.uci = obj.uci;
rtn.fromBos = obj.fromBos;
rtn.toBos = obj.toBos;
rtn.enPassantBos = obj.enPassantBos;
rtn.piece = obj.piece;
rtn.captured = obj.captured;
rtn.promotion = obj.promotion;
rtn.comment = obj.comment;
rtn.moveResult = obj.moveResult;
rtn.canDraw = obj.canDraw;
rtn.isEnPassantCapture = obj.isEnPassantCapture;
return rtn;
}
function _nullboardHelper(board_name) {
let rtn;
let pre_rtn = getBoard(board_name);
if (pre_rtn === null) {
_BOARDS[board_name] = {
boardName: board_name,
getSquare: _getSquare,
setSquare: _setSquare,
attackersFromActive: _attackersFromActive,
attackersFromNonActive: _attackersFromNonActive,
toggleActiveNonActive: _toggleActiveNonActive,
toggleIsRotated: _toggleIsRotated,
setPromoteTo: _setPromoteTo,
silentlyResetOptions: _silentlyResetOptions,
silentlyResetManualResult: _silentlyResetManualResult,
setManualResult: _setManualResult,
setCurrentMove: _setCurrentMove,
loadFen: _loadFen,
loadValidatedFen: _loadValidatedFen,
getClocklessFenHelper: _getClocklessFenHelper,
updateFenAndMisc: _updateFenAndMisc,
refinedFenTest: _refinedFenTest,
testCollision: _testCollision,
isLegalMove: _isLegalMove,
legalMovesHelper: _legalMovesHelper,
legalMoves: _legalMoves,
legalFenMoves: _legalFenMoves,
legalSanMoves: _legalSanMoves,
legalUciMoves: _legalUciMoves,
getCheckmateMoves: _getCheckmateMoves,
getDrawMoves: _getDrawMoves,
fenHistoryExport: _fenHistoryExport,
pgnExport: _pgnExport,
uciExport: _uciExport,
ascii: _ascii,
boardHash: _boardHash,
isEqualBoard: _isEqualBoard,
cloneBoardFrom: _cloneBoardFrom,
cloneBoardTo: _cloneBoardTo,
reset: _reset,
undoMove: _undoMove,
undoMoves: _undoMoves,
countLightDarkBishops: _countLightDarkBishops,
updateHelper: _updateHelper,
fenWrapmoveHelper: _fenWrapmoveHelper,
sanWrapmoveHelper: _sanWrapmoveHelper,
getWrappedMove: _getWrappedMove,
draftMove: _draftMove,
playMove: _playMove,
playMoves: _playMoves,
playRandomMove: _playRandomMove,
navFirst: _navFirst,
navPrevious: _navPrevious,
navNext: _navNext,
navLast: _navLast,
navLinkMove: _navLinkMove,
refreshUi: _refreshUi,
};
pre_rtn = _BOARDS[board_name];
}
pre_rtn.w = {
//static
isBlack: false,
sign: 1,
firstRankPos: 7,
secondRankPos: 6,
lastRankPos: 0,
singlePawnRankShift: -1,
pawn: _PAWN_W,
knight: _KNIGHT_W,
bishop: _BISHOP_W,
rook: _ROOK_W,
queen: _QUEEN_W,
king: _KING_W,
//mutable
kingBos: null,
castling: null,
materialDiff: null,
};
pre_rtn.b = {
//static
isBlack: true,
sign: -1,
firstRankPos: 0,
secondRankPos: 1,
lastRankPos: 7,
singlePawnRankShift: 1,
pawn: _PAWN_B,
knight: _KNIGHT_B,
bishop: _BISHOP_B,
rook: _ROOK_B,
queen: _QUEEN_B,
king: _KING_B,
//mutable
kingBos: null,
castling: null,
materialDiff: null,
};
pre_rtn.activeColor = null;
pre_rtn.nonActiveColor = null;
pre_rtn.fen = null;
pre_rtn.enPassantBos = null;
pre_rtn.halfMove = null;
pre_rtn.fullMove = null;
pre_rtn.moveList = null;
pre_rtn.currentMove = null;
pre_rtn.isRotated = null;
pre_rtn.isPuzzleMode = null;
pre_rtn.checks = null;
pre_rtn.isCheck = null;
pre_rtn.isCheckmate = null;
pre_rtn.isStalemate = null;
pre_rtn.isThreefold = null;
pre_rtn.isInsufficientMaterial = null;
pre_rtn.isFiftyMove = null;
pre_rtn.inDraw = null;
pre_rtn.promoteTo = null;
pre_rtn.manualResult = null;
pre_rtn.isHidden = null;
pre_rtn.legalUci = null;
pre_rtn.legalUciTree = null;
pre_rtn.legalRevTree = null;
pre_rtn.squares = {};
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
let validated_pos = [i, j];
let validated_bos = toBos(validated_pos);
pre_rtn.squares[validated_bos] = {
//static
pos: validated_pos,
bos: validated_bos,
rankPos: getRankPos(validated_pos),
filePos: getFilePos(validated_pos),
rankBos: getRankBos(validated_pos),
fileBos: getFileBos(validated_pos),
//mutable
bal: null,
absBal: null,
val: null,
absVal: null,
className: null,
sign: null,
isEmptySquare: null,
isPawn: null,
isKnight: null,
isBishop: null,
isRook: null,
isQueen: null,
isKing: null,
};
}
}
rtn = pre_rtn;
return rtn;
}
//!---------------- utilities
function _consoleLog(msg, alert_type) {
let rtn = false;
if (!_SILENT_MODE) {
rtn = true;
switch (alert_type) {
case _ALERT_LIGHT:
console.log(msg);
break;
case _ALERT_DARK:
console.log(msg);
break;
case _ALERT_SUCCESS:
console.log(msg);
break;
case _ALERT_WARNING:
console.warn(msg);
break;
case _ALERT_ERROR:
console.error(msg);
break;
default:
console.log(msg);
alert_type = _ALERT_LIGHT;
}
if (_WIN?.IcUi?.pushAlert) {
_WIN.IcUi.pushAlert.apply(null, [msg, alert_type]);
}
}
return rtn;
}
function _isObject(obj) {
return typeof obj === 'object' && obj !== null && !_isArray(obj);
}
function _isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
function _isSquare(obj) {
return _isObject(obj) && typeof obj.bos === 'string';
}
function _isBoard(obj) {
return _isObject(obj) && typeof obj.boardName === 'string';
}
function _isMove(obj) {
return _isObject(obj) && typeof obj.fromBos === 'string' && typeof obj.toBos === 'string';
}
function _trimSpaces(str) {
return String(str)
.replace(/^\s+|\s+$/g, '')
.replace(/\s\s+/g, ' ');
}
function _formatName(str) {
return _trimSpaces(str)
.replace(/[^a-z0-9]/gi, '_')
.replace(/__+/g, '_');
}
function _strContains(str, str_to_find) {
return String(str).indexOf(str_to_find) !== -1;
}
function _occurrences(str, str_rgxp) {
let rtn = 0;
if (_isNonEmptyStr(str) && _isNonEmptyStr(str_rgxp)) {
rtn = (str.match(RegExp(str_rgxp, 'g')) || []).length;
}
return rtn;
}
function _toInt(num, min_val, max_val) {
num = Number(num) || 0;
num = num < 0 ? Math.ceil(num) : Math.floor(num);
min_val = Number(min_val);
max_val = Number(max_val);
/*! NO remove default 0, (-0 || 0) = 0*/
min_val = (Number.isNaN(min_val) ? -Infinity : min_val) || 0;
max_val = (Number.isNaN(max_val) ? Infinity : max_val) || 0;
return Math.min(Math.max(num, min_val), max_val);
}
function _isIntOrStrInt(num) {
return String(_toInt(num)) === String(num);
}
function _isNonEmptyStr(val) {
return !!(typeof val === 'string' && val);
}
function _isNonBlankStr(val) {
return !!(typeof val === 'string' && _trimSpaces(val));
}
function _hashCode(val) {
let rtn = 0;
val = _isNonEmptyStr(val) ? val : '';
for (let i = 0, len = val.length; i < len; i++) {
rtn = (rtn << 5) - rtn + val.charCodeAt(i);
rtn |= 0;
}
return rtn;
}
function _castlingChars(num) {
const castling_chars = ['', 'k', 'q', 'kq'];
return castling_chars[_toInt(num, 0, castling_chars.length - 1)];
}
function _unreferenceP(p, changes) {
let rtn = _isObject(p) ? { ...p } : {};
if (_isArray(changes)) {
for (let i = 0, len = changes.length; i < len; i++) {
if (!_isArray(changes?.[i]) || changes?.[i].length !== 2 || !_isNonBlankStr(changes[i][0])) {
_consoleLog('[_unreferenceP]: unexpected format', _ALERT_ERROR);
continue;
}
rtn[_trimSpaces(changes[i][0])] = changes[i][1];
}
}
return rtn;
}
function _cleanSan(rtn) {
rtn = _isNonBlankStr(rtn) ? rtn : '';
if (rtn) {
while (rtn !== (rtn = rtn.replace(/\{[^{}]*\}/g, '\n')));
/*! TODO: keep comment*/
while (rtn !== (rtn = rtn.replace(/\([^()]*\)/g, '\n')));
while (rtn !== (rtn = rtn.replace(/\<[^<>]*\>/g, '\n')));
rtn = rtn.replace(/(\t)|(\r?\n)|(\r\n?)/g, '\n');
rtn = rtn.replace(/;+[^\n]*(\n|$)/g, '\n');
/*! TODO: keep comment*/
rtn = rtn
.replace(/^%.*\n?/gm, '')
.replace(/^\n+|\n+$/g, '')
.replace(/\n/g, ' ');
rtn = rtn.replace(/\$\d+/g, ' ');
/*! TODO: keep NAG*/
rtn = rtn.replace(/[^a-h0-9nrqkxo /½=-]/gi, '');
rtn = rtn.replace(/\s*\-+\s*/g, '-');
rtn = rtn.replace(/0-0-0/g, 'w').replace(/0-0/g, 'v');
rtn = rtn.replace(/o-o-o/gi, 'w').replace(/o-o/gi, 'v');
rtn = rtn.replace(/o/gi, '0').replace(/½/g, '1/2');
rtn = rtn
.replace(/1\-0/g, ' i ')
.replace(/0\-1/g, ' j ')
.replace(/1\/2\-1\/2/g, ' z ');
rtn = rtn.replace(/\-/g, ' ');
rtn = rtn.replace(/w/g, 'O-O-O').replace(/v/g, 'O-O');
rtn = rtn.replace(/i/g, _RESULT_W_WINS).replace(/j/g, _RESULT_B_WINS).replace(/z/g, _RESULT_DRAW);
rtn = _trimSpaces(rtn);
}
return rtn;
}
function _cloneBoardToObj(to_obj = {}, from_woard) {
block: {
if (!_isObject(to_obj)) {
_consoleLog('[_cloneBoardToObj]: to_obj must be Object type', _ALERT_ERROR);
break block;
}
let from_board = getBoard(from_woard);
if (from_board === null) {
_consoleLog("[_cloneBoardToObj]: from_woard doesn't exist", _ALERT_ERROR);
break block;
}
if (to_obj === from_board) {
_consoleLog('[_cloneBoardToObj]: trying to self clone', _ALERT_ERROR);
break block;
}
to_obj.moveList = [];
to_obj.legalUci = [];
to_obj.legalUciTree = {};
to_obj.legalRevTree = {};
for (let i = 0, len = _MUTABLE_KEYS.length; i < len; i++) {
let current_key = _MUTABLE_KEYS[i];
let to_prop = to_obj[current_key];
let from_prop = from_board[current_key];
if (!to_prop && (current_key === 'w' || current_key === 'b' || current_key === 'squares')) {
to_obj[current_key] = {};
to_prop = to_obj[current_key];
}
if (!_isObject(from_prop) && !_isArray(from_prop)) {
to_obj[current_key] = from_prop;
continue;
}
if (current_key === 'legalUci') {
to_obj.legalUci = from_board.legalUci.slice(0);
continue;
}
if (current_key === 'w' || current_key === 'b') {
to_prop.materialDiff = from_prop.materialDiff.slice(0);
to_prop.isBlack = from_prop.isBlack;
to_prop.sign = from_prop.sign;
to_prop.firstRankPos = from_prop.firstRankPos;
to_prop.secondRankPos = from_prop.secondRankPos;
to_prop.lastRankPos = from_prop.lastRankPos;
to_prop.singlePawnRankShift = from_prop.singlePawnRankShift;
to_prop.pawn = from_prop.pawn;
to_prop.knight = from_prop.knight;
to_prop.bishop = from_prop.bishop;
to_prop.rook = from_prop.rook;
to_prop.queen = from_prop.queen;
to_prop.king = from_prop.king;
to_prop.kingBos = from_prop.kingBos;
to_prop.castling = from_prop.castling;
continue;
}
let sub_keys = Object.keys(from_prop);
for (let j = 0, len2 = sub_keys.length; j < len2; j++) {
let sub_current_key = sub_keys[j];
let sub_to_prop = to_prop[sub_current_key];
let sub_from_prop = from_prop[sub_current_key];
if (!sub_to_prop && current_key === 'squares') {
to_prop[sub_current_key] = {};
sub_to_prop = to_prop[sub_current_key];
}
if (!_isObject(sub_from_prop) && !_isArray(sub_from_prop)) {
_consoleLog('[_cloneBoardToObj]: unexpected primitive data type', _ALERT_ERROR);
continue;
}
if (current_key === 'legalUciTree') {
to_prop[sub_current_key] = sub_from_prop.slice(0);
continue;
}
if (current_key === 'squares') {
sub_to_prop.pos = sub_from_prop.pos.slice(0);
sub_to_prop.bos = sub_from_prop.bos;
sub_to_prop.rankPos = sub_from_prop.rankPos;
sub_to_prop.filePos = sub_from_prop.filePos;
sub_to_prop.rankBos = sub_from_prop.rankBos;
sub_to_prop.fileBos = sub_from_prop.fileBos;
sub_to_prop.bal = sub_from_prop.bal;
sub_to_prop.absBal = sub_from_prop.absBal;
sub_to_prop.val = sub_from_prop.val;
sub_to_prop.absVal = sub_from_prop.absVal;
sub_to_prop.className = sub_from_prop.className;
sub_to_prop.sign = sub_from_prop.sign;
sub_to_prop.isEmptySquare = sub_from_prop.isEmptySquare;
sub_to_prop.isPawn = sub_from_prop.isPawn;
sub_to_prop.isKnight = sub_from_prop.isKnight;
sub_to_prop.isBishop = sub_from_prop.isBishop;
sub_to_prop.isRook = sub_from_prop.isRook;
sub_to_prop.isQueen = sub_from_prop.isQueen;
sub_to_prop.isKing = sub_from_prop.isKing;
continue;
}
let sub_sub_keys = Object.keys(sub_from_prop);
if (current_key === 'moveList' || current_key === 'legalRevTree') {
to_prop[sub_current_key] = {};
sub_to_prop = to_prop[sub_current_key];
/*! NO put a "continue" in here*/
}
for (let k = 0, len3 = sub_sub_keys.length; k < len3; k++) {
let sub_sub_current_key = sub_sub_keys[k];
let sub_sub_from_prop = sub_from_prop[sub_sub_current_key];
if (current_key === 'legalRevTree') {
sub_to_prop[sub_sub_current_key] = sub_sub_from_prop.slice(0);
continue;
}
if (_isObject(sub_sub_from_prop) || _isArray(sub_sub_from_prop)) {
_consoleLog('[_cloneBoardToObj]: unexpected type in key "' + sub_sub_current_key + '"', _ALERT_ERROR);
continue;
}
sub_to_prop[sub_sub_current_key] = sub_sub_from_prop;
}
}
}
}
return to_obj;
}
function _basicFenTest(fen) {
let rtn_msg = '';
block: {
fen = String(fen);
if (fen.length < 20) {
rtn_msg = 'Error [0] fen is too short';
break block;
}
fen = _trimSpaces(fen);
let optional_clocks = fen.replace(
/^([rnbqkRNBQK1-8]+\/)([rnbqkpRNBQKP1-8]+\/){6}([rnbqkRNBQK1-8]+)\s[bw]\s(-|K?Q?k?q?)\s(-|[a-h][36])($|\s)/,
''
);
if (fen.length === optional_clocks.length) {
rtn_msg = 'Error [1] invalid fen structure';
break block;
}
if (optional_clocks.length) {
if (!/^(0|[1-9][0-9]*)\s([1-9][0-9]*)$/.test(optional_clocks)) {
rtn_msg = 'Error [2] invalid half/full move';
break block;
}
}
let fen_board = fen.split(' ')[0];
let fen_board_arr = fen_board.split('/');
for (let i = 0; i < 8; i++) {
let total_files_in_current_rank = 0;
let last_is_num = false;
for (let j = 0, len = fen_board_arr[i].length; j < len; j++) {
let current_num_or_nan = Number(fen_board_arr[i].charAt(j));
let current_is_num = !!current_num_or_nan;
if (last_is_num && current_is_num) {
rtn_msg = 'Error [3] two consecutive numeric values';
break block;
}
last_is_num = current_is_num;
total_files_in_current_rank += current_num_or_nan || 1;
}
if (total_files_in_current_rank !== 8) {
rtn_msg = 'Error [4] rank without exactly 8 columns';
break block;
}
}
let index_w_king = fen_board.indexOf('K');
if (index_w_king === -1 || fen_board.lastIndexOf('K') !== index_w_king) {
rtn_msg = 'Error [5] board without exactly one white king';
break block;
}
let index_b_king = fen_board.indexOf('k');
if (index_b_king === -1 || fen_board.lastIndexOf('k') !== index_b_king) {
rtn_msg = 'Error [6] board without exactly one black king';
break block;
}
}
return rtn_msg;
}
function _perft(woard, depth, specific_uci) {
let rtn = 1;
block: {
if (depth < 1) {
break block;
}
let board = getBoard(woard);
if (board === null) {
break block;
}
if (board.isPuzzleMode) {
break block;
}
let count = 0;
for (let i = 0, len = board.legalUci.length; i < len; i++) {
if (specific_uci && specific_uci !== board.legalUci[i]) {
continue;
}
if (depth === 1) {
count++;
} else {
board.playMove(board.legalUci[i], { isLegalMove: true });
count += _perft(board, depth - 1);
board.navPrevious();
}
}
rtn = count;
}
return rtn;
}
//!---------------- board
function _getSquare(qos, p) {
let that = this;
let rtn = null;
function _squareHelper(my_square, is_unreferenced) {
let rtn_square = my_square;
if (is_unreferenced) {
let square_pos = toPos(my_square.pos);
let square_rank_pos = getRankPos(my_square.pos);
let square_file_pos = getFilePos(my_square.pos);
let square_rank_bos = getRankBos(my_square.pos);
let square_file_bos = getFileBos(my_square.pos);
rtn_square = {
pos: square_pos,
//unreference
bos: my_square.bos,
rankPos: square_rank_pos,
filePos: square_file_pos,
rankBos: square_rank_bos,
fileBos: square_file_bos,
bal: my_square.bal,
absBal: my_square.absBal,
val: my_square.val,
absVal: my_square.absVal,
className: my_square.className,
sign: my_square.sign,
isEmptySquare: my_square.isEmptySquare,
isPawn: my_square.isPawn,
isKnight: my_square.isKnight,
isBishop: my_square.isBishop,
isRook: my_square.isRook,
isQueen: my_square.isQueen,
isKing: my_square.isKing,
};
}
return rtn_square;
}
p = _unreferenceP(p);
let temp_pos = toPos(qos);
p.isUnreferenced = p.isUnreferenced === true;
if (temp_pos !== null) {
let pre_validated_pos = [temp_pos[0] + _toInt(p.rankShift), temp_pos[1] + _toInt(p.fileShift)];
if (isInsideBoard(pre_validated_pos)) {
let validated_pos = pre_validated_pos;
rtn = _squareHelper(that.squares[String(toBos(validated_pos) || '')], p.isUnreferenced);
}
}
return rtn;
}
function _setSquare(qos, new_qal, p) {
let that = this;
let rtn = that.getSquare(qos, _unreferenceP(p, [['isUnreferenced', false]])) || null;
block: {
if (rtn === null) {
break block;
}
let new_val = toVal(new_qal);
if (rtn.val === new_val) {
break block;
}
let new_abs_val = toAbsVal(new_val);
rtn.bal = toBal(new_val);
rtn.absBal = toAbsBal(new_val);
rtn.val = new_val;
rtn.absVal = new_abs_val;
rtn.className = toClassName(new_val);
rtn.sign = getSign(new_val);
rtn.isEmptySquare = new_abs_val === _EMPTY_SQR;
rtn.isPawn = new_abs_val === _PAWN_W;
rtn.isKnight = new_abs_val === _KNIGHT_W;
rtn.isBishop = new_abs_val === _BISHOP_W;
rtn.isRook = new_abs_val === _ROOK_W;
rtn.isQueen = new_abs_val === _QUEEN_W;
rtn.isKing = new_abs_val === _KING_W;
if (rtn.isKing) {
let king_bos = toBos(qos);
let current_side = rtn.sign < 0 ? that.b : that.w;
current_side.kingBos = king_bos;
}
}
return rtn;
}
function _attackersFromActive(target_qos, early_break) {
let that = this;
that.toggleActiveNonActive();
let rtn_total_attackers = that.attackersFromNonActive(target_qos, early_break);
that.toggleActiveNonActive();
return rtn_total_attackers;
}
function _attackersFromNonActive(target_qos, early_break) {
let that = this;
function _isAttacked(qos, piece_direction, as_knight) {
let rtn_is_attacked = that.testCollision(
_TEST_COLLISION_OP_IS_ATTACKED,
qos,
piece_direction,
as_knight,
0,
false
).isAttacked;
return rtn_is_attacked;
}
let rtn_total_attackers = 0;
let active_side = that[that.activeColor];
let king_bos = active_side.kingBos;
target_qos = target_qos || king_bos;
outer: for (let i = 0; i < 2; i++) {
let as_knight = !!i;
for (let j = _DIRECTION_TOP; j <= _DIRECTION_TOP_LEFT; j++) {
if (_isAttacked(target_qos, j, as_knight)) {
rtn_total_attackers++;
if (early_break) {
break outer;
}
}
}
}
return rtn_total_attackers;
}
function _toggleActiveNonActive(new_active) {
let that = this;
let rtn_changed = false;
let temp = typeof new_active === 'boolean' ? new_active : !that[that.activeColor].isBlack;
if ((temp ? 'b' : 'w') !== that.activeColor || (!temp ? 'b' : 'w') !== that.nonActiveColor) {
rtn_changed = true;
that.activeColor = temp ? 'b' : 'w';
that.nonActiveColor = !temp ? 'b' : 'w';
}
return rtn_changed;
}
function _toggleIsRotated(new_is_rotated) {
let that = this;
let rtn_changed = false;
let temp = typeof new_is_rotated === 'boolean' ? new_is_rotated : !that.isRotated;
if (temp !== that.isRotated) {
rtn_changed = true;
that.isRotated = temp;
that.refreshUi(0, false);
}
return rtn_changed;
}
function _setPromoteTo(qal) {
let that = this;
let rtn_changed = false;
let temp = _promoteValHelper(qal);
if (temp !== that.promoteTo) {
rtn_changed = true;
that.promoteTo = temp;
that.refreshUi(0, false);
}
return rtn_changed;
}
function _silentlyResetOptions() {
let that = this;
that.isHidden = true;
that.isRotated = false;
that.setPromoteTo(_QUEEN_W);
that.isHidden = false;
}
function _silentlyResetManualResult() {
let that = this;
let cache_is_hidden = that.isHidden;
that.isHidden = true;
that.setManualResult(_RESULT_ONGOING);
that.isHidden = cache_is_hidden;
}
function _setManualResult(str) {
let that = this;
let rtn_changed = false;
let temp = _pgnResultHelper(str) || _RESULT_ONGOING;
if (temp !== that.manualResult) {
rtn_changed = true;
that.manualResult = temp;
that.refreshUi(0, false);
}
return rtn_changed;
}
function _setCurrentMove(num, is_goto, is_puzzle_move) {
let that = this;
let rtn_changed = false;
block: {
if (that.isPuzzleMode && !is_puzzle_move) {
break block;
}
let len = that.moveList.length;
if (len < 2) {
break block;
}
if (typeof is_goto !== 'boolean') {
num = _toInt(num, 0, len - 1);
let diff = num - that.currentMove;
is_goto = Math.abs(diff) !== 1;
num = is_goto ? num : diff;
}
num = _toInt(num);
let temp = _toInt(is_goto ? num : num + that.currentMove, 0, len - 1);
if (temp === that.currentMove) {
break block;
}
that.updateHelper({
currentMove: temp,
fen: that.moveList[temp].fen,
skipFenValidation: true,
});
/*! NO remove skipFenValidation*/
that.refreshUi(is_goto ? 0 : num, true);
rtn_changed = true;
}
return rtn_changed;
}
function _navFirst() {
let that = this;
return that.setCurrentMove(0);
}
function _navPrevious() {
let that = this;
return that.setCurrentMove(that.currentMove - 1);
}
function _navNext() {
let that = this;
return that.setCurrentMove(that.currentMove + 1);
}
function _navLast() {
let that = this;
return that.setCurrentMove(that.moveList.length - 1);
}
function _navLinkMove(move_index) {
let that = this;
return that.setCurrentMove(move_index);
}
function _loadFen(fen, p) {
let that = this;
let rtn_changed = false;
p = _unreferenceP(p);
block: {
if (that.isPuzzleMode) {
break block;
}
p.skipFenValidation = p.skipFenValidation === true;
p.keepOptions = p.keepOptions === true;
let hash_cache = that.boardHash();
let temp = that.updateHelper({
currentMove: 0,
fen,
skipFenValidation: p.skipFenValidation,
resetOptions: !p.keepOptions,
resetMoveList: true,
});
if (!temp) {
_consoleLog('[_loadFen]: bad FEN', _ALERT_ERROR);
break block;
}
that.silentlyResetManualResult();
if (that.boardHash() !== hash_cache) {
rtn_changed = true;
that.refreshUi(0, false);
}
}
return rtn_changed;
}
function _loadValidatedFen(fen) {
let that = this;
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
that.setSquare([i, j], _EMPTY_SQR);
}
}
fen = _trimSpaces(fen);
let fen_parts = fen.split(' ');
let fen_board_arr = fen_parts[0].split('/');
for (let i = 0; i < 8; i++) {
let current_file = 0;
for (let j = 0, len = fen_board_arr[i].length; j < len; j++) {
let current_char = fen_board_arr[i].charAt(j);
let current_num_or_nan = Number(current_char);
if (!current_num_or_nan) {
that.setSquare([i, current_file], current_char);
}
current_file += current_num_or_nan || 1;
}
}
let castling_w_rights =
(_strContains(fen_parts[2], 'K') ? _SHORT_CASTLE : 0) + (_strContains(fen_parts[2], 'Q') ? _LONG_CASTLE : 0);
that.w.castling = castling_w_rights;
let castling_b_rights =
(_strContains(fen_parts[2], 'k') ? _SHORT_CASTLE : 0) + (_strContains(fen_parts[2], 'q') ? _LONG_CASTLE : 0);
that.b.castling = castling_b_rights;
let en_passant_bos = fen_parts[3].replace('-', '');
that.enPassantBos = en_passant_bos;
that.toggleActiveNonActive(fen_parts[1] === 'b');
that.halfMove = Number(fen_parts[4]) || 0;
that.fullMove = Number(fen_parts[5]) || 1;
}
function _getClocklessFenHelper() {
let that = this;
let rtn = '';
let fen_board = '';
for (let i = 0; i < 8; i++) {
let consecutive_empty_squares = 0;
for (let j = 0; j < 8; j++) {
let current_square = that.getSquare([i, j]);
if (current_square !== null && !current_square.isEmptySquare) {
fen_board += (consecutive_empty_squares || '') + (current_square.bal || '');
consecutive_empty_squares = -1;
}
consecutive_empty_squares++;
}
fen_board += (consecutive_empty_squares || '') + (i !== 7 ? '/' : '');
}
rtn += fen_board;
rtn += ' ' + that.activeColor;
rtn += ' ' + (_castlingChars(that.w.castling).toUpperCase() + '' + _castlingChars(that.b.castling) || '-');
rtn += ' ' + (that.enPassantBos || '-');
return rtn;
}
function _updateFenAndMisc(sliced_fen_history) {
let that = this;
that.checks = that.attackersFromNonActive(null);
that.isCheck = !!that.checks;
/*! NO move below legalMovesHelper()*/
that.legalUci = [];
that.legalUciTree = {};
that.legalRevTree = {};
for (let i = 0; i < 8; i++) {
for (let j = 0; j < 8; j++) {
let current_pos = [i, j];
let legal_moves = that.legalMovesHelper(current_pos);
let len = legal_moves.uciMoves.length;
if (!len) {
continue;
}
let from_bos = toBos([i, j]);
that.legalUciTree[from_bos] = [];
for (let k = 0; k < len; k++) {
let uci_move = legal_moves.uciMoves[k];
if (legal_moves.isPromotion) {
for (let m = _KNIGHT_W; m <= _QUEEN_W; m++) {
let uci_promotion_move = uci_move + toBal(m).toLowerCase();
that.legalUci.push(uci_promotion_move);
that.legalUciTree[from_bos].push(uci_promotion_move);
}
} else {
that.legalUci.push(uci_move);
that.legalUciTree[from_bos].push(uci_move);
}
let to_bos = uci_move.slice(2, 4);
if (!that.legalRevTree[to_bos]) {
that.legalRevTree[to_bos] = {};
}
if (!that.legalRevTree[to_bos][legal_moves.piece]) {
that.legalRevTree[to_bos][legal_moves.piece] = [];
}
that.legalRevTree[to_bos][legal_moves.piece].push(from_bos);
}
}
}
that.isCheckmate = that.isCheck && !that.legalUci.length;
that.isStalemate = !that.isCheck && !that.legalUci.length;
if (that.enPassantBos) {
let can_en_passant = false;
if (that.legalRevTree[that.enPassantBos] && that.legalRevTree[String(that.enPassantBos)]['p']) {
can_en_passant = true;
}
if (!can_en_passant) {
that.enPassantBos = '';
}
}
let clockless_fen = that.getClocklessFenHelper();
that.fen = clockless_fen + ' ' + that.halfMove + ' ' + that.fullMove;
that.isThreefold = false;
if (sliced_fen_history || (that.moveList && that.currentMove > 7 && that.halfMove > 7)) {
let times_found = 1;
let fen_arr = sliced_fen_history || that.fenHistoryExport();
let i = sliced_fen_history ? sliced_fen_history.length - 1 : that.currentMove - 1;
for (; i >= 0; i--) {
let fen_parts = fen_arr[i].split(' ');
if (fen_parts.slice(0, 4).join(' ') === clockless_fen) {
times_found++;
if (times_found > 2) {
that.isThreefold = true;
break;
}
}
if (fen_parts[4] === '0') {
break;
}
}
}
let total_pieces = countPieces(clockless_fen);
that.isInsufficientMaterial = false;
if (
!(
total_pieces.w.p +
total_pieces.b.p +
total_pieces.w.r +
total_pieces.b.r +
total_pieces.w.q +
total_pieces.b.q
)
) {
if (total_pieces.w.n + total_pieces.b.n) {
that.isInsufficientMaterial = total_pieces.w.n + total_pieces.b.n + total_pieces.w.b + total_pieces.b.b === 1;
} else if (total_pieces.w.b + total_pieces.b.b) {
let bishop_count = that.countLightDarkBishops();
let at_least_one_light = !!(bishop_count.w.lightSquaredBishops + bishop_count.b.lightSquaredBishops);
let at_least_one_dark = !!(bishop_count.w.darkSquaredBishops + bishop_count.b.darkSquaredBishops);
that.isInsufficientMaterial = at_least_one_light !== at_least_one_dark;
} else {
that.isInsufficientMaterial = true;
}
}
that.isFiftyMove = that.halfMove >= 100;
that.inDraw =
!that.isCheckmate && (that.isStalemate || that.isThreefold || that.isInsufficientMaterial || that.isFiftyMove);
that.w.materialDiff = [];
that.b.materialDiff = [];
for (let i = _PAWN_W; i <= _KING_W; i++) {
let piece_bal = toBal(-i);
let current_diff = total_pieces.w[piece_bal] - total_pieces.b[piece_bal];
for (let j = 0, len = Math.abs(current_diff); j < len; j++) {
if (current_diff > 0) {
let w_piece_val = i;
that.w.materialDiff.push(w_piece_val);
} else {
let b_piece_val = -i;
that.b.materialDiff.push(b_piece_val);
}
}
}
}
function _refinedFenTest() {
let that = this;
let rtn_msg = '';
block: {
let active_side = that[that.activeColor];
let non_active_side = that[that.nonActiveColor];
if (that.halfMove - Number(active_side.isBlack) + 1 >= that.fullMove * 2) {
rtn_msg = 'Error [0] exceeding half moves ratio';
break block;
}
if (that.checks > 2) {
rtn_msg = 'Error [1] king is checked more times than possible';
break block;
}
if (that.attackersFromActive(null, true)) {
rtn_msg = 'Error [2] non-active king in check';
break block;
}
if (that.enPassantBos) {
let en_passant_square = that.getSquare(that.enPassantBos);
let infront_ep_is_empty = that.getSquare(en_passant_square, {
rankShift: active_side.singlePawnRankShift,
}).isEmptySquare;
let behind_ep_val = that.getSquare(en_passant_square, {
rankShift: non_active_side.singlePawnRankShift,
}).val;
if (
that.halfMove ||
!en_passant_square.isEmptySquare ||
en_passant_square.rankPos !== (active_side.isBlack ? 5 : 2) ||
!infront_ep_is_empty ||
behind_ep_val !== non_active_side.pawn
) {
rtn_msg = 'Error [3] bad en-passant';
break block;
}
}
let total_pieces = countPieces(that.fen);
let bishop_count = that.countLightDarkBishops();
for (let i = 0; i < 2; i++) {
let current_side = i ? total_pieces.b : total_pieces.w;
let current_other_side = i ? total_pieces.w : total_pieces.b;
let current_bishop_count = i ? bishop_count.b : bishop_count.w;
if (current_side.p > 8) {
rtn_msg = 'Error [' + (i + 4) + '] more than 8 pawns';
break block;
}
let current_promoted_count =
Math.max(current_side.n - 2, 0) +
Math.max(current_bishop_count.lightSquaredBishops - 1, 0) +
Math.max(current_bishop_count.darkSquaredBishops - 1, 0) +
Math.max(current_side.r - 2, 0) +
Math.max(current_side.q - 1, 0);
let temp =
current_other_side.p +
current_other_side.n +
current_other_side.b +
current_other_side.r +
current_other_side.q +
current_other_side.k;
if (temp === 16 && current_promoted_count) {
rtn_msg = 'Error [' + (i + 6) + '] promoted pieces without capturing any piece';
break block;
}
if (current_promoted_count > 8 - current_side.p) {
rtn_msg = 'Error [' + (i + 8) + '] promoted pieces exceed the number of missing pawns';
break block;
}
}
let fen_board = that.fen.split(' ')[0];
for (let i = 0; i < 2; i++) {
let current_side = i ? that.b : that.w;
let min_captured = 0;
for (let j = 0; j < 8; j++) {
let total_pawns_in_current_file = 0;
for (let k = 0; k < 8; k++) {
total_pawns_in_current_file += Number(that.getSquare([k, j]).val === current_side.pawn);
}
if (total_pawns_in_current_file > 1) {
let temp = j === 0 || j === 7 ? [1, 3, 6, 10, 99] : [1, 2, 4, 6, 9];
min_captured += temp[total_pawns_in_current_file - 2];
}
}
if (min_captured > 15 - _occurrences(fen_board, i ? 'P|N|B|R|Q' : 'p|n|b|r|q')) {
rtn_msg = 'Error [10] not enough captured pieces to support the total doubled pawns';
break block;
}
}
for (let i = 0; i < 2; i++) {
let current_side = i ? that.b : that.w;
if (!current_side.castling) {
continue;
}
let temp = {
completeActiveColor: i ? 'black' : 'white',
originalKingBos: i ? 'e8' : 'e1',
originalLongRookBos: i ? 'a8' : 'a1',
originalShortRookBos: i ? 'h8' : 'h1',
};
if (that.getSquare(temp.originalKingBos).val !== current_side.king) {
rtn_msg = 'Error [11] ' + temp.completeActiveColor + ' castling rights without king in original square';
break block;
}
if (
current_side.castling !== _LONG_CASTLE &&
that.getSquare(temp.originalShortRookBos).val !== current_side.rook
) {
rtn_msg = 'Error [12] ' + temp.completeActiveColor + ' short castling rights with missing H-file rook';
break block;
}
if (
current_side.castling !== _SHORT_CASTLE &&
that.getSquare(temp.originalLongRookBos).val !== current_side.rook
) {
rtn_msg = 'Error [13] ' + temp.completeActiveColor + ' long castling rights with missing A-file rook';
break block;
}
}
}
return rtn_msg;
}
function _testCollision(op, initial_qos, piece_direction, as_knight, max_shifts, allow_capture) {
let that = this;
let rtn = {
candidateMoves: [],
isAttacked: false,
};
let active_side = that[that.activeColor];
piece_direction = _toInt(piece_direction, 1, 8);
max_shifts = _toInt(as_knight ? 1 : max_shifts || 7);
let rank_change = (as_knight ? [-2, -1, 1, 2, 2, 1, -1, -2] : [-1, -1, 0, 1, 1, 1, 0, -1])[piece_direction - 1];
let file_change = (as_knight ? [1, 2, 2, 1, -1, -2, -2, -1] : [0, 1, 1, 1, 0, -1, -1, -1])[piece_direction - 1];
for (let i = 0; i < max_shifts; i++) {
let current_square = that.getSquare(initial_qos, {
rankShift: rank_change * (i + 1),
fileShift: file_change * (i + 1),
});
if (current_square === null) {
break;
}
if (current_square.isEmptySquare) {
if (op === 1) {
rtn.candidateMoves.push(current_square.bos);
}
continue;
}
if (current_square.sign === active_side.sign) {
break;
}
if (op === 1) {
if (allow_capture && !current_square.isKing) {
rtn.candidateMoves.push(current_square.bos);
}
}
if (op === 2) {
if (as_knight) {
if (current_square.isKnight) {
rtn.isAttacked = true;
}
} else if (current_square.isKing) {
if (!i) {
rtn.isAttacked = true;
}
} else if (current_square.isQueen) {
rtn.isAttacked = true;
} else if (piece_direction % 2) {
if (current_square.isRook) {
rtn.isAttacked = true;
}
} else if (current_square.isBishop) {
rtn.isAttacked = true;
} else if (!i && current_square.isPawn) {
if (Number(current_square.sign) > 0) {
if (piece_direction === _DIRECTION_BOTTOM_RIGHT || piece_direction === _DIRECTION_BOTTOM_LEFT) {
rtn.isAttacked = true;
}
} else {
if (piece_direction === _DIRECTION_TOP_RIGHT || piece_direction === _DIRECTION_TOP_LEFT) {
rtn.isAttacked = true;
}
}
}
}
break;
}
return rtn;
}
function _legalMovesHelper(target_qos) {
let that = this;
let rtn = {
uciMoves: [],
piece: '',
isPromotion: false,
};
function _candidateMoves(qos, piece_direction, as_knight, max_shifts, allow_capture) {
let rtn_candidate_moves =