UNPKG

isepic-chess

Version:

Chess utility library written in JavaScript

1,824 lines (1,449 loc) 115 kB
/** Copyright (c) 2025 Ajax Isepic (ajax333221) Licensed MIT */ /* jshint undef:true, unused:true, jquery:false, curly:true, latedef:nofunc, bitwise:false, eqeqeq:true, esversion:9 */ /* globals exports, define */ (function (windw, expts, defin) { var Ic = (function (_WIN) { var _VERSION = '8.7.1'; var _SILENT_MODE = true; var _BOARDS = {}; var _EMPTY_SQR = 0; var _PAWN = 1; var _KNIGHT = 2; var _BISHOP = 3; var _ROOK = 4; var _QUEEN = 5; var _KING = 6; var _DIRECTION_TOP = 1; var _DIRECTION_TOP_RIGHT = 2; var _DIRECTION_RIGHT = 3; var _DIRECTION_BOTTOM_RIGHT = 4; var _DIRECTION_BOTTOM = 5; var _DIRECTION_BOTTOM_LEFT = 6; var _DIRECTION_LEFT = 7; var _DIRECTION_TOP_LEFT = 8; var _SHORT_CASTLE = 1; var _LONG_CASTLE = 2; var _RESULT_ONGOING = '*'; var _RESULT_W_WINS = '1-0'; var _RESULT_B_WINS = '0-1'; var _RESULT_DRAW = '1/2-1/2'; var _DEFAULT_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1'; var _ALERT_LIGHT = 'light'; var _ALERT_DARK = 'dark'; var _ALERT_SUCCESS = 'success'; var _ALERT_WARNING = 'warning'; var _ALERT_ERROR = 'error'; var _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(qal) { return _toInt(toAbsVal(qal) || _QUEEN, _KNIGHT, _QUEEN); } function _pgnResultHelper(str) { var rtn; rtn = ''; 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) { var temp, pc_exec, rtn; rtn = 0; block: { if (!str) { break block; } if (!Number.isNaN(str * 1) && _isIntOrStrInt(str)) { rtn = _toInt(str, -_KING, _KING); break block; } str = _trimSpaces(str); if (/^[pnbrqk]$/i.test(str)) { temp = str.toLowerCase(); rtn = ('pnbrqk'.indexOf(temp) + 1) * getSign(str === temp); break block; } pc_exec = /^([wb])([pnbrqk])$/.exec(str.toLowerCase()); if (pc_exec) { rtn = ('pnbrqk'.indexOf(pc_exec[2]) + 1) * getSign(pc_exec[1] === 'b'); break block; } } return rtn; } function _strToBosHelper(str) { var rtn; rtn = null; str = _trimSpaces(str); if (str && /^[a-h][1-8]$/i.test(str)) { rtn = str.toLowerCase(); } return rtn; } function _arrToPosHelper(arr) { var rank_pos, file_pos, rtn; rtn = null; if (_isArray(arr) && arr.length === 2) { rank_pos = _toInt(arr[0]); file_pos = _toInt(arr[1]); if (rank_pos <= 7 && rank_pos >= 0 && file_pos <= 7 && file_pos >= 0) { rtn = [rank_pos, file_pos]; } } return rtn; } function _pgnParserHelper(str) { var g, temp, rgxp, mtch, meta_tags, move_list, game_result, last_index, rtn; rtn = null; block: { if (!_isNonBlankStr(str)) { break block; } meta_tags = {}; last_index = -1; rgxp = /\[\s*(\w+)\s+\"([^\"]*)\"\s*\]/g; str = str.replace(/“|”/g, '"'); while ((mtch = rgxp.exec(str))) { last_index = rgxp.lastIndex; meta_tags[_trimSpaces(mtch[1])] = _trimSpaces(mtch[2]); } if (last_index === -1) { last_index = 0; } g = ' ' + _cleanSan(str.slice(last_index)); move_list = []; last_index = -1; rgxp = /\s+([1-9][0-9]*)*\s*\.*\s*\.*\s*([^\s]+)/g; while ((mtch = rgxp.exec(g))) { last_index = rgxp.lastIndex; temp = mtch[0]; move_list.push(mtch[2]); } if (last_index === -1) { break block; } game_result = _RESULT_ONGOING; temp = _pgnResultHelper(temp); if (temp) { move_list.pop(); game_result = temp; } if (meta_tags.Result) { temp = _pgnResultHelper(meta_tags.Result); if (temp) { meta_tags.Result = temp; game_result = temp; } } rtn = { tags: meta_tags, sanMoves: move_list, result: game_result, }; } return rtn; } function _uciParserHelper(str) { var rtn; 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) { var temp, possible_promote, rtn; rtn = null; block: { if (!_isNonBlankStr(mov)) { break block; } mov = _trimSpaces(mov); if (mov.length !== 4 && mov.length !== 5) { break block; } temp = [_strToBosHelper(mov.slice(0, 2)), _strToBosHelper(mov.slice(2, 4))]; if (temp[0] === null || temp[1] === null) { break block; } possible_promote = mov.charAt(4) || ''; rtn = [temp, possible_promote]; } return rtn; } //p = {delimiter} function _joinedWrapmoveHelper(mov, p) { var temp, rtn; rtn = null; p = _unreferenceP(p); block: { p.delimiter = _isNonEmptyStr(p.delimiter) ? p.delimiter.charAt(0) : '-'; if (!_isNonBlankStr(mov)) { break block; } mov = _trimSpaces(mov); if (mov.length !== 5 || mov.charAt(2) !== p.delimiter) { break block; } temp = mov.split(p.delimiter); temp = [_strToBosHelper(temp[0]), _strToBosHelper(temp[1])]; if (temp[0] === null || temp[1] === null) { break block; } rtn = temp; } return rtn; } function _fromToWrapmoveHelper(mov) { var rtn; rtn = null; block: { if (!_isArray(mov) || mov.length !== 2) { break block; } if (!isInsideBoard(mov[0]) || !isInsideBoard(mov[1])) { break block; } rtn = [toBos(mov[0]), toBos(mov[1])]; } return rtn; } function _moveWrapmoveHelper(mov) { var possible_promote, rtn; rtn = null; block: { if (!_isMove(mov)) { break block; } possible_promote = mov.promotion || ''; rtn = [[mov.fromBos, mov.toBos], possible_promote]; } return rtn; } function _unreferencedMoveHelper(obj) { var rtn; 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) { var i, j, temp, current_pos, current_bos, target; target = getBoard(board_name); if (target === 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, }; target = _BOARDS[board_name]; } target.w = { //static isBlack: false, sign: 1, firstRankPos: 7, secondRankPos: 6, lastRankPos: 0, singlePawnRankShift: -1, pawn: _PAWN, knight: _KNIGHT, bishop: _BISHOP, rook: _ROOK, queen: _QUEEN, king: _KING, //mutable kingBos: null, castling: null, materialDiff: null, }; target.b = { //static isBlack: true, sign: -1, firstRankPos: 0, secondRankPos: 1, lastRankPos: 7, singlePawnRankShift: 1, pawn: -_PAWN, knight: -_KNIGHT, bishop: -_BISHOP, rook: -_ROOK, queen: -_QUEEN, king: -_KING, //mutable kingBos: null, castling: null, materialDiff: null, }; target.activeColor = null; target.nonActiveColor = null; target.fen = null; target.enPassantBos = null; target.halfMove = null; target.fullMove = null; target.moveList = null; target.currentMove = null; target.isRotated = null; target.isPuzzleMode = null; target.checks = null; target.isCheck = null; target.isCheckmate = null; target.isStalemate = null; target.isThreefold = null; target.isInsufficientMaterial = null; target.isFiftyMove = null; target.inDraw = null; target.promoteTo = null; target.manualResult = null; target.isHidden = null; target.legalUci = null; target.legalUciTree = null; target.legalRevTree = null; target.squares = {}; for (i = 0; i < 8; i++) { //0...7 for (j = 0; j < 8; j++) { //0...7 current_pos = [i, j]; current_bos = toBos(current_pos); target.squares[current_bos] = {}; temp = target.squares[current_bos]; //static temp.pos = current_pos; temp.bos = current_bos; temp.rankPos = getRankPos(current_pos); temp.filePos = getFilePos(current_pos); temp.rankBos = getRankBos(current_pos); temp.fileBos = getFileBos(current_pos); //mutable temp.bal = null; temp.absBal = null; temp.val = null; temp.absVal = null; temp.className = null; temp.sign = null; temp.isEmptySquare = null; temp.isPawn = null; temp.isKnight = null; temp.isBishop = null; temp.isRook = null; temp.isQueen = null; temp.isKing = null; } } return target; } //---------------- utilities function _consoleLog(msg, alert_type) { var rtn; 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 && _WIN.IcUi && _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) { var rtn; 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 = num * 1 || 0; num = num < 0 ? Math.ceil(num) : Math.floor(num); min_val *= 1; max_val *= 1; /*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) { var i, len, hash; hash = 0; val = _isNonEmptyStr(val) ? val : ''; for (i = 0, len = val.length; i < len; i++) { //0<len hash = (hash << 5) - hash + val.charCodeAt(i); hash |= 0; //to 32bit integer } return hash; } function _castlingChars(num) { return ['', 'k', 'q', 'kq'][_toInt(num, 0, 3)]; } function _unreferenceP(p, changes) { var i, len, rtn; rtn = _isObject(p) ? { ...p } : {}; if (_isArray(changes)) { for (i = 0, len = changes.length; i < len; i++) { //0<len 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, ''); //no planned support for P and e.p. 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) { var i, j, k, len, len2, len3, current_key, to_prop, from_prop, sub_current_key, sub_from_prop, sub_to_prop, sub_sub_current_key, sub_sub_from_prop, //sub_sub_to_prop, sub_keys, sub_sub_keys, from_board; block: { if (!_isObject(to_obj)) { _consoleLog('[_cloneBoardToObj]: to_obj must be Object type', _ALERT_ERROR); break block; } 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 (i = 0, len = _MUTABLE_KEYS.length; i < len; i++) { //0<len current_key = _MUTABLE_KEYS[i]; to_prop = to_obj[current_key]; 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]; } //primitive data type if (!_isObject(from_prop) && !_isArray(from_prop)) { to_obj[current_key] = from_prop; //can't use to_prop, it's not a reference here continue; } if (current_key === 'legalUci') { to_obj.legalUci = from_board.legalUci.slice(0); continue; } if (current_key === 'w' || current_key === 'b') { //["w" | "b"] object of (12 static + 3 mutables = 15) Note: materialDiff is array //object or array data type to_prop.materialDiff = from_prop.materialDiff.slice(0); //mutables //primitive data type to_prop.isBlack = from_prop.isBlack; //static to_prop.sign = from_prop.sign; //static to_prop.firstRankPos = from_prop.firstRankPos; //static to_prop.secondRankPos = from_prop.secondRankPos; //static to_prop.lastRankPos = from_prop.lastRankPos; //static to_prop.singlePawnRankShift = from_prop.singlePawnRankShift; //static to_prop.pawn = from_prop.pawn; //static to_prop.knight = from_prop.knight; //static to_prop.bishop = from_prop.bishop; //static to_prop.rook = from_prop.rook; //static to_prop.queen = from_prop.queen; //static to_prop.king = from_prop.king; //static to_prop.kingBos = from_prop.kingBos; //mutables to_prop.castling = from_prop.castling; //mutables continue; } sub_keys = Object.keys(from_prop); for (j = 0, len2 = sub_keys.length; j < len2; j++) { //0<len2 sub_current_key = sub_keys[j]; sub_to_prop = to_prop[sub_current_key]; 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]; } //primitive data type if (!_isObject(sub_from_prop) && !_isArray(sub_from_prop)) { _consoleLog('[_cloneBoardToObj]: unexpected primitive data type', _ALERT_ERROR); continue; } if (current_key === 'legalUciTree') { //["legalUciTree"] object of (0-64), array of (0-N) to_prop[sub_current_key] = sub_from_prop.slice(0); //can't use sub_to_prop, it's not a reference here continue; } if (current_key === 'squares') { //["squares"] object of (64), object of (6 static + 13 mutables = 19) Note: pos is array //object or array data type sub_to_prop.pos = sub_from_prop.pos.slice(0); //static //primitive data type sub_to_prop.bos = sub_from_prop.bos; //static sub_to_prop.rankPos = sub_from_prop.rankPos; //static sub_to_prop.filePos = sub_from_prop.filePos; //static sub_to_prop.rankBos = sub_from_prop.rankBos; //static sub_to_prop.fileBos = sub_from_prop.fileBos; //static sub_to_prop.bal = sub_from_prop.bal; //mutables sub_to_prop.absBal = sub_from_prop.absBal; //mutables sub_to_prop.val = sub_from_prop.val; //mutables sub_to_prop.absVal = sub_from_prop.absVal; //mutables sub_to_prop.className = sub_from_prop.className; //mutables sub_to_prop.sign = sub_from_prop.sign; //mutables sub_to_prop.isEmptySquare = sub_from_prop.isEmptySquare; //mutables sub_to_prop.isPawn = sub_from_prop.isPawn; //mutables sub_to_prop.isKnight = sub_from_prop.isKnight; //mutables sub_to_prop.isBishop = sub_from_prop.isBishop; //mutables sub_to_prop.isRook = sub_from_prop.isRook; //mutables sub_to_prop.isQueen = sub_from_prop.isQueen; //mutables sub_to_prop.isKing = sub_from_prop.isKing; //mutables continue; } 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 (k = 0, len3 = sub_sub_keys.length; k < len3; k++) { //0<len3 sub_sub_current_key = sub_sub_keys[k]; //sub_sub_to_prop = sub_to_prop[sub_sub_current_key]; 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); //can't use sub_sub_to_prop, it's not a reference here continue; } //object or array data type if (_isObject(sub_sub_from_prop) || _isArray(sub_sub_from_prop)) { _consoleLog('[_cloneBoardToObj]: unexpected type in key "' + sub_sub_current_key + '"', _ALERT_ERROR); continue; } //primitive data type sub_to_prop[sub_sub_current_key] = sub_sub_from_prop; //can't use sub_sub_to_prop, it's not a reference here } } } } return to_obj; } function _basicFenTest(fen) { var i, j, len, temp, optional_clocks, last_is_num, current_is_num, fen_board, fen_board_arr, total_files_in_current_rank, rtn_msg; rtn_msg = ''; block: { fen = String(fen); if (fen.length < 20) { rtn_msg = 'Error [0] fen is too short'; break block; } fen = _trimSpaces(fen); 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; } } fen_board = fen.split(' ')[0]; fen_board_arr = fen_board.split('/'); for (i = 0; i < 8; i++) { //0...7 total_files_in_current_rank = 0; last_is_num = false; for (j = 0, len = fen_board_arr[i].length; j < len; j++) { //0<len temp = fen_board_arr[i].charAt(j) * 1; current_is_num = !!temp; 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 += temp || 1; } if (total_files_in_current_rank !== 8) { rtn_msg = 'Error [4] rank without exactly 8 columns'; break block; } } temp = fen_board.indexOf('K'); if (temp === -1 || fen_board.lastIndexOf('K') !== temp) { rtn_msg = 'Error [5] board without exactly one white king'; break block; } temp = fen_board.indexOf('k'); if (temp === -1 || fen_board.lastIndexOf('k') !== temp) { rtn_msg = 'Error [6] board without exactly one black king'; break block; } } return rtn_msg; } function _perft(woard, depth, specific_uci) { var i, len, board, count, rtn; rtn = 1; block: { if (depth < 1) { break block; } board = getBoard(woard); if (board === null) { break block; } if (board.isPuzzleMode) { break block; } count = 0; for (i = 0, len = board.legalUci.length; i < len; i++) { //0<len 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 //p = {rankShift, fileShift, isUnreferenced} function _getSquare(qos, p) { var that, temp_pos, pre_validated_pos, rtn; that = this; function _squareHelper(my_square, is_unreferenced) { //uses: that var temp, rtn_square; rtn_square = my_square; if (is_unreferenced) { temp = {}; temp.pos = toPos(my_square.pos); //unreference temp.bos = my_square.bos; temp.rankPos = getRankPos(my_square.pos); temp.filePos = getFilePos(my_square.pos); temp.rankBos = getRankBos(my_square.pos); temp.fileBos = getFileBos(my_square.pos); temp.bal = my_square.bal; temp.absBal = my_square.absBal; temp.val = my_square.val; temp.absVal = my_square.absVal; temp.className = my_square.className; temp.sign = my_square.sign; temp.isEmptySquare = my_square.isEmptySquare; temp.isPawn = my_square.isPawn; temp.isKnight = my_square.isKnight; temp.isBishop = my_square.isBishop; temp.isRook = my_square.isRook; temp.isQueen = my_square.isQueen; temp.isKing = my_square.isKing; rtn_square = temp; } return rtn_square; } rtn = null; p = _unreferenceP(p); temp_pos = toPos(qos); p.isUnreferenced = p.isUnreferenced === true; if (temp_pos !== null) { pre_validated_pos = [temp_pos[0] + _toInt(p.rankShift), temp_pos[1] + _toInt(p.fileShift)]; if (isInsideBoard(pre_validated_pos)) { rtn = _squareHelper(that.squares[toBos(pre_validated_pos)], p.isUnreferenced); } } return rtn; } //p = {rankShift, fileShift} function _setSquare(qos, new_qal, p) { var that, current_side, new_val, new_abs_val, rtn; that = this; rtn = that.getSquare(qos, _unreferenceP(p, [['isUnreferenced', false]])); block: { if (rtn === null) { break block; } new_val = toVal(new_qal); if (rtn.val === new_val) { break block; } 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; rtn.isKnight = new_abs_val === _KNIGHT; rtn.isBishop = new_abs_val === _BISHOP; rtn.isRook = new_abs_val === _ROOK; rtn.isQueen = new_abs_val === _QUEEN; rtn.isKing = new_abs_val === _KING; if (rtn.isKing) { current_side = rtn.sign < 0 ? that.b : that.w; current_side.kingBos = toBos(qos); } } return rtn; } function _attackersFromActive(target_qos, early_break) { var that, rtn_total_attackers; that = this; that.toggleActiveNonActive(); rtn_total_attackers = that.attackersFromNonActive(target_qos, early_break); that.toggleActiveNonActive(); return rtn_total_attackers; } function _attackersFromNonActive(target_qos, early_break) { var i, j, that, as_knight, active_side, rtn_total_attackers; that = this; function _isAttacked(qos, piece_direction, as_knight) { //uses: that return that.testCollision(2, qos, piece_direction, as_knight, null, null).isAttacked; } rtn_total_attackers = 0; active_side = that[that.activeColor]; target_qos = target_qos || active_side.kingBos; outer: for (i = 0; i < 2; i++) { //0...1 as_knight = !!i; for (j = _DIRECTION_TOP; j <= _DIRECTION_TOP_LEFT; j++) { //1...8 if (_isAttacked(target_qos, j, as_knight)) { rtn_total_attackers++; if (early_break) { break outer; } } } } return rtn_total_attackers; } function _toggleActiveNonActive(new_active) { var that, temp, rtn_changed; that = this; rtn_changed = false; 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) { var that, temp, rtn_changed; that = this; rtn_changed = false; 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); //autorefresh } return rtn_changed; } function _setPromoteTo(qal) { var that, temp, rtn_changed; that = this; rtn_changed = false; temp = _promoteValHelper(qal); if (temp !== that.promoteTo) { rtn_changed = true; that.promoteTo = temp; that.refreshUi(0, false); //autorefresh } return rtn_changed; } function _silentlyResetOptions() { var that; that = this; that.isHidden = true; //prevents ui refresh from setPromoteTo() that.isRotated = false; that.setPromoteTo(_QUEEN); that.isHidden = false; } function _silentlyResetManualResult() { var that, temp; that = this; temp = that.isHidden; that.isHidden = true; that.setManualResult(_RESULT_ONGOING); that.isHidden = temp; } function _setManualResult(str) { var that, temp, rtn_changed; that = this; rtn_changed = false; temp = _pgnResultHelper(str) || _RESULT_ONGOING; if (temp !== that.manualResult) { rtn_changed = true; that.manualResult = temp; that.refreshUi(0, false); //autorefresh } return rtn_changed; } function _setCurrentMove(num, is_goto, is_puzzle_move) { var len, that, temp, diff, rtn_changed; that = this; rtn_changed = false; block: { if (that.isPuzzleMode && !is_puzzle_move) { break block; } len = that.moveList.length; if (len < 2) { break block; } if (typeof is_goto !== 'boolean') { num = _toInt(num, 0, len - 1); diff = num - that.currentMove; is_goto = Math.abs(diff) !== 1; num = is_goto ? num : diff; } num = _toInt(num); 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); //autorefresh rtn_changed = true; } return rtn_changed; } function _navFirst() { var that; that = this; return that.setCurrentMove(0); //autorefresh (sometimes) } function _navPrevious() { var that; that = this; return that.setCurrentMove(that.currentMove - 1); //autorefresh (sometimes) } function _navNext() { var that; that = this; return that.setCurrentMove(that.currentMove + 1); //autorefresh (sometimes) } function _navLast() { var that; that = this; return that.setCurrentMove(that.moveList.length - 1); //autorefresh (sometimes) } function _navLinkMove(move_index) { var that; that = this; return that.setCurrentMove(move_index); //autorefresh (sometimes) } //p = {skipFenValidation, keepOptions} function _loadFen(fen, p) { var that, temp, hash_cache, rtn_changed; that = this; rtn_changed = false; p = _unreferenceP(p); block: { if (that.isPuzzleMode) { break block; } p.skipFenValidation = p.skipFenValidation === true; p.keepOptions = p.keepOptions === true; hash_cache = that.boardHash(); temp = that.updateHelper({ currentMove: 0, fen: 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); //autorefresh } } return rtn_changed; } function _loadValidatedFen(fen) { var i, j, len, that, fen_parts, current_file, current_char, fen_board_arr, skip_files; that = this; for (i = 0; i < 8; i++) { //0...7 for (j = 0; j < 8; j++) { //0...7 that.setSquare([i, j], _EMPTY_SQR); } } fen = _trimSpaces(fen); fen_parts = fen.split(' '); fen_board_arr = fen_parts[0].split('/'); for (i = 0; i < 8; i++) { //0...7 current_file = 0; for (j = 0, len = fen_board_arr[i].length; j < len; j++) { //0<len current_char = fen_board_arr[i].charAt(j); skip_files = current_char * 1; if (!skip_files) { that.setSquare([i, current_file], current_char); } current_file += skip_files || 1; } } that.w.castling = (_strContains(fen_parts[2], 'K') ? _SHORT_CASTLE : 0) + (_strContains(fen_parts[2], 'Q') ? _LONG_CASTLE : 0); that.b.castling = (_strContains(fen_parts[2], 'k') ? _SHORT_CASTLE : 0) + (_strContains(fen_parts[2], 'q') ? _LONG_CASTLE : 0); that.enPassantBos = fen_parts[3].replace('-', ''); that.toggleActiveNonActive(fen_parts[1] === 'b'); that.halfMove = fen_parts[4] * 1 || 0; that.fullMove = fen_parts[5] * 1 || 1; } function _getClocklessFenHelper() { var i, j, that, fen_board, current_square, consecutive_empty_squares, rtn; that = this; fen_board = ''; for (i = 0; i < 8; i++) { //0...7 consecutive_empty_squares = 0; for (j = 0; j < 8; j++) { //0...7 current_square = that.getSquare([i, j]); if (!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) { var i, j, k, m, len, that, temp, temp2, current_diff, from_bos, to_bos, can_en_passant, total_pieces, clockless_fen, times_found, bishop_count, at_least_one_light, at_least_one_dark; that = this; that.checks = that.attackersFromNonActive(null); that.isCheck = !!that.checks; /*NO move below legalMovesHelper()*/ that.legalUci = []; that.legalUciTree = {}; that.legalRevTree = {}; for (i = 0; i < 8; i++) { //0...7 for (j = 0; j < 8; j++) { //0...7 temp = that.legalMovesHelper([i, j]); len = temp.uciMoves.length; if (!len) { continue; } from_bos = toBos([i, j]); that.legalUciTree[from_bos] = []; for (k = 0; k < len; k++) { //0<len temp2 = temp.uciMoves[k]; if (temp.isPromotion) { for (m = _KNIGHT; m <= _QUEEN; m++) { //2...5 that.legalUci.push(temp2 + toBal(m).toLowerCase()); that.legalUciTree[from_bos].push(temp2 + toBal(m).toLowerCase()); } } else { that.legalUci.push(temp2); that.legalUciTree[from_bos].push(temp2); } to_bos = temp2.slice(2, 4); if (!that.legalRevTree[to_bos]) { that.legalRevTree[to_bos] = {}; } if (!that.legalRevTree[to_bos][temp.piece]) { that.legalRevTree[to_bos][temp.piece] = []; } that.legalRevTree[to_bos][temp.piece].push(from_bos); } } } that.isCheckmate = that.isCheck && !that.legalUci.length; that.isStalemate = !that.isCheck && !that.legalUci.length; if (that.enPassantBos) { can_en_passant = false; if (that.legalRevTree[that.enPassantBos] && that.legalRevTree[that.enPassantBos]['p']) { can_en_passant = true; } if (!can_en_passant) { that.enPassantBos = ''; //remove inexecutable en passants } } 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)) { times_found = 1; temp = sliced_fen_history || that.fenHistoryExport(); i = sliced_fen_history ? sliced_fen_history.length - 1 : that.currentMove - 1; for (; i >= 0; i--) { //(len-1)...0 temp2 = temp[i].split(' '); if (temp2.slice(0, 4).join(' ') === clockless_fen) { times_found++; if (times_found > 2) { that.isThreefold = true; break; } } if (temp2[4] === '0') { break; } } } 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; //k vs kn } else if (total_pieces.w.b + total_pieces.b.b) { bishop_count = that.countLightDarkBishops(); at_least_one_light = !!(bishop_count.w.lightSquaredBishops + bishop_count.b.lightSquaredBishops); at_least_one_dark = !!(bishop_count.w.darkSquaredBishops + bishop_count.b.darkSquaredBishops); that.isInsufficientMaterial = at_least_one_light !== at_least_one_dark; //k(b*x) vs k(b*x) } else { //k vs k 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 (i = _PAWN; i <= _KING; i++) { //1...6 temp = toBal(-i); current_diff = total_pieces.w[temp] - total_pieces.b[temp]; for (j = 0, len = Math.abs(current_diff); j < len; j++) { //0<len if (current_diff > 0) { that.w.materialDiff.push(i); } else { that.b.materialDiff.push(-i); } } } } function _refinedFenTest() { var i, j, k, that, temp, en_passant_square, behind_ep_val, infront_ep_is_empty, bishop_count, total_pieces, fen_board, total_pawns_in_current_file, min_captured, active_side, non_active_side, current_side, current_other_side, current_bishop_count, current_promoted_count, rtn_msg; that = this; rtn_msg = ''; block: { active_side = that[that.activeColor]; non_active_side = that[that.nonActiveColor]; if (that.halfMove - 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) { en_passant_square = that.getSquare(that.enPassantBos); infront_ep_is_empty = that.getSquare(en_passant_square, { rankShift: active_side.singlePawnRankShift, }).isEmptySquare; behind_ep_val = that.getSquare(en_passant_square, { rankShift: non_active_side.singlePawnRankShift, }).val; //it is OK if the en passant can't be played next turn or no adjacent pawns exist 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; } } total_pieces = countPieces(that.fen); bishop_count = that.countLightDarkBishops(); for (i = 0; i < 2; i++) { //0...1 current_side = i ? total_pieces.b : total_pieces.w; current_other_side = i ? total_pieces.w : total_pieces.b; current_bishop_count = i ? bishop_count.b : bishop_count.w; //if(current_side.k!==1){...} done in _basicFenTest if (current_side.p > 8) { rtn_msg = 'Error [' + (i + 4) + '] more than 8 pawns'; break block; } 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); 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; } } fen_board = that.fen.split(' ')[0]; for (i = 0; i < 2; i++) { //0...1 current_side = i ? that.b : that.w; min_captured = 0; for (j = 0; j < 8; j++) { //0...7 total_pawns_in_current_file = 0; for (k = 0; k < 8; k++) { //0...7 total_pawns_in_current_file += that.getSquare([k, j]).val === current_side.pawn; } if (total_pawns_in_current_file > 1) { 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 (i = 0; i < 2; i++) { //0...1 current_side = i ? that.b : that.w; if (!current_side.castling) { continue; } 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) { var i, that, current_square, rank_change, file_change, active_side, rtn; that = this; rtn = { candidateMoves: [], isAttacked: false, }; acti