A.C.A.S (Advanced Chess Assistance System)

Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system

目前為 2023-07-10 提交的版本,檢視 最新版本

  1. // ==UserScript==
  2. // @name A.C.A.S (Advanced Chess Assistance System)
  3. // @name:en A.C.A.S (Advanced Chess Assistance System)
  4. // @name:fi A.C.A.S (Edistynyt shakkiavustusjärjestelmä)
  5. // @name:zh-CN A.C.A.S(高级国际象棋辅助系统)
  6. // @name:es A.C.A.S (Sistema Avanzado de Asistencia al Ajedrez)
  7. // @name:hi A.C.A.S (उन्नत शतरंज सहायता प्रणाली)
  8. // @name:ar A.C.A.S (نظام المساعدة المتقدم في الشطرنج)
  9. // @name:pt A.C.A.S (Sistema Avançado de Assistência ao Xadrez)
  10. // @name:ja A.C.A.S(先進的なチェス支援システム)
  11. // @name:de A.C.A.S (Fortgeschrittenes Schach-Hilfesystem)
  12. // @name:fr A.C.A.S (Système Avancé d'Assistance aux Échecs)
  13. // @name:it A.C.A.S (Sistema Avanzato di Assistenza agli Scacchi)
  14. // @name:ko A.C.A.S (고급 체스 보조 시스템)
  15. // @name:nl A.C.A.S (Geavanceerd Schaakondersteuningssysteem)
  16. // @name:pl A.C.A.S (Zaawansowany System Pomocy Szachowej)
  17. // @name:tr A.C.A.S (Gelişmiş Satranç Yardım Sistemi)
  18. // @name:vi A.C.A.S (Hệ Thống Hỗ Trợ Cờ Vua Nâng Cao)
  19. // @name:uk A.C.A.S (Система передової допомоги в шахах)
  20. // @name:ru A.C.A.S (Система расширенной помощи в шахматах)
  21. // @description Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
  22. // @description:en Enhance your chess performance with a cutting-edge real-time move analysis and strategy assistance system
  23. // @description:fi Paranna shakkipelisi suorituskykyä huippuluokan reaaliaikaisen siirtoanalyysin ja strategisen avustusjärjestelmän avulla
  24. // @description:zh-CN 利用尖端实时走法分析和策略辅助系统,提升您的国际象棋水平
  25. // @description:es Mejora tu rendimiento en ajedrez con un sistema de análisis de movimientos en tiempo real y asistencia estratégica de vanguardia
  26. // @description:hi अपने शतरंज प्रदर्शन को उन्नत करें, एक कटिंग-एज रियल-टाइम मूव विश्लेषण और रणनीति सहायता प्रणाली के साथ
  27. // @description:ar قم بتحسين أداءك في الشطرنج مع تحليل حركات اللعب في الوقت الحقيقي ونظام مساعدة استراتيجية حديث
  28. // @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
  29. // @description:ja 最新のリアルタイムのムーブ分析と戦略支援システムでチェスのパフォーマンスを向上させましょう
  30. // @description:de Verbessern Sie Ihre Schachleistung mit einer hochmodernen Echtzeitzug-Analyse- und Strategiehilfe-System
  31. // @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
  32. // @description:it Migliora le tue prestazioni agli scacchi con un sistema all'avanguardia di analisi dei movimenti in tempo reale e assistenza strategica
  33. // @description:ko 최첨단 실시간 움직임 분석 및 전략 지원 시스템으로 체스 성과 향상
  34. // @description:nl Verbeter je schaakprestaties met een geavanceerd systeem voor realtime zetanalyse en strategische ondersteuning
  35. // @description:pl Popraw swoje osiągnięcia w szachach dzięki zaawansowanemu systemowi analizy ruchów w czasie rzeczywistym i wsparciu strategicznemu
  36. // @description:tr Keskinleşmiş gerçek zamanlı hareket analizi ve strateji yardım sistemiyle satranç performansınızı artırın
  37. // @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
  38. // @description:uk Покращуйте свою шахову гру з використанням передової системи аналізу ходів в режимі реального часу та стратегічної підтримки
  39. // @description:ru Слава Украине
  40. // @homepageURL https://hakorr.github.io/A.C.A.S
  41. // @supportURL https://github.com/Hakorr/A.C.A.S/tree/main#why-doesnt-it-work
  42. // @match https://www.chess.com/*
  43. // @match https://lichess.org/*
  44. // @match https://playstrategy.org/*
  45. // @match https://www.pychess.org/*
  46. // @match https://chess.org/*
  47. // @match https://papergames.io/*
  48. // @match https://vole.wtf/kilobytes-gambit/
  49. // @match https://hakorr.github.io/A.C.A.S/*
  50. // @match http://localhost/*
  51. // @grant GM_getValue
  52. // @grant GM_setValue
  53. // @grant GM_deleteValue
  54. // @grant GM_listValues
  55. // @grant GM_registerMenuCommand
  56. // @grant GM_openInTab
  57. // @grant GM_addStyle
  58. // @grant unsafeWindow
  59. // @run-at document-start
  60. // @version 2.0.3
  61. // @namespace HKR
  62. // @author HKR
  63. // @require https://gf.qytechs.cn/scripts/470418-commlink-js/code/CommLinkjs.js
  64. // @require https://gf.qytechs.cn/scripts/470417-universalboarddrawer-js/code/UniversalBoardDrawerjs.js
  65. // ==/UserScript==
  66.  
  67. /*
  68. e e88~-_ e ,d88~~\
  69. d8b d888 \ d8b 8888
  70. /Y88b 8888 /Y88b `Y88b
  71. / Y88b 8888 / Y88b `Y88b,
  72. /____Y88b d88b Y888 / d88b /____Y88b d88b 8888
  73. / Y88b Y88P "88_-~ Y88P / Y88b Y88P \__88P'
  74.  
  75. Advanced Chess Assistance System (A.C.A.S) v2 | Q3 2023
  76.  
  77. [WARNING]
  78. - 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.
  79. - The developers of A.C.A.S and related systems will NOT be held accountable for any consequences resulting from its use.
  80. - We strongly advise to use A.C.A.S only in a controlled environment ethically.*/
  81.  
  82. // DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING //
  83. /*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/
  84. //////////////////////////////////////////////////////////////////////
  85. // DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING //
  86.  
  87. const backendURL = 'https://hakorr.github.io/A.C.A.S/'; // FOR DEVELOPMENT USE: 'http://localhost/A.C.A.S/';
  88.  
  89. const domain = window.location.hostname.replace('www.', '');
  90. const tempValueIndicator = '-temp-value-';
  91.  
  92. const dbValues = {
  93. AcasConfig: 'AcasConfig',
  94. playerColor: instanceID => 'playerColor' + tempValueIndicator + instanceID,
  95. turn: instanceID => 'turn' + tempValueIndicator + instanceID,
  96. fen: instanceID => 'fen' + tempValueIndicator + instanceID
  97. };
  98.  
  99. function createInstanceVariable(dbValue) {
  100. return {
  101. set: (instanceID, value) => GM_setValue(dbValues[dbValue](instanceID), { value, 'date': Date.now() }),
  102. get: instanceID => {
  103. const data = GM_getValue(dbValues[dbValue](instanceID));
  104.  
  105. if(data?.date) {
  106. data.date = Date.now();
  107.  
  108. GM_setValue(dbValues[dbValue](instanceID), data);
  109. }
  110.  
  111. return data?.value;
  112. }
  113. }
  114. }
  115.  
  116. const instanceVars = {
  117. playerColor: createInstanceVariable('playerColor'),
  118. turn: createInstanceVariable('turn'),
  119. fen: createInstanceVariable('fen')
  120. };
  121.  
  122. if(window?.location?.href?.includes(backendURL)) {
  123. // expose variables and functions
  124. unsafeWindow.USERSCRIPT = {
  125. 'GM_info': GM_info,
  126. 'GM_getValue': val => GM_getValue(val),
  127. 'GM_setValue': (val, data) => GM_setValue(val, data),
  128. 'GM_deleteValue': val => GM_deleteValue(val),
  129. 'GM_listValues': val => GM_listValues(val),
  130. 'tempValueIndicator': tempValueIndicator,
  131. 'dbValues': dbValues,
  132. 'instanceVars': instanceVars,
  133. 'CommLinkHandler': CommLinkHandler,
  134. };
  135.  
  136. return;
  137. }
  138.  
  139. // DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING //
  140. /*\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\*/
  141. //////////////////////////////////////////////////////////////////////
  142. // DANGER ZONE - DO NOT PROCEED IF YOU DON'T KNOW WHAT YOU'RE DOING //
  143.  
  144. function getUniqueID() {
  145. return ([1e7]+-1e3+4e3+-8e3+-1e11).replace(/[018]/g, c =>
  146. (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
  147. )
  148. }
  149.  
  150. const commLinkInstanceID = getUniqueID();
  151.  
  152. const blacklistedURLs = [
  153. 'https://www.chess.com/play',
  154. 'https://lichess.org/',
  155. 'https://chess.org/',
  156. 'https://papergames.io/en/chess',
  157. 'https://playstrategy.org/',
  158. 'https://www.pychess.org/',
  159. 'https://hakorr.github.io/A.C.A.S/',
  160. 'https://hakorr.github.io/A.C.A.S/why/',
  161. 'https://hakorr.github.io/A.C.A.S/tos/',
  162. 'http://localhost/A.C.A.S/'
  163. ];
  164.  
  165. const configKeys = {
  166. 'engineElo': 'engineElo',
  167. 'moveSuggestionAmount': 'moveSuggestionAmount',
  168. 'arrowOpacity': 'arrowOpacity',
  169. 'displayMovesOnExternalSite': 'displayMovesOnExternalSite',
  170. 'showMoveGhost': 'showMoveGhost',
  171. 'showOpponentMoveGuess': 'showOpponentMoveGuess',
  172. 'maxMovetime': 'maxMovetime',
  173. 'chessVariant': 'chessVariant',
  174. 'chessFont': 'chessFont',
  175. 'useChess960': 'useChess960'
  176. };
  177.  
  178. const config = {};
  179.  
  180. Object.values(configKeys).forEach(key => {
  181. config[key] = {
  182. get: () => getGmConfigValue(key, commLinkInstanceID),
  183. set: null
  184. };
  185. });
  186.  
  187. const debugModeActivated = false;
  188.  
  189. let BoardDrawer = null;
  190. let chessBoardElem = null;
  191. let lastBasicFen = null;
  192. let chesscomVariantBoardCoordsTable = null;
  193. let activeSiteMoveHighlights = [];
  194. let inactiveGuiMoveMarkings = [];
  195.  
  196. let lastBoardRanks = null;
  197. let lastBoardFiles = null;
  198.  
  199. let lastBoardSize = null;
  200. let lastPieceSize = null;
  201.  
  202. let lastBoardOrientation = null;
  203.  
  204. const domainsWithoutDifferentBoardDimensionsArr = ['chess.org', 'lichess.org', 'papergames.io', 'vole.wtf'];
  205.  
  206. const arrowStyles = {
  207. 'best': `
  208. fill: limegreen;
  209. opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.9'};
  210. stroke: rgb(0 0 0 / 50%);
  211. stroke-width: 2px;
  212. stroke-linejoin: round;
  213. `,
  214. 'secondary': `
  215. fill: dodgerblue;
  216. opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.7'};
  217. stroke: rgb(0 0 0 / 50%);
  218. stroke-width: 2px;
  219. stroke-linejoin: round;
  220. `,
  221. 'opponent': `
  222. fill: crimson;
  223. stroke: rgb(0 0 0 / 25%);
  224. stroke-width: 2px;
  225. stroke-linejoin: round;
  226. display: none;
  227. opacity: ${getConfigValue(configKeys.arrowOpacity)/100 || '0.3'};
  228. `
  229. };
  230.  
  231. const CommLink = new CommLinkHandler(`frontend_${commLinkInstanceID}`, {
  232. 'singlePacketResponseWaitTime': 1500,
  233. 'maxSendAttempts': 3,
  234. 'statusCheckInterval': 1,
  235. 'silentMode': true
  236. });
  237.  
  238. // manually register a command so that the variables are dynamic
  239. CommLink.commands['createInstance'] = async () => {
  240. return await CommLink.send('mum', 'createInstance', {
  241. 'domain': domain,
  242. 'instanceID': commLinkInstanceID,
  243. 'chessVariant': getChessVariant(),
  244. 'playerColor': getPlayerColorVariable()
  245. });
  246. }
  247.  
  248. CommLink.registerSendCommand('ping', { commlinkID: 'mum', data: 'ping' });
  249. CommLink.registerSendCommand('pingInstance', { data: 'ping' });
  250. CommLink.registerSendCommand('log');
  251. CommLink.registerSendCommand('updateBoardOrientation');
  252. CommLink.registerSendCommand('updateBoardFen');
  253. CommLink.registerSendCommand('calculateBestMoves');
  254.  
  255. CommLink.registerListener(`backend_${commLinkInstanceID}`, packet => {
  256. try {
  257. switch(packet.command) {
  258. case 'ping':
  259. return `pong (took ${Date.now() - packet.date}ms)`;
  260. case 'getFen':
  261. return getFen();
  262. case 'removeSiteMoveMarkings':
  263. boardUtils.removeBestMarkings();
  264. return true;
  265. case 'markMoveToSite':
  266. boardUtils.markMove(packet.data);
  267. return true;
  268. }
  269. } catch(e) {
  270. return null;
  271. }
  272. });
  273.  
  274. function filterInvisibleElems(elementArr, inverse) {
  275. return [...elementArr].filter(elem => {
  276. const style = getComputedStyle(elem);
  277. const bounds = elem.getBoundingClientRect();
  278.  
  279. const isHidden =
  280. style.visibility === 'hidden' ||
  281. style.display === 'none' ||
  282. style.opacity === '0' ||
  283. bounds.width == 0 ||
  284. bounds.height == 0;
  285.  
  286. return inverse ? isHidden : !isHidden;
  287. });
  288. }
  289.  
  290. function getElementSize(elem) {
  291. const rect = elem.getBoundingClientRect();
  292.  
  293. if(rect.width !== 0 && rect.height !== 0) {
  294. return { width: rect.width, height: rect.height };
  295. }
  296.  
  297. const computedStyle = window.getComputedStyle(elem);
  298. const width = parseFloat(computedStyle.width);
  299. const height = parseFloat(computedStyle.height);
  300.  
  301. return { width, height };
  302. }
  303.  
  304. function extractElemTransformData(elem) {
  305. const computedStyle = window.getComputedStyle(elem);
  306. const transformMatrix = new DOMMatrix(computedStyle.transform);
  307.  
  308. const x = transformMatrix.e;
  309. const y = transformMatrix.f;
  310.  
  311. return [x, y];
  312. }
  313.  
  314. function getElemCoordinatesFromTransform(elem) {
  315. if (!lastBoardSize) {
  316. lastBoardSize = getElementSize(chessBoardElem);
  317. }
  318.  
  319. if (!lastBoardRanks) {
  320. const [files, ranks] = getBoardDimensions();
  321.  
  322. lastBoardRanks = ranks;
  323. lastBoardFiles = files;
  324. }
  325.  
  326. const boardOrientation = getPlayerColorVariable();
  327.  
  328. const [x, y] = extractElemTransformData(elem);
  329.  
  330. const boardDimensions = lastBoardSize;
  331. const squareDimensions = boardDimensions.width / lastBoardRanks;
  332.  
  333. const normalizedX = Math.round(x / squareDimensions);
  334. const normalizedY = Math.round(y / squareDimensions);
  335.  
  336. if (boardOrientation === 'w') {
  337. const flippedY = lastBoardFiles - normalizedY - 1;
  338.  
  339. return [normalizedX, flippedY];
  340. } else {
  341. const flippedX = lastBoardRanks - normalizedX - 1;
  342.  
  343. return [flippedX, normalizedY];
  344. }
  345. }
  346.  
  347.  
  348. function createChesscomVariantBoardCoordsTable() {
  349. chesscomVariantBoardCoordsTable = {};
  350.  
  351. const boardElem = getBoardElem();
  352. const [boardWidth, boardHeight] = getBoardDimensions();
  353. const boardOrientation = getBoardOrientation();
  354.  
  355. const squareElems = getSquareElems(boardElem);
  356.  
  357. let squareIndex = 0;
  358.  
  359. for(let x = 0; x < boardWidth; x++) {
  360. for(let y = boardHeight; y > 0; y--) {
  361. const squareElem = squareElems[squareIndex];
  362. const id = squareElem?.id;
  363.  
  364. const xIdx = x;
  365. const yIdx = y - 1;
  366.  
  367. if(id) {
  368. chesscomVariantBoardCoordsTable[id] = [xIdx, yIdx];
  369. }
  370.  
  371. squareIndex++;
  372. }
  373. }
  374. }
  375.  
  376. function chessCoordinatesToIndex(coord) {
  377. const x = coord.charCodeAt(0) - 97;
  378. let y = null;
  379.  
  380. const lastHalf = coord.slice(1);
  381.  
  382. if(lastHalf === ':') {
  383. y = 9;
  384. } else {
  385. y = Number(coord.slice(1)) - 1;
  386. }
  387.  
  388. return [x, y];
  389. }
  390.  
  391. function getGmConfigValue(key, instanceID) {
  392. const config = GM_getValue(dbValues.AcasConfig);
  393.  
  394. const instanceValue = config?.instance?.[instanceID]?.[key];
  395. const globalValue = config?.global?.[key];
  396.  
  397. if(instanceValue !== undefined) {
  398. return instanceValue;
  399. }
  400.  
  401. if(globalValue !== undefined) {
  402. return globalValue;
  403. }
  404.  
  405. return null;
  406. }
  407.  
  408. function getConfigValue(key) {
  409. return config[key]?.get();
  410. }
  411.  
  412. function setConfigValue(key, val) {
  413. return config[key]?.set(val);
  414. }
  415.  
  416. function squeezeEmptySquares(fenStr) {
  417. return fenStr.replace(/1+/g, match => match.length);
  418. }
  419.  
  420. function getPlayerColorVariable() {
  421. return instanceVars.playerColor.get(commLinkInstanceID);
  422. }
  423.  
  424. function getFenPieceColor(pieceFenStr) {
  425. return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b';
  426. }
  427.  
  428. function getFenPieceOppositeColor(pieceFenStr) {
  429. return getFenPieceColor(pieceFenStr) == 'w' ? 'b' : 'w';
  430. }
  431.  
  432. function convertPieceStrToFen(str) {
  433. if(!str || str.length !== 2) {
  434. return null;
  435. }
  436.  
  437. const firstChar = str[0].toLowerCase();
  438. const secondChar = str[1];
  439.  
  440. if(firstChar === 'w') {
  441. return secondChar.toUpperCase();
  442. } else if (firstChar === 'b') {
  443. return secondChar.toLowerCase();
  444. }
  445.  
  446. return null;
  447. }
  448.  
  449. function getBoardElem() {
  450. const pathname = window.location.pathname;
  451.  
  452. switch(domain) {
  453. case 'chess.com': {
  454. if(pathname?.includes('/variants')) {
  455. return document.querySelector('#board');
  456. }
  457.  
  458. return document.querySelector('chess-board');
  459. }
  460.  
  461. case 'lichess.org': {
  462. return document.querySelector('cg-board');
  463. }
  464.  
  465. case 'playstrategy.org': {
  466. return document.querySelector('cg-board');
  467. }
  468.  
  469. case 'pychess.org': {
  470. return document.querySelector('cg-board');
  471. }
  472.  
  473. case 'chess.org': {
  474. return document.querySelector('.cg-board');
  475. }
  476.  
  477. case 'papergames.io': {
  478. return document.querySelector('#chessboard');
  479. }
  480.  
  481. case 'vole.wtf': {
  482. return document.querySelector('#board');
  483. }
  484. }
  485.  
  486. return null;
  487. }
  488.  
  489. function getChessPieceElem(getAll) {
  490. const pathname = window.location.pathname;
  491. const boardElem = getBoardElem();
  492.  
  493. const querySelector = (getAll ? query => [...boardElem?.querySelectorAll(query)] : boardElem?.querySelector?.bind(boardElem));
  494.  
  495. switch(domain) {
  496. case 'chess.com': {
  497. if(pathname?.includes('/variants')) {
  498. const filteredPieceElems = filterInvisibleElems(document.querySelectorAll('#board *[data-piece]'))
  499. .filter(elem => Number(elem?.dataset?.player) <= 2);
  500.  
  501. return getAll ? filteredPieceElems : filteredPieceElems[0];
  502. }
  503.  
  504. return querySelector('.piece');
  505. }
  506.  
  507. case 'lichess.org': {
  508. return querySelector('piece:not(.ghost)');
  509. }
  510.  
  511. case 'playstrategy.org': {
  512. return querySelector('piece[class*="-piece"]:not(.ghost)');
  513. }
  514.  
  515. case 'pychess.org': {
  516. return querySelector('piece[class*="-piece"]:not(.ghost)');
  517. }
  518.  
  519. case 'chess.org': {
  520. return querySelector('piece:not(.ghost)');
  521. }
  522.  
  523. case 'papergames.io': {
  524. return querySelector('*[data-piece][data-square]');
  525. }
  526.  
  527. case 'vole.wtf': {
  528. return querySelector('*[data-t][data-l][data-p]:not([data-p="0"]');
  529. }
  530. }
  531.  
  532. return null;
  533. }
  534.  
  535. function getSquareElems(element) {
  536. const pathname = window.location.pathname;
  537.  
  538. switch(domain) {
  539. case 'chess.com': {
  540. if(pathname?.includes('/variants')) {
  541. return [...element.querySelectorAll('.square-4pc.ui-droppable')]
  542. .filter(elem => {
  543. const pieceElem = elem.querySelector('[data-player]');
  544. const playerNum = Number(pieceElem?.dataset?.player);
  545.  
  546. return (!playerNum || playerNum <= 2);
  547. });
  548. }
  549.  
  550. break;
  551. }
  552. }
  553.  
  554. return null;
  555. }
  556.  
  557. function isMutationNewMove(mutationArr) {
  558. const pathname = window.location.pathname;
  559.  
  560. switch(domain) {
  561. case 'chess.com': {
  562. if(pathname?.includes('/variants')) {
  563. return mutationArr.find(m => m.attributeName == 'class') ? true : false;
  564. }
  565.  
  566. if(mutationArr.length == 1)
  567. return false;
  568.  
  569. const modifiedHoverSquare = mutationArr.find(m => m?.target?.classList?.contains('hover-square')) ? true : false;
  570. const modifiedHighlight = mutationArr.find(m => m?.target?.classList?.contains('highlight')) ? true : false;
  571. const modifiedElemPool = mutationArr.find(m => m?.target?.classList?.contains('element-pool')) ? true : false;
  572.  
  573. return (mutationArr.length >= 4 && !modifiedHoverSquare)
  574. || mutationArr.length >= 7
  575. || modifiedHighlight
  576. || modifiedElemPool;
  577. }
  578.  
  579. case 'lichess.org': {
  580. return mutationArr.length >= 4
  581. || mutationArr.find(m => m.type === 'childList') ? true : false
  582. || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
  583. }
  584.  
  585. case 'playstrategy.org': {
  586. return mutationArr.length >= 4
  587. || mutationArr.find(m => m.type === 'childList') ? true : false
  588. || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
  589. }
  590.  
  591. case 'pychess.org': {
  592. return mutationArr.length >= 4
  593. || mutationArr.find(m => m.type === 'childList') ? true : false
  594. || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
  595. }
  596.  
  597. case 'chess.org': {
  598. return mutationArr.length >= 4
  599. || mutationArr.find(m => m.type === 'childList') ? true : false
  600. || mutationArr.find(m => m?.target?.classList?.contains('last-move')) ? true : false;
  601. }
  602.  
  603. case 'papergames.io': {
  604. return mutationArr.length >= 12;
  605. }
  606.  
  607. case 'vole.wtf': {
  608. return mutationArr.length >= 12;
  609. }
  610. }
  611.  
  612. return false;
  613. }
  614.  
  615. function getChessVariant() {
  616. const pathname = window.location.pathname;
  617.  
  618. switch(domain) {
  619. case 'chess.com': {
  620. if(pathname?.includes('/variants')) {
  621. const variant = pathname.match(/variants\/([^\/]*)/)?.[1]
  622. .replaceAll('-chess', '')
  623. .replaceAll('-', '');
  624.  
  625. const replacementTable = {
  626. 'doubles-bughouse': 'bughouse',
  627. 'paradigm-chess30': 'paradigm'
  628. };
  629.  
  630. return replacementTable[variant] || variant;
  631. }
  632.  
  633. break;
  634. }
  635.  
  636. case 'lichess.org': {
  637. const variantLinkElem = document.querySelector('.variant-link');
  638.  
  639. if(variantLinkElem) {
  640. let variant = variantLinkElem?.innerText?.toLowerCase()?.replaceAll(' ', '-');
  641.  
  642. const replacementTable = {
  643. 'correspondence': 'chess',
  644. 'koth': 'kingofthehill',
  645. 'three-check': '3check'
  646. };
  647.  
  648. return replacementTable[variant] || variant;
  649. }
  650.  
  651. break;
  652. }
  653.  
  654. case 'playstrategy.org': {
  655. const variantLinkElem = document.querySelector('.variant-link');
  656.  
  657. if(variantLinkElem) {
  658. let variant = variantLinkElem?.innerText
  659. ?.toLowerCase()
  660. ?.replaceAll(' ', '-');
  661.  
  662. const replacementTable = {
  663. 'correspondence': 'chess',
  664. 'koth': 'kingofthehill',
  665. 'three-check': '3check',
  666. 'five-check': '5check',
  667. 'no-castling': 'nocastle'
  668. };
  669.  
  670. return replacementTable[variant] || variant;
  671. }
  672.  
  673. break;
  674. }
  675.  
  676. case 'pychess.org': {
  677. const variantLinkElem = document.querySelector('#main-wrap .tc .user-link');
  678.  
  679. if(variantLinkElem) {
  680. let variant = variantLinkElem?.innerText
  681. ?.toLowerCase()
  682. ?.replaceAll(' ', '')
  683. ?.replaceAll('-', '');
  684.  
  685. const replacementTable = {
  686. 'correspondence': 'chess',
  687. 'koth': 'kingofthehill',
  688. 'nocastling': 'nocastle',
  689. 'gorogoro+': 'gorogoro',
  690. 'oukchaktrang': 'cambodian'
  691. };
  692.  
  693. return replacementTable[variant] || variant;
  694. }
  695.  
  696. break;
  697. }
  698.  
  699. case 'chess.org': {
  700. const variantNum = unsafeWindow?.GameConfig?.instance?.variant;
  701. let variant = GameConfig?.VARIANT_NAMES?.[variantNum]?.toLowerCase();
  702.  
  703. if(variant) {
  704. const replacementTable = {
  705. 'standard': 'chess'
  706. };
  707.  
  708. return replacementTable[variant] || variant;
  709. }
  710.  
  711. break;
  712. }
  713.  
  714. case 'papergames.io': {
  715. return 'chess';
  716. }
  717.  
  718. case 'vole.wtf': {
  719. return 'chess';
  720. }
  721. }
  722.  
  723. return 'chess';
  724. }
  725.  
  726. function getBoardOrientation() {
  727. const pathname = window.location.pathname;
  728.  
  729. switch(domain) {
  730. case 'chess.com': {
  731. if(pathname?.includes('/variants')) {
  732. const playerNumberStr = document.querySelector('.playerbox-bottom [data-player]')?.dataset?.player;
  733.  
  734. return playerNumberStr == '0' ? 'w' : 'b';
  735. }
  736.  
  737. const boardElem = getBoardElem();
  738.  
  739. return boardElem?.classList.contains('flipped') ? 'b' : 'w';
  740. }
  741.  
  742. case 'lichess.org': {
  743. const filesElem = document.querySelector('coords.files');
  744.  
  745. return filesElem?.classList?.contains('black') ? 'b' : 'w';
  746. }
  747.  
  748. case 'playstrategy.org': {
  749. const cgWrapElem = document.querySelector('.cg-wrap');
  750.  
  751. return cgWrapElem.classList?.contains('orientation-p1') ? 'w' : 'b';
  752. }
  753.  
  754. case 'pychess.org': {
  755. const cgWrapElem = document.querySelector('.cg-wrap');
  756.  
  757. return cgWrapElem.classList?.contains('orientation-black') ? 'b' : 'w';
  758. }
  759.  
  760. case 'chess.org': {
  761. const filesElem = document.querySelector('coords.files');
  762.  
  763. return filesElem?.classList?.contains('black') ? 'b' : 'w';
  764. }
  765.  
  766. case 'papergames.io': {
  767. const boardElem = getBoardElem();
  768.  
  769. if(boardElem) {
  770. const firstRankText = [...boardElem.querySelector('.coordinates').childNodes]?.[0].textContent;
  771.  
  772. return firstRankText == 'h' ? 'b' : 'w';
  773. }
  774. }
  775.  
  776. case 'vole.wtf': {
  777. return 'w';
  778. }
  779. }
  780.  
  781. return null;
  782. }
  783.  
  784. function getPieceElemFen(pieceElem) {
  785. const pathname = window.location.pathname;
  786.  
  787. const pieceNameToFen = {
  788. 'pawn': 'p',
  789. 'knight': 'n',
  790. 'bishop': 'b',
  791. 'rook': 'r',
  792. 'queen': 'q',
  793. 'king': 'k'
  794. };
  795.  
  796. switch(domain) {
  797. case 'chess.com': {
  798. let pieceColor = null;
  799. let pieceName = null;
  800.  
  801. if(pathname?.includes('/variants')) {
  802. pieceColor = pieceElem?.dataset?.player == '0' ? 'w' : 'b';
  803. pieceName = pieceElem?.dataset?.piece;
  804. } else {
  805. const pieceStr = [...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/));
  806.  
  807. [pieceColor, pieceName] = pieceStr.split('');
  808. }
  809.  
  810. return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
  811. }
  812.  
  813. case 'lichess.org': {
  814. const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
  815. const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
  816.  
  817. if(pieceColor && elemPieceName) {
  818. const pieceName = pieceNameToFen[elemPieceName];
  819.  
  820. return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
  821. }
  822.  
  823. break;
  824. }
  825.  
  826. case 'playstrategy.org': {
  827. const playerColor = getPlayerColorVariable();
  828. const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w');
  829.  
  830. let pieceName = null;
  831.  
  832. [...pieceElem?.classList]?.forEach(className => {
  833. if(className?.includes('-piece')) {
  834. const elemPieceName = className?.split('-piece')?.[0];
  835.  
  836. if(elemPieceName && elemPieceName?.length === 1) {
  837. pieceName = elemPieceName;
  838. }
  839. }
  840. });
  841.  
  842. if(pieceColor && pieceName) {
  843. return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
  844. }
  845.  
  846. break;
  847. }
  848.  
  849. case 'pychess.org': {
  850. const playerColor = getPlayerColorVariable();
  851. const pieceColor = pieceElem?.classList?.contains('ally') ? playerColor : (playerColor == 'w' ? 'b' : 'w');
  852.  
  853. let pieceName = null;
  854.  
  855. [...pieceElem?.classList]?.forEach(className => {
  856. if(className?.includes('-piece')) {
  857. const elemPieceName = className?.split('-piece')?.[0];
  858.  
  859. if(elemPieceName && elemPieceName?.length === 1) {
  860. pieceName = elemPieceName;
  861. }
  862. }
  863. });
  864.  
  865. if(pieceColor && pieceName) {
  866. return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
  867. }
  868.  
  869. break;
  870. }
  871.  
  872. case 'chess.org': {
  873. const pieceColor = pieceElem?.classList?.contains('white') ? 'w' : 'b';
  874. const elemPieceName = [...pieceElem?.classList]?.find(className => Object.keys(pieceNameToFen).includes(className));
  875.  
  876. if(pieceColor && elemPieceName) {
  877. const pieceName = pieceNameToFen[elemPieceName];
  878.  
  879. return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
  880. }
  881.  
  882. break;
  883. }
  884.  
  885. case 'papergames.io': {
  886. return convertPieceStrToFen(pieceElem?.dataset?.piece);
  887. }
  888.  
  889. case 'vole.wtf': {
  890. const pieceNum = Number(pieceElem?.dataset?.p);
  891. const pieceFenStr = 'pknbrq';
  892.  
  893. if(pieceNum > 8) {
  894. return pieceFenStr[pieceNum - 9].toUpperCase();
  895. } else {
  896. return pieceFenStr[pieceNum - 1];
  897. }
  898. }
  899. }
  900.  
  901. return null;
  902. }
  903.  
  904.  
  905. // this function gets called a lot, needs to be optimized
  906. function getPieceElemCoords(pieceElem) {
  907. const pathname = window.location.pathname;
  908.  
  909. switch(domain) {
  910. case 'chess.com': {
  911. if(pathname?.includes('/variants')) {
  912. const squareElem = pieceElem.parentElement;
  913. const squareId = squareElem.id;
  914.  
  915. if(!chesscomVariantBoardCoordsTable) {
  916. createChesscomVariantBoardCoordsTable();
  917. }
  918.  
  919. return chesscomVariantBoardCoordsTable[squareId];
  920. }
  921.  
  922. return pieceElem.classList.toString()
  923. ?.match(/square-(\d)(\d)/)
  924. ?.slice(1)
  925. ?.map(x => Number(x) - 1);
  926. }
  927.  
  928. case 'lichess.org': {
  929. const key = pieceElem?.cgKey;
  930.  
  931. if(!key) break;
  932.  
  933. return chessCoordinatesToIndex(key);
  934. }
  935.  
  936. case 'playstrategy.org': {
  937. const key = pieceElem?.cgKey;
  938.  
  939. if(!key) break;
  940.  
  941. return chessCoordinatesToIndex(key);
  942. }
  943.  
  944. case 'pychess.org': {
  945. const key = pieceElem?.cgKey;
  946.  
  947. if(!key) break;
  948.  
  949. return chessCoordinatesToIndex(key);
  950. }
  951.  
  952. case 'chess.org': {
  953. return getElemCoordinatesFromTransform(pieceElem);
  954. }
  955.  
  956. case 'papergames.io': {
  957. const key = pieceElem?.dataset?.square;
  958.  
  959. if(!key) break;
  960.  
  961. return chessCoordinatesToIndex(key);
  962. }
  963.  
  964. case 'vole.wtf': {
  965. return [Number(pieceElem?.dataset?.l), 7 - Number(pieceElem?.dataset?.t)];
  966. }
  967. }
  968.  
  969. return null;
  970. }
  971.  
  972. function getBoardDimensions() {
  973. const pathname = window.location.pathname;
  974.  
  975. switch(domain) {
  976. case 'chess.com': {
  977. if(pathname?.includes('/variants')) {
  978. const rankElems = chessBoardElem?.querySelectorAll('.rank');
  979. const visibleRankElems = filterInvisibleElems(rankElems)
  980. .filter(rankElem => [...rankElem.childNodes]
  981. .find(elem => {
  982. const pieceElem = elem.querySelector('[data-player]');
  983. const playerNum = Number(pieceElem?.dataset?.player);
  984.  
  985. return playerNum <= 2;
  986. }));
  987.  
  988.  
  989. if(visibleRankElems.length) {
  990. const rankElem = visibleRankElems[0];
  991. const squareElems = getSquareElems(rankElem);
  992.  
  993. const ranks = visibleRankElems?.length;
  994. const files = squareElems?.length;
  995.  
  996. return [ranks, files];
  997. }
  998. }
  999.  
  1000. break;
  1001. }
  1002.  
  1003. default: {
  1004. if(domainsWithoutDifferentBoardDimensionsArr?.includes(domain)) break;
  1005.  
  1006. const boardDimensions = getElementSize(chessBoardElem);
  1007.  
  1008. lastBoardSize = getElementSize(chessBoardElem);
  1009.  
  1010. const boardWidth = boardDimensions?.width;
  1011. const boardHeight = boardDimensions.height;
  1012.  
  1013. const boardPiece = getChessPieceElem();
  1014.  
  1015. if(boardPiece) {
  1016. const pieceDimensions = getElementSize(boardPiece);
  1017.  
  1018. lastPieceSize = getElementSize(boardPiece);
  1019.  
  1020. const boardPieceWidth = pieceDimensions?.width;
  1021. const boardPieceHeight = pieceDimensions?.height;
  1022.  
  1023. const boardRanks = Math.floor(boardWidth / boardPieceWidth);
  1024. const boardFiles = Math.floor(boardHeight / boardPieceHeight);
  1025.  
  1026. const ranksInAllowedRange = 0 < boardRanks && boardRanks <= 69;
  1027. const filesInAllowedRange = 0 < boardFiles && boardFiles <= 69;
  1028.  
  1029. if(ranksInAllowedRange && filesInAllowedRange) {
  1030. lastBoardRanks = boardRanks;
  1031. lastBoardFiles = boardFiles;
  1032.  
  1033. return [boardRanks, boardFiles];
  1034. }
  1035. }
  1036.  
  1037. break;
  1038. }
  1039. }
  1040.  
  1041. lastBoardRanks = 8;
  1042. lastBoardFiles = 8;
  1043.  
  1044. return [8, 8];
  1045. }
  1046.  
  1047. function getFen(onlyBasic) {
  1048. const [boardRanks, boardFiles] = getBoardDimensions();
  1049.  
  1050. if(debugModeActivated) console.warn('getFen()', 'onlyBasic:', onlyBasic, 'Ranks:', boardRanks, 'Files:', boardFiles);
  1051.  
  1052. const board = Array.from({ length: boardFiles }, () => Array(boardRanks).fill(1));
  1053.  
  1054. function getBasicFen() {
  1055. const pieceElems = getChessPieceElem(true);
  1056. const isValidPieceElemsArray = Array.isArray(pieceElems) || pieceElems instanceof NodeList;
  1057.  
  1058. if(isValidPieceElemsArray) {
  1059. pieceElems.forEach(pieceElem => {
  1060. const pieceFenCode = getPieceElemFen(pieceElem);
  1061. const pieceCoordsArr = getPieceElemCoords(pieceElem);
  1062.  
  1063. if(debugModeActivated) console.warn('pieceElem', pieceElem, 'pieceFenCode', pieceFenCode, 'pieceCoordsArr', pieceCoordsArr);
  1064.  
  1065. try {
  1066. const [xIdx, yIdx] = pieceCoordsArr;
  1067.  
  1068. board[boardFiles - (yIdx + 1)][xIdx] = pieceFenCode;
  1069. } catch(e) {
  1070. if(debugModeActivated) console.error(e);
  1071. }
  1072. });
  1073. }
  1074.  
  1075. return squeezeEmptySquares(board.map(x => x.join('')).join('/'));
  1076. }
  1077.  
  1078. const basicFen = getBasicFen();
  1079.  
  1080. if(onlyBasic) {
  1081. return basicFen;
  1082. }
  1083.  
  1084. return `${basicFen} ${getPlayerColorVariable()} - - - -`;
  1085. }
  1086.  
  1087. const boardUtils = {
  1088. markMove: moveObj => {
  1089. if(!getConfigValue(configKeys.displayMovesOnExternalSite)) return;
  1090.  
  1091. const [from, to] = moveObj.player;
  1092. const [opponentFrom, opponentTo] = moveObj.opponent;
  1093. const ranking = moveObj.ranking;
  1094.  
  1095. const existingExactSameMoveObj = activeSiteMoveHighlights.find(obj => {
  1096. const [activeFrom, activeTo] = obj.player;
  1097. const [activeOpponentFrom, activeOpponentTo] = obj.opponent;
  1098.  
  1099. return from == activeFrom
  1100. && to == activeTo
  1101. && opponentFrom == activeOpponentFrom
  1102. && opponentTo == activeOpponentTo;
  1103. });
  1104.  
  1105. activeSiteMoveHighlights.map(obj => {
  1106. const [activeFrom, activeTo] = obj.player;
  1107.  
  1108. const existingSameMoveObj = from == activeFrom && to == activeTo;
  1109.  
  1110. if(existingSameMoveObj) {
  1111. obj.promotedRanking = 1;
  1112. }
  1113.  
  1114. return obj;
  1115. });
  1116.  
  1117. const exactSameMoveDoesNotExist = typeof existingExactSameMoveObj !== 'object';
  1118.  
  1119. if(exactSameMoveDoesNotExist) {
  1120.  
  1121. const showOpponentMoveGuess = getConfigValue(configKeys.showOpponentMoveGuess);
  1122.  
  1123. const opponentMoveGuessExists = typeof opponentFrom == 'string';
  1124.  
  1125. const arrowStyle = ranking == 1 ? arrowStyles.best : arrowStyles.secondary;
  1126.  
  1127. let opponentArrowElem = null;
  1128.  
  1129. // create player move arrow element
  1130. const arrowElem = BoardDrawer.createShape('arrow', [from, to],
  1131. { style: arrowStyle }
  1132. );
  1133.  
  1134. // create opponent move arrow element
  1135. if(opponentMoveGuessExists && showOpponentMoveGuess) {
  1136. opponentArrowElem = BoardDrawer.createShape('arrow', [opponentFrom, opponentTo],
  1137. { style: arrowStyles.opponent }
  1138. );
  1139.  
  1140. const squareListener = BoardDrawer.addSquareListener(from, type => {
  1141. if(!opponentArrowElem) {
  1142. squareListener.remove();
  1143. }
  1144.  
  1145. switch(type) {
  1146. case 'enter':
  1147. opponentArrowElem.style.display = 'inherit';
  1148. break;
  1149. case 'leave':
  1150. opponentArrowElem.style.display = 'none';
  1151. break;
  1152. }
  1153. });
  1154. }
  1155.  
  1156. activeSiteMoveHighlights.push({
  1157. ...moveObj,
  1158. 'opponentArrowElem': opponentArrowElem,
  1159. 'playerArrowElem': arrowElem
  1160. });
  1161. }
  1162.  
  1163. boardUtils.removeOldMarkings();
  1164. boardUtils.paintMarkings();
  1165. },
  1166. removeOldMarkings: () => {
  1167. const markingLimit = getConfigValue(configKeys.moveSuggestionAmount);
  1168. const showGhost = getConfigValue(configKeys.showMoveGhost);
  1169.  
  1170. const exceededMarkingLimit = activeSiteMoveHighlights.length > markingLimit;
  1171.  
  1172. if(exceededMarkingLimit) {
  1173. const amountToRemove = activeSiteMoveHighlights.length - markingLimit;
  1174.  
  1175. for(let i = 0; i < amountToRemove; i++) {
  1176. const oldestMarkingObj = activeSiteMoveHighlights[0];
  1177.  
  1178. activeSiteMoveHighlights = activeSiteMoveHighlights.slice(1);
  1179.  
  1180. if(oldestMarkingObj?.playerArrowElem?.style) {
  1181. oldestMarkingObj.playerArrowElem.style.fill = 'grey';
  1182. oldestMarkingObj.playerArrowElem.style.opacity = '0';
  1183. oldestMarkingObj.playerArrowElem.style.transition = 'opacity 2.5s ease-in-out';
  1184. }
  1185.  
  1186. if(oldestMarkingObj?.opponentArrowElem?.style) {
  1187. oldestMarkingObj.opponentArrowElem.style.fill = 'grey';
  1188. oldestMarkingObj.opponentArrowElem.style.opacity = '0';
  1189. oldestMarkingObj.opponentArrowElem.style.transition = 'opacity 2.5s ease-in-out';
  1190. }
  1191.  
  1192. if(showGhost) {
  1193. inactiveGuiMoveMarkings.push(oldestMarkingObj);
  1194. } else {
  1195. oldestMarkingObj.playerArrowElem?.remove();
  1196. oldestMarkingObj.opponentArrowElem?.remove();
  1197. }
  1198. }
  1199. }
  1200.  
  1201. if(showGhost) {
  1202. inactiveGuiMoveMarkings.forEach(markingObj => {
  1203. const activeDuplicateArrow = activeSiteMoveHighlights.find(x => {
  1204. const samePlayerArrow = x.player?.toString() == markingObj.player?.toString();
  1205. const sameOpponentArrow = x.opponent?.toString() == markingObj.opponent?.toString();
  1206.  
  1207. return samePlayerArrow && sameOpponentArrow;
  1208. });
  1209.  
  1210. const duplicateExists = activeDuplicateArrow ? true : false;
  1211.  
  1212. const removeArrows = () => {
  1213. inactiveGuiMoveMarkings = inactiveGuiMoveMarkings.filter(x => x.playerArrowElem != markingObj.playerArrowElem);
  1214.  
  1215. markingObj.playerArrowElem?.remove();
  1216. markingObj.opponentArrowElem?.remove();
  1217. }
  1218.  
  1219. if(duplicateExists) {
  1220. removeArrows();
  1221. } else {
  1222. setTimeout(removeArrows, 2500);
  1223. }
  1224. });
  1225. }
  1226. },
  1227. paintMarkings: () => {
  1228. const newestBestMarkingIndex = activeSiteMoveHighlights.findLastIndex(obj => obj.ranking == 1);
  1229. const newestPromotedBestMarkingIndex = activeSiteMoveHighlights.findLastIndex(obj => obj?.promotedRanking == 1);
  1230. const lastMarkingIndex = activeSiteMoveHighlights.length - 1;
  1231.  
  1232. const isLastMarkingBest = newestBestMarkingIndex == -1 && newestPromotedBestMarkingIndex == -1;
  1233. const bestIndex = isLastMarkingBest ? lastMarkingIndex : Math.max(...[newestBestMarkingIndex, newestPromotedBestMarkingIndex]);
  1234.  
  1235. let bestMoveMarked = false;
  1236.  
  1237. activeSiteMoveHighlights.forEach((markingObj, idx) => {
  1238. const isBestMarking = idx == bestIndex;
  1239.  
  1240. if(isBestMarking) {
  1241. markingObj.playerArrowElem.style.cssText = arrowStyles.best;
  1242.  
  1243. const playerArrowElem = markingObj.playerArrowElem
  1244. const opponentArrowElem = markingObj.opponentArrowElem;
  1245.  
  1246. // move best arrow element on top (multiple same moves can hide the best move)
  1247. const parentElem = markingObj.playerArrowElem.parentElement;
  1248.  
  1249. parentElem.appendChild(playerArrowElem);
  1250.  
  1251. if(opponentArrowElem) {
  1252. parentElem.appendChild(opponentArrowElem);
  1253. }
  1254.  
  1255. bestMoveMarked = true;
  1256. } else {
  1257. markingObj.playerArrowElem.style.cssText = arrowStyles.secondary;
  1258. }
  1259. });
  1260. },
  1261. removeBestMarkings: () => {
  1262. activeSiteMoveHighlights.forEach(markingObj => {
  1263. markingObj.opponentArrowElem?.remove();
  1264. markingObj.playerArrowElem?.remove();
  1265. });
  1266.  
  1267. activeSiteMoveHighlights = [];
  1268. },
  1269. setBoardOrientation: orientation => {
  1270. if(BoardDrawer) {
  1271. if(debugModeActivated) console.warn('setBoardOrientation', orientation);
  1272.  
  1273. BoardDrawer.setOrientation(orientation);
  1274. }
  1275. },
  1276. setBoardDimensions: dimensionArr => {
  1277. if(BoardDrawer) {
  1278. if(debugModeActivated) console.warn('setBoardDimensions', dimensionArr);
  1279.  
  1280. BoardDrawer.setBoardDimensions(dimensionArr);
  1281. }
  1282. }
  1283. };
  1284.  
  1285. function onNewMove(mutationArr) {
  1286. if(debugModeActivated) console.warn('NEW MOVE DETECTED!');
  1287.  
  1288. chesscomVariantBoardCoordsTable = null;
  1289.  
  1290. boardUtils.setBoardDimensions(getBoardDimensions());
  1291.  
  1292. const lastPlayerColor = getPlayerColorVariable();
  1293.  
  1294. updatePlayerColor();
  1295.  
  1296. const playerColor = getPlayerColorVariable();
  1297. const orientationChanged = playerColor != lastPlayerColor;
  1298.  
  1299. if(orientationChanged) {
  1300. CommLink.commands.log(`Player color (e.g. board orientation) changed from ${lastPlayerColor} to ${playerColor}!`);
  1301.  
  1302. chesscomVariantBoardCoordsTable = null;
  1303.  
  1304. instanceVars.turn.set(commLinkInstanceID, playerColor);
  1305.  
  1306. CommLink.commands.log(`Turn updated to ${playerColor}!`);
  1307. }
  1308.  
  1309. const currentFullFen = getFen();
  1310. const currentBasicFen = currentFullFen?.split(' ')?.[0];
  1311.  
  1312. const fenChanged = currentBasicFen != lastBasicFen;
  1313.  
  1314. if(fenChanged) {
  1315. lastBasicFen = currentBasicFen;
  1316.  
  1317. boardUtils.removeBestMarkings();
  1318.  
  1319. /*
  1320. if(!orientationChanged) {
  1321. const allChessPieceElems = getChessPieceElem(true);
  1322.  
  1323. const attributeMutationArr = mutationArr.filter(m => allChessPieceElems.includes(m.target));
  1324. const movedChessPieceElem = attributeMutationArr?.[0]?.target; // doesn't work for chess.com variants
  1325.  
  1326. if(movedChessPieceElem) {
  1327. const newTurn = getFenPieceOppositeColor(getPieceElemFen(movedChessPieceElem));
  1328.  
  1329. if(newTurn?.length === 1) {
  1330. instanceVars.turn.set(commLinkInstanceID, newTurn);
  1331.  
  1332. CommLink.commands.log(`Turn updated to ${newTurn}!`);
  1333. }
  1334. }
  1335. }
  1336. */
  1337.  
  1338. instanceVars.fen.set(commLinkInstanceID, currentFullFen);
  1339.  
  1340. CommLink.commands.updateBoardFen(currentFullFen);
  1341. CommLink.commands.calculateBestMoves(currentFullFen);
  1342. }
  1343. }
  1344.  
  1345. function observeNewMoves() {
  1346. let lastProcessedFen = null;
  1347.  
  1348. const boardObserver = new MutationObserver(mutationArr => {
  1349. if(debugModeActivated) console.log(mutationArr);
  1350.  
  1351. if(isMutationNewMove(mutationArr))
  1352. {
  1353. if(debugModeActivated) console.warn('Mutation is a new move:', mutationArr);
  1354.  
  1355. if(domain === 'chess.org')
  1356. {
  1357. setTimeout(() => onNewMove(mutationArr), 250);
  1358. }
  1359. else
  1360. {
  1361. onNewMove(mutationArr);
  1362. }
  1363. }
  1364. });
  1365.  
  1366. boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true });
  1367. }
  1368.  
  1369. async function updatePlayerColor() {
  1370. const boardOrientation = getBoardOrientation();
  1371.  
  1372. const boardOrientationChanged = lastBoardOrientation !== boardOrientation;
  1373. const boardOrientationDiffers = BoardDrawer && BoardDrawer?.orientation !== boardOrientation;
  1374.  
  1375. if(boardOrientationChanged || boardOrientationDiffers) {
  1376. lastBoardOrientation = boardOrientation;
  1377.  
  1378. instanceVars.playerColor.set(commLinkInstanceID, boardOrientation);
  1379. instanceVars.turn.set(commLinkInstanceID, boardOrientation);
  1380.  
  1381. boardUtils.setBoardOrientation(boardOrientation);
  1382.  
  1383. await CommLink.commands.updateBoardOrientation(boardOrientation);
  1384. }
  1385. }
  1386.  
  1387. async function isAcasBackendReady() {
  1388. const res = await CommLink.commands.ping();
  1389.  
  1390. return res ? true : false;
  1391. }
  1392.  
  1393. async function start() {
  1394. await CommLink.commands.createInstance(commLinkInstanceID);
  1395.  
  1396. const pathname = window.location.pathname;
  1397. const adjustSizeByDimensions = domain === 'chess.com' && pathname?.includes('/variants');
  1398.  
  1399. const boardOrientation = getBoardOrientation();
  1400.  
  1401. instanceVars.playerColor.set(commLinkInstanceID, boardOrientation);
  1402. instanceVars.turn.set(commLinkInstanceID, boardOrientation);
  1403. instanceVars.fen.set(commLinkInstanceID, getFen());
  1404.  
  1405. if(getConfigValue(configKeys.displayMovesOnExternalSite)) {
  1406. BoardDrawer = new UniversalBoardDrawer(chessBoardElem, {
  1407. 'window': window,
  1408. 'boardDimensions': getBoardDimensions(),
  1409. 'playerColor': getPlayerColorVariable(),
  1410. 'zIndex': 500,
  1411. 'prepend': true,
  1412. 'debugMode': debugModeActivated,
  1413. 'adjustSizeByDimensions': adjustSizeByDimensions ? true : false,
  1414. 'adjustSizeConfig': {
  1415. 'noLeftAdjustment': true
  1416. }
  1417. });
  1418. }
  1419.  
  1420. await updatePlayerColor();
  1421.  
  1422. observeNewMoves();
  1423.  
  1424. CommLink.setIntervalAsync(async () => {
  1425. await CommLink.commands.createInstance(commLinkInstanceID);
  1426. }, 1000);
  1427. }
  1428.  
  1429. function startWhenBackendReady() {
  1430. const interval = CommLink.setIntervalAsync(async () => {
  1431. if(await isAcasBackendReady()) {
  1432. start();
  1433.  
  1434. interval.stop();
  1435. } else {
  1436. GM_openInTab(backendURL, true);
  1437.  
  1438. if(await isAcasBackendReady()) {
  1439. start();
  1440.  
  1441. interval.stop();
  1442. }
  1443. }
  1444. }, 1000);
  1445. }
  1446.  
  1447. function initializeIfSiteReady() {
  1448. const boardElem = getBoardElem();
  1449. const firstPieceElem = getChessPieceElem();
  1450.  
  1451. const bothElemsExist = boardElem && firstPieceElem;
  1452. const boardElemChanged = chessBoardElem != boardElem;
  1453.  
  1454. if(bothElemsExist && boardElemChanged) {
  1455. chessBoardElem = boardElem;
  1456.  
  1457. if(!blacklistedURLs.includes(window.location.href)) {
  1458. startWhenBackendReady();
  1459. }
  1460. }
  1461. }
  1462.  
  1463. if(typeof GM_registerMenuCommand == 'function') {
  1464. GM_registerMenuCommand('Open A.C.A.S', e => {
  1465. if(chessBoardElem) {
  1466. startWhenBackendReady();
  1467. }
  1468. }, 's');
  1469. }
  1470.  
  1471. setInterval(initializeIfSiteReady, 1000);

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址