// ==UserScript== // @name 🏆 [#1 Chess Cheat] A.C.A.S (Advanced Chess Assistance System) // @name:en 🏆 [#1 Chess Cheat] A.C.A.S (Advanced Chess Assistance System) // @name:fi 🏆 [#1 Chess Cheat] A.C.A.S (Edistynyt shakkiavustusjärjestelmä) // @name:zh-CN 🏆 [#1 Chess Cheat] A.C.A.S(高级国际象棋辅助系统) // @name:es 🏆 [#1 Chess Cheat] A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez) // @name:hi 🏆 [#1 Chess Cheat] A.C.A.S (उन्नत शतरंज सहायता प्रणाली) // @name:ar 🏆 [#1 Chess Cheat] A.C.A.S (نظام المساعدة المتقدم في الشطرنج) // @name:pt 🏆 [#1 Chess Cheat] A.C.A.S (Sistema Avançado de Assistência ao Xadrez) // @name:ja 🏆 [#1 Chess Cheat] A.C.A.S(先進的なチェス支援システム) // @name:de 🏆 [#1 Chess Cheat] A.C.A.S (Fortgeschrittenes Schach-Hilfesystem) // @name:fr 🏆 [#1 Chess Cheat] A.C.A.S (Système Avancé d'Assistance aux Échecs) // @name:it 🏆 [#1 Chess Cheat] A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi) // @name:ko 🏆 [#1 Chess Cheat] A.C.A.S (고급 체스 보조 시스템) // @name:nl 🏆 [#1 Chess Cheat] A.C.A.S (Geavanceerd Schaakondersteuningssysteem) // @name:pl 🏆 [#1 Chess Cheat] A.C.A.S (Zaawansowany System Pomocy Szachowej) // @name:tr 🏆 [#1 Chess Cheat] A.C.A.S (Gelişmiş Satranç Yardım Sistemi) // @name:vi 🏆 [#1 Chess Cheat] A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao) // @name:uk 🏆 [#1 Chess Cheat] A.C.A.S (Система передової допомоги в шахах) // @name:ru 🏆 [#1 Chess Cheat] A.C.A.S (Система расширенной помощи в шахматах) // @description Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system // @description:en Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system // @description:fi Paranna shakkipelisi suorituskykyä huippuluokan reaaliaikaisen siirtoanalyysin ja strategisen avustusjärjestelmän avulla // @description:zh-CN 利用尖端实时走法分析和策略辅助系统,提升您的国际象棋水平 // @description:es Mejora tu rendimiento en ajedrez con un sistema de análisis de movimientos en tiempo real y asistencia estratégica de vanguardia // @description:hi अपने शतरंज प्रदर्शन को उन्नत करें, एक कटिंग-एज रियल-टाइम मूव विश्लेषण और रणनीति सहायता प्रणाली के साथ // @description:ar قم بتحسين أداءك في الشطرنج مع تحليل حركات اللعب في الوقت الحقيقي ونظام مساعدة استراتيجية حديث // @description:pt Melhore seu desempenho no xadrez com uma análise de movimentos em tempo real e um sistema avançado de assistência estratégica // @description:ja 最新のリアルタイムのムーブ分析と戦略支援システムでチェスのパフォーマンスを向上させましょう // @description:de Verbessern Sie Ihre Schachleistung mit einer hochmodernen Echtzeitzug-Analyse- und Strategiehilfe-System // @description:fr Améliorez vos performances aux échecs avec une analyse de mouvement en temps réel de pointe et un système d'assistance stratégique // @description:it Migliora le tue prestazioni agli scacchi con un sistema all'avanguardia di analisi dei movimenti in tempo reale e assistenza strategica // @description:ko 최첨단 실시간 움직임 분석 및 전략 지원 시스템으로 체스 성과 향상 // @description:nl Verbeter je schaakprestaties met een geavanceerd systeem voor realtime zetanalyse en strategische ondersteuning // @description:pl Popraw swoje osiągnięcia w szachach dzięki zaawansowanemu systemowi analizy ruchów w czasie rzeczywistym i wsparciu strategicznemu // @description:tr Keskinleşmiş gerçek zamanlı hareket analizi ve strateji yardım sistemiyle satranç performansınızı artırın // @description:vi Nâng cao hiệu suất cờ vua của bạn với hệ thống phân tích nước đi và hỗ trợ chiến thuật hiện đại // @description:uk Покращуйте свою шахову гру з використанням передової системи аналізу ходів в режимі реального часу та стратегічної підтримки // @description:ru Слава Украине // @homepageURL https://hakorr.github.io/A.C.A.S // @supportURL https://github.com/Hakorr/A.C.A.S/tree/main#why-doesnt-it-work // @match https://hakorr.github.io/A.C.A.S/* // @match http://localhost/* // @match https://www.chess.com/* // @match https://lichess.org/* // @match https://playstrategy.org/* // @match https://www.pychess.org/* // @match https://chess.org/* // @match https://papergames.io/* // @match https://vole.wtf/kilobytes-gambit/ // @match https://chess.coolmath-games.com/* // @match https://www.coolmathgames.com/0-chess/* // @match https://immortal.game/* // @match https://chessarena.com/* // @match http://chess.net/* // @match https://www.freechess.club/* // @match https://*chessclub.com/* // @match https://gameknot.com/* // @match https://chesstempo.com/* // @match https://www.redhotpawn.com/* // @match https://www.chessanytime.com/* // @match https://chessworld.net/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_addStyle // @grant GM_setClipboard // @grant GM_notification // @grant unsafeWindow // @run-at document-start // @require https://greasyfork.org/scripts/470418-commlink-js/code/CommLinkjs.js // @require https://greasyfork.org/scripts/470417-universalboarddrawer-js/code/UniversalBoardDrawerjs.js // @icon https://raw.githubusercontent.com/Hakorr/A.C.A.S/main/assets/images/grey-logo.png // @version 2.0.8 // @namespace HKR // @author HKR // @license GPL-3.0 // @downloadURL none // ==/UserScript== /* e e88~-_ e ,d88~~\ d8b d888 \ d8b 8888 /Y88b 8888 /Y88b `Y88b / Y88b 8888 / Y88b `Y88b, /____Y88b d88b Y888 / d88b /____Y88b d88b 8888 / Y88b Y88P "88_-~ Y88P / Y88b Y88P \__88P' Advanced Chess Assistance System (A.C.A.S) v2 | Q3 2023 [WARNING] - Please be advised that the use of A.C.A.S may violate the rules and lead to disqualification or banning from tournaments and online platforms. - The developers of A.C.A.S and related systems will NOT be held accountable for any consequences resulting from its use. - We strongly advise to use A.C.A.S only in a controlled environment ethically. [ADDITIONAL] - Big fonts created with: https://www.patorjk.com/software/taag/ (Cyberlarge) JOIN THE DISCUSSION @ https://discord.gg/xkYydUyaem ("Userscript Hub") DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*\ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ ////////////////////////////////////////////////////////////////// DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*/ /* ______ _____ ______ _______ | ____ | | | |_____] |_____| | |_____| |_____ |_____| |_____] | | |_____ Code below this point runs on any site, including the GUI. */ // KEEP THESE AS FALSE ON PRODUCTION const debugModeActivated = false; const onlyUseDevelopmentBackend = false; const backendURLs = { 'production': 'https://hakorr.github.io/A.C.A.S/', 'development': 'http://localhost/A.C.A.S/' }; const greasyforkURL = 'https://greasyfork.org/en/scripts/459137'; const domain = window.location.hostname.replace('www.', ''); const isBackendUrlUpToDate = Object.values(backendURLs).includes(GM_getValue('currentBackendURL')); function isRunningOnBackend() { const foundBackendURL = Object.values(backendURLs).find(url => window?.location?.href.includes(url)); if(foundBackendURL) { GM_setValue('currentBackendURL', foundBackendURL); return true; } return false; } function getCurrentBackendURL(skipGmStorage) { const backendURL = skipGmStorage ? (backendURLs?.production || backendURLs?.development) : (GM_getValue('currentBackendURL') || backendURLs?.production || backendURLs?.development); return onlyUseDevelopmentBackend ? backendURLs?.development : backendURL; } if(!isBackendUrlUpToDate) { GM_setValue('currentBackendURL', getCurrentBackendURL(true)); } function createInstanceVariable(dbValue) { return { set: (instanceID, value) => GM_setValue(dbValues[dbValue](instanceID), { value, 'date': Date.now() }), get: instanceID => { const data = GM_getValue(dbValues[dbValue](instanceID)); if(data?.date) { data.date = Date.now(); GM_setValue(dbValues[dbValue](instanceID), data); } return data?.value; } } } const tempValueIndicator = '-temp-value-'; const dbValues = { AcasConfig: 'AcasConfig', playerColor: instanceID => 'playerColor' + tempValueIndicator + instanceID, turn: instanceID => 'turn' + tempValueIndicator + instanceID, fen: instanceID => 'fen' + tempValueIndicator + instanceID }; const instanceVars = { playerColor: createInstanceVariable('playerColor'), turn: createInstanceVariable('turn'), fen: createInstanceVariable('fen') }; if(isRunningOnBackend()) { // expose variables and functions unsafeWindow.USERSCRIPT = { 'GM_info': GM_info, 'GM_getValue': val => GM_getValue(val), 'GM_setValue': (val, data) => GM_setValue(val, data), 'GM_deleteValue': val => GM_deleteValue(val), 'GM_listValues': val => GM_listValues(val), 'tempValueIndicator': tempValueIndicator, 'dbValues': dbValues, 'instanceVars': instanceVars, 'CommLinkHandler': CommLinkHandler, }; return; } /* _______ _ _ _______ _______ _______ _______ _____ _______ _______ _______ | |_____| |______ |______ |______ |______ | | |______ |______ |_____ | | |______ ______| ______| ______| __|__ | |______ ______| Code below this point only runs on chess sites, not on the GUI itself. */ function getUniqueID() { return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ) } const commLinkInstanceID = getUniqueID(); const blacklistedURLs = [ 'https://hakorr.github.io/A.C.A.S/', 'https://hakorr.github.io/A.C.A.S/why/', 'https://hakorr.github.io/A.C.A.S/tos/', 'http://localhost/A.C.A.S/', 'https://www.chess.com/play', 'https://lichess.org/', 'https://chess.org/', 'https://papergames.io/en/chess', 'https://playstrategy.org/', 'https://www.pychess.org/', 'https://www.coolmathgames.com/0-chess', 'https://chess.net/' ]; const configKeys = { 'engineElo': 'engineElo', 'moveSuggestionAmount': 'moveSuggestionAmount', 'arrowOpacity': 'arrowOpacity', 'displayMovesOnExternalSite': 'displayMovesOnExternalSite', 'showMoveGhost': 'showMoveGhost', 'showOpponentMoveGuess': 'showOpponentMoveGuess', 'onlyShowTopMoves': 'onlyShowTopMoves', 'maxMovetime': 'maxMovetime', 'chessVariant': 'chessVariant', 'chessFont': 'chessFont', 'useChess960': 'useChess960', 'onlyCalculateOwnTurn': 'onlyCalculateOwnTurn' }; const config = {}; Object.values(configKeys).forEach(key => { config[key] = { get: () => getGmConfigValue(key, commLinkInstanceID), set: null }; }); let BoardDrawer = null; let chessBoardElem = null; let chesscomVariantBoardCoordsTable = null; let activeSiteMoveHighlights = []; let inactiveGuiMoveMarkings = []; let lastBoardRanks = null; let lastBoardFiles = null; let lastBoardSize = null; let lastPieceSize = null; let lastBoardOrientation = null; const supportedSites = {}; const pieceNameToFen = { 'pawn': 'p', 'knight': 'n', 'bishop': 'b', 'rook': 'r', 'queen': 'q', 'king': 'k' }; const arrowStyles = { 'best': ` fill: limegreen; opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.9'}; stroke: rgb(0 0 0 / 50%); stroke-width: 2px; stroke-linejoin: round; `, 'secondary': ` fill: dodgerblue; opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.7'}; stroke: rgb(0 0 0 / 50%); stroke-width: 2px; stroke-linejoin: round; `, 'opponent': ` fill: crimson; stroke: rgb(0 0 0 / 25%); stroke-width: 2px; stroke-linejoin: round; display: none; opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.3'}; ` }; const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, { 'singlePacketResponseWaitTime': 1500, 'maxSendAttempts': 3, 'statusCheckInterval': 1, 'silentMode': true }); // manually register a command so that the variables are dynamic CommLink.commands['createInstance'] = async () => { return await CommLink.send('mum', 'createInstance', { 'domain': domain, 'instanceID': commLinkInstanceID, 'chessVariant': getChessVariant(), 'playerColor': getPlayerColorVariable() }); } CommLink.registerSendCommand('ping', { commlinkID: 'mum', data: 'ping' }); CommLink.registerSendCommand('pingInstance', { data: 'ping' }); CommLink.registerSendCommand('log'); CommLink.registerSendCommand('updateBoardOrientation'); CommLink.registerSendCommand('updateBoardFen'); CommLink.registerSendCommand('calculateBestMoves'); CommLink.registerListener(`backend_${commLinkInstanceID}`, packet => { try { switch(packet.command) { case 'ping': return `pong (took ${Date.now() - packet.date}ms)`; case 'getFen': return getFen(); case 'removeSiteMoveMarkings': boardUtils.removeBestMarkings(); return true; case 'markMoveToSite': boardUtils.markMove(packet.data); return true; } } catch(e) { return null; } }); const boardUtils = { markMove: moveObj => { if(!getConfigValue(configKeys.displayMovesOnExternalSite)) return; const [from, to] = moveObj.player; const [opponentFrom, opponentTo] = moveObj.opponent; const ranking = moveObj.ranking; const existingExactSameMoveObj = activeSiteMoveHighlights.find(obj => { const [activeFrom, activeTo] = obj.player; const [activeOpponentFrom, activeOpponentTo] = obj.opponent; return from == activeFrom && to == activeTo && opponentFrom == activeOpponentFrom && opponentTo == activeOpponentTo; }); activeSiteMoveHighlights.map(obj => { const [activeFrom, activeTo] = obj.player; const existingSameMoveObj = from == activeFrom && to == activeTo; if(existingSameMoveObj) { obj.promotedRanking = 1; } return obj; }); const exactSameMoveDoesNotExist = typeof existingExactSameMoveObj !== 'object'; if(exactSameMoveDoesNotExist) { const showOpponentMoveGuess = getConfigValue(configKeys.showOpponentMoveGuess); const opponentMoveGuessExists = typeof opponentFrom == 'string'; const arrowStyle = ranking == 1 ? arrowStyles.best : arrowStyles.secondary; let opponentArrowElem = null; // create player move arrow element const arrowElem = BoardDrawer.createShape('arrow', [from, to], { style: arrowStyle } ); // create opponent move arrow element if(opponentMoveGuessExists && showOpponentMoveGuess) { opponentArrowElem = BoardDrawer.createShape('arrow', [opponentFrom, opponentTo], { style: arrowStyles.opponent } ); const squareListener = BoardDrawer.addSquareListener(from, type => { if(!opponentArrowElem) { squareListener.remove(); } switch(type) { case 'enter': opponentArrowElem.style.display = 'inherit'; break; case 'leave': opponentArrowElem.style.display = 'none'; break; } }); } activeSiteMoveHighlights.push({ ...moveObj, 'opponentArrowElem': opponentArrowElem, 'playerArrowElem': arrowElem }); } boardUtils.removeOldMarkings(); boardUtils.paintMarkings(); }, removeOldMarkings: () => { const markingLimit = getConfigValue(configKeys.moveSuggestionAmount); const showGhost = getConfigValue(configKeys.showMoveGhost); const exceededMarkingLimit = activeSiteMoveHighlights.length > markingLimit; if(exceededMarkingLimit) { const amountToRemove = activeSiteMoveHighlights.length - markingLimit; for(let i = 0; i < amountToRemove; i++) { const oldestMarkingObj = activeSiteMoveHighlights[0]; activeSiteMoveHighlights = activeSiteMoveHighlights.slice(1); if(oldestMarkingObj?.playerArrowElem?.style) { oldestMarkingObj.playerArrowElem.style.fill = 'grey'; oldestMarkingObj.playerArrowElem.style.opacity = '0'; oldestMarkingObj.playerArrowElem.style.transition = 'opacity 2.5s ease-in-out'; } if(oldestMarkingObj?.opponentArrowElem?.style) { oldestMarkingObj.opponentArrowElem.style.fill = 'grey'; oldestMarkingObj.opponentArrowElem.style.opacity = '0'; oldestMarkingObj.opponentArrowElem.style.transition = 'opacity 2.5s ease-in-out'; } if(showGhost) { inactiveGuiMoveMarkings.push(oldestMarkingObj); } else { oldestMarkingObj.playerArrowElem?.remove(); oldestMarkingObj.opponentArrowElem?.remove(); } } } if(showGhost) { inactiveGuiMoveMarkings.forEach(markingObj => { const activeDuplicateArrow = activeSiteMoveHighlights.find(x => { const samePlayerArrow = x.player?.toString() == markingObj.player?.toString(); const sameOpponentArrow = x.opponent?.toString() == markingObj.opponent?.toString(); return samePlayerArrow && sameOpponentArrow; }); const duplicateExists = activeDuplicateArrow ? true : false; const removeArrows = () => { inactiveGuiMoveMarkings = inactiveGuiMoveMarkings.filter(x => x.playerArrowElem != markingObj.playerArrowElem); markingObj.playerArrowElem?.remove(); markingObj.opponentArrowElem?.remove(); } if(duplicateExists) { removeArrows(); } else { setTimeout(removeArrows, 2500); } }); } }, paintMarkings: () => { const newestBestMarkingIndex = activeSiteMoveHighlights.findLastIndex(obj => obj.ranking == 1); const newestPromotedBestMarkingIndex = activeSiteMoveHighlights.findLastIndex(obj => obj?.promotedRanking == 1); const lastMarkingIndex = activeSiteMoveHighlights.length - 1; const isLastMarkingBest = newestBestMarkingIndex == -1 && newestPromotedBestMarkingIndex == -1; const bestIndex = isLastMarkingBest ? lastMarkingIndex : Math.max(...[newestBestMarkingIndex, newestPromotedBestMarkingIndex]); let bestMoveMarked = false; activeSiteMoveHighlights.forEach((markingObj, idx) => { const isBestMarking = idx == bestIndex; if(isBestMarking) { markingObj.playerArrowElem.style.cssText = arrowStyles.best; const playerArrowElem = markingObj.playerArrowElem const opponentArrowElem = markingObj.opponentArrowElem; // move best arrow element on top (multiple same moves can hide the best move) const parentElem = markingObj.playerArrowElem.parentElement; parentElem.appendChild(playerArrowElem); if(opponentArrowElem) { parentElem.appendChild(opponentArrowElem); } bestMoveMarked = true; } else { markingObj.playerArrowElem.style.cssText = arrowStyles.secondary; } }); }, removeBestMarkings: () => { activeSiteMoveHighlights.forEach(markingObj => { markingObj.opponentArrowElem?.remove(); markingObj.playerArrowElem?.remove(); }); activeSiteMoveHighlights = []; }, setBoardOrientation: orientation => { if(BoardDrawer) { if(debugModeActivated) console.warn('setBoardOrientation', orientation); BoardDrawer.setOrientation(orientation); } }, setBoardDimensions: dimensionArr => { if(BoardDrawer) { if(debugModeActivated) console.warn('setBoardDimensions', dimensionArr); BoardDrawer.setBoardDimensions(dimensionArr); } } }; function displayImportantNotification(title, text) { if(typeof GM_notification === 'function') { GM_notification({ title: title, text: text }); } else { alert(`[${title}]` + '\n\n' + text); } } function filterInvisibleElems(elementArr, inverse) { return [...elementArr].filter(elem => { const style = getComputedStyle(elem); const bounds = elem.getBoundingClientRect(); const isHidden = style.visibility === 'hidden' || style.display === 'none' || style.opacity === '0' || bounds.width == 0 || bounds.height == 0; return inverse ? isHidden : !isHidden; }); } function getElementSize(elem) { const rect = elem.getBoundingClientRect(); if(rect.width !== 0 && rect.height !== 0) { return { width: rect.width, height: rect.height }; } const computedStyle = window.getComputedStyle(elem); const width = parseFloat(computedStyle.width); const height = parseFloat(computedStyle.height); return { width, height }; } function extractElemTransformData(elem) { const computedStyle = window.getComputedStyle(elem); const transformMatrix = new DOMMatrix(computedStyle.transform); const x = transformMatrix.e; const y = transformMatrix.f; return [x, y]; } function getElemCoordinatesFromTransform(elem, config) { const onlyFlipX = config?.onlyFlipX; const onlyFlipY = config?.onlyFlipY; lastBoardSize = getElementSize(chessBoardElem); const [files, ranks] = getBoardDimensions(); lastBoardRanks = ranks; lastBoardFiles = files; const boardOrientation = getPlayerColorVariable(); let [x, y] = extractElemTransformData(elem); const boardDimensions = lastBoardSize; let squareDimensions = boardDimensions.width / lastBoardRanks; const normalizedX = Math.round(x / squareDimensions); const normalizedY = Math.round(y / squareDimensions); if(onlyFlipY || boardOrientation === 'w') { const flippedY = lastBoardFiles - normalizedY - 1; return [normalizedX, flippedY]; } else { const flippedX = lastBoardRanks - normalizedX - 1; return [flippedX, normalizedY]; } } function getElemCoordinatesFromLeftBottomPercentages(elem) { if(!lastBoardRanks || !lastBoardFiles) { const [files, ranks] = getBoardDimensions(); lastBoardRanks = ranks; lastBoardFiles = files; } const boardOrientation = getPlayerColorVariable(); const leftPercentage = parseFloat(elem.style.left?.replace('%', '')); const bottomPercentage = parseFloat(elem.style.bottom?.replace('%', '')); const x = Math.max(Math.round(leftPercentage / (100 / lastBoardRanks)), 0); const y = Math.max(Math.round(bottomPercentage / (100 / lastBoardFiles)), 0); if (boardOrientation === 'w') { return [x, y]; } else { const flippedX = lastBoardRanks - (x + 1); const flippedY = lastBoardFiles - (y + 1); return [flippedX, flippedY]; } } function getElemCoordinatesFromLeftTopPixels(elem) { const pieceSize = getElementSize(elem); const leftPixels = parseFloat(elem.style.left?.replace('px', '')); const topPixels = parseFloat(elem.style.top?.replace('px', '')); const x = Math.max(Math.round(leftPixels / pieceSize.width), 0); const y = Math.max(Math.round(topPixels / pieceSize.width), 0); const boardOrientation = getPlayerColorVariable(); if (boardOrientation === 'w') { const flippedY = lastBoardFiles - (y + 1); return [x, flippedY]; } else { const flippedX = lastBoardRanks - (x + 1); return [flippedX, y]; } } function createChesscomVariantBoardCoordsTable() { chesscomVariantBoardCoordsTable = {}; const boardElem = getBoardElem(); const [boardWidth, boardHeight] = getBoardDimensions(); const boardOrientation = getBoardOrientation(); const squareElems = getSquareElems(boardElem); let squareIndex = 0; for(let x = 0; x < boardWidth; x++) { for(let y = boardHeight; y > 0; y--) { const squareElem = squareElems[squareIndex]; const id = squareElem?.id; const xIdx = x; const yIdx = y - 1; if(id) { chesscomVariantBoardCoordsTable[id] = [xIdx, yIdx]; } squareIndex++; } } } function getBoardDimensionsFromSize() { const boardDimensions = getElementSize(chessBoardElem); lastBoardSize = getElementSize(chessBoardElem); const boardWidth = boardDimensions?.width; const boardHeight = boardDimensions.height; const boardPiece = getPieceElem(); if(boardPiece) { const pieceDimensions = getElementSize(boardPiece); lastPieceSize = getElementSize(boardPiece); const boardPieceWidth = pieceDimensions?.width; const boardPieceHeight = pieceDimensions?.height; const boardRanks = Math.floor(boardWidth / boardPieceWidth); const boardFiles = Math.floor(boardHeight / boardPieceHeight); const ranksInAllowedRange = 0 < boardRanks && boardRanks <= 69; const filesInAllowedRange = 0 < boardFiles && boardFiles <= 69; if(ranksInAllowedRange && filesInAllowedRange) { return [boardRanks, boardFiles]; } } } function defaultTurnFromMutation(mutationArr) { const allChessPieceElems = getPieceElem(true); const attributeMutationArr = mutationArr.filter(m => allChessPieceElems.includes(m.target)); const movedChessPieceElem = attributeMutationArr?.[0]?.target; if(movedChessPieceElem) { const pieceFen = getPieceElemFen(movedChessPieceElem); if(pieceFen) { const newTurn = getFenPieceOppositeColor(pieceFen); if(newTurn?.length === 1) { instanceVars.turn.set(commLinkInstanceID, newTurn); return newTurn; } } } } function chessCoordinatesToIndex(coord) { const x = coord.charCodeAt(0) - 97; let y = null; const lastHalf = coord.slice(1); if(lastHalf === ':') { y = 9; } else { y = Number(coord.slice(1)) - 1; } return [x, y]; } function getGmConfigValue(key, instanceID) { const config = GM_getValue(dbValues.AcasConfig); const instanceValue = config?.instance?.[instanceID]?.[key]; const globalValue = config?.global?.[key]; if(instanceValue !== undefined) { return instanceValue; } if(globalValue !== undefined) { return globalValue; } return null; } function getConfigValue(key) { return config[key]?.get(); } function setConfigValue(key, val) { return config[key]?.set(val); } function squeezeEmptySquares(fenStr) { return fenStr.replace(/1+/g, match => match.length); } function getPlayerColorVariable() { return instanceVars.playerColor.get(commLinkInstanceID); } function getFenPieceColor(pieceFenStr) { return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b'; } function getFenPieceOppositeColor(pieceFenStr) { return getFenPieceColor(pieceFenStr) == 'w' ? 'b' : 'w'; } function convertPieceStrToFen(str) { if(!str || str.length !== 2) { return null; } const firstChar = str[0].toLowerCase(); const secondChar = str[1]; if(firstChar === 'w') { return secondChar.toUpperCase(); } else if (firstChar === 'b') { return secondChar.toLowerCase(); } return null; } function getCanvasPixelColor(canvas, [xPercentage, yPercentage], debug) { const ctx = canvas.getContext('2d'); const x = xPercentage * canvas.width; const y = yPercentage * canvas.height; const imageData = ctx.getImageData(x, y, 1, 1); const pixel = imageData.data; const brightness = (pixel[0] + pixel[1] + pixel[2]) / 3; if(debug) { const clonedCanvas = document.createElement('canvas'); clonedCanvas.width = canvas.width; clonedCanvas.height = canvas.height; const clonedCtx = clonedCanvas.getContext('2d'); clonedCtx.drawImage(canvas, 0, 0); clonedCtx.fillStyle = 'red'; clonedCtx.beginPath(); clonedCtx.arc(x, y, 1, 0, Math.PI * 2); clonedCtx.fill(); const dataURL = clonedCanvas.toDataURL(); console.log(canvas, pixel, dataURL); } return brightness < 128 ? 'b' : 'w'; } function canvasHasPixelAt(canvas, [xPercentage, yPercentage], debug) { xPercentage = Math.min(Math.max(xPercentage, 0), 100); yPercentage = Math.min(Math.max(yPercentage, 0), 100); const ctx = canvas.getContext('2d'); const x = xPercentage * canvas.width; const y = yPercentage * canvas.height; const imageData = ctx.getImageData(x, y, 1, 1); const pixel = imageData.data; if(debug) { const clonedCanvas = document.createElement('canvas'); clonedCanvas.width = canvas.width; clonedCanvas.height = canvas.height; const clonedCtx = clonedCanvas.getContext('2d'); clonedCtx.drawImage(canvas, 0, 0); clonedCtx.fillStyle = 'red'; clonedCtx.beginPath(); clonedCtx.arc(x, y, 1, 0, Math.PI * 2); clonedCtx.fill(); const dataURL = clonedCanvas.toDataURL(); console.log(canvas, pixel, dataURL); } return pixel[3] !== 0; } function getSiteData(dataType, obj) { const pathname = window.location.pathname; let dataObj = { pathname }; if(obj && typeof obj === 'object') { dataObj = { ...dataObj, ...obj }; } const dataHandlerFunction = supportedSites[domain]?.[dataType]; if(typeof dataHandlerFunction !== 'function') { return null; } const result = dataHandlerFunction(dataObj); //if(debugModeActivated) console.warn('GET_SITE_DATA', '| DATA_TYPE:', dataType, '| INPUT_OBJ:', obj, '| DATA_OBJ:', dataObj, '| RESULT:', result); return result; } function addSupportedChessSite(domain, typeHandlerObj) { supportedSites[domain] = typeHandlerObj; } function getBoardElem() { const boardElem = getSiteData('boardElem'); return boardElem || null; } function getPieceElem(getAll) { const boardElem = getBoardElem(); const boardQuerySelector = (getAll ? query => [...boardElem?.querySelectorAll(query)] : boardElem?.querySelector?.bind(boardElem)); if(typeof boardQuerySelector !== 'function') return null; const pieceElem = getSiteData('pieceElem', { boardQuerySelector, getAll }); return pieceElem || null; } function getSquareElems(element) { const squareElems = getSiteData('squareElems', { element }); return squareElems || null; } function getChessVariant() { const chessVariant = getSiteData('chessVariant'); return chessVariant || null; } function getBoardOrientation() { const boardOrientation = getSiteData('boardOrientation'); return boardOrientation || null; } function getPieceElemFen(pieceElem) { const pieceFen = getSiteData('pieceElemFen', { pieceElem }); return pieceFen || null; } // this function gets called a lot, needs to be optimized function getPieceElemCoords(pieceElem) { const pieceCoords = getSiteData('pieceElemCoords', { pieceElem }); return pieceCoords || null; } function getBoardDimensions() { const boardDimensionArr = getSiteData('boardDimensions'); if(boardDimensionArr) { lastBoardRanks = boardDimensionArr[0]; lastBoardFiles = boardDimensionArr[1]; return boardDimensionArr; } else { lastBoardRanks = 8; lastBoardFiles = 8; return [8, 8]; } } function isMutationNewMove(mutationArr) { const isNewMove = getSiteData('isMutationNewMove', { mutationArr }); return isNewMove || false; } function getMutationTurn(mutationArr) { const turn = getSiteData('turnFromMutation', { mutationArr }); return turn || getPlayerColorVariable(); } function getFen(onlyBasic) { const [boardRanks, boardFiles] = getBoardDimensions(); if(debugModeActivated) console.warn('getFen()', 'onlyBasic:', onlyBasic, 'Ranks:', boardRanks, 'Files:', boardFiles); const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1)); function getBasicFen() { const pieceElems = getPieceElem(true); const isValidPieceElemsArray = Array.isArray(pieceElems) || pieceElems instanceof NodeList; if(isValidPieceElemsArray) { pieceElems.forEach(pieceElem => { const pieceFenCode = getPieceElemFen(pieceElem); const pieceCoordsArr = getPieceElemCoords(pieceElem); if(debugModeActivated) console.warn('pieceElem', pieceElem, 'pieceFenCode', pieceFenCode, 'pieceCoordsArr', pieceCoordsArr); try { const [xIdx, yIdx] = pieceCoordsArr; board[boardFiles - (yIdx + 1)][xIdx] = pieceFenCode; } catch(e) { if(debugModeActivated) console.error(e); } }); } return squeezeEmptySquares(board.map(x => x.join('')).join('/')); } const basicFen = getBasicFen(); if(debugModeActivated) console.warn('basicFen', basicFen); if(onlyBasic) { return basicFen; } return `${basicFen} ${getPlayerColorVariable()} - - - -`; } function onNewMove(mutationArr, bypassFenChangeDetection) { const currentFullFen = getFen(); const lastFullFen = instanceVars.fen.get(commLinkInstanceID); const fenChanged = currentFullFen !== lastFullFen; setTimeout(() => { if(getFen() !== instanceVars.fen.get(commLinkInstanceID)) { onNewMove(null, true); } }, 500); if(fenChanged || bypassFenChangeDetection) { if(debugModeActivated) console.warn('NEW MOVE DETECTED!'); chesscomVariantBoardCoordsTable = null; boardUtils.setBoardDimensions(getBoardDimensions()); const lastPlayerColor = getPlayerColorVariable(); updatePlayerColor(); const playerColor = getPlayerColorVariable(); const orientationChanged = playerColor != lastPlayerColor; if(orientationChanged) { CommLink.commands.log(`Player color (e.g. board orientation) changed from ${lastPlayerColor} to ${playerColor}!`); chesscomVariantBoardCoordsTable = null; instanceVars.turn.set(commLinkInstanceID, playerColor); CommLink.commands.log(`Turn updated to ${playerColor}!`); } boardUtils.removeBestMarkings(); CommLink.commands.updateBoardFen(currentFullFen); const turn = mutationArr ? getMutationTurn(mutationArr) : playerColor; const onlyCalculateOwnTurn = getConfigValue(configKeys.onlyCalculateOwnTurn); if(debugModeActivated) console.warn('TURN:', turn, '| PLAYERCOLOR:', playerColor, '| ORIENTATION_CHANGED:', orientationChanged, '| ONLY_CALC_OWN_TURN:', onlyCalculateOwnTurn); if(orientationChanged || !onlyCalculateOwnTurn || turn === playerColor) { CommLink.commands.calculateBestMoves(currentFullFen); } } } function observeNewMoves() { let lastProcessedFen = null; const boardObserver = new MutationObserver(mutationArr => { if(debugModeActivated) console.log(mutationArr); if(isMutationNewMove(mutationArr)) { if(debugModeActivated) console.warn('Mutation is a new move:', mutationArr); if(domain === 'chess.org') { setTimeout(() => onNewMove(mutationArr), 250); } else { onNewMove(mutationArr); } } }); boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true }); } async function updatePlayerColor() { const boardOrientation = getBoardOrientation(); const boardOrientationChanged = lastBoardOrientation !== boardOrientation; const boardOrientationDiffers = BoardDrawer && BoardDrawer?.orientation !== boardOrientation; if(boardOrientationChanged || boardOrientationDiffers) { lastBoardOrientation = boardOrientation; instanceVars.playerColor.set(commLinkInstanceID, boardOrientation); instanceVars.turn.set(commLinkInstanceID, boardOrientation); boardUtils.setBoardOrientation(boardOrientation); await CommLink.commands.updateBoardOrientation(boardOrientation); } } /* _______ _____ _______ _______ _______ _____ _______ _______ _____ _______ _____ _______ |______ | | |______ |______ |_____] |______ | | |______ | | ______| __|__ | |______ ______| | |______ |_____ __|__ | __|__ |_____ Code below this point handles chess site specific things. (e.g. which element is the board or the pieces) */ addSupportedChessSite('chess.com', { 'boardElem': obj => { const pathname = obj.pathname; if(pathname?.includes('/variants')) { return document.querySelector('#board'); } return document.querySelector('chess-board'); }, 'pieceElem': obj => { const pathname = obj.pathname; const getAll = obj.getAll; if(pathname?.includes('/variants')) { const filteredPieceElems = filterInvisibleElems(document.querySelectorAll('#board *[data-piece]')) .filter(elem => Number(elem?.dataset?.player) <= 2); return getAll ? filteredPieceElems : filteredPieceElems[0]; } return obj.boardQuerySelector('.piece'); }, 'squareElems': obj => { const pathname = obj.pathname; const element = obj.element; if(pathname?.includes('/variants')) { return [...element.querySelectorAll('.square-4pc.ui-droppable')] .filter(elem => { const pieceElem = elem.querySelector('[data-player]'); const playerNum = Number(pieceElem?.dataset?.player); return (!playerNum || playerNum <= 2); }); } }, 'chessVariant': obj => { const pathname = obj.pathname; if(pathname?.includes('/variants')) { const variant = pathname.match(/variants\/([^\/]*)/)?.[1] .replaceAll('-chess', '') .replaceAll('-', ''); const replacementTable = { 'doubles-bughouse': 'bughouse', 'paradigm-chess30': 'paradigm' }; return replacementTable[variant] || variant; } }, 'boardOrientation': obj => { const pathname = obj.pathname; if(pathname?.includes('/variants')) { const playerNumberStr = document.querySelector('.playerbox-bottom [data-player]')?.dataset?.player; return playerNumberStr === '0' ? 'w' : 'b'; } const boardElem = getBoardElem(); return boardElem?.classList.contains('flipped') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pathname = obj.pathname; const pieceElem = obj.pieceElem; let pieceColor = null; let pieceName = null; if(pathname?.includes('/variants')) { pieceColor = pieceElem?.dataset?.player == '0' ? 'w' : 'b'; pieceName = pieceElem?.dataset?.piece; } else { const pieceStr = [...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/)); [pieceColor, pieceName] = pieceStr.split(''); } return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); }, 'pieceElemCoords': obj => { const pathname = obj.pathname; const pieceElem = obj.pieceElem; if(pathname?.includes('/variants')) { const squareElem = pieceElem.parentElement; const squareId = squareElem.id; if(!chesscomVariantBoardCoordsTable) { createChesscomVariantBoardCoordsTable(); } return chesscomVariantBoardCoordsTable[squareId]; } return pieceElem.classList.toString() ?.match(/square-(\d)(\d)/) ?.slice(1) ?.map(x => Number(x) - 1); }, 'boardDimensions': obj => { const pathname = obj.pathname; if(pathname?.includes('/variants')) { const rankElems = chessBoardElem?.querySelectorAll('.rank'); const visibleRankElems = filterInvisibleElems(rankElems) .filter(rankElem => [...rankElem.childNodes] .find(elem => { const pieceElem = elem.querySelector('[data-player]'); const playerNum = Number(pieceElem?.dataset?.player); return playerNum <= 2; })); if(visibleRankElems.length) { const rankElem = visibleRankElems[0]; const squareElems = getSquareElems(rankElem); const ranks = visibleRankElems?.length; const files = squareElems?.length; return [ranks, files]; } } else { return [8, 8]; } }, 'isMutationNewMove': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; if(pathname?.includes('/variants')) { return mutationArr.find(m => m.attributeName == 'class') ? true : false; } if(mutationArr.length == 1) return false; const modifiedHoverSquare = mutationArr.find(m => m?.target?.classList?.contains('hover-square')) ? true : false; const modifiedHighlight = mutationArr.find(m => m?.target?.classList?.contains('highlight')) ? true : false; const modifiedElemPool = mutationArr.find(m => m?.target?.classList?.contains('element-pool')) ? true : false; return (mutationArr.length >= 4 && !modifiedHoverSquare) || mutationArr.length >= 7 || modifiedHighlight || modifiedElemPool; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('lichess.org', { 'boardElem': obj => { return document.querySelector('cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece:not(.ghost)'); }, 'chessVariant': obj => { const variantLinkElem = document.querySelector('.variant-link'); if(variantLinkElem) { let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-'); const replacementTable = { 'correspondence': 'chess', 'koth': 'kingofthehill', 'three-check': '3check' }; return replacementTable[variant] || variant; } }, 'boardOrientation': obj => { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('playstrategy.org', { 'boardElem': obj => { return document.querySelector('cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece[class*="-piece"]:not(.ghost)'); }, 'chessVariant': obj => { const variantLinkElem = document.querySelector('.variant-link'); if(variantLinkElem) { let variant = variantLinkElem?.innerText ?.toLowerCase() ?.replaceAll(' ', '-'); const replacementTable = { 'correspondence': 'chess', 'koth': 'kingofthehill', 'three-check': '3check', 'five-check': '5check', 'no-castling': 'nocastle' }; return replacementTable[variant] || variant; } }, 'boardOrientation': obj => { const cgWrapElem = document.querySelector('.cg-wrap'); return cgWrapElem.classList?.contains('orientation-p1') ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const playerColor = getPlayerColorVariable(); const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w'); let pieceName = null; [...pieceElem?.classList]?.forEach(className => { if(className?.includes('-piece')) { const elemPieceName = className?.split('-piece')?.[0]; if(elemPieceName && elemPieceName?.length === 1) { pieceName = elemPieceName; } } }); if(pieceColor && pieceName) { return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return getBoardDimensionsFromSize(); }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('pychess.org', { 'boardElem': obj => { return document.querySelector('cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece[class*="-piece"]:not(.ghost)'); }, 'chessVariant': obj => { const variantLinkElem = document.querySelector('#main-wrap .tc .user-link'); if(variantLinkElem) { let variant = variantLinkElem?.innerText ?.toLowerCase() ?.replaceAll(' ', '') ?.replaceAll('-', ''); const replacementTable = { 'correspondence': 'chess', 'koth': 'kingofthehill', 'nocastling': 'nocastle', 'gorogoro+': 'gorogoro', 'oukchaktrang': 'cambodian' }; return replacementTable[variant] || variant; } }, 'boardOrientation': obj => { const cgWrapElem = document.querySelector('.cg-wrap'); return cgWrapElem.classList?.contains('orientation-black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const playerColor = getPlayerColorVariable(); const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w'); let pieceName = null; [...pieceElem?.classList]?.forEach(className => { if(className?.includes('-piece')) { const elemPieceName = className?.split('-piece')?.[0]; if(elemPieceName && elemPieceName?.length === 1) { pieceName = elemPieceName; } } }); if(pieceColor && pieceName) { return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return getBoardDimensionsFromSize(); }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chess.org', { 'boardElem': obj => { return document.querySelector('.cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece:not(.ghost)'); }, 'chessVariant': obj => { const variantNum = unsafeWindow?.GameConfig?.instance?.variant; const variant = GameConfig?.VARIANT_NAMES?.[variantNum]?.toLowerCase(); if(variant) { const replacementTable = { 'standard': 'chess' }; return replacementTable[variant] || variant; } }, 'boardOrientation': obj => { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromTransform(pieceElem); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chess.coolmath-games.com', { 'boardElem': obj => { return document.querySelector('.cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece:not(.ghost)'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const boardElem = getBoardElem(); return boardElem.classList?.contains('orientation-black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromLeftBottomPercentages(pieceElem?.parentElement); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('papergames.io', { 'boardElem': obj => { return document.querySelector('#chessboard'); }, 'pieceElem': obj => { return obj.boardQuerySelector('*[data-piece][data-square]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const boardElem = getBoardElem(); if(boardElem) { const firstRankText = [...boardElem.querySelector('.coordinates').childNodes]?.[0].textContent; return firstRankText == 'h' ? 'b' : 'w'; } }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; return convertPieceStrToFen(pieceElem?.dataset?.piece); }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.dataset?.square; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 12; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('vole.wtf', { 'boardElem': obj => { return document.querySelector('#board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('*[data-t][data-l][data-p]:not([data-p="0"]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceNum = Number(pieceElem?.dataset?.p); const pieceFenStr = 'pknbrq'; if(pieceNum > 8) { return pieceFenStr[pieceNum - 9].toUpperCase(); } else { return pieceFenStr[pieceNum - 1]; } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return [Number(pieceElem?.dataset?.l), 7 - Number(pieceElem?.dataset?.t)]; }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 12; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('immortal.game', { 'boardElem': obj => { return document.querySelector('div.pawn.relative, div.knight.relative, div.bishop.relative, div.rook.relative, div.queen.relative, div.king.relative')?.parentElement?.parentElement; }, 'pieceElem': obj => { return obj.boardQuerySelector('div.pawn.relative, div.knight.relative, div.bishop.relative, div.rook.relative, div.queen.relative, div.king.relative'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const coordA = [...document.querySelectorAll('svg text[x]')] .find(elem => elem?.textContent == 'a'); const coordAX = Number(coordA?.getAttribute('x')) || 10; return coordAX < 15 ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromTransform(pieceElem?.parentElement); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 5; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chessarena.com', { 'boardElem': obj => { return document.querySelector('cb-view'); }, 'pieceElem': obj => { return obj.boardQuerySelector('figure-view:not(.dead)'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const boardElem = getBoardElem(); return boardElem?.classList?.contains('rotated') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.values(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { return pieceColor === 'w' ? elemPieceName.toUpperCase() : elemPieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromTransform(pieceElem, { 'onlyFlipY': true }); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 5; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chess.net', { 'boardElem': obj => { return document.querySelector('cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece:not(.ghost)'); }, 'chessVariant': obj => { const variantLinkElem = document.querySelector('.variant-link'); if(variantLinkElem) { let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-'); const replacementTable = { 'correspondence': 'chess', 'koth': 'kingofthehill', 'three-check': '3check' }; return replacementTable[variant] || variant; } }, 'boardOrientation': obj => { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('freechess.club', { 'boardElem': obj => { return document.querySelector('cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece:not(.ghost)'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('play.chessclub.com', { 'boardElem': obj => { return document.querySelector('cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('piece:not(.ghost)'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b'; const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className)); if(pieceColor && elemPieceName) { const pieceName = pieceNameToFen[elemPieceName]; return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('gameknot.com', { 'boardElem': obj => { return document.querySelector('#chess-board-acboard'); }, 'pieceElem': obj => { return obj.boardQuerySelector('*[class*="square_"] > img[src*="chess36."][style*="visible"]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return document.querySelector('#chess-board-my-side-color .player_white') ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const left = Number(pieceElem.style.left.replace('px', '')); const top = Number(pieceElem.style.top.replace('px', '')); const pieceColor = left >= 0 ? 'w' : 'b'; const pieceName = 'kqrnbp'[(top * -1) / 60]; return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromLeftTopPixels(pieceElem.parentElement); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chesstempo.com', { 'boardElem': obj => { return document.querySelector('.ct-board-squares'); }, 'pieceElem': obj => { return obj.boardQuerySelector('*[class*="ct-pieceClass"][class*="ct-piece-"]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return document.querySelector('.ct-coord-column').innerText === 'a' ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceNameClass = [...pieceElem.classList].find(x => x?.includes('ct-piece-')); const colorNameCombo = pieceNameClass?.split('ct-piece-')?.pop(); const elemPieceColor = colorNameCombo.startsWith('white') ? 'w' : 'b'; const elemPieceName = colorNameCombo.substring(5); const pieceName = pieceNameToFen[elemPieceName]; return elemPieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return [pieceElem?.ct?.piece?.piece?.column, pieceElem?.ct?.piece?.piece?.row]; }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('redhotpawn.com', { 'boardElem': obj => { return document.querySelector('#board-0_1'); }, 'pieceElem': obj => { return obj.boardQuerySelector('li.piece[id*="-pc-"]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const aCoordLeftStyleNum = Number([...document.querySelectorAll('.boardCoordinate')] .find(elem => elem?.innerText === 'a') ?.style?.left?.replace('px', '')); return aCoordLeftStyleNum < 200 ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; return (pieceElem?.id?.match(/-pc-(.*?)-/) || [])[1]; }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromLeftTopPixels(pieceElem); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chessanytime.com', { 'boardElem': obj => { return document.querySelector('#play'); }, 'pieceElem': obj => { const getAll = obj.getAll; const pieceElems = [...document.querySelectorAll('canvas.canvas_piece')].filter(elem => canvasHasPixelAt(elem, [0.5, 0.5])); return getAll ? pieceElems : pieceElems[0]; }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return document.querySelector('#play_coordy0')?.innerText === '8' ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceTypeCoordPercentages = [ { 'name' : 'k', 'coords': [52/60, 26/60] }, { 'name' : 'q', 'coords': [8/60, 16/60] }, { 'name' : 'n', 'coords': [51/60, 42/60] }, { 'name' : 'b', 'coords': [9/60, 50/60] }, { 'name' : 'r', 'coords': [45/60, 15/60] }, { 'name' : 'p', 'coords': [0.5, 0.5] } ]; const pieceColorCoordPercentages = { 'k': [42/60, 27/60], 'q': [30/60, 50/60], 'n': [38/60, 41/60], 'b': [30/60, 20/60] }; let pieceName = null; for(obj of pieceTypeCoordPercentages) { const isThisPiece = canvasHasPixelAt(pieceElem, obj.coords); if(isThisPiece) { pieceName = obj.name; break; } } if(pieceName) { const colorCoords = pieceColorCoordPercentages[pieceName] || [0.5, 0.5]; const pieceColor = getCanvasPixelColor(pieceElem, colorCoords); //console.log(pieceElem, pieceName, colorCoords, pieceColor); return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromLeftTopPixels(pieceElem); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 7; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); addSupportedChessSite('chessworld.net', { 'boardElem': obj => { return document.querySelector('#ChessWorldChessBoard'); }, 'pieceElem': obj => { return obj.boardQuerySelector('img[src*="merida"'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return document.querySelector('div[style*="boardb.jpg"]') ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const [elemPieceColor, elemPieceName] = pieceElem ?.src ?.split('/') ?.pop() ?.replace('.png', '') ?.split('_'); const pieceColor = elemPieceColor === 'white' ? 'w' : 'b'; const pieceName = pieceNameToFen[elemPieceName]; return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return chessCoordinatesToIndex(pieceElem?.id); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 2; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } }); /* The site is very shitty I don't feel like finishing this addSupportedChessSite('chessfriends.com', { 'boardElem': obj => { return document.querySelector('div[id*="id_board_"'); }, 'pieceElem': obj => { return obj.boardQuerySelector('img[name][id*="id_piece_"][src*="/pieces/"]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const firstSquareTop = document.querySelector('*[id*="id_square_00_"]')?.style?.top; return firstSquareTop === '0px' ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const dataStr = pieceElem.getAttribute('name'); if(dataStr?.length === 2) { const pieceColor = dataStr[0]; const elemPieceName = dataStr[1]; return pieceColor == 'w' ? elemPieceName.toUpperCase() : elemPieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; //console.log(getElemCoordinatesFromLeftTopPixels(pieceElem)); return getElemCoordinatesFromLeftTopPixels(pieceElem); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; }, 'turnFromMutation': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; return defaultTurnFromMutation(mutationArr); } });*/ /* _____ __ _ _____ _______ _____ _______ _____ ______ _______ _______ _____ _____ __ _ | | \ | | | | |_____| | | ____/ |_____| | | | | | \ | __|__ | \_| __|__ | __|__ | | |_____ __|__ /_____ | | | __|__ |_____| | \_| Code below this point is related to initialization. (e.g. wait for chess board and create the instance) */ async function isAcasBackendReady() { const res = await CommLink.commands.ping(); return res ? true : false; } async function start() { await CommLink.commands.createInstance(commLinkInstanceID); const pathname = window.location.pathname; const adjustSizeByDimensions = domain === 'chess.com' && pathname?.includes('/variants'); const boardOrientation = getBoardOrientation(); instanceVars.playerColor.set(commLinkInstanceID, boardOrientation); instanceVars.turn.set(commLinkInstanceID, boardOrientation); instanceVars.fen.set(commLinkInstanceID, getFen()); if(getConfigValue(configKeys.displayMovesOnExternalSite)) { BoardDrawer = new UniversalBoardDrawer(chessBoardElem, { 'window': window, 'boardDimensions': getBoardDimensions(), 'playerColor': getPlayerColorVariable(), 'zIndex': domain === 'chessarena.com' ? 9999 : 500, 'prepend': true, 'debugMode': debugModeActivated, 'adjustSizeByDimensions': adjustSizeByDimensions ? true : false, 'adjustSizeConfig': { 'noLeftAdjustment': true } }); } await updatePlayerColor(); observeNewMoves(); CommLink.setIntervalAsync(async () => { await CommLink.commands.createInstance(commLinkInstanceID); }, 1000); } function startWhenBackendReady() { const interval = CommLink.setIntervalAsync(async () => { if(await isAcasBackendReady()) { start(); interval.stop(); } else { GM_openInTab(getCurrentBackendURL(), true); if(await isAcasBackendReady()) { start(); interval.stop(); } } }, 1000); } function initializeIfSiteReady() { const boardElem = getBoardElem(); const firstPieceElem = getPieceElem(); const bothElemsExist = boardElem && firstPieceElem; const boardElemChanged = chessBoardElem != boardElem; if(bothElemsExist && boardElemChanged) { chessBoardElem = boardElem; if(!blacklistedURLs.includes(window.location.href)) { startWhenBackendReady(); } } } if(typeof GM_registerMenuCommand === 'function') { GM_registerMenuCommand('[u] Open GreasyFork Page', e => { GM_openInTab(greasyforkURL, true); }, 'u'); GM_registerMenuCommand('[o] Open GUI Manually', e => { GM_openInTab(getCurrentBackendURL(), true); }, 'o'); GM_registerMenuCommand('[s] Start Manually', e => { if(chessBoardElem) { start(); } else { displayImportantNotification('Failed to start manually', 'No chessboard element found!'); } }, 's'); GM_registerMenuCommand('[g] Get Moves Manually', e => { if(chessBoardElem) { onNewMove(null, true); } else { displayImportantNotification('Failed to get moves', 'No chessboard element found!'); } }, 'g'); GM_registerMenuCommand('[r] Render BoardDrawer Manually', e => { if(typeof BoardDrawer?.updateDimensions === 'function') { BoardDrawer.updateDimensions(); } else { displayImportantNotification('Failed to render BoardDrawer', 'BoardDrawer not initialized or something else went wrong!'); } }, 'r'); if(typeof GM_setClipboard === 'function') { GM_registerMenuCommand('[c] Copy FEN to Clipboard', e => { if(chessBoardElem) { GM_setClipboard(getFen()); } else { displayImportantNotification('Failed to get FEN', 'No chessboard element found!'); } }, 'c'); } } setInterval(initializeIfSiteReady, 1000);