Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
Du vil måske også kunne lide [Discord] Status Animator (Manual edit/Non-UI)
// ==UserScript== // @name 🏆 [#1 Chess Assistant] A.C.A.S (Advanced Chess Assistance System) // @name:en 🏆 [#1 Chess Assistant] A.C.A.S (Advanced Chess Assistance System) // @name:fi 🏆 [#1 Chess Assistant] A.C.A.S (Edistynyt shakkiavustusjärjestelmä) // @name:sw 🏆 [#1 Chess Assistant] A.C.A.S (Advanserad Schack Assitant System) // @name:zh-CN 🏆 [#1 Chess Assistant] A.C.A.S(高级国际象棋辅助系统) // @name:es 🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez) // @name:hi 🏆 [#1 Chess Assistant] A.C.A.S (उन्नत शतरंज सहायता प्रणाली) // @name:ar 🏆 [#1 Chess Assistant] A.C.A.S (نظام المساعدة المتقدم في الشطرنج) // @name:pt 🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avançado de Assistência ao Xadrez) // @name:ja 🏆 [#1 Chess Assistant] A.C.A.S(先進的なチェス支援システム) // @name:de 🏆 [#1 Chess Assistant] A.C.A.S (Fortgeschrittenes Schach-Hilfesystem) // @name:fr 🏆 [#1 Chess Assistant] A.C.A.S (Système Avancé d'Assistance aux Échecs) // @name:it 🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi) // @name:ko 🏆 [#1 Chess Assistant] A.C.A.S (고급 체스 보조 시스템) // @name:nl 🏆 [#1 Chess Assistant] A.C.A.S (Geavanceerd Schaakondersteuningssysteem) // @name:pl 🏆 [#1 Chess Assistant] A.C.A.S (Zaawansowany System Pomocy Szachowej) // @name:tr 🏆 [#1 Chess Assistant] A.C.A.S (Gelişmiş Satranç Yardım Sistemi) // @name:vi 🏆 [#1 Chess Assistant] A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao) // @name:uk 🏆 [#1 Chess Assistant] A.C.A.S (Система передової допомоги в шахах) // @name:ru 🏆 [#1 Chess Assistant] 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:sw Förbättra dina schackprestationer med ett banbrytande rörelseanalys i realtid och strategiassistans // @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://psyyke.github.io/A.C.A.S // @supportURL https://github.com/Psyyke/A.C.A.S/tree/main#why-doesnt-it-work // @match https://psyyke.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.coolmathgames.com/* // @match https://www.coolmathgames.com/0-chess/* // @match https://immortal.game/* // @match https://chessarena.com/* // @match http://chess.net/* // @match https://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://www.simplechess.com/* // @match https://chessworld.net/* // @match https://app.edchess.io/* // @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/Psyyke/A.C.A.S/main/assets/images/grey-logo.png // @version 2.2.5 // @namespace HKR // @author HKR // @license GPL-3.0 // ==/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 r###lting 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 ABOUT USERSCRIPTS IN GENERAL @ https://hakorr.github.io/Userscripts/community/invite ("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 domain = window.location.hostname.replace('www.', ''); const greasyforkURL = 'https://greasyfork.org/en/scripts/459137'; const backendConfig = { 'hosts': { 'prod': 'psyyke.github.io', 'dev': 'localhost' }, 'path': '/A.C.A.S/' }; const currentBackendUrlKey = 'currentBackendURL'; const isBackendUrlUpToDate = Object.values(backendConfig.hosts).some(x => GM_getValue(currentBackendUrlKey)?.includes(x)); function constructBackendURL(host) { const protocol = window.location.protocol + '//'; const hosts = backendConfig.hosts; return protocol + (host || (hosts?.prod || hosts?.path)) + backendConfig.path; } function isRunningOnBackend() { const hostsArr = Object.values(backendConfig.hosts); const foundHost = hostsArr.find(host => host === window?.location?.host); const isCorrectPath = window?.location?.pathname?.includes(backendConfig.path); const isBackend = typeof foundHost === 'string' && isCorrectPath; if(isBackend) { GM_setValue(currentBackendUrlKey, constructBackendURL(foundHost)); return true; } return false; } function prependProtocolWhenNeeded(url) { if(!url.startsWith('http://') && !url.startsWith('https://')) { return 'http://' + url; } return url; } function getCurrentBackendURL(skipGmStorage) { if(onlyUseDevelopmentBackend) { return constructBackendURL(backendConfig.hosts?.dev); } const gmStorageUrl = GM_getValue(currentBackendUrlKey); if(skipGmStorage || !gmStorageUrl) { return constructBackendURL(); } return prependProtocolWhenNeeded(gmStorageUrl); } if(!isBackendUrlUpToDate) { GM_setValue(currentBackendUrlKey, 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'), 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 = [ constructBackendURL(backendConfig?.hosts?.prod), constructBackendURL(backendConfig?.hosts?.dev), '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', 'mov###ggestionAmount': 'mov###ggestionAmount', 'arrowOpacity': 'arrowOpacity', 'displayMovesOnExternalSite': 'displayMovesOnExternalSite', 'showMoveGhost': 'showMoveGhost', 'showOpponentMoveGuess': 'showOpponentMoveGuess', 'showOpponentMoveGuessConstantly': 'showOpponentMoveGuessConstantly', 'onlyShowTopMoves': 'onlyShowTopMoves', 'maxMovetime': 'maxMovetime', 'chessVariant': 'chessVariant', 'chessEngine': 'chessEngine', 'lc0Weight': 'lc0Weight', 'engineNodes': 'engineNodes', 'chessFont': 'chessFont', 'useChess960': 'useChess960', 'onlyCalculateOwnTurn': 'onlyCalculateOwnTurn', 'ttsVoiceEnabled': 'ttsVoiceEnabled', 'ttsVoiceName': 'ttsVoiceName', 'ttsVoiceSpeed': 'ttsVoiceSpeed', 'chessEngineProfile': 'chessEngineProfile', 'primaryArrowColorHex': 'primaryArrowColorHex', 'secondaryArrowColorHex': 'secondaryArrowColorHex', 'opponentArrowColorHex': 'opponentArrowColorHex', 'reverseSide': 'reverseSide', 'autoMove': 'autoMove', 'autoMoveLegit': 'autoMoveLegit', 'autoMoveRandom': 'autoMoveRandom', 'autoMoveAfterUser': 'autoMoveAfterUser', 'legitModeType': 'legitModeType', 'moveDisplayDelay': 'moveDisplayDelay' }; const config = {}; Object.values(configKeys).forEach(key => { config[key] = { get: profile => getGmConfigValue(key, commLinkInstanceID, profile), set: null }; }); let BoardDrawer = null; let chessBoardElem = null; let chesscomVariantPlayerColorsTable = null; let activeGuiMoveMarkings = []; let lastBoardRanks = null; let lastBoardFiles = null; let lastBoardSize = null; let lastPieceSize = null; let lastBoardMatrix = null; let lastBoardOrientation = null; let matchFirstSuggestionGiven = false; let lastMoveRequestTime = 0; let lastPieceAmount = 0; let isUserMouseDown = false; let activeAutomoves = []; const supportedSites = {}; const pieceNameToFen = { 'pawn': 'p', 'knight': 'n', 'bishop': 'b', 'rook': 'r', 'queen': 'q', 'king': 'k' }; function getArrowStyle(type, fill, opacity) { const baseStyleArr = [ 'stroke: rgb(0 0 0 / 50%);', 'stroke-width: 2px;', 'stroke-linejoin: round;' ]; switch(type) { case 'best': return [ `fill: ${fill || 'limegreen'};`, `opacity: ${opacity || 0.9};`, ...baseStyleArr ].join('\n'); case 'secondary': return [ ...baseStyleArr, `fill: ${fill ? fill : 'dodgerblue'};`, `opacity: ${opacity || 0.7};`, ].join('\n'); case 'opponent': return [ ...baseStyleArr, `fill: ${fill ? fill : 'crimson'};`, `opacity: ${opacity || 0.3};`, ].join('\n'); } }; 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.removeMarkings(); return true; case 'markMoveToSite': boardUtils.markMoves(packet.data); const profile = packet.data?.[0]?.profile; const isAutoMove = getConfigValue(configKeys.autoMove, profile); const isAutoMoveAfterUser = getConfigValue(configKeys.autoMoveAfterUser, profile); if (isAutoMove && (!isAutoMoveAfterUser || matchFirstSuggestionGiven)) { const existingAutomoves = activeAutomoves.filter(x => x.move.active); // Stop all existing automoves for(const x of existingAutomoves) { x.move.stop(); } const isLegit = getConfigValue(configKeys.autoMoveLegit, profile); const isRandom = getConfigValue(configKeys.autoMoveRandom, profile); const move = isRandom ? packet.data[Math.floor(Math.random() * Math.random() * packet.data.length)]?.player : packet.data[0]?.player; makeMove(profile, move, isLegit); } matchFirstSuggestionGiven = true; return true; } } catch(e) { return null; } }); const boardUtils = { markMoves: moveObjArr => { const maxScale = 1; const minScale = 0.5; const totalRanks = moveObjArr.length; moveObjArr.forEach((markingObj, idx) => { const profile = markingObj.profile; if(idx === 0) boardUtils.removeMarkings(profile); const [from, to] = markingObj.player; const [oppFrom, oppTo] = markingObj.opponent; const oppMovesExist = oppFrom && oppTo; const rank = idx + 1; const showOpponentMoveGuess = getConfigValue(configKeys.showOpponentMoveGuess, profile); const showOpponentMoveGuessConstantly = getConfigValue(configKeys.showOpponentMoveGuessConstantly, profile); const arrowOpacity = getConfigValue(configKeys.arrowOpacity, profile) / 100; const primaryArrowColorHex = getConfigValue(configKeys.primaryArrowColorHex, profile); const secondaryArrowColorHex = getConfigValue(configKeys.secondaryArrowColorHex, profile); const opponentArrowColorHex = getConfigValue(configKeys.opponentArrowColorHex, profile); let playerArrowElem = null; let oppArrowElem = null; let arrowStyle = getArrowStyle('best', primaryArrowColorHex, arrowOpacity); let lineWidth = 30; let arrowheadWidth = 80; let arrowheadHeight = 60; let startOffset = 30; if(idx !== 0) { arrowStyle = getArrowStyle('secondary', secondaryArrowColorHex, arrowOpacity); const arrowScale = totalRanks === 2 ? 0.75 : maxScale - (maxScale - minScale) * ((rank - 1) / (totalRanks - 1)); lineWidth = lineWidth * arrowScale; arrowheadWidth = arrowheadWidth * arrowScale; arrowheadHeight = arrowheadHeight * arrowScale; startOffset = startOffset; } playerArrowElem = BoardDrawer.createShape('arrow', [from, to], { style: arrowStyle, lineWidth, arrowheadWidth, arrowheadHeight, startOffset } ); if(oppMovesExist && showOpponentMoveGuess) { oppArrowElem = BoardDrawer.createShape('arrow', [oppFrom, oppTo], { style: getArrowStyle('opponent', opponentArrowColorHex, arrowOpacity), lineWidth, arrowheadWidth, arrowheadHeight, startOffset } ); if(showOpponentMoveGuessConstantly) { oppArrowElem.style.display = 'block'; } else { const squareListener = BoardDrawer.addSquareListener(from, type => { if(!oppArrowElem) { squareListener.remove(); } switch(type) { case 'enter': oppArrowElem.style.display = 'inherit'; break; case 'leave': oppArrowElem.style.display = 'none'; break; } }); } } if(idx === 0 && playerArrowElem) { const parentElem = playerArrowElem.parentElement; // move best arrow element on top (multiple same moves can hide the best move) parentElem.appendChild(playerArrowElem); if(oppArrowElem) { parentElem.appendChild(oppArrowElem); } } activeGuiMoveMarkings.push({ ...markingObj, playerArrowElem, oppArrowElem, profile }); }); }, removeMarkings: profile => { let removalArr = activeGuiMoveMarkings; if(profile) { removalArr = removalArr.filter(obj => obj.profile === profile); activeGuiMoveMarkings = activeGuiMoveMarkings.filter(obj => obj.profile !== profile); } else { activeGuiMoveMarkings = []; } removalArr.forEach(markingObj => { markingObj.oppArrowElem?.remove(); markingObj.playerArrowElem?.remove(); }); }, 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); lastPieceSize = pieceSize; 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 updateChesscomVariantPlayerColorsTable() { let colors = []; document.querySelectorAll('*[data-color]').forEach(pieceElem => { const colorCode = Number(pieceElem?.dataset?.color); if(!colors?.includes(colorCode)) { colors.push(colorCode); } }); if(colors?.length > 1) { colors = colors.sort((a, b) => a - b); chesscomVariantPlayerColorsTable = { [colors[0]]: 'w', [colors[1]]: 'b' }; } } function getBoardDimensionsFromSize() { const boardDimensions = getElementSize(chessBoardElem); lastBoardSize = boardDimensions; 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 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]; } /* Need to make the board matricies more cohesive, right now it's really confusing flipping them * differently for each function. I just can't be bothered right now so please don't make fun of it. * Thanks, Haka * */ function chessCoordinatesToMatrixIndex(coord) { const [boardRanks, boardFiles] = getBoardDimensions(); const indexArr = chessCoordinatesToIndex(coord); let x, y; y = boardFiles - (indexArr[1] + 1); x = indexArr[0]; return [x, y]; } function chessCoordinatesToDomIndex(coord) { const [boardRanks, boardFiles] = getBoardDimensions(); const indexArr = chessCoordinatesToIndex(coord); const boardOrientation = getBoardOrientation(); let x, y; if(boardOrientation === 'w') { x = indexArr[0]; y = boardFiles - (indexArr[1] + 1); } else { x = boardRanks - (indexArr[0] + 1); y = indexArr[1]; } return [x, y]; } function indexToChessCoordinates(coord) { const [boardRanks, boardFiles] = getBoardDimensions(); const boardOrientation = getBoardOrientation(); const [x, y] = coord; const file = String.fromCharCode('a'.charCodeAt(0) + x); let rank; if (boardOrientation === 'w') { rank = boardRanks - y; } else { rank = boardRanks - y; } return `${file}${rank}`; } function isPawnPromotion(bestMove) { const [fenCoordFrom, fenCoordTo] = bestMove; const piece = getBoardPiece(fenCoordFrom); if(typeof piece !== 'string' || piece.toLowerCase() !== 'p') return false; // Determine the row from the ending coordinate (assumes standard algebraic notation, e.g., 'e8') const endingRow = parseInt(fenCoordTo[1], 10); // Check if the pawn reaches the promotion row if ((piece === 'P' && endingRow === (lastBoardFiles ?? 8)) || (piece === 'p' && endingRow === 1)) { return true; } return false; } function fenCoordArrToDomCoord(fenCoordArr) { // fenCoordArr e.g. ["e6", "e5"] const boardClientRect = chessBoardElem.getBoundingClientRect(); const pieceElem = getPieceElem(); const pieceDimensions = getElementSize(pieceElem); const pieceWidth = pieceDimensions?.width; const pieceHeight = pieceDimensions?.height; lastPieceSize = pieceDimensions; const [boardRanks, boardFiles] = getBoardDimensions(); // Array to hold the center coordinates of each square const centerCoordinates = fenCoordArr.map(coord => { const [x, y] = chessCoordinatesToDomIndex(coord); const centerX = boardClientRect.x + (x * pieceWidth) + (pieceWidth / 2); const centerY = boardClientRect.y + (y * pieceHeight) + (pieceHeight / 2); return [centerX, centerY]; }); return centerCoordinates; } function getRandomOwnPieceDomCoord(fenCoord, boardMatrix) { const boardOrientation = getBoardOrientation(); let [x, y] = chessCoordinatesToMatrixIndex(fenCoord); const pieceAtFenCoord = boardMatrix[y][x]; if(pieceAtFenCoord === 1) { return null; } const isWhitePiece = pieceAtFenCoord === pieceAtFenCoord.toUpperCase(); const getDistance = (row1, col1, row2, col2) => { return Math.abs(row1 - row2) + Math.abs(col1 - col2); }; let candidatePieces = []; // Loop through the board matrix to find all close own pieces for(let row = 0; row < boardMatrix.length; row++) { for(let col = 0; col < boardMatrix[row].length; col++) { const currentPiece = boardMatrix[row][col]; // Skip if no piece is found or if the piece is of the wrong color if(currentPiece === 1 || (isWhitePiece && currentPiece === currentPiece.toLowerCase()) || (!isWhitePiece && currentPiece === currentPiece.toUpperCase())) { continue; } const distance = getDistance(y, x, row, col); if(distance < 6) { candidatePieces.push({ distance, coord: [col, row], piece: currentPiece }); } } } if (candidatePieces.length > 0) { // Choose a random piece from the candidates const randomIndex = Math.floor(Math.random() * candidatePieces.length); const chosenPiece = candidatePieces[randomIndex]; return fenCoordArrToDomCoord([indexToChessCoordinates(chosenPiece.coord)])[0]; } return null; } function getPieceAmount() { return getPieceElem(true)?.length ?? 0; } class AutomaticMove { constructor(profile, fenMoveArr, isLegit, callback) { this.id = getUniqueID(); // activeAutomoves is an external variable, not a child of AutomaticMove activeAutomoves.push({ 'id': this.id, 'move': this }); this.profile = profile; this.fenMoveArr = fenMoveArr; this.isLegit = isLegit; this.active = true; this.isPromotingPawn = false; this.onFinished = function(...args) { activeAutomoves.filter(x => x.id !== this.id); // remove the move from the active automove list this.active = false; callback(...args); }; this.moveDomCoords = fenCoordArrToDomCoord(fenMoveArr); this.isPromotion = isPawnPromotion(fenMoveArr); if(this.isLegit) { const legitModeType = getConfigValue(configKeys.legitModeType, this.profile) ?? 'casual'; const pieceRanges = [ { minPieces: 30, maxPieces: Infinity }, // Opening (60+ pieces) { minPieces: 23, maxPieces: 29 }, // Early Middlegame (48 to 64 pieces) { minPieces: 16, maxPieces: 22 }, // Mid Middlegame (32 to 48 pieces) { minPieces: 10, maxPieces: 15 }, // Late Middlegame (16 to 32 pieces) { minPieces: 6, maxPieces: 9 }, // Endgame (8 to 16 pieces) { minPieces: 3, maxPieces: 5 }, // Very Endgame (2 to 8 pieces) { minPieces: 1, maxPieces: 2 }, // Extremely Few Pieces (1 piece) ]; const timeRanges = { beginner: [ [2000, 4000], [3000, 15000], [5000, 25000], [4000, 30000], [3000, 15000], [2000, 10000], [1000, 4000], ], casual: [ [900, 3000], // Opening [1000, 15000], // Early Middlegame [3000, 20000], // Mid Middlegame [2000, 13000], // Late Middlegame [1500, 10000], // Endgame [1000, 9000], // Very Endgame [500, 3000], // Extremely Few Pieces ], intermediate: [ [750, 2000], [1000, 10000], [2000, 15000], [1500, 12000], [1000, 8000], [750, 7000], [500, 2000], ], advanced: [ [500, 1500], [1000, 8000], [750, 8000], [750, 12000], [750, 5000], [750, 3000], [500, 1200], ], master: [ [333, 999], [400, 2000], [400, 3000], [400, 2500], [400, 2000], [400, 1500], [333, 750], ], professional: [ [333, 666], [333, 666], [333, 1000], [333, 1500], [333, 1000], [333, 666], [333, 666], ], god: [ [50, 333], [50, 233], [50, 300], [50, 250], [50, 200], [50, 150], [50, 100], ] }; this.timeRanges = pieceRanges.map((range, index) => ({ ...range, timeRange: timeRanges[legitModeType][index], })); this.shouldHesitate = this.isLegit && Math.random() < 0.15; this.shouldHesitateTwice = this.isLegit && Math.random() < 0.25; this.hesitationTypeOne = this.isLegit && Math.random() < 0.35; const legitTotalMoveTime = this.calculateMoveTime(getPieceAmount()); const elapsedMoveTime = (Date.now() - lastMoveRequestTime); // How long did it take for the engine to calculate the move const remainingTime = Math.max(legitTotalMoveTime - elapsedMoveTime, 500); const delays = this.generateDelaysForDesiredTime(remainingTime); for(const key of Object.keys(delays)) { this[key] = delays[key]; } } this.start(); } generateDelaysForDesiredTime(desiredTotalTime) { // Fixed minimum values const PROMOTION_DELAY = this.getRandomIntegerBetween(1000, 1111); // just can't be done very fast for some reason at least on Chess.com if(desiredTotalTime > 6000) { const timelines = [ { move: .4, to: .2, hesitation: .15, hesitationResolve: .15, secondHesitationResolve: .15 }, { move: .1, to: .3, hesitation: .25, hesitationResolve: .15, secondHesitationResolve: .2 }, { move: .2, to: .25, hesitation: .2, hesitationResolve: .2, secondHesitationResolve: .15 } ]; const timeline = timelines[Math.floor(Math.random() * timelines.length)]; return { promotionDelay: PROMOTION_DELAY, moveDelay: desiredTotalTime * timeline.move, toSquareSelectDelay: desiredTotalTime * timeline.to, hesitationDelay: desiredTotalTime * timeline.hesitation, hesitationResolveDelay: desiredTotalTime * timeline.hesitationResolve, secondHesitationResolveDelay: desiredTotalTime * timeline.secondHesitationResolve }; } // There is time for one hesitation if(desiredTotalTime > 3000) { const timelines = [ { move: .3, to: .2, hesitation: .25, hesitationResolve: .25 }, { move: .1, to: .3, hesitation: .45, hesitationResolve: .15 }, { move: .2, to: .25, hesitation: .2, hesitationResolve: .35 } ]; const timeline = timelines[Math.floor(Math.random() * timelines.length)]; return { promotionDelay: PROMOTION_DELAY, moveDelay: desiredTotalTime * timeline.move, toSquareSelectDelay: desiredTotalTime * timeline.to, hesitationDelay: desiredTotalTime * timeline.hesitation, hesitationResolveDelay: desiredTotalTime * timeline.hesitationResolve, secondHesitationResolveDelay: -1 }; } // There is not enough time to hesitate else { const timelines = [ { move: .9, to: .1 }, { move: .45, to: .55 }, { move: .6, to: .4 }, { move: .4, to: .6 }, { move: .1, to: .9 } ]; const timeline = timelines[Math.floor(Math.random() * timelines.length)]; return { promotionDelay: PROMOTION_DELAY, moveDelay: desiredTotalTime * timeline.move, toSquareSelectDelay: desiredTotalTime * timeline.to, hesitationDelay: -1, hesitationResolveDelay: -1, secondHesitationResolveDelay: -1 }; } } calculateMoveTime(pieceCount) { for(let range of this.timeRanges) { if(pieceCount >= range.minPieces && pieceCount <= range.maxPieces) { return this.getRandomIntegerBetween(range.timeRange[0], range.timeRange[1]); } } return 500; } getRandomIntegerBetween(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } getRandomIntegerNearAverage(min, max) { const mid = (min + max) / 2; const range = (max - min) / 2; let value = Math.floor(mid + (Math.random() - 0.5) * range * 1.5); return Math.max(min, Math.min(max, value)); } delay(ms) { return this.active ? new Promise(resolve => setTimeout(resolve, ms)) : true; } async triggerPieceClick(input) { const parentExists = activeAutomoves.find(x => x.move === this) ? true : false; if(!parentExists) { return; } let clientX, clientY; if(input instanceof Element) { const rect = input.getBoundingClientRect(); clientX = rect.left + rect.width / 2; clientY = rect.top + rect.height / 2; } else if (typeof input === 'object') { clientX = input[0]; clientY = input[1]; } else { return; } const xDivider = Math.random() < 0.85 ? 4 : Math.random() < 0.15 ? 3 : 2; const yDivider = Math.random() < 0.65 ? 3 : Math.random() < 0.35 ? 2 : 4; const randomVariationX = (lastPieceSize?.width - 4) / xDivider; const randomVariationY = (lastPieceSize?.height - 4) / yDivider; const randomOffsetX = (Math.random() - 0.5) * 2 * randomVariationX; const randomOffsetY = (Math.pow(Math.random(), 0.5) - 0.5) * 2 * randomVariationY; const randomizedX = clientX + randomOffsetX; const randomizedY = clientY + randomOffsetY; const pointerEventOptions = { bubbles: true, cancelable: true, clientX: randomizedX, clientY: randomizedY, }; const elementToTrigger = (input instanceof Element) ? input : document.elementFromPoint(clientX, clientY); if(elementToTrigger) { switch(domain) { case 'chess.com': elementToTrigger.dispatchEvent(new PointerEvent('pointerdown', pointerEventOptions)); if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125)); elementToTrigger.dispatchEvent(new PointerEvent('pointerup', pointerEventOptions)); break; case 'lichess.org': elementToTrigger.dispatchEvent(new MouseEvent('mousedown', pointerEventOptions)); if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125)); elementToTrigger.dispatchEvent(new MouseEvent('mouseup', pointerEventOptions)); break; case 'chessarena.com': elementToTrigger.dispatchEvent(new MouseEvent('mousedown', pointerEventOptions)); if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125)); elementToTrigger.dispatchEvent(new MouseEvent('mouseup', pointerEventOptions)); break; } } if(debugModeActivated) { const dot = document.createElement('div'); dot.style.position = 'absolute'; dot.style.width = '7px'; dot.style.height = '7px'; dot.style.borderRadius = '50%'; dot.style.backgroundColor = 'lime'; dot.style.left = `${randomizedX - 2.5}px`; dot.style.top = `${randomizedY - 2.5}px`; const container = document.createElement('div'); container.style.position = 'absolute'; container.style.width = `${Math.round(randomVariationX * 2)}px`; container.style.height = `${Math.round(randomVariationY * 2)}px`; container.style.border = '2px dashed green'; container.style.backgroundColor = 'rgba(0, 0, 0, 0.3)'; container.style.left = `${clientX - randomVariationX}px`; container.style.top = `${clientY - randomVariationY}px`; document.body.appendChild(container); document.body.appendChild(dot); setTimeout(() => { dot.remove(); container.remove(); }, 1000); } } click(domCoord) { if(this.active) this.triggerPieceClick(domCoord); } async hesitate() { const hesitationPieceDomCoord = getRandomOwnPieceDomCoord(this.fenMoveArr[0], getBoardMatrix()); if(hesitationPieceDomCoord) { if(this.hesitationTypeOne) { this.click(this.moveDomCoords[0]); await this.delay(this.hesitationDelay); } this.click(hesitationPieceDomCoord); await this.delay(this.hesitationResolveDelay); if(this.shouldHesitateTwice && this.secondHesitationResolveDelay !== -1) { const secondHesitationPieceDomCoord = getRandomOwnPieceDomCoord(this.fenMoveArr[0], getBoardMatrix()); this.click(secondHesitationPieceDomCoord); await this.delay(this.secondHesitationResolveDelay); } } this.finishMove(this.toSquareSelectDelay, this.promotionDelay); } async finishMove(delay01, delay02) { this.click(this.moveDomCoords[0]); await this.delay(delay01); this.click(this.moveDomCoords[1]); // Handle promotion click if necessary if(this.isPromotion) { this.isPromotingPawn = true; await this.delay(delay02); this.click(this.moveDomCoords[1]); this.isPromotingPawn = false; } this.onFinished(true); } async playLegit() { await this.delay(this.moveDelay); if(this.shouldHesitate && this.hesitationDelay !== -1) this.hesitate(); else this.finishMove(this.toSquareSelectDelay, this.promotionDelay); } async start() { if(this.isLegit) { this.playLegit(); } else { this.finishMove(5, 1111); } } async stop() { if(this.isPromotingPawn) { // Attempt to promote the pawn before closing this.click(this.moveDomCoords[1]); } this.onFinished(false); } } async function makeMove(profile, fenMoveArr, isLegit) { const move = new AutomaticMove(profile, fenMoveArr, isLegit, e => { // This is ran when the move finished if(debugModeActivated) console.warn('Move', fenMoveArr, move.id, 'finished', 'for profile:', profile); }); } function getGmConfigValue(key, instanceID, profileID) { if(typeof profileID === 'object') { profileID = profileID.name; } 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; } if(profileID) { const globalProfileValue = config?.global?.['profiles']?.[profileID]?.[key]; const instanceProfileValue = config?.instance?.[instanceID]?.['profiles']?.[profileID]?.[key]; if(instanceProfileValue !== undefined) { return instanceProfileValue; } if(globalProfileValue !== undefined) { return globalProfileValue; } } return null; } function isBoardDrawerNeeded() { const config = GM_getValue(dbValues.AcasConfig); const gP = config?.global?.['profiles']; const iP = config?.instance?.[commLinkInstanceID]?.['profiles']; if(gP) { const globalProfiles = Object.keys(gP); for(profileName of globalProfiles) { if(gP[profileName][configKeys.displayMovesOnExternalSite]) { return true; } } } if(iP) { const instanceProfiles = Object.keys(iP); for(profileName of instanceProfiles) { if(iP[profileName][configKeys.displayMovesOnExternalSite]) { return true; } } } return false; } function getConfigValue(key, profile) { return config[key]?.get(profile); } 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 r###lt = dataHandlerFunction(dataObj); return r###lt; } 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 getBoardMatrix() { const [boardRanks, boardFiles] = getBoardDimensions(); const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1)); 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); } }); } lastBoardMatrix = board; return board; } function getBoardPiece(fenCoord) { const [boardRanks, boardFiles] = getBoardDimensions(); const indexArr = chessCoordinatesToIndex(fenCoord); return getBoardMatrix()?.[boardFiles - (indexArr[1] + 1)]?.[indexArr[0]]; } // Works on 8x8 boards only function getRights() { let rights = ''; // check for white const e1 = getBoardPiece('e1'), h1 = getBoardPiece('h1'), a1 = getBoardPiece('a1'); if(e1 == 'K' && h1 == 'R') rights += 'K'; if(e1 == 'K' && a1 == 'R') rights += 'Q'; //check for black const e8 = getBoardPiece('e8'), h8 = getBoardPiece('h8'), a8 = getBoardPiece('a8'); if(e8 == 'k' && h8 == 'r') rights += 'k'; if(e8 == 'k' && a8 == 'r') rights += 'q'; return rights ? rights : '-'; } function getBasicFen() { const boardMatrix = getBoardMatrix(); return squeezeEmptySquares(boardMatrix.map(x => x.join('')).join('/')); } function getFen(onlyBasic) { const basicFen = getBasicFen(); if(debugModeActivated) console.warn('basicFen', basicFen); if(onlyBasic) { return basicFen; } // FEN structure: [fen] [player color] [castling rights] [en passant targets] [halfmove clock] [fullmove clock] const fullFen = `${basicFen} ${getPlayerColorVariable()} ${getRights()} - 0 1`; return fullFen; } function resetCachedValues() { chesscomVariantPlayerColorsTable = null; } function fenToArray(fen) { const rows = fen.split('/'); const board = []; for (let row of rows) { const boardRow = []; for (let char of row) { if (isNaN(char)) { boardRow.push(char); } else { boardRow.push(...Array(parseInt(char)).fill('')); } } board.push(boardRow); } return board; } function onNewMove(mutationArr, bypassFenChangeDetection) { const currentFullFen = getFen(); const lastFullFen = instanceVars.fen.get(commLinkInstanceID); const fenChanged = currentFullFen !== lastFullFen; if((fenChanged || bypassFenChangeDetection)) { if(debugModeActivated) console.warn('NEW MOVE DETECTED!'); const pieceAmount = getPieceAmount(); const pieceAmountChange = Math.abs(pieceAmount - lastPieceAmount); // Possibly new match due to large piece amount change if(pieceAmountChange > 7) { matchFirstSuggestionGiven = false; } resetCachedValues(); 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}!`); resetCachedValues(); matchFirstSuggestionGiven = false; CommLink.commands.log(`Turn updated to ${playerColor}!`); } boardUtils.removeMarkings(); CommLink.commands.updateBoardFen(currentFullFen); lastMoveRequestTime = Date.now(); lastPieceAmount = pieceAmount; if(orientationChanged) { CommLink.commands.calculateBestMoves(currentFullFen); } } } function observeNewMoves() { const boardObserver = new MutationObserver(mutationArr => { if(debugModeActivated) console.log(mutationArr); if(isMutationNewMove(mutationArr)) { if(debugModeActivated) console.warn('Mutation is a new move:', mutationArr); try { if(domain === 'chess.org' || domain === 'chessarena.com') { setTimeout(() => onNewMove(mutationArr), 250); } else { onNewMove(mutationArr); } } catch(e) { if(debugModeActivated) console.error('Error while running onNewMove:', e); } } }); 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); 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('.TheBoard-layers'); } return document.querySelector('#board-layout-chessboard > .board'); }, 'pieceElem': obj => { const pathname = obj.pathname; const getAll = obj.getAll; if(pathname?.includes('/variants')) { const filteredPieceElems = filterInvisibleElems(document.querySelectorAll('.TheBoard-layers *[data-piece]')) .filter(elem => elem?.dataset?.piece?.toLowerCase() !== 'x'); 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')]; } }, '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; if(!playerNumberStr) return 'w'; 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')) { if(!chesscomVariantPlayerColorsTable) { updateChesscomVariantPlayerColorsTable(); } const pieceFenStr = pieceElem?.dataset?.piece; pieceColor = chesscomVariantPlayerColorsTable?.[pieceElem?.dataset?.color]; pieceName = pieceElem?.dataset?.piece; if(pieceName?.length > 1) { pieceName = pieceName[0]; } } 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 coords = getElemCoordinatesFromTransform(pieceElem); return coords; } 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 squaresContainerElem = document.querySelector('.TheBoard-squares'); let ranks = 0; let files = 0; [...squaresContainerElem.childNodes].forEach((x, i) => { const visibleChildElems = filterInvisibleElems([...x.childNodes]); if(visibleChildElems?.length > 0) { ranks = ranks + 1; if(visibleChildElems.length > files) { files = visibleChildElems.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.type === 'childList') ? 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; const isPremove = mutationArr.filter(m => m?.target?.classList?.contains('highlight')) .map(x => x?.target?.style?.['background-color']) .find(x => x === 'rgb(244, 42, 50)') ? true : false; return ( (mutationArr.length >= 4 && !modifiedHoverSquare) || mutationArr.length >= 7 || modifiedHighlight || modifiedElemPool ) && !isPremove; } }); 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; } }); 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; } }); 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; } }); 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; if(isUserMouseDown) { return false; } return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } }); addSupportedChessSite('chess.coolmathgames.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 document.querySelector('.ranks.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; if(isUserMouseDown) { return false; } return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } }); addSupportedChessSite('papergames.io', { 'boardElem': obj => { return document.querySelector('.cm-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; } }); 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; } }); 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; if(isUserMouseDown) { return false; } return mutationArr.length >= 5; } }); addSupportedChessSite('chessarena.com', { 'boardElem': obj => { return document.querySelector('*[data-component="GameLayoutBoard"] cg-board'); }, 'pieceElem': obj => { return obj.boardQuerySelector('cg-piece:not(*[style*="visibility: hidden;"])'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { const titlesElem = document.querySelector('cg-titles'); return titlesElem?.classList?.contains('rotated') ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const pieceColor = pieceElem?.className?.[0]; const elemPieceName = pieceElem?.className?.[1]; if(pieceColor && elemPieceName) { const pieceName = elemPieceName; // pieceNameToFen[elemPieceName] return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return getElemCoordinatesFromTransform(pieceElem, { 'onlyFlipY': true }); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; if(isUserMouseDown) { return false; } return mutationArr.find(m => m?.attributeName === 'style') ? true : false; } }); 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; } }); 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; } }); 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; } }); addSupportedChessSite('gameknot.com', { 'boardElem': obj => { return document.querySelector('#chess-board-acboard'); }, 'pieceElem': obj => { return obj.boardQuerySelector('*[class*="chess-board-piece"] > img[src*="chess56."][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.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } }); 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; } }); 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; if(isUserMouseDown) { return false; } return mutationArr.length >= 4; } }); addSupportedChessSite('simplechess.com', { 'boardElem': obj => { return document.querySelector('#chessboard'); }, '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('#chessboard_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); 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; if(isUserMouseDown) { return false; } return mutationArr.length >= 7; } }); 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; } }); addSupportedChessSite('app.edchess.io', { 'boardElem': obj => { return document.querySelector('*[data-boardid="chessboard"]'); }, 'pieceElem': obj => { return obj.boardQuerySelector('*[data-piece]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return document.querySelector('*[data-square]')?.dataset?.square == 'h1' ? 'b' : 'w'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const [pieceColor, pieceName] = pieceElem?.dataset?.piece?.split(''); return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; return chessCoordinatesToIndex(pieceElem?.parentElement?.parentElement?.dataset?.square); }, 'boardDimensions': obj => { return [8, 8]; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; return mutationArr.length >= 2; } }); /* _____ __ _ _____ _______ _____ _______ _____ ______ _______ _______ _____ _____ __ _ | | \ | | | | |_____| | | ____/ |_____| | | | | | \ | __|__ | \_| __|__ | __|__ | | |_____ __|__ /_____ | | | __|__ |_____| | \_| 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 ignoreBodyRectLeft = domain === 'app.edchess.io'; const boardOrientation = getBoardOrientation(); instanceVars.playerColor.set(commLinkInstanceID, boardOrientation); instanceVars.fen.set(commLinkInstanceID, getFen()); if(isBoardDrawerNeeded()) { 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 }, 'ignoreBodyRectLeft': ignoreBodyRectLeft }); } await updatePlayerColor(); observeNewMoves(); CommLink.setIntervalAsync(async () => { await CommLink.commands.createInstance(commLinkInstanceID); }, 1000); } function startWhenBackendReady() { let tim###rlForceOpened = 0; const interval = CommLink.setIntervalAsync(async () => { if(await isAcasBackendReady()) { start(); interval.stop(); } else if(tim###rlForceOpened < 1) { tim###rlForceOpened++; GM_openInTab(getCurrentBackendURL(), true); } }, 1000); } function initializeIfSiteReady() { const boardElem = getBoardElem(); const firstPieceElem = getPieceElem(); const bothElemsExist = boardElem && firstPieceElem; const isChessComImageBoard = domain === 'chess.com' && boardElem?.className.includes('webgl-2d'); const boardElemChanged = chessBoardElem != boardElem; if((bothElemsExist || isChessComImageBoard) && boardElemChanged) { chessBoardElem = boardElem; chessBoardElem.addEventListener('mousedown', () => { isUserMouseDown = true; }); chessBoardElem.addEventListener('mouseup', () => { isUserMouseDown = false; }); chessBoardElem.addEventListener('touchstart', () => { isUserMouseDown = true; }); chessBoardElem.addEventListener('touchend', () => { isUserMouseDown = false; }); 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);