// ==UserScript== // @name Smart Chess Bot: The Ultimate Chess Analysis System // @name:fr Smart Chess Bot: Le système d'analyse ultime pour les échecs // @namespace sayfpack13 // @author sayfpack13 // @version 6.4 // @homepageURL https://github.com/sayfpack13/chess-analysis-bot // @supportURL https://mmgc.life/ // @match https://www.chess.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @grant GM_getResourceText // @grant GM_registerMenuCommand // @description Our chess analysis system is designed to give players the edge they need to win. By using advanced algorithms and cutting-edge technology, our system can analyze any chess position and suggest the best possible move, helping players to make smarter and more informed decisions on the board. // @description:fr Notre système d'analyse d'échecs est conçu pour donner aux joueurs l'avantage dont ils ont besoin pour gagner. En utilisant des algorithmes avancés et des technologies de pointe, notre système peut analyser n'importe quelle position d'échecs et suggérer le meilleur coup possible, aidant les joueurs à prendre des décisions plus intelligentes et plus éclairées sur l'échiquier. // @require https://greasyfork.org/scripts/460400-usergui-js/code/userguijs.js?version=1152084 // @resource jquery.js https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js // @resource chessboard.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/chessboard.js // @resource chessboard.css https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/chessboard.css // @resource lozza.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/lozza.js // @resource stockfish-5.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/stockfish-5.js // @resource stockfish-2018.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/stockfish-2018.js // @run-at document-start // @inject-into content // @downloadURL none // ==/UserScript== // VARS const repositoryRawURL = 'https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script'; const LICHESS_API = "https://lichess.org/api/cloud-eval"; const MAX_DEPTH = 20; const MIN_DEPTH = 1; const MAX_MOVETIME = 2000; const MIN_MOVETIME = 50; const MAX_ELO = 3500; const DEPTH_MODE = 0; const MOVETIME_MODE = 1; const rank = ["Beginner", "Intermediate", "Advanced", "Expert", "Master", "Grand Master"]; let nightMode = false; let engineMode = 0; // engine mode (0:depth / 1:movetime) let engineIndex = 0; // engine index (lozza => 0, stockfish => 1...) let reload_every = 10; // reload engine after x moves let reload_engine = false; // reload engine let enableUserLog = true; // enable interface log let enableEngineLog = true; // enable engine log let displayMovesOnSite = true; // display moves on chess board let show_opposite_moves = false; // show opponent best moves if available let use_book_moves = false; // use lichess api to get book moves let node_engine_url = "http://localhost:5000"; // node server api url let node_engine_name = "stockfish-15.exe" // default engine name (node server engine only) let current_depth = Math.round(MAX_DEPTH / 2); // current engine depth let current_movetime = Math.round(MAX_MOVETIME / 3); // current engine move time const TURN_UPDATE_FIX = false; let bestMoveQueue = [{ id: 0, fen: "" }]; let lastBestMoveID = 0; const dbValues = { nightMode: 'nightMode', engineMode: 'engineMode', engineIndex: 'engineIndex', reload_every: 'reload_every', reload_engine: 'reload_engine', enableUserLog: 'enableUserLog', enableEngineLog: 'enableEngineLog', displayMovesOnSite: 'displayMovesOnSite', show_opposite_moves: "show_opposite_moves", use_book_moves: "use_book_moves", node_engine_url: "node_engine_url", node_engine_name: "node_engine_name", current_depth: "current_depth", current_movetime: "current_movetime" }; let Gui; let closedGui = false; let reload_count = 1; let node_engine_id = 3; let Interface = null; let LozzaUtils = null; let initialized = false; let firstMoveMade = false; let forcedBestMove = false; let engine = null; let engineObjectURL = null; let lastEngine = engineIndex; let chessBoardElem = null; let turn = '-'; let playerColor = null; let isPlayerTurn = null; let lastFen = null; let uiChessBoard = null; let activeGuiMoveHighlights = []; let activeSiteMoveHighlights = []; let engineLogNum = 1; let userscriptLogNum = 1; let enemyScore = 0; let myScore = 0; function moveResult(from, to, power, clear = true) { if (from.length < 2 || to.length < 2) { return; } if (clear) { clearBoard(); } if (forcedBestMove == false) { if (isPlayerTurn) // my turn myScore = myScore + Number(power); else enemyScore = enemyScore + Number(power); Interface.boardUtils.updateBoardPower(myScore, enemyScore); } else { forcedBestMove = false; } if (displayMovesOnSite || (!isPlayerTurn && show_opposite_moves)) { markMoveToSite(from, to); } Interface.boardUtils.markMove(from, to); Interface.stopBestMoveProcessingAnimation(); } function getLastBestMoveRequest() { if (bestMoveQueue.length == 0) { // increment to stop last engine requests lastBestMoveID++; return { id: lastBestMoveID, fen: "" }; } return bestMoveQueue[bestMoveQueue.length - 1]; } function getBookMoves() { let request = getLastBestMoveRequest(); GM_xmlhttpRequest({ method: "GET", url: LICHESS_API + "?fen=" + request.fen + "&multiPv=1&variant=fromPosition", headers: { "Content-Type": "application/json" }, onload: function (response) { if (response.response.includes("error")) { if (getLastBestMoveRequest().id != request.id) { return; } getBestMoves(request); } else { if (getLastBestMoveRequest().id != request.id) { return; } let data = JSON.parse(response.response); let nextMove = data.pvs[0].moves.split(' ')[0]; moveResult(nextMove.slice(0, 2), nextMove.slice(2, 4), current_depth, true); } }, onerror: function (error) { if (getLastBestMoveRequest().id != request.id) { return; } getBestMoves(request); } }); } function getNodeBestMoves() { let lastBestMoveRequest = getLastBestMoveRequest(); GM_xmlhttpRequest({ method: "GET", url: node_engine_url + "/getBestMove?fen=" + lastBestMoveRequest.fen + "&engine_mode=" + engineMode + "&depth=" + current_depth + "&movetime=" + current_movetime + "&turn=" + turn + "&engine_name=" + node_engine_name, headers: { "Content-Type": "application/json" }, onload: function (response) { if (response.response == "false") { return; } if (getLastBestMoveRequest().id != lastBestMoveRequest.id) { return; } let data = JSON.parse(response.response); let server_fen = data.fen; let depth = data.depth; let movetime = data.movetime; let power = data.score; let move = data.move; if (engineMode == DEPTH_MODE) { Interface.updateBestMoveProgress(`Depth: ${depth}`); } else { Interface.updateBestMoveProgress(`Move time: ${movetime} ms`); } moveResult(move.slice(0, 2), move.slice(2, 4), power, true); }, onerror: function () { Interface.log("check node server !!"); } }); } function getElo() { let elo; if (engineMode == DEPTH_MODE) { elo = MAX_ELO / MAX_DEPTH; elo *= current_depth; } else { elo = MAX_ELO / MAX_MOVETIME; elo *= current_movetime; } elo = Math.round(elo); return elo; } function getRank() { let part; if (engineMode == DEPTH_MODE) { part = current_depth / (MAX_DEPTH / rank.length); } else { part = current_movetime / (MAX_MOVETIME / rank.length); } part = Math.round(part); if (part >= rank.length) { part = rank.length - 1; } return rank[part]; } function getEloDescription() { let desc = `Elo: ${getElo()}, Rank: ${getRank()}, `; if (engineMode == DEPTH_MODE) { desc += `Depth: ${current_depth}`; } else { desc += `Move Time: ${current_movetime} ms`; } return desc; } function isNotCompatibleBrowser() { return navigator.userAgent.toLowerCase().includes("firefox") } onload = function () { if (isNotCompatibleBrowser()) { Gui = new UserGui; } } if (!isNotCompatibleBrowser()) { Gui = new UserGui; } else { onload(); } Gui.settings.window.title = 'Smart Chess Bot'; Gui.settings.window.external = true; Gui.settings.window.size.width = 500; Gui.settings.gui.external.popup = false; Gui.settings.gui.external.style += GM_getResourceText('chessboard.css'); Gui.settings.gui.external.style += ` div[class^='board'] { background-color: black; } .best-move-from { background-color: #31ff7f; transform: scale(0.85); } .best-move-to { background-color: #31ff7f; } .negative-best-move-from { background-color: #fd0000; transform: scale(0.85); } .negative-best-move-to { background-color: #fd0000; } body { display: block; margin-left: auto; margin-right: auto; width: 360px; } #fen { margin-left: 10px; } #engine-log-container { max-height: 35vh; overflow: auto!important; } #userscript-log-container { max-height: 35vh; overflow: auto!important; } .sideways-card { display: flex; align-items: center; justify-content: space-between; } .rendered-form .card { margin-bottom: 10px; } .hidden { display: none; } .main-title-bar { display: flex; justify-content: space-between; } @keyframes wiggle { 0% { transform: scale(1); } 80% { transform: scale(1); } 85% { transform: scale(1.1); } 95% { transform: scale(1); } 100% { transform: scale(1); } } .wiggle { display: inline-block; animation: wiggle 1s infinite; } `; function FenUtils() { this.board = [ [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1], ]; this.pieceCodeToFen = pieceStr => { let [pieceColor, pieceName] = pieceStr.split(''); return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } this.getFenCodeFromPieceElem = pieceElem => { return this.pieceCodeToFen([...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/))); } this.getPieceColor = pieceFenStr => { return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b'; } this.getPieceOppositeColor = pieceFenStr => { return this.getPieceColor(pieceFenStr) == 'w' ? 'b' : 'w'; } this.squeezeEmptySquares = fenStr => { return fenStr.replace(/11111111/g, '8') .replace(/1111111/g, '7') .replace(/111111/g, '6') .replace(/11111/g, '5') .replace(/1111/g, '4') .replace(/111/g, '3') .replace(/11/g, '2'); } this.posToIndex = pos => { let [x, y] = pos.split(''); return { 'y': 8 - y, 'x': 'abcdefgh'.indexOf(x) }; } this.getBoardPiece = pos => { let indexObj = this.posToIndex(pos); return this.board[indexObj.y][indexObj.x]; } this.getRights = () => { let rights = ''; // check for white let e1 = this.getBoardPiece('e1'), h1 = this.getBoardPiece('h1'), a1 = this.getBoardPiece('a1'); if (e1 == 'K' && h1 == 'R') rights += 'K'; if (e1 == 'K' && a1 == 'R') rights += 'Q'; //check for black let e8 = this.getBoardPiece('e8'), h8 = this.getBoardPiece('h8'), a8 = this.getBoardPiece('a8'); if (e8 == 'k' && h8 == 'r') rights += 'k'; if (e8 == 'k' && a8 == 'r') rights += 'q'; return rights ? rights : '-'; } this.getBasicFen = () => { let pieceElems = [...chessBoardElem.querySelectorAll('.piece')]; pieceElems.forEach(pieceElem => { let pieceFenCode = this.getFenCodeFromPieceElem(pieceElem); let [xPos, yPos] = pieceElem.classList.toString().match(/square-(\d)(\d)/).slice(1); this.board[8 - yPos][xPos - 1] = pieceFenCode; }); let basicFen = this.squeezeEmptySquares(this.board.map(x => x.join('')).join('/')); return basicFen; } this.getFen = () => { let basicFen = this.getBasicFen(); let rights = this.getRights(); return `${basicFen} ${turn} ${rights} - 0 1`; } } function InterfaceUtils() { this.boardUtils = { findSquareElem: (squareCode) => { if (!Gui?.document) return; return Gui.document.querySelector(`.square-${squareCode}`); }, markMove: (fromSquare, toSquare) => { if (!Gui?.document) return; const [fromElem, toElem] = [this.boardUtils.findSquareElem(fromSquare), this.boardUtils.findSquareElem(toSquare)]; if (isPlayerTurn) { fromElem.classList.add('best-move-from'); toElem.classList.add('best-move-to'); } else { fromElem.classList.add('negative-best-move-from'); toElem.classList.add('negative-best-move-to'); } activeGuiMoveHighlights.push(fromElem); activeGuiMoveHighlights.push(toElem); }, removeBestMarkings: () => { if (!Gui?.document) return; activeGuiMoveHighlights.forEach(elem => { elem.classList.remove('best-move-from', 'best-move-to', 'negative-best-move-from', 'negative-best-move-to'); }); activeGuiMoveHighlights = []; }, updateBoardFen: fen => { if (!Gui?.document) return; Gui.document.querySelector('#fen').textContent = fen; }, updateBoardPower: (myScore, enemyScore) => { if (!Gui?.document) return; Gui.document.querySelector('#enemy-score').textContent = enemyScore; Gui.document.querySelector('#my-score').textContent = myScore; }, updateBoardOrientation: orientation => { if (!Gui?.document) return; const orientationElem = Gui?.document?.querySelector('#orientation'); if (orientationElem) { orientationElem.textContent = orientation; } } } this.engineLog = str => { if (!Gui?.document || enableEngineLog == false) return; const logElem = document.createElement('div'); logElem.classList.add('list-group-item'); if (str.includes('info')) logElem.classList.add('list-group-item-info'); if (str.includes('bestmove')) logElem.classList.add('list-group-item-success'); logElem.innerText = `#${engineLogNum++} ${str}`; Gui.document.querySelector('#engine-log-container').prepend(logElem); } this.log = str => { if (!Gui?.document || enableUserLog == false) return; const logElem = document.createElement('div'); logElem.classList.add('list-group-item'); if (str.includes('info')) logElem.classList.add('list-group-item-info'); if (str.includes('bestmove')) logElem.classList.add('list-group-item-success'); const container = Gui?.document?.querySelector('#userscript-log-container'); if (container) { logElem.innerText = `#${userscriptLogNum++} ${str}`; container.prepend(logElem); } } this.getBoardOrientation = () => { return document.querySelector('.board.flipped') ? 'b' : 'w'; } this.updateBestMoveProgress = text => { if (!Gui?.document) return; const progressBarElem = Gui.document.querySelector('#best-move-progress'); progressBarElem.innerText = text; progressBarElem.classList.remove('hidden'); progressBarElem.classList.add('wiggle'); } this.stopBestMoveProcessingAnimation = () => { if (!Gui?.document) return; const progressBarElem = Gui.document.querySelector('#best-move-progress'); progressBarElem.classList.remove('wiggle'); } this.hideBestMoveProgress = () => { if (!Gui?.document) return; const progressBarElem = Gui.document.querySelector('#best-move-progress'); if (!progressBarElem.classList.contains('hidden')) { progressBarElem.classList.add('hidden'); this.stopBestMoveProcessingAnimation(); } } } function LozzaUtility() { this.separateMoveCodes = moveCode => { moveCode = moveCode.trim(); let move = moveCode.split(' ')[1]; return [move.slice(0, 2), move.slice(2, 4)]; } this.extractInfo = str => { const keys = ['time', 'nps', 'depth']; return keys.reduce((acc, key) => { const match = str.match(`${key} (\\d+)`); if (match) { acc[key] = Number(match[1]); } return acc; }, {}); } } function fenSquareToChessComSquare(fenSquareCode) { const [x, y] = fenSquareCode.split(''); return `square-${['abcdefgh'.indexOf(x) + 1]}${y}`; } function markMoveToSite(fromSquare, toSquare) { const highlight = (fenSquareCode, style) => { const squareClass = fenSquareToChessComSquare(fenSquareCode); const highlightElem = document.createElement('div'); highlightElem.classList.add('custom'); highlightElem.classList.add('highlight'); highlightElem.classList.add(squareClass); highlightElem.dataset.testElement = 'highlight'; highlightElem.style = style; activeSiteMoveHighlights.push(highlightElem); let existingHighLight; if (TURN_UPDATE_FIX == true) { existingHighLight = document.querySelector(`.custom.highlight.${squareClass}`); } else { existingHighLight = document.querySelector(`.highlight.${squareClass}`); } if (existingHighLight) { existingHighLight.remove(); } chessBoardElem.prepend(highlightElem); } const defaultFromSquareStyle = 'background-color: rgb(120 130 255 / 90%); border: 4px solid rgb(0 0 0 / 50%);'; const defaultToSquareStyle = 'background-color: rgb(60 140 255 / 90%); border: 4px dashed rgb(0 0 0 / 50%);'; const negativeFromSquareStyle = 'background-color: rgb(255 0 0 / 30%); border: 4px solid rgb(0 0 0 / 50%);'; const negativeToSquareStyle = 'background-color: rgb(255 0 0 / 30%); border: 4px dashed rgb(0 0 0 / 50%);'; highlight(fromSquare, (isPlayerTurn ? defaultFromSquareStyle : negativeFromSquareStyle)); highlight(toSquare, (isPlayerTurn ? defaultToSquareStyle : negativeToSquareStyle)); } function removeSiteMoveMarkings() { activeSiteMoveHighlights.forEach(elem => { elem?.remove(); }); activeSiteMoveHighlights = []; } function getTurn() { const getSquareNumber = (elem) => { for (var a = 0; a < elem.classList.length; a++) { if (elem.classList[a].includes("square")) { return elem.classList[a]; } } return ""; } const getSimiliarChessPiece = (squareNumber) => { let similiarChessPieces = chessBoardElem.querySelectorAll("." + squareNumber); for (var a = 0; a < similiarChessPieces.length; a++) { if (similiarChessPieces[a].classList.contains("piece") == true) { return similiarChessPieces[a]; } } return null; } const getChessPieceColor = (elem) => { for (var a = 0; a < elem.classList.length; a++) { if (elem.classList[a].length <= 2) { if (elem.classList[a][0] == "b") { return "b"; } else { return "w"; } } } return ""; } bestMoveQueue = []; Interface.boardUtils.removeBestMarkings(); removeSiteMoveMarkings(); let chessPieces = chessBoardElem.querySelectorAll(".highlight"); for (var a = 0; a < chessPieces.length; a++) { // exclude custom highlighted squares if (chessPieces[a].classList.contains("custom") == true) { continue; } let squareNumber = getSquareNumber(chessPieces[a]); if (squareNumber == "") { return ""; } let similiarChessPiece = getSimiliarChessPiece(squareNumber); if (similiarChessPiece == null) { continue; } let chessPieceColor = getChessPieceColor(similiarChessPiece); if (chessPieceColor == "") { return ""; } if (chessPieceColor == "b") { return "w"; } else if (chessPieceColor == "w") { return "b"; } } return ""; } function updateBestMove(mutationArr) { let fenUtil = new FenUtils(); let currentFen = fenUtil.getFen(); if (currentFen != lastFen) { lastFen = currentFen; if (mutationArr) { let attributeMutationArr = mutationArr.filter(m => m.target.classList.contains('piece') && m.attributeName == 'class'); if (attributeMutationArr?.length) { turn = fenUtil.getPieceOppositeColor(fenUtil.getFenCodeFromPieceElem(attributeMutationArr[0].target)); // fix turn when going back to last fen if (getTurn() != "" && TURN_UPDATE_FIX == true) { turn = getTurn(); } Interface.log(`Turn updated to ${turn}!`); isPlayerTurn = playerColor == null || turn == playerColor; currentFen=updateBoard(); lastBestMoveID++; bestMoveQueue.push({ id: lastBestMoveID, fen: currentFen }); sendBestMove(); } } else { updateBoard(); } } } function sendBestMove() { if (!isPlayerTurn && !show_opposite_moves) { return; } sendBestMoveRequest(); } function sendBestMoveRequest() { reloadChessEngine(false, () => { Interface.log('Sending best move request to the engine!'); if (use_book_moves) { getBookMoves(); } else { getBestMoves(); } }); } function clearBoard() { bestMoveQueue = []; Interface.stopBestMoveProcessingAnimation(); Interface.boardUtils.removeBestMarkings(); removeSiteMoveMarkings(); } function updateBoard() { clearBoard(); let fenUtil=new FenUtils(); currentFen=fenUtil.getFen(); Interface.boardUtils.updateBoardFen(currentFen); return currentFen; } function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } function getBestMoves(request = null) { if (request == null) { request = getLastBestMoveRequest(); } if (engineIndex != node_engine_id) { // local engines while (!engine) { sleep(100); } engine.postMessage(`position fen ${request.fen}`); if (engineMode == DEPTH_MODE) { engine.postMessage('go depth ' + current_depth); } else { engine.postMessage('go movetime ' + current_movetime); } engine.onmessage = e => { if (getLastBestMoveRequest().id != request.id) { return; } if (e.data.includes('bestmove')) { let move = e.data.split(' ')[1]; let opponent_move = e.data.split(' ')[3]; moveResult(move.slice(0, 2), move.slice(2, 4), current_depth, true); } else if (e.data.includes('info')) { const infoObj = LozzaUtils.extractInfo(e.data); let depth = infoObj.depth || current_depth; let move_time = infoObj.time || current_movetime; if (engineMode == DEPTH_MODE) { Interface.updateBestMoveProgress(`Depth: ${depth}`); } else { Interface.updateBestMoveProgress(`Move time: ${move_time} ms`); } } Interface.engineLog(e.data); }; } else { getNodeBestMoves(); } } function observeNewMoves() { updateBestMove(); const boardObserver = new MutationObserver(mutationArr => { const lastPlayerColor = playerColor; updatePlayerColor(); if (playerColor != lastPlayerColor) { Interface.log(`Player color changed from ${lastPlayerColor} to ${playerColor}!`); updateBestMove(); } else { updateBestMove(mutationArr); } }); boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true }); } function addGuiPages() { if (Gui?.document) return; Gui.addPage("Main", `