🏠 Home 

🏆 [#1 Chess Assistant] A.C.A.S (Système Avancé d'Assistance aux Échecs)

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


Installer ce script?
Script suggéré par l'auteur

Vous pourriez également aimer [Discord] Status Animator (Manual edit/Non-UI).


Installer ce script
// ==UserScript==
// @name        🏆 [#1 Chess Assistant] A.C.A.S (Advanced Chess Assistance System)
// @name:en     🏆 [#1 Chess Assistant] A.C.A.S (Advanced Chess Assistance System)
// @name:fi     🏆 [#1 Chess Assistant] A.C.A.S (Edistynyt shakkiavustusjärjestelmä)
// @name:sw     🏆 [#1 Chess Assistant] A.C.A.S (Advanserad Schack Assitant System)
// @name:zh-CN  🏆 [#1 Chess Assistant] A.C.A.S(高级国际象棋辅助系统)
// @name:es     🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez)
// @name:hi     🏆 [#1 Chess Assistant] A.C.A.S (उन्नत शतरंज सहायता प्रणाली)
// @name:ar     🏆 [#1 Chess Assistant] A.C.A.S (نظام المساعدة المتقدم في الشطرنج)
// @name:pt     🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avançado de Assistência ao Xadrez)
// @name:ja     🏆 [#1 Chess Assistant] A.C.A.S(先進的なチェス支援システム)
// @name:de     🏆 [#1 Chess Assistant] A.C.A.S (Fortgeschrittenes Schach-Hilfesystem)
// @name:fr     🏆 [#1 Chess Assistant] A.C.A.S (Système Avancé d'Assistance aux Échecs)
// @name:it     🏆 [#1 Chess Assistant] A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi)
// @name:ko     🏆 [#1 Chess Assistant] A.C.A.S (고급 체스 보조 시스템)
// @name:nl     🏆 [#1 Chess Assistant] A.C.A.S (Geavanceerd Schaakondersteuningssysteem)
// @name:pl     🏆 [#1 Chess Assistant] A.C.A.S (Zaawansowany System Pomocy Szachowej)
// @name:tr     🏆 [#1 Chess Assistant] A.C.A.S (Gelişmiş Satranç Yardım Sistemi)
// @name:vi     🏆 [#1 Chess Assistant] A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao)
// @name:uk     🏆 [#1 Chess Assistant] A.C.A.S (Система передової допомоги в шахах)
// @name:ru     🏆 [#1 Chess Assistant] A.C.A.S (Система расширенной помощи в шахматах)
// @description        Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
// @description:en     Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
// @description:fi     Paranna shakkipelisi suorituskykyä huippuluokan reaaliaikaisen siirtoanalyysin ja strategisen avustusjärjestelmän avulla
// @description:sw     Förbättra dina schackprestationer med ett banbrytande rörelseanalys i realtid och strategiassistans
// @description:zh-CN  利用尖端实时走法分析和策略辅助系统,提升您的国际象棋水平
// @description:es     Mejora tu rendimiento en ajedrez con un sistema de análisis de movimientos en tiempo real y asistencia estratégica de vanguardia
// @description:hi     अपने शतरंज प्रदर्शन को उन्नत करें, एक कटिंग-एज रियल-टाइम मूव विश्लेषण और रणनीति सहायता प्रणाली के साथ
// @description:ar     قم بتحسين أداءك في الشطرنج مع تحليل حركات اللعب في الوقت الحقيقي ونظام مساعدة استراتيجية حديث
// @description:pt     Melhore seu desempenho no xadrez com uma análise de movimentos em tempo real e um sistema avançado de assistência estratégica
// @description:ja     最新のリアルタイムのムーブ分析と戦略支援システムでチェスのパフォーマンスを向上させましょう
// @description:de     Verbessern Sie Ihre Schachleistung mit einer hochmodernen Echtzeitzug-Analyse- und Strategiehilfe-System
// @description:fr     Améliorez vos performances aux échecs avec une analyse de mouvement en temps réel de pointe et un système d'assistance stratégique
// @description:it     Migliora le tue prestazioni agli scacchi con un sistema all'avanguardia di analisi dei movimenti in tempo reale e assistenza strategica
// @description:ko     최첨단 실시간 움직임 분석 및 전략 지원 시스템으로 체스 성과 향상
// @description:nl     Verbeter je schaakprestaties met een geavanceerd systeem voor realtime zetanalyse en strategische ondersteuning
// @description:pl     Popraw swoje osiągnięcia w szachach dzięki zaawansowanemu systemowi analizy ruchów w czasie rzeczywistym i wsparciu strategicznemu
// @description:tr     Keskinleşmiş gerçek zamanlı hareket analizi ve strateji yardım sistemiyle satranç performansınızı artırın
// @description:vi     Nâng cao hiệu suất cờ vua của bạn với hệ thống phân tích nước đi và hỗ trợ chiến thuật hiện đại
// @description:uk     Покращуйте свою шахову гру з використанням передової системи аналізу ходів в режимі реального часу та стратегічної підтримки
// @description:ru     Слава Украине
// @homepageURL https://psyyke.github.io/A.C.A.S
// @supportURL  https://github.com/Psyyke/A.C.A.S/tree/main#why-doesnt-it-work
// @match       https://psyyke.github.io/A.C.A.S/*
// @match       http://localhost/*
// @match       https://www.chess.com/*
// @match       https://lichess.org/*
// @match       https://playstrategy.org/*
// @match       https://www.pychess.org/*
// @match       https://chess.org/*
// @match       https://papergames.io/*
// @match       https://vole.wtf/kilobytes-gambit/
// @match       https://chess.coolmathgames.com/*
// @match       https://www.coolmathgames.com/0-chess/*
// @match       https://immortal.game/*
// @match       https://chessarena.com/*
// @match       http://chess.net/*
// @match       https://chess.net/*
// @match       https://www.freechess.club/*
// @match       https://*chessclub.com/*
// @match       https://gameknot.com/*
// @match       https://chesstempo.com/*
// @match       https://www.redhotpawn.com/*
// @match       https://www.chessanytime.com/*
// @match       https://www.simplechess.com/*
// @match       https://chessworld.net/*
// @match       https://app.edchess.io/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @grant       GM_registerMenuCommand
// @grant       GM_openInTab
// @grant       GM_addStyle
// @grant       GM_setClipboard
// @grant       GM_notification
// @grant       unsafeWindow
// @run-at      document-start
// @require     https://greasyfork.org/scripts/470418-commlink-js/code/CommLinkjs.js
// @require     https://greasyfork.org/scripts/470417-universalboarddrawer-js/code/UniversalBoardDrawerjs.js
// @icon        https://raw.githubusercontent.com/Psyyke/A.C.A.S/main/assets/images/grey-logo.png
// @version     2.2.5
// @namespace   HKR
// @author      HKR
// @license     GPL-3.0
// ==/UserScript==
/*
e            e88~-_            e           ,d88~~\
d8b          d888   \          d8b          8888
/Y88b         8888             /Y88b         `Y88b
/  Y88b        8888            /  Y88b         `Y88b,
/____Y88b  d88b Y888   / d88b  /____Y88b  d88b    8888
/      Y88b Y88P  "88_-~  Y88P /      Y88b Y88P \__88P'
Advanced Chess Assistance System (A.C.A.S) v2 | Q3 2023
[WARNING]
- Please be advised that the use of A.C.A.S may violate the rules and lead to disqualification or banning from tournaments and online platforms.
- The developers of A.C.A.S and related systems will NOT be held accountable for any consequences r###lting from its use.
- We strongly advise to use A.C.A.S only in a controlled environment ethically.
[ADDITIONAL]
- Big fonts created with: https://www.patorjk.com/software/taag/ (Cyberlarge)
JOIN THE DISCUSSION ABOUT USERSCRIPTS IN GENERAL @ https://hakorr.github.io/Userscripts/community/invite ("Userscript Hub")
DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*\
\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\
//////////////////////////////////////////////////////////////////
DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING*/
/*
______         _____  ______  _______
|  ____ |      |     | |_____] |_____| |
|_____| |_____ |_____| |_____] |     | |_____
Code below this point runs on any site, including the GUI.
*/
// KEEP THESE AS FALSE ON PRODUCTION
const debugModeActivated = false;
const onlyUseDevelopmentBackend = false;
const domain = window.location.hostname.replace('www.', '');
const greasyforkURL = 'https://greasyfork.org/en/scripts/459137';
const backendConfig = {
'hosts': { 'prod': 'psyyke.github.io', 'dev': 'localhost' },
'path': '/A.C.A.S/'
};
const currentBackendUrlKey = 'currentBackendURL';
const isBackendUrlUpToDate = Object.values(backendConfig.hosts).some(x => GM_getValue(currentBackendUrlKey)?.includes(x));
function constructBackendURL(host) {
const protocol = window.location.protocol + '//';
const hosts = backendConfig.hosts;
return protocol + (host || (hosts?.prod || hosts?.path)) + backendConfig.path;
}
function isRunningOnBackend() {
const hostsArr = Object.values(backendConfig.hosts);
const foundHost = hostsArr.find(host => host === window?.location?.host);
const isCorrectPath = window?.location?.pathname?.includes(backendConfig.path);
const isBackend = typeof foundHost === 'string' && isCorrectPath;
if(isBackend) {
GM_setValue(currentBackendUrlKey, constructBackendURL(foundHost));
return true;
}
return false;
}
function prependProtocolWhenNeeded(url) {
if(!url.startsWith('http://') && !url.startsWith('https://')) {
return 'http://' + url;
}
return url;
}
function getCurrentBackendURL(skipGmStorage) {
if(onlyUseDevelopmentBackend) {
return constructBackendURL(backendConfig.hosts?.dev);
}
const gmStorageUrl = GM_getValue(currentBackendUrlKey);
if(skipGmStorage || !gmStorageUrl) {
return constructBackendURL();
}
return prependProtocolWhenNeeded(gmStorageUrl);
}
if(!isBackendUrlUpToDate) {
GM_setValue(currentBackendUrlKey, getCurrentBackendURL(true));
}
function createInstanceVariable(dbValue) {
return {
set: (instanceID, value) => GM_setValue(dbValues[dbValue](instanceID), { value, 'date': Date.now() }),
get: instanceID => {
const data = GM_getValue(dbValues[dbValue](instanceID));
if(data?.date) {
data.date = Date.now();
GM_setValue(dbValues[dbValue](instanceID), data);
}
return data?.value;
}
}
}
const tempValueIndicator = '-temp-value-';
const dbValues = {
AcasConfig: 'AcasConfig',
playerColor: instanceID => 'playerColor' + tempValueIndicator + instanceID,
turn: instanceID => 'turn' + tempValueIndicator + instanceID,
fen: instanceID => 'fen' + tempValueIndicator + instanceID
};
const instanceVars = {
playerColor: createInstanceVariable('playerColor'),
fen: createInstanceVariable('fen')
};
if(isRunningOnBackend()) {
// expose variables and functions
unsafeWindow.USERSCRIPT = {
'GM_info': GM_info,
'GM_getValue': val => GM_getValue(val),
'GM_setValue': (val, data) => GM_setValue(val, data),
'GM_deleteValue': val => GM_deleteValue(val),
'GM_listValues': val => GM_listValues(val),
'tempValueIndicator': tempValueIndicator,
'dbValues': dbValues,
'instanceVars': instanceVars,
'CommLinkHandler': CommLinkHandler,
};
return;
}
/*
_______ _     _ _______ _______ _______      _______ _____ _______ _______ _______
|       |_____| |______ |______ |______      |______   |      |    |______ |______
|_____  |     | |______ ______| ______|      ______| __|__    |    |______ ______|
Code below this point only runs on chess sites, not on the GUI itself.
*/
function getUniqueID() {
return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
)
}
const commLinkInstanceID = getUniqueID();
const blacklistedURLs = [
constructBackendURL(backendConfig?.hosts?.prod),
constructBackendURL(backendConfig?.hosts?.dev),
'https://www.chess.com/play',
'https://lichess.org/',
'https://chess.org/',
'https://papergames.io/en/chess',
'https://playstrategy.org/',
'https://www.pychess.org/',
'https://www.coolmathgames.com/0-chess',
'https://chess.net/'
];
const configKeys = {
'engineElo': 'engineElo',
'mov###ggestionAmount': 'mov###ggestionAmount',
'arrowOpacity': 'arrowOpacity',
'displayMovesOnExternalSite': 'displayMovesOnExternalSite',
'showMoveGhost': 'showMoveGhost',
'showOpponentMoveGuess': 'showOpponentMoveGuess',
'showOpponentMoveGuessConstantly': 'showOpponentMoveGuessConstantly',
'onlyShowTopMoves': 'onlyShowTopMoves',
'maxMovetime': 'maxMovetime',
'chessVariant': 'chessVariant',
'chessEngine': 'chessEngine',
'lc0Weight': 'lc0Weight',
'engineNodes': 'engineNodes',
'chessFont': 'chessFont',
'useChess960': 'useChess960',
'onlyCalculateOwnTurn': 'onlyCalculateOwnTurn',
'ttsVoiceEnabled': 'ttsVoiceEnabled',
'ttsVoiceName': 'ttsVoiceName',
'ttsVoiceSpeed': 'ttsVoiceSpeed',
'chessEngineProfile': 'chessEngineProfile',
'primaryArrowColorHex': 'primaryArrowColorHex',
'secondaryArrowColorHex': 'secondaryArrowColorHex',
'opponentArrowColorHex': 'opponentArrowColorHex',
'reverseSide': 'reverseSide',
'autoMove': 'autoMove',
'autoMoveLegit': 'autoMoveLegit',
'autoMoveRandom': 'autoMoveRandom',
'autoMoveAfterUser': 'autoMoveAfterUser',
'legitModeType': 'legitModeType',
'moveDisplayDelay': 'moveDisplayDelay'
};
const config = {};
Object.values(configKeys).forEach(key => {
config[key] = {
get:  profile => getGmConfigValue(key, commLinkInstanceID, profile),
set: null
};
});
let BoardDrawer = null;
let chessBoardElem = null;
let chesscomVariantPlayerColorsTable = null;
let activeGuiMoveMarkings = [];
let lastBoardRanks = null;
let lastBoardFiles = null;
let lastBoardSize = null;
let lastPieceSize = null;
let lastBoardMatrix = null;
let lastBoardOrientation = null;
let matchFirstSuggestionGiven = false;
let lastMoveRequestTime = 0;
let lastPieceAmount = 0;
let isUserMouseDown = false;
let activeAutomoves = [];
const supportedSites = {};
const pieceNameToFen = {
'pawn': 'p',
'knight': 'n',
'bishop': 'b',
'rook': 'r',
'queen': 'q',
'king': 'k'
};
function getArrowStyle(type, fill, opacity) {
const baseStyleArr = [
'stroke: rgb(0 0 0 / 50%);',
'stroke-width: 2px;',
'stroke-linejoin: round;'
];
switch(type) {
case 'best':
return [
`fill: ${fill || 'limegreen'};`,
`opacity: ${opacity || 0.9};`,
...baseStyleArr
].join('\n');
case 'secondary':
return [
...baseStyleArr,
`fill: ${fill ? fill : 'dodgerblue'};`,
`opacity: ${opacity || 0.7};`,
].join('\n');
case 'opponent':
return [
...baseStyleArr,
`fill: ${fill ? fill : 'crimson'};`,
`opacity: ${opacity || 0.3};`,
].join('\n');
}
};
const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, {
'singlePacketResponseWaitTime': 1500,
'maxSendAttempts': 3,
'statusCheckInterval': 1,
'silentMode': true
});
// manually register a command so that the variables are dynamic
CommLink.commands['createInstance'] = async () => {
return await CommLink.send('mum', 'createInstance', {
'domain': domain,
'instanceID': commLinkInstanceID,
'chessVariant': getChessVariant(),
'playerColor': getPlayerColorVariable()
});
}
CommLink.registerSendCommand('ping', { commlinkID: 'mum', data: 'ping' });
CommLink.registerSendCommand('pingInstance', { data: 'ping' });
CommLink.registerSendCommand('log');
CommLink.registerSendCommand('updateBoardOrientation');
CommLink.registerSendCommand('updateBoardFen');
CommLink.registerSendCommand('calculateBestMoves');
CommLink.registerListener(`backend_${commLinkInstanceID}`, packet => {
try {
switch(packet.command) {
case 'ping':
return `pong (took ${Date.now() - packet.date}ms)`;
case 'getFen':
return getFen();
case 'removeSiteMoveMarkings':
boardUtils.removeMarkings();
return true;
case 'markMoveToSite':
boardUtils.markMoves(packet.data);
const profile = packet.data?.[0]?.profile;
const isAutoMove = getConfigValue(configKeys.autoMove, profile);
const isAutoMoveAfterUser = getConfigValue(configKeys.autoMoveAfterUser, profile);
if (isAutoMove && (!isAutoMoveAfterUser || matchFirstSuggestionGiven)) {
const existingAutomoves = activeAutomoves.filter(x => x.move.active);
// Stop all existing automoves
for(const x of existingAutomoves) {
x.move.stop();
}
const isLegit = getConfigValue(configKeys.autoMoveLegit, profile);
const isRandom = getConfigValue(configKeys.autoMoveRandom, profile);
const move = isRandom
? packet.data[Math.floor(Math.random() * Math.random() * packet.data.length)]?.player
: packet.data[0]?.player;
makeMove(profile, move, isLegit);
}
matchFirstSuggestionGiven = true;
return true;
}
} catch(e) {
return null;
}
});
const boardUtils = {
markMoves: moveObjArr => {
const maxScale = 1;
const minScale = 0.5;
const totalRanks = moveObjArr.length;
moveObjArr.forEach((markingObj, idx) => {
const profile = markingObj.profile;
if(idx === 0)
boardUtils.removeMarkings(profile);
const [from, to] = markingObj.player;
const [oppFrom, oppTo] = markingObj.opponent;
const oppMovesExist = oppFrom && oppTo;
const rank = idx + 1;
const showOpponentMoveGuess = getConfigValue(configKeys.showOpponentMoveGuess, profile);
const showOpponentMoveGuessConstantly = getConfigValue(configKeys.showOpponentMoveGuessConstantly, profile);
const arrowOpacity = getConfigValue(configKeys.arrowOpacity, profile) / 100;
const primaryArrowColorHex = getConfigValue(configKeys.primaryArrowColorHex, profile);
const secondaryArrowColorHex = getConfigValue(configKeys.secondaryArrowColorHex, profile);
const opponentArrowColorHex = getConfigValue(configKeys.opponentArrowColorHex, profile);
let playerArrowElem = null;
let oppArrowElem = null;
let arrowStyle = getArrowStyle('best', primaryArrowColorHex, arrowOpacity);
let lineWidth = 30;
let arrowheadWidth = 80;
let arrowheadHeight = 60;
let startOffset = 30;
if(idx !== 0) {
arrowStyle = getArrowStyle('secondary', secondaryArrowColorHex, arrowOpacity);
const arrowScale = totalRanks === 2
? 0.75
: maxScale - (maxScale - minScale) * ((rank - 1) / (totalRanks - 1));
lineWidth = lineWidth * arrowScale;
arrowheadWidth = arrowheadWidth * arrowScale;
arrowheadHeight = arrowheadHeight * arrowScale;
startOffset = startOffset;
}
playerArrowElem = BoardDrawer.createShape('arrow', [from, to],
{
style: arrowStyle,
lineWidth, arrowheadWidth, arrowheadHeight, startOffset
}
);
if(oppMovesExist && showOpponentMoveGuess) {
oppArrowElem = BoardDrawer.createShape('arrow', [oppFrom, oppTo],
{
style: getArrowStyle('opponent', opponentArrowColorHex, arrowOpacity),
lineWidth, arrowheadWidth, arrowheadHeight, startOffset
}
);
if(showOpponentMoveGuessConstantly) {
oppArrowElem.style.display = 'block';
} else {
const squareListener = BoardDrawer.addSquareListener(from, type => {
if(!oppArrowElem) {
squareListener.remove();
}
switch(type) {
case 'enter':
oppArrowElem.style.display = 'inherit';
break;
case 'leave':
oppArrowElem.style.display = 'none';
break;
}
});
}
}
if(idx === 0 && playerArrowElem) {
const parentElem = playerArrowElem.parentElement;
// move best arrow element on top (multiple same moves can hide the best move)
parentElem.appendChild(playerArrowElem);
if(oppArrowElem) {
parentElem.appendChild(oppArrowElem);
}
}
activeGuiMoveMarkings.push({ ...markingObj, playerArrowElem, oppArrowElem, profile });
});
},
removeMarkings: profile => {
let removalArr = activeGuiMoveMarkings;
if(profile) {
removalArr = removalArr.filter(obj => obj.profile === profile);
activeGuiMoveMarkings =  activeGuiMoveMarkings.filter(obj => obj.profile !== profile);
} else {
activeGuiMoveMarkings = [];
}
removalArr.forEach(markingObj => {
markingObj.oppArrowElem?.remove();
markingObj.playerArrowElem?.remove();
});
},
setBoardOrientation: orientation => {
if(BoardDrawer) {
if(debugModeActivated) console.warn('setBoardOrientation', orientation);
BoardDrawer.setOrientation(orientation);
}
},
setBoardDimensions: dimensionArr => {
if(BoardDrawer) {
if(debugModeActivated) console.warn('setBoardDimensions', dimensionArr);
BoardDrawer.setBoardDimensions(dimensionArr);
}
}
};
function displayImportantNotification(title, text) {
if(typeof GM_notification === 'function') {
GM_notification({ title: title, text: text });
} else {
alert(`[${title}]` + '\n\n' + text);
}
}
function filterInvisibleElems(elementArr, inverse) {
return [...elementArr].filter(elem => {
const style = getComputedStyle(elem);
const bounds = elem.getBoundingClientRect();
const isHidden =
style.visibility === 'hidden' ||
style.display === 'none' ||
style.opacity === '0' ||
bounds.width == 0 ||
bounds.height == 0;
return inverse ? isHidden : !isHidden;
});
}
function getElementSize(elem) {
const rect = elem.getBoundingClientRect();
if(rect.width !== 0 && rect.height !== 0) {
return { width: rect.width, height: rect.height };
}
const computedStyle = window.getComputedStyle(elem);
const width = parseFloat(computedStyle.width);
const height = parseFloat(computedStyle.height);
return { width, height };
}
function extractElemTransformData(elem) {
const computedStyle = window.getComputedStyle(elem);
const transformMatrix = new DOMMatrix(computedStyle.transform);
const x = transformMatrix.e;
const y = transformMatrix.f;
return [x, y];
}
function getElemCoordinatesFromTransform(elem, config) {
const onlyFlipX = config?.onlyFlipX;
const onlyFlipY = config?.onlyFlipY;
lastBoardSize = getElementSize(chessBoardElem);
const [files, ranks] = getBoardDimensions();
lastBoardRanks = ranks;
lastBoardFiles = files;
const boardOrientation = getPlayerColorVariable();
let [x, y] = extractElemTransformData(elem);
const boardDimensions = lastBoardSize;
let squareDimensions = boardDimensions.width / lastBoardRanks;
const normalizedX = Math.round(x / squareDimensions);
const normalizedY = Math.round(y / squareDimensions);
if(onlyFlipY || boardOrientation === 'w') {
const flippedY = lastBoardFiles - normalizedY - 1;
return [normalizedX, flippedY];
} else {
const flippedX = lastBoardRanks - normalizedX - 1;
return [flippedX, normalizedY];
}
}
function getElemCoordinatesFromLeftBottomPercentages(elem) {
if(!lastBoardRanks || !lastBoardFiles) {
const [files, ranks] = getBoardDimensions();
lastBoardRanks = ranks;
lastBoardFiles = files;
}
const boardOrientation = getPlayerColorVariable();
const leftPercentage = parseFloat(elem.style.left?.replace('%', ''));
const bottomPercentage = parseFloat(elem.style.bottom?.replace('%', ''));
const x = Math.max(Math.round(leftPercentage / (100 / lastBoardRanks)), 0);
const y = Math.max(Math.round(bottomPercentage / (100 / lastBoardFiles)), 0);
if (boardOrientation === 'w') {
return [x, y];
} else {
const flippedX = lastBoardRanks - (x + 1);
const flippedY = lastBoardFiles - (y + 1);
return [flippedX, flippedY];
}
}
function getElemCoordinatesFromLeftTopPixels(elem) {
const pieceSize = getElementSize(elem);
lastPieceSize = pieceSize;
const leftPixels = parseFloat(elem.style.left?.replace('px', ''));
const topPixels = parseFloat(elem.style.top?.replace('px', ''));
const x = Math.max(Math.round(leftPixels / pieceSize.width), 0);
const y = Math.max(Math.round(topPixels / pieceSize.width), 0);
const boardOrientation = getPlayerColorVariable();
if (boardOrientation === 'w') {
const flippedY = lastBoardFiles - (y + 1);
return [x, flippedY];
} else {
const flippedX = lastBoardRanks - (x + 1);
return [flippedX, y];
}
}
function updateChesscomVariantPlayerColorsTable() {
let colors = [];
document.querySelectorAll('*[data-color]').forEach(pieceElem => {
const colorCode = Number(pieceElem?.dataset?.color);
if(!colors?.includes(colorCode)) {
colors.push(colorCode);
}
});
if(colors?.length > 1) {
colors = colors.sort((a, b) => a - b);
chesscomVariantPlayerColorsTable = { [colors[0]]: 'w', [colors[1]]: 'b' };
}
}
function getBoardDimensionsFromSize() {
const boardDimensions = getElementSize(chessBoardElem);
lastBoardSize = boardDimensions;
const boardWidth = boardDimensions?.width;
const boardHeight = boardDimensions.height;
const boardPiece = getPieceElem();
if(boardPiece) {
const pieceDimensions = getElementSize(boardPiece);
lastPieceSize = getElementSize(boardPiece);
const boardPieceWidth = pieceDimensions?.width;
const boardPieceHeight = pieceDimensions?.height;
const boardRanks = Math.floor(boardWidth / boardPieceWidth);
const boardFiles = Math.floor(boardHeight / boardPieceHeight);
const ranksInAllowedRange = 0 < boardRanks && boardRanks <= 69;
const filesInAllowedRange = 0 < boardFiles && boardFiles <= 69;
if(ranksInAllowedRange && filesInAllowedRange) {
return [boardRanks, boardFiles];
}
}
}
function chessCoordinatesToIndex(coord) {
const x = coord.charCodeAt(0) - 97;
let y = null;
const lastHalf = coord.slice(1);
if(lastHalf === ':') {
y = 9;
} else {
y = Number(coord.slice(1)) - 1;
}
return [x, y];
}
/* Need to make the board matricies more cohesive, right now it's really confusing flipping them
* differently for each function. I just can't be bothered right now so please don't make fun of it.
* Thanks, Haka
* */
function chessCoordinatesToMatrixIndex(coord) {
const [boardRanks, boardFiles] = getBoardDimensions();
const indexArr = chessCoordinatesToIndex(coord);
let x, y;
y = boardFiles - (indexArr[1] + 1);
x = indexArr[0];
return [x, y];
}
function chessCoordinatesToDomIndex(coord) {
const [boardRanks, boardFiles] = getBoardDimensions();
const indexArr = chessCoordinatesToIndex(coord);
const boardOrientation = getBoardOrientation();
let x, y;
if(boardOrientation === 'w') {
x = indexArr[0];
y = boardFiles - (indexArr[1] + 1);
} else {
x = boardRanks - (indexArr[0] + 1);
y = indexArr[1];
}
return [x, y];
}
function indexToChessCoordinates(coord) {
const [boardRanks, boardFiles] = getBoardDimensions();
const boardOrientation = getBoardOrientation();
const [x, y] = coord;
const file = String.fromCharCode('a'.charCodeAt(0) + x);
let rank;
if (boardOrientation === 'w') {
rank = boardRanks - y;
} else {
rank = boardRanks - y;
}
return `${file}${rank}`;
}
function isPawnPromotion(bestMove) {
const [fenCoordFrom, fenCoordTo] = bestMove;
const piece = getBoardPiece(fenCoordFrom);
if(typeof piece !== 'string' || piece.toLowerCase() !== 'p')
return false;
// Determine the row from the ending coordinate (assumes standard algebraic notation, e.g., 'e8')
const endingRow = parseInt(fenCoordTo[1], 10);
// Check if the pawn reaches the promotion row
if ((piece === 'P' && endingRow === (lastBoardFiles ?? 8)) || (piece === 'p' && endingRow === 1)) {
return true;
}
return false;
}
function fenCoordArrToDomCoord(fenCoordArr) {
// fenCoordArr e.g. ["e6", "e5"]
const boardClientRect = chessBoardElem.getBoundingClientRect();
const pieceElem = getPieceElem();
const pieceDimensions = getElementSize(pieceElem);
const pieceWidth = pieceDimensions?.width;
const pieceHeight = pieceDimensions?.height;
lastPieceSize = pieceDimensions;
const [boardRanks, boardFiles] = getBoardDimensions();
// Array to hold the center coordinates of each square
const centerCoordinates = fenCoordArr.map(coord => {
const [x, y] = chessCoordinatesToDomIndex(coord);
const centerX = boardClientRect.x + (x * pieceWidth) + (pieceWidth / 2);
const centerY = boardClientRect.y + (y * pieceHeight) + (pieceHeight / 2);
return [centerX, centerY];
});
return centerCoordinates;
}
function getRandomOwnPieceDomCoord(fenCoord, boardMatrix) {
const boardOrientation = getBoardOrientation();
let [x, y] = chessCoordinatesToMatrixIndex(fenCoord);
const pieceAtFenCoord = boardMatrix[y][x];
if(pieceAtFenCoord === 1) {
return null;
}
const isWhitePiece = pieceAtFenCoord === pieceAtFenCoord.toUpperCase();
const getDistance = (row1, col1, row2, col2) => {
return Math.abs(row1 - row2) + Math.abs(col1 - col2);
};
let candidatePieces = [];
// Loop through the board matrix to find all close own pieces
for(let row = 0; row < boardMatrix.length; row++) {
for(let col = 0; col < boardMatrix[row].length; col++) {
const currentPiece = boardMatrix[row][col];
// Skip if no piece is found or if the piece is of the wrong color
if(currentPiece === 1 || (isWhitePiece && currentPiece === currentPiece.toLowerCase()) || (!isWhitePiece && currentPiece === currentPiece.toUpperCase())) {
continue;
}
const distance = getDistance(y, x, row, col);
if(distance < 6) {
candidatePieces.push({ distance, coord: [col, row], piece: currentPiece });
}
}
}
if (candidatePieces.length > 0) {
// Choose a random piece from the candidates
const randomIndex = Math.floor(Math.random() * candidatePieces.length);
const chosenPiece = candidatePieces[randomIndex];
return fenCoordArrToDomCoord([indexToChessCoordinates(chosenPiece.coord)])[0];
}
return null;
}
function getPieceAmount() {
return getPieceElem(true)?.length ?? 0;
}
class AutomaticMove {
constructor(profile, fenMoveArr, isLegit, callback) {
this.id = getUniqueID();
// activeAutomoves is an external variable, not a child of AutomaticMove
activeAutomoves.push({ 'id': this.id, 'move': this });
this.profile = profile;
this.fenMoveArr = fenMoveArr;
this.isLegit = isLegit;
this.active = true;
this.isPromotingPawn = false;
this.onFinished = function(...args) {
activeAutomoves.filter(x => x.id !== this.id); // remove the move from the active automove list
this.active = false;
callback(...args);
};
this.moveDomCoords = fenCoordArrToDomCoord(fenMoveArr);
this.isPromotion = isPawnPromotion(fenMoveArr);
if(this.isLegit) {
const legitModeType = getConfigValue(configKeys.legitModeType, this.profile) ?? 'casual';
const pieceRanges = [
{ minPieces: 30, maxPieces: Infinity }, // Opening (60+ pieces)
{ minPieces: 23, maxPieces: 29 },       // Early Middlegame (48 to 64 pieces)
{ minPieces: 16, maxPieces: 22 },       // Mid Middlegame (32 to 48 pieces)
{ minPieces: 10, maxPieces: 15 },       // Late Middlegame (16 to 32 pieces)
{ minPieces: 6, maxPieces: 9 },         // Endgame (8 to 16 pieces)
{ minPieces: 3, maxPieces: 5 },         // Very Endgame (2 to 8 pieces)
{ minPieces: 1, maxPieces: 2 },         // Extremely Few Pieces (1 piece)
];
const timeRanges = {
beginner: [
[2000, 4000],
[3000, 15000],
[5000, 25000],
[4000, 30000],
[3000, 15000],
[2000, 10000],
[1000, 4000],
],
casual: [
[900, 3000],    // Opening
[1000, 15000],  // Early Middlegame
[3000, 20000],  // Mid Middlegame
[2000, 13000],  // Late Middlegame
[1500, 10000],  // Endgame
[1000, 9000],   // Very Endgame
[500, 3000],    // Extremely Few Pieces
],
intermediate: [
[750, 2000],
[1000, 10000],
[2000, 15000],
[1500, 12000],
[1000, 8000],
[750, 7000],
[500, 2000],
],
advanced: [
[500, 1500],
[1000, 8000],
[750, 8000],
[750, 12000],
[750, 5000],
[750, 3000],
[500, 1200],
],
master: [
[333, 999],
[400, 2000],
[400, 3000],
[400, 2500],
[400, 2000],
[400, 1500],
[333, 750],
],
professional: [
[333, 666],
[333, 666],
[333, 1000],
[333, 1500],
[333, 1000],
[333, 666],
[333, 666],
],
god: [
[50, 333],
[50, 233],
[50, 300],
[50, 250],
[50, 200],
[50, 150],
[50, 100],
]
};
this.timeRanges = pieceRanges.map((range, index) => ({
...range,
timeRange: timeRanges[legitModeType][index],
}));
this.shouldHesitate = this.isLegit && Math.random() < 0.15;
this.shouldHesitateTwice = this.isLegit && Math.random() < 0.25;
this.hesitationTypeOne = this.isLegit && Math.random() < 0.35;
const legitTotalMoveTime = this.calculateMoveTime(getPieceAmount());
const elapsedMoveTime = (Date.now() - lastMoveRequestTime); // How long did it take for the engine to calculate the move
const remainingTime = Math.max(legitTotalMoveTime - elapsedMoveTime, 500);
const delays = this.generateDelaysForDesiredTime(remainingTime);
for(const key of Object.keys(delays)) {
this[key] = delays[key];
}
}
this.start();
}
generateDelaysForDesiredTime(desiredTotalTime) {
// Fixed minimum values
const PROMOTION_DELAY = this.getRandomIntegerBetween(1000, 1111); // just can't be done very fast for some reason at least on Chess.com
if(desiredTotalTime > 6000) {
const timelines = [
{ move: .4, to: .2, hesitation: .15, hesitationResolve: .15, secondHesitationResolve: .15 },
{ move: .1, to: .3, hesitation: .25, hesitationResolve: .15, secondHesitationResolve: .2 },
{ move: .2, to: .25, hesitation: .2, hesitationResolve: .2, secondHesitationResolve: .15 }
];
const timeline = timelines[Math.floor(Math.random() * timelines.length)];
return {
promotionDelay: PROMOTION_DELAY,
moveDelay: desiredTotalTime * timeline.move,
toSquareSelectDelay: desiredTotalTime * timeline.to,
hesitationDelay: desiredTotalTime * timeline.hesitation,
hesitationResolveDelay: desiredTotalTime * timeline.hesitationResolve,
secondHesitationResolveDelay: desiredTotalTime * timeline.secondHesitationResolve
};
}
// There is time for one hesitation
if(desiredTotalTime > 3000) {
const timelines = [
{ move: .3, to: .2, hesitation: .25, hesitationResolve: .25 },
{ move: .1, to: .3, hesitation: .45, hesitationResolve: .15 },
{ move: .2, to: .25, hesitation: .2, hesitationResolve: .35 }
];
const timeline = timelines[Math.floor(Math.random() * timelines.length)];
return {
promotionDelay: PROMOTION_DELAY,
moveDelay: desiredTotalTime * timeline.move,
toSquareSelectDelay: desiredTotalTime * timeline.to,
hesitationDelay: desiredTotalTime * timeline.hesitation,
hesitationResolveDelay: desiredTotalTime * timeline.hesitationResolve,
secondHesitationResolveDelay: -1
};
}
// There is not enough time to hesitate
else {
const timelines = [
{ move: .9, to: .1 },
{ move: .45, to: .55 },
{ move: .6, to: .4 },
{ move: .4, to: .6 },
{ move: .1, to: .9 }
];
const timeline = timelines[Math.floor(Math.random() * timelines.length)];
return {
promotionDelay: PROMOTION_DELAY,
moveDelay: desiredTotalTime * timeline.move,
toSquareSelectDelay: desiredTotalTime * timeline.to,
hesitationDelay: -1, hesitationResolveDelay: -1, secondHesitationResolveDelay: -1
};
}
}
calculateMoveTime(pieceCount) {
for(let range of this.timeRanges) {
if(pieceCount >= range.minPieces && pieceCount <= range.maxPieces) {
return this.getRandomIntegerBetween(range.timeRange[0], range.timeRange[1]);
}
}
return 500;
}
getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
getRandomIntegerNearAverage(min, max) {
const mid = (min + max) / 2;
const range = (max - min) / 2;
let value = Math.floor(mid + (Math.random() - 0.5) * range * 1.5);
return Math.max(min, Math.min(max, value));
}
delay(ms) {
return this.active ? new Promise(resolve => setTimeout(resolve, ms)) : true;
}
async triggerPieceClick(input) {
const parentExists = activeAutomoves.find(x => x.move === this) ? true : false;
if(!parentExists) {
return;
}
let clientX, clientY;
if(input instanceof Element) {
const rect = input.getBoundingClientRect();
clientX = rect.left + rect.width / 2;
clientY = rect.top + rect.height / 2;
} else if (typeof input === 'object') {
clientX = input[0];
clientY = input[1];
} else {
return;
}
const xDivider = Math.random() < 0.85 ? 4 : Math.random() < 0.15 ? 3 : 2;
const yDivider = Math.random() < 0.65 ? 3 : Math.random() < 0.35 ? 2 : 4;
const randomVariationX = (lastPieceSize?.width - 4) / xDivider;
const randomVariationY = (lastPieceSize?.height - 4) / yDivider;
const randomOffsetX = (Math.random() - 0.5) * 2 * randomVariationX;
const randomOffsetY = (Math.pow(Math.random(), 0.5) - 0.5) * 2 * randomVariationY;
const randomizedX = clientX + randomOffsetX;
const randomizedY = clientY + randomOffsetY;
const pointerEventOptions = {
bubbles: true,
cancelable: true,
clientX: randomizedX,
clientY: randomizedY,
};
const elementToTrigger = (input instanceof Element) ? input : document.elementFromPoint(clientX, clientY);
if(elementToTrigger) {
switch(domain) {
case 'chess.com':
elementToTrigger.dispatchEvent(new PointerEvent('pointerdown', pointerEventOptions));
if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125));
elementToTrigger.dispatchEvent(new PointerEvent('pointerup', pointerEventOptions));
break;
case 'lichess.org':
elementToTrigger.dispatchEvent(new MouseEvent('mousedown', pointerEventOptions));
if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125));
elementToTrigger.dispatchEvent(new MouseEvent('mouseup', pointerEventOptions));
break;
case 'chessarena.com':
elementToTrigger.dispatchEvent(new MouseEvent('mousedown', pointerEventOptions));
if(this.isLegit) await this.delay(this.getRandomIntegerNearAverage(35, 125));
elementToTrigger.dispatchEvent(new MouseEvent('mouseup', pointerEventOptions));
break;
}
}
if(debugModeActivated) {
const dot = document.createElement('div');
dot.style.position = 'absolute';
dot.style.width = '7px';
dot.style.height = '7px';
dot.style.borderRadius = '50%';
dot.style.backgroundColor = 'lime';
dot.style.left = `${randomizedX - 2.5}px`;
dot.style.top = `${randomizedY - 2.5}px`;
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.width = `${Math.round(randomVariationX * 2)}px`;
container.style.height = `${Math.round(randomVariationY * 2)}px`;
container.style.border = '2px dashed green';
container.style.backgroundColor = 'rgba(0, 0, 0, 0.3)';
container.style.left = `${clientX - randomVariationX}px`;
container.style.top = `${clientY - randomVariationY}px`;
document.body.appendChild(container);
document.body.appendChild(dot);
setTimeout(() => {
dot.remove();
container.remove();
}, 1000);
}
}
click(domCoord) {
if(this.active)
this.triggerPieceClick(domCoord);
}
async hesitate() {
const hesitationPieceDomCoord = getRandomOwnPieceDomCoord(this.fenMoveArr[0], getBoardMatrix());
if(hesitationPieceDomCoord) {
if(this.hesitationTypeOne) {
this.click(this.moveDomCoords[0]);
await this.delay(this.hesitationDelay);
}
this.click(hesitationPieceDomCoord);
await this.delay(this.hesitationResolveDelay);
if(this.shouldHesitateTwice && this.secondHesitationResolveDelay !== -1) {
const secondHesitationPieceDomCoord = getRandomOwnPieceDomCoord(this.fenMoveArr[0], getBoardMatrix());
this.click(secondHesitationPieceDomCoord);
await this.delay(this.secondHesitationResolveDelay);
}
}
this.finishMove(this.toSquareSelectDelay, this.promotionDelay);
}
async finishMove(delay01, delay02) {
this.click(this.moveDomCoords[0]);
await this.delay(delay01);
this.click(this.moveDomCoords[1]);
// Handle promotion click if necessary
if(this.isPromotion) {
this.isPromotingPawn = true;
await this.delay(delay02);
this.click(this.moveDomCoords[1]);
this.isPromotingPawn = false;
}
this.onFinished(true);
}
async playLegit() {
await this.delay(this.moveDelay);
if(this.shouldHesitate && this.hesitationDelay !== -1)
this.hesitate();
else
this.finishMove(this.toSquareSelectDelay, this.promotionDelay);
}
async start() {
if(this.isLegit) {
this.playLegit();
} else {
this.finishMove(5, 1111);
}
}
async stop() {
if(this.isPromotingPawn) {
// Attempt to promote the pawn before closing
this.click(this.moveDomCoords[1]);
}
this.onFinished(false);
}
}
async function makeMove(profile, fenMoveArr, isLegit) {
const move = new AutomaticMove(profile, fenMoveArr, isLegit, e => {
// This is ran when the move finished
if(debugModeActivated) console.warn('Move', fenMoveArr, move.id, 'finished', 'for profile:', profile);
});
}
function getGmConfigValue(key, instanceID, profileID) {
if(typeof profileID === 'object') {
profileID = profileID.name;
}
const config = GM_getValue(dbValues.AcasConfig);
const instanceValue = config?.instance?.[instanceID]?.[key];
const globalValue = config?.global?.[key];
if(instanceValue !== undefined) {
return instanceValue;
}
if(globalValue !== undefined) {
return globalValue;
}
if(profileID) {
const globalProfileValue = config?.global?.['profiles']?.[profileID]?.[key];
const instanceProfileValue = config?.instance?.[instanceID]?.['profiles']?.[profileID]?.[key];
if(instanceProfileValue !== undefined) {
return instanceProfileValue;
}
if(globalProfileValue !== undefined) {
return globalProfileValue;
}
}
return null;
}
function isBoardDrawerNeeded() {
const config = GM_getValue(dbValues.AcasConfig);
const gP = config?.global?.['profiles'];
const iP = config?.instance?.[commLinkInstanceID]?.['profiles'];
if(gP) {
const globalProfiles = Object.keys(gP);
for(profileName of globalProfiles) {
if(gP[profileName][configKeys.displayMovesOnExternalSite]) {
return true;
}
}
}
if(iP) {
const instanceProfiles = Object.keys(iP);
for(profileName of instanceProfiles) {
if(iP[profileName][configKeys.displayMovesOnExternalSite]) {
return true;
}
}
}
return false;
}
function getConfigValue(key, profile) {
return config[key]?.get(profile);
}
function setConfigValue(key, val) {
return config[key]?.set(val);
}
function squeezeEmptySquares(fenStr) {
return fenStr.replace(/1+/g, match => match.length);
}
function getPlayerColorVariable() {
return instanceVars.playerColor.get(commLinkInstanceID);
}
function getFenPieceColor(pieceFenStr) {
return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b';
}
function getFenPieceOppositeColor(pieceFenStr) {
return getFenPieceColor(pieceFenStr) == 'w' ? 'b' : 'w';
}
function convertPieceStrToFen(str) {
if(!str || str.length !== 2) {
return null;
}
const firstChar = str[0].toLowerCase();
const secondChar = str[1];
if(firstChar === 'w') {
return secondChar.toUpperCase();
} else if (firstChar === 'b') {
return secondChar.toLowerCase();
}
return null;
}
function getCanvasPixelColor(canvas, [xPercentage, yPercentage], debug) {
const ctx = canvas.getContext('2d');
const x = xPercentage * canvas.width;
const y = yPercentage * canvas.height;
const imageData = ctx.getImageData(x, y, 1, 1);
const pixel = imageData.data;
const brightness = (pixel[0] + pixel[1] + pixel[2]) / 3;
if(debug) {
const clonedCanvas = document.createElement('canvas');
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
const clonedCtx = clonedCanvas.getContext('2d');
clonedCtx.drawImage(canvas, 0, 0);
clonedCtx.fillStyle = 'red';
clonedCtx.beginPath();
clonedCtx.arc(x, y, 1, 0, Math.PI * 2);
clonedCtx.fill();
const dataURL = clonedCanvas.toDataURL();
console.log(canvas, pixel, dataURL);
}
return brightness < 128 ? 'b' : 'w';
}
function canvasHasPixelAt(canvas, [xPercentage, yPercentage], debug) {
xPercentage = Math.min(Math.max(xPercentage, 0), 100);
yPercentage = Math.min(Math.max(yPercentage, 0), 100);
const ctx = canvas.getContext('2d');
const x = xPercentage * canvas.width;
const y = yPercentage * canvas.height;
const imageData = ctx.getImageData(x, y, 1, 1);
const pixel = imageData.data;
if(debug) {
const clonedCanvas = document.createElement('canvas');
clonedCanvas.width = canvas.width;
clonedCanvas.height = canvas.height;
const clonedCtx = clonedCanvas.getContext('2d');
clonedCtx.drawImage(canvas, 0, 0);
clonedCtx.fillStyle = 'red';
clonedCtx.beginPath();
clonedCtx.arc(x, y, 1, 0, Math.PI * 2);
clonedCtx.fill();
const dataURL = clonedCanvas.toDataURL();
console.log(canvas, pixel, dataURL);
}
return pixel[3] !== 0;
}
function getSiteData(dataType, obj) {
const pathname = window.location.pathname;
let dataObj = { pathname };
if(obj && typeof obj === 'object') {
dataObj = { ...dataObj, ...obj };
}
const dataHandlerFunction = supportedSites[domain]?.[dataType];
if(typeof dataHandlerFunction !== 'function') {
return null;
}
const r###lt = dataHandlerFunction(dataObj);
return r###lt;
}
function addSupportedChessSite(domain, typeHandlerObj) {
supportedSites[domain] = typeHandlerObj;
}
function getBoardElem() {
const boardElem = getSiteData('boardElem');
return boardElem || null;
}
function getPieceElem(getAll) {
const boardElem = getBoardElem();
const boardQuerySelector = (getAll ? query => [...boardElem?.querySelectorAll(query)] : boardElem?.querySelector?.bind(boardElem));
if(typeof boardQuerySelector !== 'function')
return null;
const pieceElem = getSiteData('pieceElem', { boardQuerySelector, getAll });
return pieceElem || null;
}
function getSquareElems(element) {
const squareElems = getSiteData('squareElems', { element });
return squareElems || null;
}
function getChessVariant() {
const chessVariant = getSiteData('chessVariant');
return chessVariant || null;
}
function getBoardOrientation() {
const boardOrientation = getSiteData('boardOrientation');
return boardOrientation || null;
}
function getPieceElemFen(pieceElem) {
const pieceFen = getSiteData('pieceElemFen', { pieceElem });
return pieceFen || null;
}
// this function gets called a lot, needs to be optimized
function getPieceElemCoords(pieceElem) {
const pieceCoords = getSiteData('pieceElemCoords', { pieceElem });
return pieceCoords || null;
}
function getBoardDimensions() {
const boardDimensionArr = getSiteData('boardDimensions');
if(boardDimensionArr) {
lastBoardRanks = boardDimensionArr[0];
lastBoardFiles = boardDimensionArr[1];
return boardDimensionArr;
} else {
lastBoardRanks = 8;
lastBoardFiles = 8;
return [8, 8];
}
}
function isMutationNewMove(mutationArr) {
const isNewMove = getSiteData('isMutationNewMove', { mutationArr });
return isNewMove || false;
}
function getBoardMatrix() {
const [boardRanks, boardFiles] = getBoardDimensions();
const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1));
const pieceElems = getPieceElem(true);
const isValidPieceElemsArray = Array.isArray(pieceElems) || pieceElems instanceof NodeList;
if(isValidPieceElemsArray) {
pieceElems.forEach(pieceElem => {
const pieceFenCode = getPieceElemFen(pieceElem);
const pieceCoordsArr = getPieceElemCoords(pieceElem);
//if(debugModeActivated) console.warn('pieceElem', pieceElem, 'pieceFenCode', pieceFenCode, 'pieceCoordsArr', pieceCoordsArr);
try {
const [xIdx, yIdx] = pieceCoordsArr;
board[boardFiles - (yIdx + 1)][xIdx] = pieceFenCode;
} catch(e) {
if(debugModeActivated) console.error(e);
}
});
}
lastBoardMatrix = board;
return board;
}
function getBoardPiece(fenCoord) {
const [boardRanks, boardFiles] = getBoardDimensions();
const indexArr = chessCoordinatesToIndex(fenCoord);
return getBoardMatrix()?.[boardFiles - (indexArr[1] + 1)]?.[indexArr[0]];
}
// Works on 8x8 boards only
function getRights() {
let rights = '';
// check for white
const e1 = getBoardPiece('e1'),
h1 = getBoardPiece('h1'),
a1 = getBoardPiece('a1');
if(e1 == 'K' && h1 == 'R') rights += 'K';
if(e1 == 'K' && a1 == 'R') rights += 'Q';
//check for black
const e8 = getBoardPiece('e8'),
h8 = getBoardPiece('h8'),
a8 = getBoardPiece('a8');
if(e8 == 'k' && h8 == 'r') rights += 'k';
if(e8 == 'k' && a8 == 'r') rights += 'q';
return rights ? rights : '-';
}
function getBasicFen() {
const boardMatrix = getBoardMatrix();
return squeezeEmptySquares(boardMatrix.map(x => x.join('')).join('/'));
}
function getFen(onlyBasic) {
const basicFen = getBasicFen();
if(debugModeActivated) console.warn('basicFen', basicFen);
if(onlyBasic) {
return basicFen;
}
// FEN structure: [fen] [player color] [castling rights] [en passant targets] [halfmove clock] [fullmove clock]
const fullFen = `${basicFen} ${getPlayerColorVariable()} ${getRights()} - 0 1`;
return fullFen;
}
function resetCachedValues() {
chesscomVariantPlayerColorsTable = null;
}
function fenToArray(fen) {
const rows = fen.split('/');
const board = [];
for (let row of rows) {
const boardRow = [];
for (let char of row) {
if (isNaN(char)) {
boardRow.push(char);
} else {
boardRow.push(...Array(parseInt(char)).fill(''));
}
}
board.push(boardRow);
}
return board;
}
function onNewMove(mutationArr, bypassFenChangeDetection) {
const currentFullFen = getFen();
const lastFullFen = instanceVars.fen.get(commLinkInstanceID);
const fenChanged = currentFullFen !== lastFullFen;
if((fenChanged || bypassFenChangeDetection)) {
if(debugModeActivated) console.warn('NEW MOVE DETECTED!');
const pieceAmount = getPieceAmount();
const pieceAmountChange = Math.abs(pieceAmount - lastPieceAmount);
// Possibly new match due to large piece amount change
if(pieceAmountChange > 7) {
matchFirstSuggestionGiven = false;
}
resetCachedValues();
boardUtils.setBoardDimensions(getBoardDimensions());
const lastPlayerColor = getPlayerColorVariable();
updatePlayerColor();
const playerColor = getPlayerColorVariable();
const orientationChanged = playerColor != lastPlayerColor;
if(orientationChanged) {
CommLink.commands.log(`Player color (e.g. board orientation) changed from ${lastPlayerColor} to ${playerColor}!`);
resetCachedValues();
matchFirstSuggestionGiven = false;
CommLink.commands.log(`Turn updated to ${playerColor}!`);
}
boardUtils.removeMarkings();
CommLink.commands.updateBoardFen(currentFullFen);
lastMoveRequestTime = Date.now();
lastPieceAmount = pieceAmount;
if(orientationChanged) {
CommLink.commands.calculateBestMoves(currentFullFen);
}
}
}
function observeNewMoves() {
const boardObserver = new MutationObserver(mutationArr => {
if(debugModeActivated) console.log(mutationArr);
if(isMutationNewMove(mutationArr))
{
if(debugModeActivated) console.warn('Mutation is a new move:', mutationArr);
try {
if(domain === 'chess.org' || domain === 'chessarena.com')
{
setTimeout(() => onNewMove(mutationArr), 250);
}
else
{
onNewMove(mutationArr);
}
} catch(e) {
if(debugModeActivated) console.error('Error while running onNewMove:', e);
}
}
});
boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true });
}
async function updatePlayerColor() {
const boardOrientation = getBoardOrientation();
const boardOrientationChanged = lastBoardOrientation !== boardOrientation;
const boardOrientationDiffers = BoardDrawer && BoardDrawer?.orientation !== boardOrientation;
if(boardOrientationChanged || boardOrientationDiffers) {
lastBoardOrientation = boardOrientation;
instanceVars.playerColor.set(commLinkInstanceID, boardOrientation);
boardUtils.setBoardOrientation(boardOrientation);
await CommLink.commands.updateBoardOrientation(boardOrientation);
}
}
/*
_______ _____ _______ _______      _______  _____  _______ _______ _____ _______ _____ _______
|______   |      |    |______      |______ |_____] |______ |         |   |______   |   |
______| __|__    |    |______      ______| |       |______ |_____  __|__ |       __|__ |_____
Code below this point handles chess site specific things. (e.g. which element is the board or the pieces)
*/
addSupportedChessSite('chess.com', {
'boardElem': obj => {
const pathname = obj.pathname;
if(pathname?.includes('/variants')) {
return document.querySelector('.TheBoard-layers');
}
return document.querySelector('#board-layout-chessboard > .board');
},
'pieceElem': obj => {
const pathname = obj.pathname;
const getAll = obj.getAll;
if(pathname?.includes('/variants')) {
const filteredPieceElems = filterInvisibleElems(document.querySelectorAll('.TheBoard-layers *[data-piece]'))
.filter(elem => elem?.dataset?.piece?.toLowerCase() !== 'x');
return getAll ? filteredPieceElems : filteredPieceElems[0];
}
return obj.boardQuerySelector('.piece');
},
'squareElems': obj => {
const pathname = obj.pathname;
const element = obj.element;
if(pathname?.includes('/variants')) {
return [...element.querySelectorAll('.square')];
}
},
'chessVariant': obj => {
const pathname = obj.pathname;
if(pathname?.includes('/variants')) {
const variant = pathname.match(/variants\/([^\/]*)/)?.[1]
.replaceAll('-chess', '')
.replaceAll('-', '');
const replacementTable = {
'doubles-bughouse': 'bughouse',
'paradigm-chess30': 'paradigm'
};
return replacementTable[variant] || variant;
}
},
'boardOrientation': obj => {
const pathname = obj.pathname;
if(pathname?.includes('/variants')) {
const playerNumberStr = document.querySelector('.playerbox-bottom [data-player]')?.dataset?.player;
if(!playerNumberStr)
return 'w';
return playerNumberStr === '0' ? 'w' : 'b';
}
const boardElem = getBoardElem();
return boardElem?.classList.contains('flipped') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pathname = obj.pathname;
const pieceElem = obj.pieceElem;
let pieceColor = null;
let pieceName = null;
if(pathname?.includes('/variants')) {
if(!chesscomVariantPlayerColorsTable) {
updateChesscomVariantPlayerColorsTable();
}
const pieceFenStr = pieceElem?.dataset?.piece;
pieceColor = chesscomVariantPlayerColorsTable?.[pieceElem?.dataset?.color];
pieceName = pieceElem?.dataset?.piece;
if(pieceName?.length > 1) {
pieceName = pieceName[0];
}
} else {
const pieceStr = [...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/));
[pieceColor, pieceName] = pieceStr.split('');
}
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
},
'pieceElemCoords': obj => {
const pathname = obj.pathname;
const pieceElem = obj.pieceElem;
if(pathname?.includes('/variants')) {
const coords = getElemCoordinatesFromTransform(pieceElem);
return coords;
}
return pieceElem.classList.toString()
?.match(/square-(\d)(\d)/)
?.slice(1)
?.map(x => Number(x) - 1);
},
'boardDimensions': obj => {
const pathname = obj.pathname;
if(pathname?.includes('/variants')) {
const squaresContainerElem = document.querySelector('.TheBoard-squares');
let ranks = 0;
let files = 0;
[...squaresContainerElem.childNodes].forEach((x, i) => {
const visibleChildElems = filterInvisibleElems([...x.childNodes]);
if(visibleChildElems?.length > 0) {
ranks = ranks + 1;
if(visibleChildElems.length > files) {
files = visibleChildElems.length;
}
}
});
return [ranks, files];
} else {
return [8, 8];
}
},
'isMutationNewMove': obj => {
const pathname = obj.pathname;
const mutationArr = obj.mutationArr;
if(pathname?.includes('/variants')) {
return mutationArr.find(m => m.type === 'childList') ? true : false;
}
if(mutationArr.length == 1)
return false;
const modifiedHoverSquare = mutationArr.find(m => m?.target?.classList?.contains('hover-square')) ? true : false;
const modifiedHighlight = mutationArr.find(m => m?.target?.classList?.contains('highlight')) ? true : false;
const modifiedElemPool = mutationArr.find(m => m?.target?.classList?.contains('element-pool')) ? true : false;
const isPremove = mutationArr.filter(m => m?.target?.classList?.contains('highlight'))
.map(x => x?.target?.style?.['background-color'])
.find(x => x === 'rgb(244, 42, 50)') ? true : false;
return (
(mutationArr.length >= 4 && !modifiedHoverSquare)
|| mutationArr.length >= 7
|| modifiedHighlight
|| modifiedElemPool
) && !isPremove;
}
});
addSupportedChessSite('lichess.org', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece:not(.ghost)');
},
'chessVariant': obj => {
const variantLinkElem = document.querySelector('.variant-link');
if(variantLinkElem) {
let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-');
const replacementTable = {
'correspondence': 'chess',
'koth': 'kingofthehill',
'three-check': '3check'
};
return replacementTable[variant] || variant;
}
},
'boardOrientation': obj => {
const filesElem = document.querySelector('coords.files');
return filesElem?.classList?.contains('black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('playstrategy.org', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece[class*="-piece"]:not(.ghost)');
},
'chessVariant': obj => {
const variantLinkElem = document.querySelector('.variant-link');
if(variantLinkElem) {
let variant = variantLinkElem?.innerText
?.toLowerCase()
?.replaceAll(' ', '-');
const replacementTable = {
'correspondence': 'chess',
'koth': 'kingofthehill',
'three-check': '3check',
'five-check': '5check',
'no-castling': 'nocastle'
};
return replacementTable[variant] || variant;
}
},
'boardOrientation': obj => {
const cgWrapElem = document.querySelector('.cg-wrap');
return cgWrapElem.classList?.contains('orientation-p1') ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const playerColor = getPlayerColorVariable();
const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w');
let pieceName = null;
[...pieceElem?.classList]?.forEach(className => {
if(className?.includes('-piece')) {
const elemPieceName = className?.split('-piece')?.[0];
if(elemPieceName && elemPieceName?.length === 1) {
pieceName = elemPieceName;
}
}
});
if(pieceColor && pieceName) {
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return getBoardDimensionsFromSize();
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('pychess.org', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece[class*="-piece"]:not(.ghost)');
},
'chessVariant': obj => {
const variantLinkElem = document.querySelector('#main-wrap .tc .user-link');
if(variantLinkElem) {
let variant = variantLinkElem?.innerText
?.toLowerCase()
?.replaceAll(' ', '')
?.replaceAll('-', '');
const replacementTable = {
'correspondence': 'chess',
'koth': 'kingofthehill',
'nocastling': 'nocastle',
'gorogoro+': 'gorogoro',
'oukchaktrang': 'cambodian'
};
return replacementTable[variant] || variant;
}
},
'boardOrientation': obj => {
const cgWrapElem = document.querySelector('.cg-wrap');
return cgWrapElem.classList?.contains('orientation-black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const playerColor = getPlayerColorVariable();
const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w');
let pieceName = null;
[...pieceElem?.classList]?.forEach(className => {
if(className?.includes('-piece')) {
const elemPieceName = className?.split('-piece')?.[0];
if(elemPieceName && elemPieceName?.length === 1) {
pieceName = elemPieceName;
}
}
});
if(pieceColor && pieceName) {
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return getBoardDimensionsFromSize();
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('chess.org', {
'boardElem': obj => {
return document.querySelector('.cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece:not(.ghost)');
},
'chessVariant': obj => {
const variantNum = unsafeWindow?.GameConfig?.instance?.variant;
const variant = GameConfig?.VARIANT_NAMES?.[variantNum]?.toLowerCase();
if(variant) {
const replacementTable = {
'standard': 'chess'
};
return replacementTable[variant] || variant;
}
},
'boardOrientation': obj => {
const filesElem = document.querySelector('coords.files');
return filesElem?.classList?.contains('black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return getElemCoordinatesFromTransform(pieceElem);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
if(isUserMouseDown) {
return false;
}
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('chess.coolmathgames.com', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece:not(.ghost)');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const boardElem = getBoardElem();
return document.querySelector('.ranks.black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
if(isUserMouseDown) {
return false;
}
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('papergames.io', {
'boardElem': obj => {
return document.querySelector('.cm-chessboard');
},
'pieceElem': obj => {
return obj.boardQuerySelector('*[data-piece][data-square]');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const boardElem = getBoardElem();
if(boardElem) {
const firstRankText = [...boardElem.querySelector('.coordinates').childNodes]?.[0].textContent;
return firstRankText == 'h' ? 'b' : 'w';
}
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
return convertPieceStrToFen(pieceElem?.dataset?.piece);
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.dataset?.square;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 12;
}
});
addSupportedChessSite('vole.wtf', {
'boardElem': obj => {
return document.querySelector('#board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('*[data-t][data-l][data-p]:not([data-p="0"]');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
return 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceNum = Number(pieceElem?.dataset?.p);
const pieceFenStr = 'pknbrq';
if(pieceNum > 8) {
return pieceFenStr[pieceNum - 9].toUpperCase();
} else {
return pieceFenStr[pieceNum - 1];
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return [Number(pieceElem?.dataset?.l), 7 - Number(pieceElem?.dataset?.t)];
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 12;
}
});
addSupportedChessSite('immortal.game', {
'boardElem': obj => {
return document.querySelector('div.pawn.relative, div.knight.relative, div.bishop.relative, div.rook.relative, div.queen.relative, div.king.relative')?.parentElement?.parentElement;
},
'pieceElem': obj => {
return obj.boardQuerySelector('div.pawn.relative, div.knight.relative, div.bishop.relative, div.rook.relative, div.queen.relative, div.king.relative');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const coordA = [...document.querySelectorAll('svg text[x]')]
.find(elem => elem?.textContent == 'a');
const coordAX = Number(coordA?.getAttribute('x')) || 10;
return coordAX < 15 ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return getElemCoordinatesFromTransform(pieceElem?.parentElement);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
if(isUserMouseDown) {
return false;
}
return mutationArr.length >= 5;
}
});
addSupportedChessSite('chessarena.com', {
'boardElem': obj => {
return document.querySelector('*[data-component="GameLayoutBoard"] cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('cg-piece:not(*[style*="visibility: hidden;"])');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const titlesElem = document.querySelector('cg-titles');
return titlesElem?.classList?.contains('rotated') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.className?.[0];
const elemPieceName = pieceElem?.className?.[1];
if(pieceColor && elemPieceName) {
const pieceName = elemPieceName; // pieceNameToFen[elemPieceName]
return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return getElemCoordinatesFromTransform(pieceElem, { 'onlyFlipY': true });
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
if(isUserMouseDown) {
return false;
}
return mutationArr.find(m => m?.attributeName === 'style') ? true : false;
}
});
addSupportedChessSite('chess.net', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece:not(.ghost)');
},
'chessVariant': obj => {
const variantLinkElem = document.querySelector('.variant-link');
if(variantLinkElem) {
let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-');
const replacementTable = {
'correspondence': 'chess',
'koth': 'kingofthehill',
'three-check': '3check'
};
return replacementTable[variant] || variant;
}
},
'boardOrientation': obj => {
const filesElem = document.querySelector('coords.files');
return filesElem?.classList?.contains('black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('freechess.club', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece:not(.ghost)');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const filesElem = document.querySelector('coords.files');
return filesElem?.classList?.contains('black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('play.chessclub.com', {
'boardElem': obj => {
return document.querySelector('cg-board');
},
'pieceElem': obj => {
return obj.boardQuerySelector('piece:not(.ghost)');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const filesElem = document.querySelector('coords.files');
return filesElem?.classList?.contains('black') ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
if(pieceColor && elemPieceName) {
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
const key = pieceElem?.cgKey;
if(key) {
return chessCoordinatesToIndex(key);
}
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4
|| mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('gameknot.com', {
'boardElem': obj => {
return document.querySelector('#chess-board-acboard');
},
'pieceElem': obj => {
return obj.boardQuerySelector('*[class*="chess-board-piece"] > img[src*="chess56."][style*="visible"]');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
return document.querySelector('#chess-board-my-side-color .player_white') ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const left = Number(pieceElem.style.left.replace('px', ''));
const top = Number(pieceElem.style.top.replace('px', ''));
const pieceColor = left >= 0 ? 'w' : 'b';
const pieceName = 'kqrnbp'[(top * -1) / 60];
return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return getElemCoordinatesFromLeftTopPixels(pieceElem.parentElement);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.find(m => m.type === 'childList') ? true : false
|| mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
}
});
addSupportedChessSite('chesstempo.com', {
'boardElem': obj => {
return document.querySelector('.ct-board-squares');
},
'pieceElem': obj => {
return obj.boardQuerySelector('*[class*="ct-pieceClass"][class*="ct-piece-"]');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
return document.querySelector('.ct-coord-column').innerText === 'a' ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceNameClass = [...pieceElem.classList].find(x => x?.includes('ct-piece-'));
const colorNameCombo = pieceNameClass?.split('ct-piece-')?.pop();
const elemPieceColor = colorNameCombo.startsWith('white') ? 'w' : 'b';
const elemPieceName = colorNameCombo.substring(5);
const pieceName = pieceNameToFen[elemPieceName];
return elemPieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return [pieceElem?.ct?.piece?.piece?.column, pieceElem?.ct?.piece?.piece?.row];
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 4;
}
});
addSupportedChessSite('redhotpawn.com', {
'boardElem': obj => {
return document.querySelector('#board-0_1');
},
'pieceElem': obj => {
return obj.boardQuerySelector('li.piece[id*="-pc-"]');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
const aCoordLeftStyleNum = Number([...document.querySelectorAll('.boardCoordinate')]
.find(elem => elem?.innerText === 'a')
?.style?.left?.replace('px', ''));
return aCoordLeftStyleNum < 200 ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
return (pieceElem?.id?.match(/-pc-(.*?)-/) || [])[1];
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return getElemCoordinatesFromLeftTopPixels(pieceElem);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
if(isUserMouseDown) {
return false;
}
return mutationArr.length >= 4;
}
});
addSupportedChessSite('simplechess.com', {
'boardElem': obj => {
return document.querySelector('#chessboard');
},
'pieceElem': obj => {
const getAll = obj.getAll;
const pieceElems = [...document.querySelectorAll('canvas.canvas_piece')].filter(elem => canvasHasPixelAt(elem, [0.5, 0.5]));
return getAll ? pieceElems : pieceElems[0];
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
return document.querySelector('#chessboard_coordy0')?.innerText === '8' ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const pieceTypeCoordPercentages = [
{ 'name' : 'k', 'coords': [52/60, 26/60] },
{ 'name' : 'q', 'coords': [8/60, 16/60] },
{ 'name' : 'n', 'coords': [51/60, 42/60] },
{ 'name' : 'b', 'coords': [9/60, 50/60] },
{ 'name' : 'r', 'coords': [45/60, 15/60] },
{ 'name' : 'p', 'coords': [0.5, 0.5] }
];
const pieceColorCoordPercentages = {
'k': [42/60, 27/60],
'q': [30/60, 50/60],
'n': [38/60, 41/60],
'b': [30/60, 20/60]
};
let pieceName = null;
for(obj of pieceTypeCoordPercentages) {
const isThisPiece = canvasHasPixelAt(pieceElem, obj.coords);
if(isThisPiece) {
pieceName = obj.name;
break;
}
}
if(pieceName) {
const colorCoords = pieceColorCoordPercentages[pieceName] || [0.5, 0.5];
const pieceColor = getCanvasPixelColor(pieceElem, colorCoords);
return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
}
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return getElemCoordinatesFromLeftTopPixels(pieceElem);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
if(isUserMouseDown) {
return false;
}
return mutationArr.length >= 7;
}
});
addSupportedChessSite('chessworld.net', {
'boardElem': obj => {
return document.querySelector('#ChessWorldChessBoard');
},
'pieceElem': obj => {
return obj.boardQuerySelector('img[src*="merida"');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
return document.querySelector('div[style*="boardb.jpg"]') ? 'w' : 'b';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const [elemPieceColor, elemPieceName] = pieceElem
?.src
?.split('/')
?.pop()
?.replace('.png', '')
?.split('_');
const pieceColor = elemPieceColor === 'white' ? 'w' : 'b';
const pieceName = pieceNameToFen[elemPieceName];
return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return chessCoordinatesToIndex(pieceElem?.id);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 2;
}
});
addSupportedChessSite('app.edchess.io', {
'boardElem': obj => {
return document.querySelector('*[data-boardid="chessboard"]');
},
'pieceElem': obj => {
return obj.boardQuerySelector('*[data-piece]');
},
'chessVariant': obj => {
return 'chess';
},
'boardOrientation': obj => {
return document.querySelector('*[data-square]')?.dataset?.square == 'h1' ? 'b' : 'w';
},
'pieceElemFen': obj => {
const pieceElem = obj.pieceElem;
const [pieceColor, pieceName] = pieceElem?.dataset?.piece?.split('');
return pieceColor === 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
},
'pieceElemCoords': obj => {
const pieceElem = obj.pieceElem;
return chessCoordinatesToIndex(pieceElem?.parentElement?.parentElement?.dataset?.square);
},
'boardDimensions': obj => {
return [8, 8];
},
'isMutationNewMove': obj => {
const mutationArr = obj.mutationArr;
return mutationArr.length >= 2;
}
});
/*
_____ __   _ _____ _______ _____ _______        _____ ______ _______ _______ _____  _____  __   _
|   | \  |   |      |      |   |_____| |        |    ____/ |_____|    |      |   |     | | \  |
__|__ |  \_| __|__    |    __|__ |     | |_____ __|__ /_____ |     |    |    __|__ |_____| |  \_|
Code below this point is related to initialization. (e.g. wait for chess board and create the instance)
*/
async function isAcasBackendReady() {
const res = await CommLink.commands.ping();
return res ? true : false;
}
async function start() {
await CommLink.commands.createInstance(commLinkInstanceID);
const pathname = window.location.pathname;
const adjustSizeByDimensions = domain === 'chess.com' && pathname?.includes('/variants');
const ignoreBodyRectLeft = domain === 'app.edchess.io';
const boardOrientation = getBoardOrientation();
instanceVars.playerColor.set(commLinkInstanceID, boardOrientation);
instanceVars.fen.set(commLinkInstanceID, getFen());
if(isBoardDrawerNeeded()) {
BoardDrawer = new UniversalBoardDrawer(chessBoardElem, {
'window': window,
'boardDimensions': getBoardDimensions(),
'playerColor': getPlayerColorVariable(),
'zIndex': domain === 'chessarena.com' ? 9999 : 500,
'prepend': true,
'debugMode': debugModeActivated,
'adjustSizeByDimensions': adjustSizeByDimensions ? true : false,
'adjustSizeConfig': {
'noLeftAdjustment': true
},
'ignoreBodyRectLeft': ignoreBodyRectLeft
});
}
await updatePlayerColor();
observeNewMoves();
CommLink.setIntervalAsync(async () => {
await CommLink.commands.createInstance(commLinkInstanceID);
}, 1000);
}
function startWhenBackendReady() {
let tim###rlForceOpened = 0;
const interval = CommLink.setIntervalAsync(async () => {
if(await isAcasBackendReady()) {
start();
interval.stop();
} else if(tim###rlForceOpened < 1) {
tim###rlForceOpened++;
GM_openInTab(getCurrentBackendURL(), true);
}
}, 1000);
}
function initializeIfSiteReady() {
const boardElem = getBoardElem();
const firstPieceElem = getPieceElem();
const bothElemsExist = boardElem && firstPieceElem;
const isChessComImageBoard = domain === 'chess.com' && boardElem?.className.includes('webgl-2d');
const boardElemChanged = chessBoardElem != boardElem;
if((bothElemsExist || isChessComImageBoard) && boardElemChanged) {
chessBoardElem = boardElem;
chessBoardElem.addEventListener('mousedown', () => { isUserMouseDown = true; });
chessBoardElem.addEventListener('mouseup', () => { isUserMouseDown = false; });
chessBoardElem.addEventListener('touchstart', () => { isUserMouseDown = true; });
chessBoardElem.addEventListener('touchend', () => { isUserMouseDown = false; });
if(!blacklistedURLs.includes(window.location.href)) {
startWhenBackendReady();
}
}
}
if(typeof GM_registerMenuCommand === 'function') {
GM_registerMenuCommand('[u] Open GreasyFork Page', e => {
GM_openInTab(greasyforkURL, true);
}, 'u');
GM_registerMenuCommand('[o] Open GUI Manually', e => {
GM_openInTab(getCurrentBackendURL(), true);
}, 'o');
GM_registerMenuCommand('[s] Start Manually', e => {
if(chessBoardElem) {
start();
} else {
displayImportantNotification('Failed to start manually', 'No chessboard element found!');
}
}, 's');
GM_registerMenuCommand('[g] Get Moves Manually', e => {
if(chessBoardElem) {
onNewMove(null, true);
} else {
displayImportantNotification('Failed to get moves', 'No chessboard element found!');
}
}, 'g');
GM_registerMenuCommand('[r] Render BoardDrawer Manually', e => {
if(typeof BoardDrawer?.updateDimensions === 'function') {
BoardDrawer.updateDimensions();
} else {
displayImportantNotification('Failed to render BoardDrawer', 'BoardDrawer not initialized or something else went wrong!');
}
}, 'r');
if(typeof GM_setClipboard === 'function') {
GM_registerMenuCommand('[c] Copy FEN to Clipboard', e => {
if(chessBoardElem) {
GM_setClipboard(getFen());
} else {
displayImportantNotification('Failed to get FEN', 'No chessboard element found!');
}
}, 'c');
}
}
setInterval(initializeIfSiteReady, 1000);