// ==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://chess.coolmathgames.com/* // @match https://www.coolmathgames.com/0-chess/* // @match https://immortal.game/* // @match https://worldchess.com/* // @match http://chess.net/* // @match https://chess.net/* // @match https://*.freechess.club/* // @match https://*.chessclub.com/* // @match https://gameknot.com/* // @match https://www.chessanytime.com/* // @match https://app.edchess.io/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_openInTab // @grant GM.getValue // @grant GM.setValue // @grant GM.deleteValue // @grant GM.listValues // @grant GM.openInTab // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_notification // @grant unsafeWindow // @run-at document-start // @require https://update.greasyfork.org/scripts/534637/LegacyGMjs.js?acasv=2 // @require https://update.greasyfork.org/scripts/470418/CommLinkjs.js?acasv=2 // @require https://update.greasyfork.org/scripts/470417/UniversalBoardDrawerjs.js?acasv=2 // @icon https://raw.githubusercontent.com/Psyyke/A.C.A.S/main/assets/images/logo-192.png // @version 2.4.3 // @namespace HKR // @author HKR // @license GPL-3.0 // @downloadURL https://update.greasyfork.org/scripts/459137/%F0%9F%8F%86%20%5B1%20Chess%20Assistant%5D%20ACAS%20%28Advanced%20Chess%20Assistance%20System%29.user.js // @updateURL https://update.greasyfork.org/scripts/459137/%F0%9F%8F%86%20%5B1%20Chess%20Assistant%5D%20ACAS%20%28Advanced%20Chess%20Assistance%20System%29.meta.js // ==/UserScript== /* e e88~-_ e ,d88~~\ d8b d888 \ d8b 8888 /Y88b 8888 /Y88b `Y88b / Y88b 8888 / Y88b `Y88b /____Y88b d88b Y88 / d88b /____Y88b d88b 8888 / Y88b Y88P "88Y-~ 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/ (Tmplr) 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*/ (async () => { try { await LOAD_LEGACY_GM_SUPPORT(); /* ┏┓┓ ┏┓┳┓┏┓┓ ┃┓┃ ┃┃┣┫┣┫┃ ┗┛┗┛┗┛┻┛┛┗┗┛ ============ Code below this point runs on any site, including the GUI. */ const backendConfig = { 'hosts': { 'prod': 'psyyke.github.io', 'dev': 'localhost' }, 'path': '/A.C.A.S/' }; const currentBackendUrlKey = 'currentBackendURL'; const currentBackendUrl = typeof GM_getValue === 'function' ? GM_getValue(currentBackendUrlKey) : await GM.getValue(currentBackendUrlKey); const isBackendUrlUpToDate = Object.values(backendConfig.hosts) .some(x => currentBackendUrl?.includes(x)); const isDevPage = window?.location?.pathname?.includes('/dev'); function constructBackendURL(host) { const protocol = window.location.protocol + '//'; const hosts = backendConfig.hosts; return protocol + (host || (hosts?.prod || hosts?.path)) + backendConfig.path; } function isRunningOnBackend(skipGM) { const hostsArr = Object.values(backendConfig.hosts); const path = window?.location?.pathname; const foundHost = hostsArr.find(host => host === window?.location?.host); const isCorrectPath = path?.includes(backendConfig.path); const isBackend = typeof foundHost === 'string' && isCorrectPath; if(isBackend && !skipGM) GM_setValue(currentBackendUrlKey, constructBackendURL(foundHost)); return isBackend; } const runningOnBackend = isRunningOnBackend(); const runningOnDevPage = runningOnBackend && isDevPage; const activeInputListeners = []; // 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'; 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 }; // Add them to /js/misc/userscriptBridge.js as well if you, // decide to add more variables here const instanceVars = { playerColor: createInstanceVariable('playerColor'), turn: createInstanceVariable('turn'), fen: createInstanceVariable('fen') }; function exposeViaMessages() { const handlers = { USERSCRIPT_getValue: (args, messageId) => { const [key] = args; const value = GM_getValue(key); window.postMessage({ messageId, value }, '*'); }, USERSCRIPT_setValue: (args, messageId) => { const [key, value] = args; GM_setValue(key, value); window.postMessage({ messageId, value: true }, '*'); }, USERSCRIPT_deleteValue: (args, messageId) => { const [key] = args; GM_deleteValue(key); window.postMessage({ messageId, value: true }, '*'); }, USERSCRIPT_listValues: (args, messageId) => { const value = GM_listValues(); window.postMessage({ messageId, value }, '*'); }, USERSCRIPT_getInfo: (args, messageId) => { const value = typeof GM_info !== 'undefined' ? JSON.parse(JSON.stringify(GM_info)) : {}; window.postMessage({ messageId, value }, '*'); }, USERSCRIPT_instanceVars: (args, messageId) => { const [instanceId, key, value] = args; if (!instanceVars.hasOwnProperty(key)) { window.postMessage({ messageId, value: false }, '*'); return; } const result = (value !== undefined) ? instanceVars[key].set(instanceId, value) : instanceVars[key].get(instanceId); window.postMessage({ messageId, value: result }, '*'); } }; window.addEventListener('message', (event) => { const handler = handlers[event.data?.type]; if(handler) handler(event.data.args, event.data.messageId); }); const script = document.createElement('script'); script.innerHTML = 'window.isUserscriptActive = true;'; document.head.appendChild(script); } function exposeViaUnsafe() { if(typeof unsafeWindow !== 'object') return; unsafeWindow.USERSCRIPT = { 'getValue': val => GM_getValue(val), 'setValue': (val, data) => GM_setValue(val, data), 'deleteValue': val => GM_deleteValue(val), 'listValues': val => GM_listValues(val), 'instanceVars': instanceVars, 'getInfo': () => GM_info }; unsafeWindow.isUserscriptActive = true; } if(runningOnBackend && !isDevPage) { if(typeof unsafeWindow === 'object') exposeViaUnsafe(); else exposeViaMessages(); return; } /*ZONE CHANGE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*\ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ //////////////////////////////////////////////////////////////////// /!ZONE CHANGE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING!/ ┏┓┓┏┏┓┏┓┏┓ ┏┓┳┏┳┓┏┓┏┓ ┃ ┣┫┣ ┗┓┗┓ ┗┓┃ ┃ ┣ ┗┓ ┗┛┛┗┗┛┗┛┗┛ ┗┛┻ ┻ ┗┛┗┛ ====================== 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 = Object.freeze([ 'engineElo', 'moveSuggestionAmount', 'arrowOpacity', 'displayMovesOnExternalSite', 'showMoveGhost', 'showOpponentMoveGuess', 'showOpponentMoveGuessConstantly', 'onlyShowTopMoves', 'maxMovetime', 'chessVariant', 'chessEngine', 'lc0Weight', 'engineNodes', 'chessFont', 'useChess960', 'onlyCalculateOwnTurn', 'ttsVoiceEnabled', 'ttsVoiceName', 'ttsVoiceSpeed', 'chessEngineProfile', 'primaryArrowColorHex', 'secondaryArrowColorHex', 'opponentArrowColorHex', 'reverseSide', 'engineEnabled', 'autoMove', 'autoMoveLegit', 'autoMoveRandom', 'autoMoveAfterUser', 'legitModeType', 'moveDisplayDelay', 'renderSquarePlayer', 'renderSquareEnemy', 'renderSquareContested', 'renderSquareSafe', 'renderPiecePlayerCapture', 'renderPieceEnemyCapture', 'renderOnExternalSite', 'feedbackOnExternalSite', 'enableMoveRatings', 'enableEnemyFeedback', 'feedbackEngineDepth', 'enableAdvancedElo', 'moveAsFilledSquares', 'movesOnDemand', 'onlySuggestPieces', 'isUserscriptGhost' ].reduce((o, k) => (o[k] = k, o), {})); const config = {}; const supportedSites = {}; const pieceNameToFen = { 'pawn': 'p', 'knight': 'n', 'bishop': 'b', 'rook': 'r', 'queen': 'q', 'king': 'k' }; let BoardDrawer = null; let chessBoardElem = null; let chesscomVariantPlayerColorsTable = null; let activeGuiMoveMarkings = []; let activeMetricRenders = []; let activeFeedback = []; let boardObserver = null; let dumbBoardObservingInterval = null; let lastMutationObservationDate = 0; let lastCalculatedFullFen = null; let lastBoardRanks = null; let lastBoardFiles = null; let lastBoardSize = null; let lastPieceSize = null; let lastTurn = null; let lastBoardMatrix = null; let lastBoardOrientation = null; let matchFirstSuggestionGiven = false; let lastMoveRequestTime = 0; let lastMutationObsProcessedTurn = null; let isUserMouseDown = false; let activeAutomoves = []; let modListeners = []; let modDrawerListeners = []; let modLastEnteredSquare = { 'squareIndex': null, 'squareFen': null, 'pieceFen': null }; let isMovesOnDemandActive = false; Object.values(configKeys).forEach(key => { config[key] = { get: profile => getGmConfigValue(key, commLinkInstanceID, profile), set: null }; }); 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 getConfigValue(key, profile) { return config[key]?.get(profile); } function setConfigValue(key, val) { return config[key]?.set(val); } const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, { 'singlePacketResponseWaitTime': 250, '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': getBoardOrientation() }); } CommLink.registerSendCommand('ping', { commlinkID: 'mum', data: 'ping' }); CommLink.registerSendCommand('pingInstance', { data: 'ping' }); CommLink.registerSendCommand('log'); CommLink.registerSendCommand('updateBoardOrientation'); CommLink.registerSendCommand('updateBoardFen'); CommLink.registerSendCommand('newMatchStarted'); CommLink.registerSendCommand('calculateBestMoves'); CommLink.registerSendCommand('calculateSpecificMoves'); CommLink.registerSendCommand('forceInstanceRestart'); CommLink.registerSendCommand('toggleConcealAssistance'); 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': const profile = packet.data?.[0]?.profile; boardUtils.removeMarkings(profile); boardUtils.markMoves(packet.data); 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; case 'renderMetricsToSite': renderMetrics(packet.data); return true; case 'feedbackToSite': displayFeedback(packet.data); return true; case 'updateRestartListener': createInputListener('instanceRestart', packet.data, () => { CommLink.commands.forceInstanceRestart(); }); return true; case 'updateConcealAssistanceListener': createInputListener('concealAssistance', packet.data, toggleConcealAssistance); return true; case 'applyAssistanceConcealment': applyAssistanceConcealment(packet.data); return true; } } catch(e) { return null; } }); const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); function getArrowStyle(type, fill, opacity) { const getBaseStyleModification = (f, o) => [ 'stroke: rgb(0 0 0 / 50%);', 'stroke-width: 2px;', 'stroke-linejoin: round;', `fill: ${fill || f};`, `opacity: ${opacity || o};` ].join('\n'); switch(type) { case 'best': return getBaseStyleModification('limegreen', 0.9); case 'secondary': return getBaseStyleModification('dodgerblue', 0.7); case 'opponent': return getBaseStyleModification('crimson', 0.3); } }; function createInputListener(listenerType, targetValue, callback) { if(typeof listenerType !== 'string' || typeof targetValue !== 'string' || !callback) return; const existingIndex = activeInputListeners .findIndex(l => l.listenerType === listenerType); if(existingIndex !== -1) { const existing = activeInputListeners[existingIndex]; if(existing.targetValue === targetValue) return; existing.listeners.forEach(({ type, fn }) => document.removeEventListener(type, fn)); activeInputListeners.splice(existingIndex, 1); } let holdTimer = null; let lastTapTime = 0; const dblTapThreshold = 300; const listeners = []; const addListener = (type, fn) => { document.addEventListener(type, fn); listeners.push({ type, fn }); }; addListener('keydown', (e) => { if(!targetValue.startsWith("Interact") && e.code === targetValue) callback(e); }); const startPress = (e) => { if(!targetValue.startsWith("Interact")) return; const match = targetValue.match(/^InteractLongPress(\d+)$/); if(match) holdTimer = setTimeout(() => { callback(e); holdTimer = null; }, parseInt(match[1], 10) * 1000); if(targetValue === "InteractDoubleClick" && e.type.startsWith("touch")) { const now = performance.now(); if(now - lastTapTime < dblTapThreshold) { callback(e); lastTapTime = 0; } else lastTapTime = now; } }; const endPress = () => { if(holdTimer) { clearTimeout(holdTimer); holdTimer = null; }}; addListener('mousedown', startPress); addListener('mouseup', endPress); addListener('touchstart', startPress); addListener('touchend', endPress); addListener('dblclick', (e) => { if(targetValue === "InteractDoubleClick") callback(e); }); activeInputListeners.push({ listenerType, targetValue, callback, listeners }); } function clearMetricRenders() { activeMetricRenders.forEach(elem => { if(elem) elem?.remove(); }); } function renderMetrics(addedMetrics) { if(!BoardDrawer) return; clearMetricRenders(); function processMetric(metric) { const data = metric?.data; if(!data) return; const shapeType = data?.shapeType; const shapeSquare = data?.shapeSquare; const shapeConfig = data?.shapeConfig; if(shapeType && shapeSquare && shapeConfig) { const shape = BoardDrawer.createShape(shapeType, shapeSquare, shapeConfig); activeMetricRenders.push(shape); } } function findMetricByType(type) { return addedMetrics.filter(metric => metric?.data?.shapeType === type) || []; } findMetricByType('text') .forEach(processMetric); findMetricByType('rectangle') .forEach(processMetric); } function clearFeedback() { activeFeedback.forEach(elem => { if(elem) elem?.remove(); }); } function displayFeedback(addedFeedback) { if(!BoardDrawer) return; clearFeedback(); function processFeedback(feedback) { const data = feedback?.data; if(!data) return; const shapeType = data?.shapeType; const shapeSquare = data?.shapeSquare; const shapeConfig = data?.shapeConfig; if(shapeType && shapeSquare && shapeConfig) { const shape = BoardDrawer.createShape(shapeType, shapeSquare, shapeConfig); activeFeedback.push(shape); } } addedFeedback.forEach(processFeedback); } function maybeAnnounceMarkingsToPage(moveMarkings) { if(!runningOnDevPage || typeof unsafeWindow === 'undefined') return; const markings = activeGuiMoveMarkings || []; let selectedMarking = null; if(markings.length === 1) { selectedMarking = markings[0].player; } else if (markings.length > 1) { const randomIndex = Math.floor(Math.random() * markings.length); selectedMarking = markings[randomIndex].player; } unsafeWindow.postMessage({ name: 'bestMoveArr', value: selectedMarking }); } const boardUtils = { markMoves: moveObjArr => { // needs refactoring but too lazy for now if(!BoardDrawer) return; const maxScale = 1; const minScale = 0.5; const totalRanks = moveObjArr.length; function fillSquare(square, style) { const shapeType = 'rectangle'; const shapeConfig = { style }; const rect = BoardDrawer.createShape(shapeType, square, shapeConfig); return rect; } const markedSquares = { 0: [], 1: [] }; moveObjArr.forEach((markingObj, idx) => { const profile = markingObj.profile; const [from, to] = markingObj.player; const [oppFrom, oppTo] = markingObj.opponent; const oppMovesExist = oppFrom && oppTo; const rank = idx + 1; const cp = markingObj?.cp; 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); const moveAsFilledSquares = getConfigValue(configKeys.moveAsFilledSquares, profile); const onlySuggestPieces = getConfigValue(configKeys.onlySuggestPieces, profile); const movesOnDemand = getConfigValue(configKeys.movesOnDemand, profile); if(onlySuggestPieces && !movesOnDemand) { const fillType = idx === 0 ? 1 : 0, fillColor = fillType ? primaryArrowColorHex : secondaryArrowColorHex; const fromSquareMarking = fillSquare(from, `opacity: ${arrowOpacity}; stroke-width: 5; stroke: black; rx: 2; ry: 2; fill: ${fillColor};`); let markedSquareElems = [fromSquareMarking]; if(oppFrom) { const oppFromSquareMarking = fillSquare(oppFrom, `opacity: ${arrowOpacity}; stroke-width: 5; stroke: black; rx: 2; ry: 2; display: none; fill: ${opponentArrowColorHex};`); const squareListener = BoardDrawer.addSquareListener(from, type => { if(!oppFromSquareMarking) squareListener.remove(); switch(type) { case 'enter': oppFromSquareMarking.style.display = 'inherit'; break; case 'leave': oppFromSquareMarking.style.display = 'none'; break; } }); markedSquareElems.push(oppFromSquareMarking); } activeGuiMoveMarkings.push( { 'otherElems': markedSquareElems }, profile ); } else if(moveAsFilledSquares) { const fillType = idx === 0 ? 1 : 0, fillColor = fillType ? primaryArrowColorHex : secondaryArrowColorHex, styling = `opacity: ${arrowOpacity}; stroke-width: 5; stroke: black; rx: 2; ry: 2; fill: ${fillColor};`, skipFromSquare = markedSquares[fillType].find(x => x === from) ? 'opacity: 0;' : '', skipToSquare = markedSquares[fillType].find(x => x === to) ? 'opacity: 0;' : ''; const fromSquareStyle = `${styling} ${skipFromSquare}`; const toSquareStyle = `filter: brightness(1.5); stroke-dasharray: 4 4; ${styling} ${skipToSquare}`; const fromSquareFill = fillSquare(from, fromSquareStyle); const toSquareFill = fillSquare(to, toSquareStyle); const markedSquareFens = [from, to]; const markedSquareElems = [fromSquareFill, toSquareFill]; if(oppMovesExist && showOpponentMoveGuess) { const oppFromSquareFill = fillSquare(oppFrom, fromSquareStyle + ` fill: ${opponentArrowColorHex};`); const oppToSquareFill = fillSquare(oppTo, toSquareStyle + ` fill: ${opponentArrowColorHex};`); markedSquareElems.push(oppFromSquareFill, oppToSquareFill); if(showOpponentMoveGuessConstantly) { oppFromSquareFill.style.display = 'block'; oppToSquareFill.style.display = 'block'; } else { oppFromSquareFill.style.display = 'none'; oppToSquareFill.style.display = 'none'; const squareListener = BoardDrawer.addSquareListener(from, type => { if(!oppFromSquareFill || !oppToSquareFill) { squareListener.remove(); } switch(type) { case 'enter': oppFromSquareFill.style.display = 'inherit'; oppToSquareFill.style.display = 'inherit'; break; case 'leave': oppFromSquareFill.style.display = 'none'; oppToSquareFill.style.display = 'none'; break; } }); } } markedSquares[fillType].push(...markedSquareFens); activeGuiMoveMarkings.push( { 'otherElems': markedSquareElems }, profile ); } else { 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 { oppArrowElem.style.display = 'none'; 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 } ); } }); maybeAnnounceMarkingsToPage(activeGuiMoveMarkings); }, 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(); markingObj?.otherElems?.forEach(x => x?.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 clearVisuals(noMetricsRemoval = false) { if(!noMetricsRemoval) clearMetricRenders(); clearFeedback(); boardUtils.removeMarkings(); } 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 = getBoardOrientation(); 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 = getBoardOrientation(); 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 = getBoardOrientation(); 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 coordinatesFromMoves(board, piecePos, moves, isPieceWhite) { // piecePos [x, y], moves [[x, y], ...] const result = []; for(let i = 0; i < moves.length; i++) { const x = piecePos[0] + moves[i][0]; const y = piecePos[1] + moves[i][1]; const square = board?.[y]?.[x]; if(!square) continue; if(square === 1) { result.push([x, y]); } else { const squareIsWhite = square === square.toUpperCase(); if(squareIsWhite !== isPieceWhite) result.push([x, y]); } } return result; } // Called by addMovesOnDemandListeners() function getPiecePaths(board, piecePos, pieceFen, isPieceWhite) { const [xPos, yPos] = piecePos; if(!pieceFen || typeof pieceFen !== 'string') return; const pieceType = pieceFen.toUpperCase(); const isLinearMovingPiece = pieceType === 'R' || pieceType === 'Q'; const isDiagonalMovingPiece = pieceType === 'B' || pieceType === 'Q'; const boardHeight = board.length; const boardWidth = board[0]?.length || 0; const longerBoardSide = Math.max(boardWidth, boardHeight); const shorterBoardSide = Math.min(boardWidth, boardHeight); function cast(directions, length) { const moves = []; for(let direction of directions) { for(let i = 1; i < length; i++) { const x = xPos + direction[0] * i; const y = yPos + direction[1] * i; const square = board?.[y]?.[x]; if(!square) break; if(square === 1) { moves.push([x, y]); } else { const squareIsWhite = square === square.toUpperCase(); if(squareIsWhite !== isPieceWhite) { moves.push([x, y]); break; } else break; } } } return moves; } function castDiagonal() { return cast( [ [1, -1], [-1, -1], // top right, top left [1, 1], [-1, 1] // bottom right, bottom left ], shorterBoardSide ); } function castStraight() { return cast( [ [0, -1], [0, 1], // top, bottom [-1, 0], [1, 0] // left, right ], longerBoardSide ); } if(pieceType === 'P') { const direction = isPieceWhite ? [[-1, -1], [1, -1], [0, -1], [0, -2]] : [[-1, 1], [1, 1], [0, 1], [0, 2]]; return coordinatesFromMoves(board, piecePos, direction, isPieceWhite); } if(pieceType === 'N') { return coordinatesFromMoves(board, piecePos, [ [-2, -1], [-2, 1], [2, -1], [2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2] ], isPieceWhite); } if(pieceType === 'K') { return coordinatesFromMoves(board, piecePos, [ [-1, 0], [-2, 0], [1, 0], [2, 0], [0, -1], [0, 1], [-1, -1], [1, 1], [-1, 1], [1, -1] ], isPieceWhite); } if(pieceType === 'B') return castDiagonal(); if(pieceType === 'R') return castStraight(); if(pieceType === 'Q') return [...castDiagonal(), ...castStraight()]; return [0, 0]; } function addMovesOnDemandListeners() { let lastProcessedSquareFen = null; if(!BoardDrawer) return; function handle() { if((lastProcessedSquareFen !== modLastEnteredSquare.squareFen) || !modLastEnteredSquare.squareFen) { const lastIdx = modLastEnteredSquare.squareIndex; if(!modLastEnteredSquare.squareFen && lastIdx) { const lastPieceFen = modLastEnteredSquare.pieceFen; modLastEnteredSquare.squareFen = indexToChessCoordinates(lastIdx); modLastEnteredSquare.pieceFen = lastBoardMatrix?.[lastIdx?.[1]]?.[lastIdx?.[0]]; if(lastPieceFen === 1) return; } lastProcessedSquareFen = modLastEnteredSquare.squareFen; const pieceFen = modLastEnteredSquare.pieceFen; const isPieceWhite = pieceFen >= 'A' && pieceFen <= 'Z'; const isPlayerPiece = (lastBoardOrientation === 'w') === isPieceWhite; if(!pieceFen) return; const legalMovesArr = getPiecePaths(lastBoardMatrix, modLastEnteredSquare.squareIndex, pieceFen, isPieceWhite) ?.map(pathArr => lastProcessedSquareFen + indexToChessCoordinates(pathArr)); if(legalMovesArr?.length > 0) CommLink.commands.calculateSpecificMoves({ 'moves': legalMovesArr, 'isOpponent': !isPlayerPiece }); } } // Clear existing listeners modListeners.forEach(({ type, handler }) => { document.removeEventListener(type, handler); }); modListeners.length = 0; modDrawerListeners.forEach(x => x?.remove()); modDrawerListeners.length = 0; const mouseDownHandler = () => handle(true); const touchStartHandler = () => handle(true); [ ['mousedown', mouseDownHandler], ['touchstart', touchStartHandler] ].forEach(([type, handler]) => { document.addEventListener(type, handler); modListeners.push({ type, handler }); }); for (let y = 0; y < lastBoardMatrix.length; y++) for (let x = 0; x < lastBoardMatrix[y].length; x++) { const squareFen = indexToChessCoordinates([x, y]); const squareListener = BoardDrawer.addSquareListener(squareFen, type => { if(!isMovesOnDemandActive) return; switch(type) { case 'enter': modLastEnteredSquare.pieceFen = lastBoardMatrix[y][x]; modLastEnteredSquare.squareFen = squareFen; modLastEnteredSquare.squareIndex = [x, y]; break; } }); modDrawerListeners.push(squareListener); } } function getRandomOwnPieceDomCoord(fenCoord, boardMatrix) { 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 'worldchess.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 isBoardDrawerNeeded() { const config = GM_getValue(dbValues.AcasConfig); const gP = config?.global?.['profiles']; const iP = config?.instance?.[commLinkInstanceID]?.['profiles']; const isGhost = config?.global?.[configKeys.isUserscriptGhost]; if(isGhost) return false; function check(cfg) { const profiles = Object.keys(cfg); for(const profileName of profiles) { const profile = cfg[profileName]; const externalMoves = profile[configKeys.displayMovesOnExternalSite]; const renderingNeeded = profile[configKeys.renderOnExternalSite]; const feedbackNeeded = profile[configKeys.feedbackOnExternalSite]; const movesOnDemand = profile[configKeys.movesOnDemand]; if(externalMoves || renderingNeeded || feedbackNeeded || movesOnDemand) { return true; } } } if(gP && check(gP)) return true; if(iP && check(iP)) return true; return false; } function squeezeEmptySquares(fenStr) { return fenStr.replace(/1+/g, match => match.length); } function getBoardOrientation() { const playerColor = instanceVars.playerColor.get(commLinkInstanceID); return playerColor; } 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); return result; } function addSupportedChessSite(domains, typeHandlerObj) { const domainList = Array.isArray(domains) ? domains : [domains]; domainList.forEach(domain => { supportedSites[domain] = typeHandlerObj; }); } function getBoardElem() { const boardElem = getSiteData('boardElem'); return boardElem || null; } function getPieceElem(getAll) { const boardElem = getBoardElem(); const boardQuerySelector = (getAll ? query => { const elems = boardElem?.querySelectorAll(query); return elems?.length ? [...elems] : null; } : 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 isNewMoveArr = getSiteData('isMutationNewMove', { mutationArr }); // [isNewMove, turn] return isNewMoveArr || false; } function getMutationTurn(mutationArr) { const turn = getSiteData('getMutationTurn', { mutationArr }); return turn || null; } 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); 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(onlyBasic) { return basicFen; } // FEN structure: [fen] [player color] [castling rights] [en passant targets] [halfmove clock] [fullmove clock] const fullFen = `${basicFen} ${getBoardOrientation()} ${getRights()} - 0 1`; return fullFen; } function resetCachedValues() { chesscomVariantPlayerColorsTable = null; } function countTotalPieces(fen) { let pieceCount = 0; const position = fen.split(' ')[0]; for(let char of position) { if(/[rnbqkpRNBQKP]/.test(char)) { pieceCount += (pieceCount[char] || 0) + 1; } } return pieceCount; } function getPieceChangeAmount(lastFen, newFen) { if(!lastFen || !newFen) return 0; const lastPieceCount = countTotalPieces(lastFen); const newPieceCount = countTotalPieces(newFen); // (need to implement fix for variants which may add pieces legally) const countChange = newPieceCount - lastPieceCount; /* Possible "countChange" value explanations, (countChange < -1) -> multiple pieces have disappeared (atomic chess variant or a faulty newFen?) (countChange = -1) -> piece has been eaten (countChange = 0) -> piece moved (countChange = 1) -> piece has spawned (countChange > 1) -> multiple pieces have spawned (possibly a new game?) */ return countChange; } function getBoardSquareChangeAmount(lastFen, newFen) { if(!lastFen || !newFen) return 0; let board1 = lastFen.split(' ')[0].replace(/\d/g, d => ' '.repeat(d)).split('/').join(''); let board2 = newFen.split(' ')[0].replace(/\d/g, d => ' '.repeat(d)).split('/').join(''); let changedFrom = []; let diff = 0; for(let i = 0; i < board1.length; i++) { if(board1[i] !== board2[i]) { if(board1[i]?.trim()?.length > 0) changedFrom.push(board1[i]?.toLowerCase()); diff += 1; } } /* Possible "diff" value explanations, (diff = 0) -> no changes, same board layout (diff = 1) -> only one tile abruptly changed, shouldn't be possible (diff = 2) -> a piece moved, maybe it ate another piece (diff = 3) -> three tiles had changes, shouldn't be possible (NOTE: it's possible if the fen has skipped one turn...) (diff > 3) -> a lot of tiles had changes, maybe a new game started, the change is significant so allowing it (diff = 4) -> takeback or castling */ return diff; } async function processBoardPosition(currentFullFen = getFen(), squareChangeAmount = 0) { lastCalculatedFullFen = currentFullFen; lastMoveRequestTime = Date.now(); clearVisuals(true); boardUtils.setBoardDimensions(getBoardDimensions()); modLastEnteredSquare.squareFen = null; if(!modListeners.length) addMovesOnDemandListeners(); const didBoardOrientationChange = await checkBoardOrientationChange(); // Most likely a new match started if(didBoardOrientationChange || squareChangeAmount > 5) { resetCachedValues(); matchFirstSuggestionGiven = false; CommLink.commands.newMatchStarted(); } CommLink.commands.updateBoardFen(currentFullFen); } // This is called by observeNewMoves() async function determineBoardPositionValidity(turn) { const currentFullFen = await getFen(); const fenChanged = currentFullFen?.split(' ', 1)?.[0] !== lastCalculatedFullFen?.split(' ', 1)?.[0]; // only compare the first part of the FENs to detect change const pieceAmountChange = getPieceChangeAmount(lastCalculatedFullFen, currentFullFen); const squareChangeAmount = getBoardSquareChangeAmount(lastCalculatedFullFen, currentFullFen); const pieceAmount = getPieceAmount(); // this depends on the current DOM, not from FEN. // A new board just appeared and is still loading, most likely a new match! if(pieceAmount === 0) { lastCalculatedFullFen = null; await wait(100); return determineBoardPositionValidity(getBoardOrientation()); } // Do not continue if FEN did not change! if(!fenChanged) return; if(turn) { if(lastTurn === turn) turn = getBoardOrientation(); lastTurn = turn; instanceVars.turn.set(commLinkInstanceID, turn); } if(pieceAmountChange === -1) { // Do not continue if a piece just disappeared, this is not possible legally! // (This happens sometimes because the mutationObserver detects DOM changes so fast) if(squareChangeAmount === 1) { return; } } processBoardPosition(currentFullFen, squareChangeAmount); } // This also updates the turn instanceVariable that the GUI uses to determine the turn! function observeNewMoves() { if(boardObserver?.disconnect) boardObserver.disconnect(); if(dumbBoardObservingInterval) clearInterval(dumbBoardObservingInterval); dumbBoardObservingInterval = setInterval(() => { if(isUserMouseDown) return; determineBoardPositionValidity(lastMutationObsProcessedTurn || getBoardOrientation()); }, 250); boardObserver = new MutationObserver(mutationArr => { try { lastMutationObservationDate = Date.now(); const mutationMoveArr = isMutationNewMove(mutationArr); // returns [isNewMove, turn] const isNewMove = mutationMoveArr?.[0]; let turn = mutationMoveArr?.[1]; if(turn) lastMutationObsProcessedTurn = turn; // Do not continue if mutation was not detected as a possible new move! (Different for each chess site) // We later compare FENs to detect if it was actually a new valid move! if(!isNewMove) return; determineBoardPositionValidity(turn); } catch(e) { if(debugModeActivated) console.error(e); } }); boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true }); } async function checkBoardOrientationChange() { 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); } return boardOrientationChanged; } /*ZONE CHANGE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*\ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ //////////////////////////////////////////////////////////////////// /!ZONE CHANGE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING!/ ┏┓┳┏┳┓┏┓ ┏┓┏┓┳┓┏┓┳┏┓ ┗┓┃ ┃ ┣ ┃ ┃┃┃┃┣ ┃┃┓ ┗┛┻ ┻ ┗┛ ┗┛┗┛┛┗┻ ┻┗┛ ===================== 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 => { if(elem?.dataset?.piece?.toLowerCase() === 'x') return false; return !elem.closest('[class*="captured-pieces"]'); }); 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]; } }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; for(let i = 0; i < classList.length; i++) { const cls = classList[i]; if(cls.length === 2) { const prefix = cls[0]; if(prefix === 'b') blacks++; else if(prefix === 'w') whites++; } } }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const pathname = obj.pathname; const mutationArr = obj.mutationArr; // Process variant boards... if(pathname?.includes('/variants')) { if(isUserMouseDown) return [false, null]; return [true, getBoardOrientation()]; // allow everything, always make own turn } // Not a variant board, processing differently... if(mutationArr.length === 1) return [false, null]; 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; const isNewMove = mutationArr.length >= 3 && !isPremove; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('black')) blacks += 1; if(classList?.contains('white')) whites += 1; }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 3; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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 = getBoardOrientation(); 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(); }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let ally = 0; let enemy = 0; const boardOrientation = getBoardOrientation(); const isPlayerWhite = boardOrientation === 'w'; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('ally')) ally += 1; if(classList?.contains('enemy')) enemy += 1; }); const turn = isPlayerWhite ? ally > enemy ? 'b' : 'w' : ally > enemy ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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 = getBoardOrientation(); 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(); }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let ally = 0; let enemy = 0; const boardOrientation = getBoardOrientation(); const isPlayerWhite = boardOrientation === 'w'; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('ally')) ally += 1; if(classList?.contains('enemy')) enemy += 1; }); const turn = isPlayerWhite ? ally > enemy ? 'b' : 'w' : ally > enemy ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); addSupportedChessSite('chess.org', { '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; return getElemCoordinatesFromTransform(pieceElem); }, 'boardDimensions': obj => { return [8, 8]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('black')) blacks += 1; if(classList?.contains('white')) whites += 1; }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; if(isUserMouseDown) { return false; } const isNewMove = true; // laggy but this is a non-popular site if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('black')) blacks += 1; if(classList?.contains('white')) whites += 1; }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; if(isUserMouseDown) { return false; } const isNewMove = true; // laggy but this is a non-popular site // NOTE! IF YOU'RE TRYING TO FIX DISAPPEARING MOVES, IT IS CAUSED BY THE BOARD CHANGING // AND THE USERSCRIPT TRIGGERING A WHOLE NEW MATCH STARTING. THIS IS A NON-POPULAR SITE // SO FIX HAS NOT BEEN MADE... if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; const playerColor = getBoardOrientation(); return playerColor || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 12; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; const playerColor = getBoardOrientation(); return playerColor || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; if(isUserMouseDown) { return false; } const isNewMove = mutationArr.length >= 5; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); addSupportedChessSite('worldchess.com', { 'boardElem': obj => { return document.querySelector('*[data-component="GameBoard"] 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation?.target?.classList; for(let i = 0; i < classList.length; i++) { const cls = classList[i]; if(cls.length === 2) { const prefix = cls[0]; if(prefix === 'b') blacks++; else if(prefix === 'w') whites++; } } }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; if(isUserMouseDown) { return false; } const isNewMove = mutationArr.find(m => m?.attributeName === 'style') ? true : false; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('black')) blacks += 1; if(classList?.contains('white')) whites += 1; }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 3; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; let blacks = 0; let whites = 0; mutationArr.forEach(mutation => { const classList = mutation.target?.classList; if(classList?.contains('black')) blacks += 1; if(classList?.contains('white')) whites += 1; }); const turn = blacks > whites ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 3; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); addSupportedChessSite('play.chessclub.com', { 'boardElem': obj => { return document.querySelector('[data-boardid]'); }, 'pieceElem': obj => { return obj.boardQuerySelector('[data-piece]'); }, 'chessVariant': obj => { return 'chess'; }, 'boardOrientation': obj => { return document.querySelector('[data-square]')?.dataset?.square === 'a8' ? 'w' : 'b'; }, 'pieceElemFen': obj => { const pieceElem = obj.pieceElem; const [pieceColor, pieceName] = (pieceElem?.dataset?.piece || 'wp'); if(pieceColor && pieceName) { return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const parentParent = pieceElem?.parentElement?.parentElement; if(parentParent) { return chessCoordinatesToIndex(parentParent?.dataset?.square); } }, 'boardDimensions': obj => { return [8, 8]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; return getBoardOrientation() || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.find(mutation => mutation?.type === 'childList') ? true : false; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; return getBoardOrientation() || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); 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]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; return getBoardOrientation() || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 2; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); addSupportedChessSite([ backendConfig?.hosts?.prod || 'psyyke.github.io', backendConfig?.hosts?.dev || 'localhost' ], { '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.side'); 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 ?? [])] .map(cls => cls.replace('-piece', '')) .find(cls => Object.values(pieceNameToFen).includes(cls)); if(pieceColor && elemPieceName) { return pieceColor == 'w' ? elemPieceName.toUpperCase() : elemPieceName.toLowerCase(); } }, 'pieceElemCoords': obj => { const pieceElem = obj.pieceElem; const key = pieceElem?.cgKey; if(key) { return chessCoordinatesToIndex(key); } }, 'boardDimensions': obj => { return [8, 8]; }, 'getMutationTurn': obj => { const mutationArr = obj.mutationArr; const mutationContainsBlack = mutationArr .find(mutation => mutation.target?.classList?.contains('black')); const turn = mutationContainsBlack ? 'w' : 'b'; return turn || null; }, 'isMutationNewMove': obj => { const mutationArr = obj.mutationArr; const isNewMove = mutationArr.length >= 2; if(isNewMove) return [isNewMove, getMutationTurn(mutationArr)]; return [isNewMove, null]; } }); /*ZONE CHANGE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*\ \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ //////////////////////////////////////////////////////////////////// /!ZONE CHANGE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING!/ ┳┓┏┓┏┓┏┳┓ ┏┓┏┓┏┓┳┳┏┓┳┓┏┓┏┓ ┣┫┃┃┃┃ ┃ ┗┓┣ ┃┃┃┃┣ ┃┃┃ ┣ ┻┛┗┛┗┛ ┻ ┗┛┗┛┗┻┗┛┗┛┛┗┗┛┗┛ =========================== 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 refreshSettings() { const config = GM_getValue(dbValues.AcasConfig); const profiles = config?.global?.profiles; if(typeof profiles != 'object') return; isMovesOnDemandActive = Object.keys(profiles).some(profileName => profiles[profileName]?.movesOnDemand === true); } async function start() { await CommLink.commands.createInstance(commLinkInstanceID); const pathname = window.location.pathname; const boardOrientation = getBoardOrientation(); instanceVars.playerColor.set(commLinkInstanceID, boardOrientation); instanceVars.fen.set(commLinkInstanceID, getFen()); if(isBoardDrawerNeeded()) { if(BoardDrawer) BoardDrawer?.terminate(); BoardDrawer = new UniversalBoardDrawer(chessBoardElem, { 'window': window, 'boardDimensions': getBoardDimensions(), 'playerColor': getBoardOrientation(), 'zIndex': Math.floor(Math.random() * (99 - 10 + 1)) + 10, 'prepend': true, 'debugMode': debugModeActivated, 'adjustSizeByDimensions': domain === 'chess.com' && pathname?.includes('/variants'), 'adjustSizeConfig': { 'noLeftAdjustment': true }, 'ignoreBodyRectLeft': domain === 'app.edchess.io' }); const waitForBoardMatrix = setInterval(() => { if(lastBoardMatrix) { clearInterval(waitForBoardMatrix); addMovesOnDemandListeners(); } }, 50); } await checkBoardOrientationChange(); refreshSettings(); observeNewMoves(); CommLink.setIntervalAsync(async () => { await CommLink.commands.createInstance(commLinkInstanceID); }, 1000); createInputListener( 'concealAssistance', await getGmConfigValue('concealAssistanceTriggerCode'), toggleConcealAssistance ); createInputListener( 'instanceRestart', await getGmConfigValue('instanceRestartTriggerCode'), () => { CommLink.commands.forceInstanceRestart() } ); } function applyAssistanceConcealment(isConcealed = false) { const BoardDrawerSvg = BoardDrawer?.boardContainerElem; if(!BoardDrawerSvg) return; if(isConcealed) BoardDrawerSvg.style.display = 'none'; else BoardDrawerSvg.style.display = 'block'; } function toggleConcealAssistance() { CommLink.commands.toggleConcealAssistance(); } function startWhenBackendReady() { let timesUrlForceOpened = 0; let i = 0; const interval = CommLink.setIntervalAsync(async () => { i++; if(await isAcasBackendReady()) { start(); interval.stop(); } else if(timesUrlForceOpened === 0 && (i % 10 === 0)) { timesUrlForceOpened++; const config = GM_getValue(dbValues.AcasConfig); const isGhost = config?.global?.[configKeys.isUserscriptGhost]; if(!isGhost) GM_openInTab(getCurrentBackendURL(), true); } }, 100); } 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) { processBoardPosition(); } 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, 100); // This slow rate might cause users to complain that settings aren't being applied fast enough setInterval(refreshSettings, 2500); } catch(e) { // Attempt to catch all errors on userscript (Note: ONLY LOG DURING DEVELOPMENT. ERRORS EXPOSE USERSCRIPT TO THE PAGE!) //console.warn(e); }})(); // Wraps around the whole userscript to enable async. /*//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// Thank you for reading through this userscript! Please visit GitHub Contributions are absolutely welcome >> github.com/Psyyke/A.C.A.S! ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// 000000000000000000000000000000000000000000000000000000000000000000 777777770000000007777777777777770000000077777777700000000077777777 777777777000000777777777777777777700000077777777770000007777777777 777077777000007777777000000077777700000777777777777000007777770000 777077777700007777770000000000000000007777770077777000007777777777 770007777770077777700000000000000000077777700077777700000777777777 700000777777007777770000000000000000077777700007777770000000007777 777777777777007777770000000077777700777777777777777777077777700000 777777777777700777777777777777777707777777777777777777007777777777 000000007777770007777777777777770077777770000000077777700777777777 000000007777777000007777777770000077777700000000007777770000777777 000000000000000000000000000000000000000000000000000000000000000000 ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////*/