A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao)

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

Tính đến 10-07-2023. Xem phiên bản mới nhất.

// ==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.4
// @namespace   HKR
// @author      HKR
// @require     https://greasyforks.org/scripts/470418-commlink-js/code/CommLinkjs.js
// @require     https://greasyforks.org/scripts/470417-universalboarddrawer-js/code/UniversalBoardDrawerjs.js
// ==/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 //

// PRODUCTION: 'https://hakorr.github.io/A.C.A.S/';
// DEVELOPMENT: 'http://localhost/A.C.A.S/';
const backendURL = 'https://hakorr.github.io/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',
    'onlyShowTopMoves': 'onlyShowTopMoves',
    '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);
长期地址
遇到问题?请前往 GitHub 提 Issues。