UNPKG

isepic-chess

Version:

Chess utility library written in JavaScript

1,425 lines 110 kB
/*! 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 =