// ==UserScript== // @name A.C.A.S (Advanced Chess Assistance System) // @name:en A.C.A.S (Advanced Chess Assistance System) // @name:fi A.C.A.S (Edistynyt shakkiavustusjärjestelmä) // @name:zh-CN A.C.A.S(高级国际象棋辅助系统) // @name:es A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez) // @name:hi A.C.A.S (उन्नत शतरंज सहायता प्रणाली) // @name:ar A.C.A.S (نظام المساعدة المتقدم في الشطرنج) // @name:pt A.C.A.S (Sistema Avançado de Assistência ao Xadrez) // @name:ja A.C.A.S(先進的なチェス支援システム) // @name:de A.C.A.S (Fortgeschrittenes Schach-Hilfesystem) // @name:fr A.C.A.S (Système Avancé d'Assistance aux Échecs) // @name:it A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi) // @name:ko A.C.A.S (고급 체스 보조 시스템) // @name:nl A.C.A.S (Geavanceerd Schaakondersteuningssysteem) // @name:pl A.C.A.S (Zaawansowany System Pomocy Szachowej) // @name:tr A.C.A.S (Gelişmiş Satranç Yardım Sistemi) // @name:vi A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao) // @name:uk A.C.A.S (Система передової допомоги в шахах) // @name:ru A.C.A.S (Система расширенной помощи в шахматах) // @description Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system // @description:en Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system // @description:fi Paranna shakkipelisi suorituskykyä huippuluokan reaaliaikaisen siirtoanalyysin ja strategisen avustusjärjestelmän avulla // @description:zh-CN 利用尖端实时走法分析和策略辅助系统,提升您的国际象棋水平 // @description:es Mejora tu rendimiento en ajedrez con un sistema de análisis de movimientos en tiempo real y asistencia estratégica de vanguardia // @description:hi अपने शतरंज प्रदर्शन को उन्नत करें, एक कटिंग-एज रियल-टाइम मूव विश्लेषण और रणनीति सहायता प्रणाली के साथ // @description:ar قم بتحسين أداءك في الشطرنج مع تحليل حركات اللعب في الوقت الحقيقي ونظام مساعدة استراتيجية حديث // @description:pt Melhore seu desempenho no xadrez com uma análise de movimentos em tempo real e um sistema avançado de assistência estratégica // @description:ja 最新のリアルタイムのムーブ分析と戦略支援システムでチェスのパフォーマンスを向上させましょう // @description:de Verbessern Sie Ihre Schachleistung mit einer hochmodernen Echtzeitzug-Analyse- und Strategiehilfe-System // @description:fr Améliorez vos performances aux échecs avec une analyse de mouvement en temps réel de pointe et un système d'assistance stratégique // @description:it Migliora le tue prestazioni agli scacchi con un sistema all'avanguardia di analisi dei movimenti in tempo reale e assistenza strategica // @description:ko 최첨단 실시간 움직임 분석 및 전략 지원 시스템으로 체스 성과 향상 // @description:nl Verbeter je schaakprestaties met een geavanceerd systeem voor realtime zetanalyse en strategische ondersteuning // @description:pl Popraw swoje osiągnięcia w szachach dzięki zaawansowanemu systemowi analizy ruchów w czasie rzeczywistym i wsparciu strategicznemu // @description:tr Keskinleşmiş gerçek zamanlı hareket analizi ve strateji yardım sistemiyle satranç performansınızı artırın // @description:vi Nâng cao hiệu suất cờ vua của bạn với hệ thống phân tích nước đi và hỗ trợ chiến thuật hiện đại // @description:uk Покращуйте свою шахову гру з використанням передової системи аналізу ходів в режимі реального часу та стратегічної підтримки // @description:ru Слава Украине // @homepageURL https://hakorr.github.io/A.C.A.S // @supportURL https://github.com/Hakorr/A.C.A.S/tree/main#why-doesnt-it-work // @match https://www.chess.com/* // @match https://lichess.org/* // @match https://playstrategy.org/* // @match https://www.pychess.org/* // @match https://chess.org/* // @match https://papergames.io/* // @match https://vole.wtf/kilobytes-gambit/ // @match https://hakorr.github.io/A.C.A.S/* // @match http://localhost/* // @grant GM_getValue // @grant GM_setValue // @grant GM_deleteValue // @grant GM_listValues // @grant GM_registerMenuCommand // @grant GM_openInTab // @grant GM_addStyle // @grant unsafeWindow // @run-at document-start // @version 2.0.3 // @namespace HKR // @author HKR // @require https://greasyfork.org/scripts/470418-commlink-js/code/CommLinkjs.js // @require https://greasyfork.org/scripts/470417-universalboarddrawer-js/code/UniversalBoardDrawerjs.js // @downloadURL none // ==/UserScript== /* e e88~-_ e ,d88~~\ d8b d888 \ d8b 8888 /Y88b 8888 /Y88b `Y88b / Y88b 8888 / Y88b `Y88b, /____Y88b d88b Y888 / d88b /____Y88b d88b 8888 / Y88b Y88P "88_-~ Y88P / Y88b Y88P \__88P' Advanced Chess Assistance System (A.C.A.S) v2 | Q3 2023 [WARNING] - Please be advised that the use of A.C.A.S may violate the rules and lead to disqualification or banning from tournaments and online platforms. - The developers of A.C.A.S and related systems will NOT be held accountable for any consequences resulting from its use. - We strongly advise to use A.C.A.S only in a controlled environment ethically.*/ // 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 // const backendURL = 'https://hakorr.github.io/A.C.A.S/'; // FOR DEVELOPMENT USE: 'http://localhost/A.C.A.S/'; const domain = window.location.hostname.replace('www.', ''); const tempValueIndicator = '-temp-value-'; const dbValues = { AcasConfig: 'AcasConfig', playerColor: instanceID => 'playerColor' + tempValueIndicator + instanceID, turn: instanceID => 'turn' + tempValueIndicator + instanceID, fen: instanceID => 'fen' + tempValueIndicator + instanceID }; 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 instanceVars = { playerColor: createInstanceVariable('playerColor'), turn: createInstanceVariable('turn'), fen: createInstanceVariable('fen') }; if(window?.location?.href?.includes(backendURL)) { // expose variables and functions unsafeWindow.USERSCRIPT = { 'GM_info': GM_info, 'GM_getValue': val => GM_getValue(val), 'GM_setValue': (val, data) => GM_setValue(val, data), 'GM_deleteValue': val => GM_deleteValue(val), 'GM_listValues': val => GM_listValues(val), 'tempValueIndicator': tempValueIndicator, 'dbValues': dbValues, 'instanceVars': instanceVars, 'CommLinkHandler': CommLinkHandler, }; return; } // 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 // function getUniqueID() { return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c => (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) ) } const commLinkInstanceID = getUniqueID(); const blacklistedURLs = [ 'https://www.chess.com/play', 'https://lichess.org/', 'https://chess.org/', 'https://papergames.io/en/chess', 'https://playstrategy.org/', 'https://www.pychess.org/', 'https://hakorr.github.io/A.C.A.S/', 'https://hakorr.github.io/A.C.A.S/why/', 'https://hakorr.github.io/A.C.A.S/tos/', 'http://localhost/A.C.A.S/' ]; const configKeys = { 'engineElo': 'engineElo', 'moveSuggestionAmount': 'moveSuggestionAmount', 'arrowOpacity': 'arrowOpacity', 'displayMovesOnExternalSite': 'displayMovesOnExternalSite', 'showMoveGhost': 'showMoveGhost', 'showOpponentMoveGuess': 'showOpponentMoveGuess', 'maxMovetime': 'maxMovetime', 'chessVariant': 'chessVariant', 'chessFont': 'chessFont', 'useChess960': 'useChess960' }; const config = {}; Object.values(configKeys).forEach(key => { config[key] = { get: () => getGmConfigValue(key, commLinkInstanceID), set: null }; }); const debugModeActivated = false; let BoardDrawer = null; let chessBoardElem = null; let lastBasicFen = null; let chesscomVariantBoardCoordsTable = null; let activeSiteMoveHighlights = []; let inactiveGuiMoveMarkings = []; let lastBoardRanks = null; let lastBoardFiles = null; let lastBoardSize = null; let lastPieceSize = null; let lastBoardOrientation = null; const domainsWithoutDifferentBoardDimensionsArr = ['chess.org', 'lichess.org', 'papergames.io', 'vole.wtf']; const arrowStyles = { 'best': ` fill: limegreen; opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.9'}; stroke: rgb(0 0 0 / 50%); stroke-width: 2px; stroke-linejoin: round; `, 'secondary': ` fill: dodgerblue; opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.7'}; stroke: rgb(0 0 0 / 50%); stroke-width: 2px; stroke-linejoin: round; `, 'opponent': ` fill: crimson; stroke: rgb(0 0 0 / 25%); stroke-width: 2px; stroke-linejoin: round; display: none; opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.3'}; ` }; const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, { 'singlePacketResponseWaitTime': 1500, 'maxSendAttempts': 3, 'statusCheckInterval': 1, 'silentMode': true }); // manually register a command so that the variables are dynamic CommLink.commands['createInstance'] = async () => { return await CommLink.send('mum', 'createInstance', { 'domain': domain, 'instanceID': commLinkInstanceID, 'chessVariant': getChessVariant(), 'playerColor': getPlayerColorVariable() }); } CommLink.registerSendCommand('ping', { commlinkID: 'mum', data: 'ping' }); CommLink.registerSendCommand('pingInstance', { data: 'ping' }); CommLink.registerSendCommand('log'); CommLink.registerSendCommand('updateBoardOrientation'); CommLink.registerSendCommand('updateBoardFen'); CommLink.registerSendCommand('calculateBestMoves'); CommLink.registerListener(`backend_${commLinkInstanceID}`, packet => { try { switch(packet.command) { case 'ping': return `pong (took ${Date.now() - packet.date}ms)`; case 'getFen': return getFen(); case 'removeSiteMoveMarkings': boardUtils.removeBestMarkings(); return true; case 'markMoveToSite': boardUtils.markMove(packet.data); return true; } } catch(e) { return null; } }); 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) { if (!lastBoardSize) { lastBoardSize = getElementSize(chessBoardElem); } if (!lastBoardRanks) { const [files, ranks] = getBoardDimensions(); lastBoardRanks = ranks; lastBoardFiles = files; } const boardOrientation = getPlayerColorVariable(); const [x, y] = extractElemTransformData(elem); const boardDimensions = lastBoardSize; const squareDimensions = boardDimensions.width / lastBoardRanks; const normalizedX = Math.round(x / squareDimensions); const normalizedY = Math.round(y / squareDimensions); if (boardOrientation === 'w') { const flippedY = lastBoardFiles - normalizedY - 1; return [normalizedX, flippedY]; } else { const flippedX = lastBoardRanks - normalizedX - 1; return [flippedX, normalizedY]; } } function createChesscomVariantBoardCoordsTable() { chesscomVariantBoardCoordsTable = {}; const boardElem = getBoardElem(); const [boardWidth, boardHeight] = getBoardDimensions(); const boardOrientation = getBoardOrientation(); const squareElems = getSquareElems(boardElem); let squareIndex = 0; for(let x = 0; x < boardWidth; x++) { for(let y = boardHeight; y > 0; y--) { const squareElem = squareElems[squareIndex]; const id = squareElem?.id; const xIdx = x; const yIdx = y - 1; if(id) { chesscomVariantBoardCoordsTable[id] = [xIdx, yIdx]; } squareIndex++; } } } function chessCoordinatesToIndex(coord) { const x = coord.charCodeAt(0) - 97; let y = null; const lastHalf = coord.slice(1); if(lastHalf === ':') { y = 9; } else { y = Number(coord.slice(1)) - 1; } return [x, y]; } function getGmConfigValue(key, instanceID) { const config = GM_getValue(dbValues.AcasConfig); const instanceValue = config?.instance?.[instanceID]?.[key]; const globalValue = config?.global?.[key]; if(instanceValue !== undefined) { return instanceValue; } if(globalValue !== undefined) { return globalValue; } return null; } function getConfigValue(key) { return config[key]?.get(); } function setConfigValue(key, val) { return config[key]?.set(val); } function squeezeEmptySquares(fenStr) { return fenStr.replace(/1+/g, match => match.length); } function getPlayerColorVariable() { return instanceVars.playerColor.get(commLinkInstanceID); } function getFenPieceColor(pieceFenStr) { return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b'; } function getFenPieceOppositeColor(pieceFenStr) { return getFenPieceColor(pieceFenStr) == 'w' ? 'b' : 'w'; } function convertPieceStrToFen(str) { if(!str || str.length !== 2) { return null; } const firstChar = str[0].toLowerCase(); const secondChar = str[1]; if(firstChar === 'w') { return secondChar.toUpperCase(); } else if (firstChar === 'b') { return secondChar.toLowerCase(); } return null; } function getBoardElem() { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { return document.querySelector('#board'); } return document.querySelector('chess-board'); } case 'lichess.org': { return document.querySelector('cg-board'); } case 'playstrategy.org': { return document.querySelector('cg-board'); } case 'pychess.org': { return document.querySelector('cg-board'); } case 'chess.org': { return document.querySelector('.cg-board'); } case 'papergames.io': { return document.querySelector('#chessboard'); } case 'vole.wtf': { return document.querySelector('#board'); } } return null; } function getChessPieceElem(getAll) { const pathname = window.location.pathname; const boardElem = getBoardElem(); const querySelector = (getAll ? query => [...boardElem?.querySelectorAll(query)] : boardElem?.querySelector?.bind(boardElem)); switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { const filteredPieceElems = filterInvisibleElems(document.querySelectorAll('#board *[data-piece]')) .filter(elem => Number(elem?.dataset?.player) <= 2); return getAll ? filteredPieceElems : filteredPieceElems[0]; } return querySelector('.piece'); } case 'lichess.org': { return querySelector('piece:not(.ghost)'); } case 'playstrategy.org': { return querySelector('piece[class*="-piece"]:not(.ghost)'); } case 'pychess.org': { return querySelector('piece[class*="-piece"]:not(.ghost)'); } case 'chess.org': { return querySelector('piece:not(.ghost)'); } case 'papergames.io': { return querySelector('*[data-piece][data-square]'); } case 'vole.wtf': { return querySelector('*[data-t][data-l][data-p]:not([data-p="0"]'); } } return null; } function getSquareElems(element) { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { return [...element.querySelectorAll('.square-4pc.ui-droppable')] .filter(elem => { const pieceElem = elem.querySelector('[data-player]'); const playerNum = Number(pieceElem?.dataset?.player); return (!playerNum || playerNum <= 2); }); } break; } } return null; } function isMutationNewMove(mutationArr) { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { return mutationArr.find(m => m.attributeName == 'class') ? true : false; } if(mutationArr.length == 1) return false; const modifiedHoverSquare = mutationArr.find(m => m?.target?.classList?.contains('hover-square')) ? true : false; const modifiedHighlight = mutationArr.find(m => m?.target?.classList?.contains('highlight')) ? true : false; const modifiedElemPool = mutationArr.find(m => m?.target?.classList?.contains('element-pool')) ? true : false; return (mutationArr.length >= 4 && !modifiedHoverSquare) || mutationArr.length >= 7 || modifiedHighlight || modifiedElemPool; } case 'lichess.org': { return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } case 'playstrategy.org': { return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } case 'pychess.org': { return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } case 'chess.org': { return mutationArr.length >= 4 || mutationArr.find(m => m.type === 'childList') ? true : false || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false; } case 'papergames.io': { return mutationArr.length >= 12; } case 'vole.wtf': { return mutationArr.length >= 12; } } return false; } function getChessVariant() { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { 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; } break; } case 'lichess.org': { 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; } break; } case 'playstrategy.org': { 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; } break; } case 'pychess.org': { 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; } break; } case 'chess.org': { const variantNum = unsafeWindow?.GameConfig?.instance?.variant; let variant = GameConfig?.VARIANT_NAMES?.[variantNum]?.toLowerCase(); if(variant) { const replacementTable = { 'standard': 'chess' }; return replacementTable[variant] || variant; } break; } case 'papergames.io': { return 'chess'; } case 'vole.wtf': { return 'chess'; } } return 'chess'; } function getBoardOrientation() { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { const playerNumberStr = document.querySelector('.playerbox-bottom [data-player]')?.dataset?.player; return playerNumberStr == '0' ? 'w' : 'b'; } const boardElem = getBoardElem(); return boardElem?.classList.contains('flipped') ? 'b' : 'w'; } case 'lichess.org': { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; } case 'playstrategy.org': { const cgWrapElem = document.querySelector('.cg-wrap'); return cgWrapElem.classList?.contains('orientation-p1') ? 'w' : 'b'; } case 'pychess.org': { const cgWrapElem = document.querySelector('.cg-wrap'); return cgWrapElem.classList?.contains('orientation-black') ? 'b' : 'w'; } case 'chess.org': { const filesElem = document.querySelector('coords.files'); return filesElem?.classList?.contains('black') ? 'b' : 'w'; } case 'papergames.io': { const boardElem = getBoardElem(); if(boardElem) { const firstRankText = [...boardElem.querySelector('.coordinates').childNodes]?.[0].textContent; return firstRankText == 'h' ? 'b' : 'w'; } } case 'vole.wtf': { return 'w'; } } return null; } function getPieceElemFen(pieceElem) { const pathname = window.location.pathname; const pieceNameToFen = { 'pawn': 'p', 'knight': 'n', 'bishop': 'b', 'rook': 'r', 'queen': 'q', 'king': 'k' }; switch(domain) { case 'chess.com': { let pieceColor = null; let pieceName = null; if(pathname?.includes('/variants')) { pieceColor = pieceElem?.dataset?.player == '0' ? 'w' : 'b'; pieceName = pieceElem?.dataset?.piece; } else { const pieceStr = [...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/)); [pieceColor, pieceName] = pieceStr.split(''); } return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } case 'lichess.org': { 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(); } break; } case 'playstrategy.org': { const playerColor = getPlayerColorVariable(); const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w'); let pieceName = null; [...pieceElem?.classList]?.forEach(className => { if(className?.includes('-piece')) { const elemPieceName = className?.split('-piece')?.[0]; if(elemPieceName && elemPieceName?.length === 1) { pieceName = elemPieceName; } } }); if(pieceColor && pieceName) { return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } break; } case 'pychess.org': { const playerColor = getPlayerColorVariable(); const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w'); let pieceName = null; [...pieceElem?.classList]?.forEach(className => { if(className?.includes('-piece')) { const elemPieceName = className?.split('-piece')?.[0]; if(elemPieceName && elemPieceName?.length === 1) { pieceName = elemPieceName; } } }); if(pieceColor && pieceName) { return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase(); } break; } case 'chess.org': { 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(); } break; } case 'papergames.io': { return convertPieceStrToFen(pieceElem?.dataset?.piece); } case 'vole.wtf': { const pieceNum = Number(pieceElem?.dataset?.p); const pieceFenStr = 'pknbrq'; if(pieceNum > 8) { return pieceFenStr[pieceNum - 9].toUpperCase(); } else { return pieceFenStr[pieceNum - 1]; } } } return null; } // this function gets called a lot, needs to be optimized function getPieceElemCoords(pieceElem) { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { const squareElem = pieceElem.parentElement; const squareId = squareElem.id; if(!chesscomVariantBoardCoordsTable) { createChesscomVariantBoardCoordsTable(); } return chesscomVariantBoardCoordsTable[squareId]; } return pieceElem.classList.toString() ?.match(/square-(\d)(\d)/) ?.slice(1) ?.map(x => Number(x) - 1); } case 'lichess.org': { const key = pieceElem?.cgKey; if(!key) break; return chessCoordinatesToIndex(key); } case 'playstrategy.org': { const key = pieceElem?.cgKey; if(!key) break; return chessCoordinatesToIndex(key); } case 'pychess.org': { const key = pieceElem?.cgKey; if(!key) break; return chessCoordinatesToIndex(key); } case 'chess.org': { return getElemCoordinatesFromTransform(pieceElem); } case 'papergames.io': { const key = pieceElem?.dataset?.square; if(!key) break; return chessCoordinatesToIndex(key); } case 'vole.wtf': { return [Number(pieceElem?.dataset?.l), 7 - Number(pieceElem?.dataset?.t)]; } } return null; } function getBoardDimensions() { const pathname = window.location.pathname; switch(domain) { case 'chess.com': { if(pathname?.includes('/variants')) { const rankElems = chessBoardElem?.querySelectorAll('.rank'); const visibleRankElems = filterInvisibleElems(rankElems) .filter(rankElem => [...rankElem.childNodes] .find(elem => { const pieceElem = elem.querySelector('[data-player]'); const playerNum = Number(pieceElem?.dataset?.player); return playerNum <= 2; })); if(visibleRankElems.length) { const rankElem = visibleRankElems[0]; const squareElems = getSquareElems(rankElem); const ranks = visibleRankElems?.length; const files = squareElems?.length; return [ranks, files]; } } break; } default: { if(domainsWithoutDifferentBoardDimensionsArr?.includes(domain)) break; const boardDimensions = getElementSize(chessBoardElem); lastBoardSize = getElementSize(chessBoardElem); const boardWidth = boardDimensions?.width; const boardHeight = boardDimensions.height; const boardPiece = getChessPieceElem(); 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) { lastBoardRanks = boardRanks; lastBoardFiles = boardFiles; return [boardRanks, boardFiles]; } } break; } } lastBoardRanks = 8; lastBoardFiles = 8; return [8, 8]; } function getFen(onlyBasic) { const [boardRanks, boardFiles] = getBoardDimensions(); if(debugModeActivated) console.warn('getFen()', 'onlyBasic:', onlyBasic, 'Ranks:', boardRanks, 'Files:', boardFiles); const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1)); function getBasicFen() { const pieceElems = getChessPieceElem(true); const isValidPieceElemsArray = Array.isArray(pieceElems) || pieceElems instanceof NodeList; if(isValidPieceElemsArray) { pieceElems.forEach(pieceElem => { const pieceFenCode = getPieceElemFen(pieceElem); const pieceCoordsArr = getPieceElemCoords(pieceElem); if(debugModeActivated) console.warn('pieceElem', pieceElem, 'pieceFenCode', pieceFenCode, 'pieceCoordsArr', pieceCoordsArr); try { const [xIdx, yIdx] = pieceCoordsArr; board[boardFiles - (yIdx + 1)][xIdx] = pieceFenCode; } catch(e) { if(debugModeActivated) console.error(e); } }); } return squeezeEmptySquares(board.map(x => x.join('')).join('/')); } const basicFen = getBasicFen(); if(onlyBasic) { return basicFen; } return `${basicFen} ${getPlayerColorVariable()} - - - -`; } const boardUtils = { markMove: moveObj => { if(!getConfigValue(configKeys.displayMovesOnExternalSite)) return; const [from, to] = moveObj.player; const [opponentFrom, opponentTo] = moveObj.opponent; const ranking = moveObj.ranking; const existingExactSameMoveObj = activeSiteMoveHighlights.find(obj => { const [activeFrom, activeTo] = obj.player; const [activeOpponentFrom, activeOpponentTo] = obj.opponent; return from == activeFrom && to == activeTo && opponentFrom == activeOpponentFrom && opponentTo == activeOpponentTo; }); activeSiteMoveHighlights.map(obj => { const [activeFrom, activeTo] = obj.player; const existingSameMoveObj = from == activeFrom && to == activeTo; if(existingSameMoveObj) { obj.promotedRanking = 1; } return obj; }); const exactSameMoveDoesNotExist = typeof existingExactSameMoveObj !== 'object'; if(exactSameMoveDoesNotExist) { const showOpponentMoveGuess = getConfigValue(configKeys.showOpponentMoveGuess); const opponentMoveGuessExists = typeof opponentFrom == 'string'; const arrowStyle = ranking == 1 ? arrowStyles.best : arrowStyles.secondary; let opponentArrowElem = null; // create player move arrow element const arrowElem = BoardDrawer.createShape('arrow', [from, to], { style: arrowStyle } ); // create opponent move arrow element if(opponentMoveGuessExists && showOpponentMoveGuess) { opponentArrowElem = BoardDrawer.createShape('arrow', [opponentFrom, opponentTo], { style: arrowStyles.opponent } ); const squareListener = BoardDrawer.addSquareListener(from, type => { if(!opponentArrowElem) { squareListener.remove(); } switch(type) { case 'enter': opponentArrowElem.style.display = 'inherit'; break; case 'leave': opponentArrowElem.style.display = 'none'; break; } }); } activeSiteMoveHighlights.push({ ...moveObj, 'opponentArrowElem': opponentArrowElem, 'playerArrowElem': arrowElem }); } boardUtils.removeOldMarkings(); boardUtils.paintMarkings(); }, removeOldMarkings: () => { const markingLimit = getConfigValue(configKeys.moveSuggestionAmount); const showGhost = getConfigValue(configKeys.showMoveGhost); const exceededMarkingLimit = activeSiteMoveHighlights.length > markingLimit; if(exceededMarkingLimit) { const amountToRemove = activeSiteMoveHighlights.length - markingLimit; for(let i = 0; i < amountToRemove; i++) { const oldestMarkingObj = activeSiteMoveHighlights[0]; activeSiteMoveHighlights = activeSiteMoveHighlights.slice(1); if(oldestMarkingObj?.playerArrowElem?.style) { oldestMarkingObj.playerArrowElem.style.fill = 'grey'; oldestMarkingObj.playerArrowElem.style.opacity = '0'; oldestMarkingObj.playerArrowElem.style.transition = 'opacity 2.5s ease-in-out'; } if(oldestMarkingObj?.opponentArrowElem?.style) { oldestMarkingObj.opponentArrowElem.style.fill = 'grey'; oldestMarkingObj.opponentArrowElem.style.opacity = '0'; oldestMarkingObj.opponentArrowElem.style.transition = 'opacity 2.5s ease-in-out'; } if(showGhost) { inactiveGuiMoveMarkings.push(oldestMarkingObj); } else { oldestMarkingObj.playerArrowElem?.remove(); oldestMarkingObj.opponentArrowElem?.remove(); } } } if(showGhost) { inactiveGuiMoveMarkings.forEach(markingObj => { const activeDuplicateArrow = activeSiteMoveHighlights.find(x => { const samePlayerArrow = x.player?.toString() == markingObj.player?.toString(); const sameOpponentArrow = x.opponent?.toString() == markingObj.opponent?.toString(); return samePlayerArrow && sameOpponentArrow; }); const duplicateExists = activeDuplicateArrow ? true : false; const removeArrows = () => { inactiveGuiMoveMarkings = inactiveGuiMoveMarkings.filter(x => x.playerArrowElem != markingObj.playerArrowElem); markingObj.playerArrowElem?.remove(); markingObj.opponentArrowElem?.remove(); } if(duplicateExists) { removeArrows(); } else { setTimeout(removeArrows, 2500); } }); } }, paintMarkings: () => { const newestBestMarkingIndex = activeSiteMoveHighlights.findLastIndex(obj => obj.ranking == 1); const newestPromotedBestMarkingIndex = activeSiteMoveHighlights.findLastIndex(obj => obj?.promotedRanking == 1); const lastMarkingIndex = activeSiteMoveHighlights.length - 1; const isLastMarkingBest = newestBestMarkingIndex == -1 && newestPromotedBestMarkingIndex == -1; const bestIndex = isLastMarkingBest ? lastMarkingIndex : Math.max(...[newestBestMarkingIndex, newestPromotedBestMarkingIndex]); let bestMoveMarked = false; activeSiteMoveHighlights.forEach((markingObj, idx) => { const isBestMarking = idx == bestIndex; if(isBestMarking) { markingObj.playerArrowElem.style.cssText = arrowStyles.best; const playerArrowElem = markingObj.playerArrowElem const opponentArrowElem = markingObj.opponentArrowElem; // move best arrow element on top (multiple same moves can hide the best move) const parentElem = markingObj.playerArrowElem.parentElement; parentElem.appendChild(playerArrowElem); if(opponentArrowElem) { parentElem.appendChild(opponentArrowElem); } bestMoveMarked = true; } else { markingObj.playerArrowElem.style.cssText = arrowStyles.secondary; } }); }, removeBestMarkings: () => { activeSiteMoveHighlights.forEach(markingObj => { markingObj.opponentArrowElem?.remove(); markingObj.playerArrowElem?.remove(); }); activeSiteMoveHighlights = []; }, setBoardOrientation: orientation => { if(BoardDrawer) { if(debugModeActivated) console.warn('setBoardOrientation', orientation); BoardDrawer.setOrientation(orientation); } }, setBoardDimensions: dimensionArr => { if(BoardDrawer) { if(debugModeActivated) console.warn('setBoardDimensions', dimensionArr); BoardDrawer.setBoardDimensions(dimensionArr); } } }; function onNewMove(mutationArr) { if(debugModeActivated) console.warn('NEW MOVE DETECTED!'); chesscomVariantBoardCoordsTable = null; boardUtils.setBoardDimensions(getBoardDimensions()); const lastPlayerColor = getPlayerColorVariable(); updatePlayerColor(); const playerColor = getPlayerColorVariable(); const orientationChanged = playerColor != lastPlayerColor; if(orientationChanged) { CommLink.commands.log(`Player color (e.g. board orientation) changed from ${lastPlayerColor} to ${playerColor}!`); chesscomVariantBoardCoordsTable = null; instanceVars.turn.set(commLinkInstanceID, playerColor); CommLink.commands.log(`Turn updated to ${playerColor}!`); } const currentFullFen = getFen(); const currentBasicFen = currentFullFen?.split(' ')?.[0]; const fenChanged = currentBasicFen != lastBasicFen; if(fenChanged) { lastBasicFen = currentBasicFen; boardUtils.removeBestMarkings(); /* if(!orientationChanged) { const allChessPieceElems = getChessPieceElem(true); const attributeMutationArr = mutationArr.filter(m => allChessPieceElems.includes(m.target)); const movedChessPieceElem = attributeMutationArr?.[0]?.target; // doesn't work for chess.com variants if(movedChessPieceElem) { const newTurn = getFenPieceOppositeColor(getPieceElemFen(movedChessPieceElem)); if(newTurn?.length === 1) { instanceVars.turn.set(commLinkInstanceID, newTurn); CommLink.commands.log(`Turn updated to ${newTurn}!`); } } } */ instanceVars.fen.set(commLinkInstanceID, currentFullFen); CommLink.commands.updateBoardFen(currentFullFen); CommLink.commands.calculateBestMoves(currentFullFen); } } function observeNewMoves() { let lastProcessedFen = null; const boardObserver = new MutationObserver(mutationArr => { if(debugModeActivated) console.log(mutationArr); if(isMutationNewMove(mutationArr)) { if(debugModeActivated) console.warn('Mutation is a new move:', mutationArr); if(domain === 'chess.org') { setTimeout(() => onNewMove(mutationArr), 250); } else { onNewMove(mutationArr); } } }); boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true }); } async function updatePlayerColor() { const boardOrientation = getBoardOrientation(); const boardOrientationChanged = lastBoardOrientation !== boardOrientation; const boardOrientationDiffers = BoardDrawer && BoardDrawer?.orientation !== boardOrientation; if(boardOrientationChanged || boardOrientationDiffers) { lastBoardOrientation = boardOrientation; instanceVars.playerColor.set(commLinkInstanceID, boardOrientation); instanceVars.turn.set(commLinkInstanceID, boardOrientation); boardUtils.setBoardOrientation(boardOrientation); await CommLink.commands.updateBoardOrientation(boardOrientation); } } async function isAcasBackendReady() { const res = await CommLink.commands.ping(); return res ? true : false; } async function start() { await CommLink.commands.createInstance(commLinkInstanceID); const pathname = window.location.pathname; const adjustSizeByDimensions = domain === 'chess.com' && pathname?.includes('/variants'); const boardOrientation = getBoardOrientation(); instanceVars.playerColor.set(commLinkInstanceID, boardOrientation); instanceVars.turn.set(commLinkInstanceID, boardOrientation); instanceVars.fen.set(commLinkInstanceID, getFen()); if(getConfigValue(configKeys.displayMovesOnExternalSite)) { BoardDrawer = new UniversalBoardDrawer(chessBoardElem, { 'window': window, 'boardDimensions': getBoardDimensions(), 'playerColor': getPlayerColorVariable(), 'zIndex': 500, 'prepend': true, 'debugMode': debugModeActivated, 'adjustSizeByDimensions': adjustSizeByDimensions ? true : false, 'adjustSizeConfig': { 'noLeftAdjustment': true } }); } await updatePlayerColor(); observeNewMoves(); CommLink.setIntervalAsync(async () => { await CommLink.commands.createInstance(commLinkInstanceID); }, 1000); } function startWhenBackendReady() { const interval = CommLink.setIntervalAsync(async () => { if(await isAcasBackendReady()) { start(); interval.stop(); } else { GM_openInTab(backendURL, true); if(await isAcasBackendReady()) { start(); interval.stop(); } } }, 1000); } function initializeIfSiteReady() { const boardElem = getBoardElem(); const firstPieceElem = getChessPieceElem(); const bothElemsExist = boardElem && firstPieceElem; const boardElemChanged = chessBoardElem != boardElem; if(bothElemsExist && boardElemChanged) { chessBoardElem = boardElem; if(!blacklistedURLs.includes(window.location.href)) { startWhenBackendReady(); } } } if(typeof GM_registerMenuCommand == 'function') { GM_registerMenuCommand('Open A.C.A.S', e => { if(chessBoardElem) { startWhenBackendReady(); } }, 's'); } setInterval(initializeIfSiteReady, 1000);