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

Chess analysis bot made for educational purposes only (Chrome + Firefox + Edge ...)

当前为 2023-03-03 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name C.A.S (Chess.com Assistance System)
  3. // @namespace sayfpack
  4. // @author sayfpack
  5. // @version 5.5
  6. // @homepageURL https://github.com/sayfpack13/chess-analysis-bot
  7. // @supportURL https://mmgc.life/
  8. // @match https://www.chess.com/*
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_getResourceText
  13. // @grant GM_registerMenuCommand
  14. // @description Chess analysis bot made for educational purposes only (Chrome + Firefox + Edge ...)
  15. // @require https://greasyfork.org/scripts/460400-usergui-js/code/userguijs.js?version=1152084
  16. // @resource jquery.js https://cdn.jsdelivr.net/npm/jquery@3.6.3/dist/jquery.min.js
  17. // @resource chessboard.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/chessboard.js
  18. // @resource chessboard.css https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/chessboard.css
  19. // @resource lozza.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/lozza.js
  20. // @resource stockfish.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/stockfish-5.js
  21. // @resource stockfish2.js https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script/content/stockfish-2018.js
  22. // @run-at document-start
  23. // @inject-into content
  24. // ==/UserScript==
  25.  
  26. /*
  27. e88~-_ e ,d88~~\
  28. d888 \ d8b 8888
  29. 8888 /Y88b `Y88b
  30. 8888 / Y88b `Y88b,
  31. Y888 / d88b /____Y88b d88b 8888
  32. "88_-~ Y88P / Y88b Y88P \__88P'
  33. */
  34.  
  35. // VARS
  36. const repositoryRawURL = 'https://raw.githubusercontent.com/sayfpack13/chess-analysis-bot/main/tampermonkey%20script';
  37. const LICHESS_API = "https://lichess.org/api/cloud-eval";
  38.  
  39. const MAX_DEPTH = 20;
  40. const MIN_DEPTH = 1;
  41. const MAX_MOVETIME = 3000;
  42. const MIN_MOVETIME = 50;
  43. const MAX_ELO = 4000;
  44. const DEPTH_MODE = 0;
  45. const MOVETIME_MODE = 1;
  46. const rank = ["Beginner", "Intermediate", "Advanced", "Expert", "Master", "Grand Master", "Cheater"];
  47.  
  48.  
  49.  
  50. let engineMode = 0; // engine mode (0:depth / 1:movetime)
  51. let engineIndex = 0; // engine index (lozza => 0, stockfish => 1...)
  52. let reload_every = 10; // reload engine after x moves
  53. let enableUserLog = true; // enable engine log
  54. let displayMovesOnSite = true; // display moves on chess board
  55. let show_opposite_moves = false; // show opponent best moves if available
  56. let use_book_moves = false; // use lichess api to get book moves
  57. let node_engine_url = "http://localhost:5000"; // node server api url
  58. let node_engine_name = "stockfish-15.exe" // default engine name (node server engine only)
  59.  
  60.  
  61.  
  62.  
  63. let current_depth = Math.round(MAX_DEPTH / 3);
  64. let current_movetime = Math.round(MAX_MOVETIME / 3);
  65. let node_engine_id = 3;
  66.  
  67. let Gui;
  68. let closedGui = false;
  69. let reload_count = 1;
  70.  
  71.  
  72.  
  73.  
  74.  
  75.  
  76. let Interface = null;
  77. let LozzaUtils = null;
  78.  
  79. let initialized = false;
  80. let firstMoveMade = false;
  81.  
  82.  
  83. let engine = null;
  84. let engineObjectURL = null;
  85. let lastEngine = engineIndex;
  86.  
  87. let chessBoardElem = null;
  88. let turn = '-';
  89. let playerColor = null;
  90. let isPlayerTurn = null;
  91. let lastFen = null;
  92.  
  93. let uiChessBoard = null;
  94.  
  95. let activeGuiMoveHighlights = [];
  96. let activeSiteMoveHighlights = [];
  97.  
  98. let engineLogNum = 1;
  99. let userscriptLogNum = 1;
  100. let enemyScore = 0;
  101. let myScore = 0;
  102.  
  103.  
  104. function moveResult(from, to, power, clear = true) {
  105.  
  106. if (from.length < 2 || to.length < 2) {
  107. return;
  108. }
  109.  
  110. if (clear) {
  111. clearBoard();
  112. }
  113.  
  114.  
  115.  
  116. if (isPlayerTurn) // my turn
  117. myScore = myScore + Number(power);
  118. else
  119. enemyScore = enemyScore + Number(power);
  120.  
  121. Interface.boardUtils.updateBoardPower(myScore, enemyScore);
  122.  
  123.  
  124.  
  125. if (displayMovesOnSite || (!isPlayerTurn && show_opposite_moves)) {
  126. markMoveToSite(from, to);
  127. }
  128.  
  129.  
  130.  
  131. Interface.boardUtils.markMove(from, to);
  132. Interface.stopBestMoveProcessingAnimation();
  133.  
  134.  
  135. }
  136.  
  137.  
  138. function getBookMoves(fen) {
  139. GM_xmlhttpRequest({
  140. method: "GET",
  141. url: LICHESS_API + "?fen=" + fen + "&multiPv=1&variant=fromPosition",
  142. headers: {
  143. "Content-Type": "application/json"
  144. },
  145. onload: function (response) {
  146. if (response.response.includes("error")) {
  147.  
  148. getBestMoves(fen);
  149.  
  150. } else {
  151. let data = JSON.parse(response.response);
  152. let nextMove = data.pvs[0].moves.split(' ')[0];
  153. let score = current_depth;
  154.  
  155. moveResult(nextMove.slice(0, 2), nextMove.slice(2, 4), score, true);
  156. }
  157.  
  158.  
  159. }, onerror: function (error) {
  160. getBestMoves(fen);
  161.  
  162. }
  163. });
  164.  
  165. }
  166.  
  167. function getNodeBestMoves(fen) {
  168.  
  169.  
  170. GM_xmlhttpRequest({
  171. method: "GET",
  172. url: node_engine_url + "/getBestMove?fen=" + fen + "&engine_mode=" + engineMode + "&depth=" + current_depth + "&movetime=" + current_movetime + "&turn=" + turn + "&engine_name=" + node_engine_name,
  173. headers: {
  174. "Content-Type": "application/json"
  175. },
  176. onload: function (response) {
  177. if (response.response == "false") {
  178. return;
  179. }
  180. let data = JSON.parse(response.response);
  181. let server_fen = data.fen;
  182. let depth = data.depth;
  183. let movetime = data.movetime;
  184. let power = data.score;
  185. let move = data.move;
  186.  
  187.  
  188.  
  189. if (engineMode == DEPTH_MODE) {
  190. Interface.updateBestMoveProgress(`Depth: ${depth}`);
  191. } else {
  192. Interface.updateBestMoveProgress(`Move time: ${movetime} ms`);
  193. }
  194.  
  195. moveResult(move.slice(0, 2), move.slice(2, 4), power, true);
  196.  
  197.  
  198.  
  199. }, onerror: function () {
  200. Interface.log("check node server !!");
  201. }
  202. });
  203.  
  204. }
  205.  
  206. function getElo() {
  207. let elo;
  208. if (engineMode == DEPTH_MODE) {
  209. elo = MAX_ELO / MAX_DEPTH;
  210. elo *= current_depth;
  211. } else {
  212. elo = MAX_ELO / MAX_MOVETIME;
  213. elo *= current_movetime;
  214. }
  215. return elo;
  216. }
  217.  
  218. function getRank() {
  219. let part;
  220. if (engineMode == DEPTH_MODE) {
  221. part = current_depth / (MAX_DEPTH / rank.length);
  222. } else {
  223. part = current_movetime / (MAX_MOVETIME / rank.length);
  224. }
  225. part = Math.round(part);
  226.  
  227. if (part >= rank.length) {
  228. part = rank.length - 1;
  229. }
  230.  
  231. return rank[part];
  232. }
  233.  
  234.  
  235.  
  236.  
  237.  
  238.  
  239. function getEloDescription() {
  240. let desc = `Elo: ${getElo()}, Rank: ${getRank()}, `;
  241. if (engineMode == DEPTH_MODE) {
  242. desc += `Depth: ${current_depth}`;
  243. } else {
  244. desc += `Move Time: ${current_movetime} ms`;
  245. }
  246. return desc;
  247. }
  248.  
  249. function isNotCompatibleBrowser() {
  250. return navigator.userAgent.toLowerCase().includes("firefox")
  251. }
  252.  
  253. onload = function () {
  254. if (isNotCompatibleBrowser()) {
  255. Gui = new UserGui;
  256. }
  257.  
  258. }
  259.  
  260. if (!isNotCompatibleBrowser()) {
  261. Gui = new UserGui;
  262. } else {
  263. onload();
  264. }
  265.  
  266.  
  267. Gui.settings.window.title = 'C.A.S';
  268. Gui.settings.window.external = true;
  269. Gui.settings.window.size.width = 500;
  270. Gui.settings.gui.external.popup = false;
  271. Gui.settings.gui.external.style += GM_getResourceText('chessboard.css');
  272. Gui.settings.gui.external.style += `
  273. div[class^='board'] {
  274. background-color: black;
  275. }
  276. .best-move-from {
  277. background-color: #31ff7f;
  278. transform: scale(0.85);
  279. }
  280. .best-move-to {
  281. background-color: #31ff7f;
  282. }
  283. .negative-best-move-from {
  284. background-color: #fd0000;
  285. transform: scale(0.85);
  286. }
  287. .negative-best-move-to {
  288. background-color: #fd0000;
  289. }
  290. body {
  291. display: block;
  292. margin-left: auto;
  293. margin-right: auto;
  294. width: 360px;
  295. }
  296. #fen {
  297. margin-left: 10px;
  298. }
  299. #engine-log-container {
  300. max-height: 35vh;
  301. overflow: auto!important;
  302. }
  303. #userscript-log-container {
  304. max-height: 35vh;
  305. overflow: auto!important;
  306. }
  307. .sideways-card {
  308. display: flex;
  309. align-items: center;
  310. justify-content: space-between;
  311. }
  312. .rendered-form .card {
  313. margin-bottom: 10px;
  314. }
  315. .hidden {
  316. display: none;
  317. }
  318. .main-title-bar {
  319. display: flex;
  320. justify-content: space-between;
  321. }
  322. @keyframes wiggle {
  323. 0% { transform: scale(1); }
  324. 80% { transform: scale(1); }
  325. 85% { transform: scale(1.1); }
  326. 95% { transform: scale(1); }
  327. 100% { transform: scale(1); }
  328. }
  329.  
  330. .wiggle {
  331. display: inline-block;
  332. animation: wiggle 1s infinite;
  333. }
  334. `;
  335.  
  336. function FenUtils() {
  337. this.board = [
  338. [1, 1, 1, 1, 1, 1, 1, 1],
  339. [1, 1, 1, 1, 1, 1, 1, 1],
  340. [1, 1, 1, 1, 1, 1, 1, 1],
  341. [1, 1, 1, 1, 1, 1, 1, 1],
  342. [1, 1, 1, 1, 1, 1, 1, 1],
  343. [1, 1, 1, 1, 1, 1, 1, 1],
  344. [1, 1, 1, 1, 1, 1, 1, 1],
  345. [1, 1, 1, 1, 1, 1, 1, 1],
  346. ];
  347.  
  348. this.pieceCodeToFen = pieceStr => {
  349. const [pieceColor, pieceName] = pieceStr.split('');
  350.  
  351. return pieceColor == 'w' ? pieceName.toUpperCase() : pieceName.toLowerCase();
  352. }
  353.  
  354. this.getFenCodeFromPieceElem = pieceElem => {
  355. return this.pieceCodeToFen([...pieceElem.classList].find(x => x.match(/^(b|w)[prnbqk]{1}$/)));
  356. }
  357.  
  358. this.getPieceColor = pieceFenStr => {
  359. return pieceFenStr == pieceFenStr.toUpperCase() ? 'w' : 'b';
  360. }
  361.  
  362. this.getPieceOppositeColor = pieceFenStr => {
  363. return this.getPieceColor(pieceFenStr) == 'w' ? 'b' : 'w';
  364. }
  365.  
  366. this.squeezeEmptySquares = fenStr => {
  367. return fenStr.replace(/11111111/g, '8')
  368. .replace(/1111111/g, '7')
  369. .replace(/111111/g, '6')
  370. .replace(/11111/g, '5')
  371. .replace(/1111/g, '4')
  372. .replace(/111/g, '3')
  373. .replace(/11/g, '2');
  374. }
  375.  
  376. this.posToIndex = pos => {
  377. const [x, y] = pos.split('');
  378.  
  379. return { 'y': 8 - y, 'x': 'abcdefgh'.indexOf(x) };
  380. }
  381.  
  382. this.getBoardPiece = pos => {
  383. const indexObj = this.posToIndex(pos);
  384.  
  385. return this.board[indexObj.y][indexObj.x];
  386. }
  387.  
  388. this.getRights = () => {
  389. let rights = '';
  390.  
  391. // check for white
  392. const e1 = this.getBoardPiece('e1'),
  393. h1 = this.getBoardPiece('h1'),
  394. a1 = this.getBoardPiece('a1');
  395.  
  396. if (e1 == 'K' && h1 == 'R') rights += 'K';
  397. if (e1 == 'K' && a1 == 'R') rights += 'Q';
  398.  
  399. //check for black
  400. const e8 = this.getBoardPiece('e8'),
  401. h8 = this.getBoardPiece('h8'),
  402. a8 = this.getBoardPiece('a8');
  403.  
  404. if (e8 == 'k' && h8 == 'r') rights += 'k';
  405. if (e8 == 'k' && a8 == 'r') rights += 'q';
  406.  
  407. return rights ? rights : '-';
  408. }
  409.  
  410.  
  411.  
  412. this.getBasicFen = () => {
  413. const pieceElems = [...chessBoardElem.querySelectorAll('.piece')];
  414.  
  415. pieceElems.forEach(pieceElem => {
  416. const pieceFenCode = this.getFenCodeFromPieceElem(pieceElem);
  417. const [xPos, yPos] = pieceElem.classList.toString().match(/square-(\d)(\d)/).slice(1);
  418.  
  419. this.board[8 - yPos][xPos - 1] = pieceFenCode;
  420. });
  421.  
  422. const basicFen = this.squeezeEmptySquares(this.board.map(x => x.join('')).join('/'));
  423.  
  424. return basicFen;
  425. }
  426.  
  427. this.getFen = () => {
  428. const basicFen = this.getBasicFen();
  429. const rights = this.getRights();
  430.  
  431.  
  432. return `${basicFen} ${turn} ${rights} - 0 1`;
  433. }
  434. }
  435.  
  436. function InterfaceUtils() {
  437. this.boardUtils = {
  438. findSquareElem: (squareCode) => {
  439. if (!Gui?.document) return;
  440.  
  441. return Gui.document.querySelector(`.square-${squareCode}`);
  442. },
  443. markMove: (fromSquare, toSquare) => {
  444. if (!Gui?.document) return;
  445.  
  446. const [fromElem, toElem] = [this.boardUtils.findSquareElem(fromSquare), this.boardUtils.findSquareElem(toSquare)];
  447.  
  448.  
  449. if (isPlayerTurn) {
  450. fromElem.classList.add('best-move-from');
  451. toElem.classList.add('best-move-to');
  452. } else {
  453. fromElem.classList.add('negative-best-move-from');
  454. toElem.classList.add('negative-best-move-to');
  455. }
  456.  
  457. activeGuiMoveHighlights.push(fromElem);
  458. activeGuiMoveHighlights.push(toElem);
  459. },
  460. removeBestMarkings: () => {
  461. if (!Gui?.document) return;
  462.  
  463. activeGuiMoveHighlights.forEach(elem => {
  464. elem.classList.remove('best-move-from', 'best-move-to', 'negative-best-move-from', 'negative-best-move-to');
  465. });
  466.  
  467. activeGuiMoveHighlights = [];
  468. },
  469. updateBoardFen: fen => {
  470. if (!Gui?.document) return;
  471.  
  472. Gui.document.querySelector('#fen').textContent = fen;
  473. },
  474. updateBoardPower: (myScore, enemyScore) => {
  475. if (!Gui?.document) return;
  476.  
  477. Gui.document.querySelector('#enemy-score').textContent = enemyScore;
  478. Gui.document.querySelector('#my-score').textContent = myScore;
  479. },
  480. updateBoardOrientation: orientation => {
  481. if (!Gui?.document) return;
  482.  
  483. const orientationElem = Gui?.document?.querySelector('#orientation');
  484.  
  485. if (orientationElem) {
  486. orientationElem.textContent = orientation;
  487. }
  488. }
  489. }
  490.  
  491. this.engineLog = str => {
  492. if (!Gui?.document || enableUserLog == 0) return;
  493.  
  494. const logElem = document.createElement('div');
  495. logElem.classList.add('list-group-item');
  496.  
  497. if (str.includes('info')) logElem.classList.add('list-group-item-info');
  498. if (str.includes('bestmove')) logElem.classList.add('list-group-item-success');
  499.  
  500. logElem.innerText = `#${engineLogNum++} ${str}`;
  501.  
  502. Gui.document.querySelector('#engine-log-container').prepend(logElem);
  503. }
  504.  
  505. this.log = str => {
  506. if (!Gui?.document || enableUserLog == 0) return;
  507.  
  508. const logElem = document.createElement('div');
  509. logElem.classList.add('list-group-item');
  510.  
  511. if (str.includes('info')) logElem.classList.add('list-group-item-info');
  512. if (str.includes('bestmove')) logElem.classList.add('list-group-item-success');
  513.  
  514. const container = Gui?.document?.querySelector('#userscript-log-container');
  515.  
  516. if (container) {
  517. logElem.innerText = `#${userscriptLogNum++} ${str}`;
  518.  
  519. container.prepend(logElem);
  520. }
  521. }
  522.  
  523. this.getBoardOrientation = () => {
  524. return document.querySelector('.board.flipped') ? 'b' : 'w';
  525. }
  526.  
  527. this.updateBestMoveProgress = text => {
  528. if (!Gui?.document) return;
  529.  
  530. const progressBarElem = Gui.document.querySelector('#best-move-progress');
  531.  
  532. progressBarElem.innerText = text;
  533.  
  534. progressBarElem.classList.remove('hidden');
  535. progressBarElem.classList.add('wiggle');
  536. }
  537.  
  538. this.stopBestMoveProcessingAnimation = () => {
  539. if (!Gui?.document) return;
  540.  
  541. const progressBarElem = Gui.document.querySelector('#best-move-progress');
  542.  
  543. progressBarElem.classList.remove('wiggle');
  544. }
  545.  
  546. this.hideBestMoveProgress = () => {
  547. if (!Gui?.document) return;
  548.  
  549. const progressBarElem = Gui.document.querySelector('#best-move-progress');
  550.  
  551. if (!progressBarElem.classList.contains('hidden')) {
  552. progressBarElem.classList.add('hidden');
  553. this.stopBestMoveProcessingAnimation();
  554. }
  555. }
  556. }
  557.  
  558. function LozzaUtility() {
  559. this.separateMoveCodes = moveCode => {
  560. moveCode = moveCode.trim();
  561.  
  562. let move = moveCode.split(' ')[1];
  563.  
  564. return [move.slice(0, 2), move.slice(2, 4)];
  565. }
  566.  
  567. this.extractInfo = str => {
  568. const keys = ['time', 'nps', 'depth'];
  569.  
  570. return keys.reduce((acc, key) => {
  571. const match = str.match(`${key} (\\d+)`);
  572.  
  573. if (match) {
  574. acc[key] = Number(match[1]);
  575. }
  576.  
  577. return acc;
  578. }, {});
  579. }
  580. }
  581.  
  582. function fenSquareToChessComSquare(fenSquareCode) {
  583. const [x, y] = fenSquareCode.split('');
  584.  
  585. return `square-${['abcdefgh'.indexOf(x) + 1]}${y}`;
  586. }
  587.  
  588. function markMoveToSite(fromSquare, toSquare) {
  589. const highlight = (fenSquareCode, style) => {
  590. const squareClass = fenSquareToChessComSquare(fenSquareCode);
  591.  
  592. const highlightElem = document.createElement('div');
  593. highlightElem.classList.add('highlight');
  594. highlightElem.classList.add(squareClass);
  595. highlightElem.dataset.testElement = 'highlight';
  596. highlightElem.style = style;
  597.  
  598. activeSiteMoveHighlights.push(highlightElem);
  599.  
  600. const existingHighLight = document.querySelector(`.highlight.${squareClass}`);
  601.  
  602. if (existingHighLight) {
  603. existingHighLight.remove();
  604. }
  605.  
  606. chessBoardElem.prepend(highlightElem);
  607. }
  608.  
  609. const defaultFromSquareStyle = 'background-color: rgb(120 130 255 / 90%); border: 4px solid rgb(0 0 0 / 50%);';
  610. const defaultToSquareStyle = 'background-color: rgb(60 140 255 / 90%); border: 4px dashed rgb(0 0 0 / 50%);';
  611. const negativeFromSquareStyle = 'background-color: rgb(255 0 0 / 30%); border: 4px solid rgb(0 0 0 / 50%);';
  612. const negativeToSquareStyle = 'background-color: rgb(255 0 0 / 30%); border: 4px dashed rgb(0 0 0 / 50%);';
  613.  
  614.  
  615.  
  616. highlight(fromSquare, (isPlayerTurn ? defaultFromSquareStyle : negativeFromSquareStyle));
  617. highlight(toSquare, (isPlayerTurn ? defaultToSquareStyle : negativeToSquareStyle));
  618. }
  619.  
  620. function removeSiteMoveMarkings() {
  621. activeSiteMoveHighlights.forEach(elem => {
  622. elem?.remove();
  623. });
  624.  
  625. activeSiteMoveHighlights = [];
  626. }
  627.  
  628. function updateBestMove(mutationArr) {
  629.  
  630. let FenUtil = new FenUtils();
  631. let currentFen = getCurrentFen();
  632.  
  633.  
  634. if (currentFen != lastFen) {
  635. lastFen = currentFen;
  636.  
  637.  
  638. if (mutationArr) {
  639. const attributeMutationArr = mutationArr.filter(m => m.target.classList.contains('piece') && m.attributeName == 'class');
  640.  
  641. if (attributeMutationArr?.length) {
  642. turn = FenUtil.getPieceOppositeColor(FenUtil.getFenCodeFromPieceElem(attributeMutationArr[0].target));
  643. Interface.log(`Turn updated to ${turn}!`);
  644. if(turn!=playerColor){
  645. Gui.document.querySelector('#bestmove-btn').disabled=true;
  646. }else{
  647. Gui.document.querySelector('#bestmove-btn').disabled=false;
  648. }
  649. }
  650. }
  651. updateBoard();
  652. sendBestMove();
  653.  
  654.  
  655. }
  656. }
  657.  
  658.  
  659.  
  660.  
  661. function sendBestMove() {
  662. if (!isPlayerTurn && !show_opposite_moves) {
  663. return;
  664. }
  665. sendBestMoveRequest();
  666.  
  667. }
  668.  
  669. function sendBestMoveRequest() {
  670. reloadChessEngine(false, () => {
  671. Interface.log('Sending best move request to the engine!');
  672. let currentFen = getCurrentFen();
  673.  
  674. if (use_book_moves) {
  675. getBookMoves(currentFen);
  676. } else {
  677. getBestMoves(currentFen);
  678. }
  679. });
  680. }
  681.  
  682. function getCurrentFen() {
  683. let FenUtil = new FenUtils();
  684.  
  685. let currentFen = FenUtil.getFen();
  686. return currentFen;
  687. }
  688.  
  689.  
  690. function clearBoard(){
  691. Interface.stopBestMoveProcessingAnimation();
  692.  
  693. Interface.boardUtils.removeBestMarkings();
  694.  
  695. removeSiteMoveMarkings();
  696. }
  697. function updateBoard() {
  698. clearBoard();
  699.  
  700. Interface.boardUtils.updateBoardFen(getCurrentFen());
  701.  
  702. isPlayerTurn = playerColor == null || turn == playerColor;
  703. }
  704.  
  705. function sleep(ms) {
  706. return new Promise(resolve => setTimeout(resolve, ms));
  707. }
  708.  
  709. function getBestMoves(fen) {
  710. if (engineIndex != node_engine_id) {
  711. // local engines
  712. while (!engine) {
  713. sleep(100);
  714. }
  715.  
  716. engine.postMessage(`position fen ${fen}`);
  717.  
  718. if (engineMode == DEPTH_MODE) {
  719. engine.postMessage('go depth ' + current_depth);
  720. } else {
  721. engine.postMessage('go movetime ' + current_movetime);
  722. }
  723.  
  724.  
  725. } else {
  726.  
  727. getNodeBestMoves(fen);
  728. }
  729. }
  730.  
  731. function observeNewMoves() {
  732. updateBestMove();
  733.  
  734. const boardObserver = new MutationObserver(mutationArr => {
  735. const lastPlayerColor = playerColor;
  736.  
  737. updatePlayerColor();
  738.  
  739.  
  740. if (playerColor != lastPlayerColor) {
  741. Interface.log(`Player color changed from ${lastPlayerColor} to ${playerColor}!`);
  742.  
  743. updateBestMove();
  744. } else {
  745.  
  746. updateBestMove(mutationArr);
  747. }
  748. });
  749.  
  750. boardObserver.observe(chessBoardElem, { childList: true, subtree: true, attributes: true });
  751. }
  752.  
  753. function addGuiPages() {
  754. if (Gui?.document) return;
  755.  
  756. Gui.addPage("Main", `
  757. <div class="rendered-form" id="main-tab">
  758. <script>${GM_getResourceText('jquery.js')}</script>
  759. <script>${GM_getResourceText('chessboard.js')}</script>
  760. <div class="card">
  761. <div class="card-body" id="chessboard">
  762. <div class="main-title-bar">
  763. <h4 class="card-title">Live Chessboard:</h4>
  764. <p id="best-move-progress"></p>
  765. </div>
  766.  
  767. <div id="board" style="width: 447px"></div>
  768. </div>
  769. <div id="orientation" class="hidden"></div>
  770. <div class="card-footer card"><input type="button" value="Get Best Move" id="bestmove-btn"></input></div>
  771. <div class="card-footer sideways-card">FEN :<small class="text-muted"><div id="fen"></div></small></div>
  772. <div class="card-footer sideways-card">ENEMY SCORE :<div id="enemy-score"></div></div>
  773. <div class="card-footer sideways-card">MY SCORE : <div id="my-score"></div></div>
  774. </div>
  775. <script>
  776. const orientationElem = document.querySelector('#orientation');
  777. const fenElem = document.querySelector('#fen');
  778.  
  779. let board = ChessBoard('board', {
  780. pieceTheme: '${repositoryRawURL}/content/chesspieces/{piece}.svg',
  781. position: 'start',
  782. orientation: '${playerColor == 'b' ? 'black' : 'white'}'
  783. });
  784.  
  785. const orientationObserver = new MutationObserver(() => {
  786. board = ChessBoard('board', {
  787. pieceTheme: '${repositoryRawURL}/content/chesspieces/{piece}.svg',
  788. position: fenElem.textContent,
  789. orientation: orientationElem.textContent == 'b' ? 'black' : 'white'
  790. });
  791. });
  792.  
  793. const fenObserver = new MutationObserver(() => {
  794. board.position(fenElem.textContent);
  795. });
  796.  
  797. orientationObserver.observe(orientationElem, { attributes: true, childList: true, characterData: true });
  798. fenObserver.observe(fenElem, { attributes: true, childList: true, characterData: true });
  799. </script>
  800. </div>
  801. `);
  802.  
  803. Gui.addPage('Log', `
  804. <div class="rendered-form" id="log-tab">
  805. <div class="card">
  806. <div class="card-body">
  807. <h4 class="card-title">Userscript Log:</h4>
  808. <ul class="list-group" id="userscript-log-container"></ul>
  809. </div>
  810. </div>
  811. <div class="card">
  812. <div class="card-body">
  813. <h4 class="card-title">Engine Log</h4>
  814. <ul class="list-group" id="engine-log-container"></ul>
  815. </div>
  816. </div>
  817. </div>
  818. `);
  819.  
  820.  
  821.  
  822.  
  823.  
  824.  
  825.  
  826. Gui.addPage('Settings', `
  827. <div class="rendered-form" id="settings-tab">
  828. <div class="card">
  829. <div class="card-body">
  830. <h4 class="card-title">Engine:</h4>
  831. <div class="form-group field-select-engine">
  832. <select class="form-control" name="select-engine" id="select-engine">
  833. <option value="option-lozza" id="select-engine-0">Lozza</option>
  834. <option value="option-stockfish" id="select-engine-2">Stockfish 5</option>
  835. <option value="option-stockfish2" id="select-engine-3">Stockfish 2018</option>
  836. <option value="option-nodeserver" id="select-engine-4">Node Server Engines</option>
  837. </select>
  838. </div>
  839.  
  840.  
  841. <div id="node-engine-div" style="display:${(engineIndex == node_engine_id) ? 'block' : 'none'};">
  842. <label for="engine-url">Engine URL:</label>
  843. <input type="text" id="engine-url" value="${node_engine_url}">
  844. <br>
  845. <label for="engine-name">Engine Name:</label>
  846. <input type="text" id="engine-name" value="${node_engine_name}">
  847. </div>
  848. </div>
  849. </div>
  850.  
  851.  
  852. <div class="card">
  853. <div class="card-body">
  854. <h4 class="card-title">Engine Strength:</h4>
  855.  
  856. <h7 class="card-title">Engine Mode:</h7>
  857. <div class="form-group field-select-engine-mode">
  858. <select class="form-control" name="select-engine-mode" id="select-engine-mode">
  859. <option value="option-depth" id="select-engine-mode-0">Depth</option>
  860. <option value="option-movetime" id="select-engine-mode-1">Move time</option>
  861. </select>
  862. </div>
  863. <h7 class="card-title">Engine Power:</h7>
  864. <input type="range" class="form-range" min="${MIN_DEPTH}" max="${MAX_DEPTH}" step="1" value="${current_depth}" id="depth-range">
  865. <input type="number" class="form-range" min="${MIN_DEPTH}" max="${MAX_DEPTH}" value="${current_depth}" id="depth-range-number">
  866. <input type="range" class="form-range" min="${MIN_MOVETIME}" max="${MAX_MOVETIME}" step="50" value="${current_movetime}" id="movetime-range">
  867. <input type="number" class="form-range" min="${MIN_MOVETIME}" max="${MAX_MOVETIME}" value="${current_movetime}" id="movetime-range-number">
  868. </div>
  869. <div class="card-footer sideways-card" id="elo">${getEloDescription()}</div>
  870. </div>
  871.  
  872.  
  873.  
  874. <div class="card">
  875. <div class="card-body">
  876. <h4 class="card-title">Visual:</h4>
  877. <div>
  878. <input type="checkbox" id="show-opposite-moves" ${show_opposite_moves ? 'checked' : ''}>
  879. <label for="show-opposite-moves">Show Opponent best moves</label>
  880. </div>
  881.  
  882.  
  883. <input type="checkbox" id="display-moves-on-site" ${displayMovesOnSite ? 'checked' : ''}>
  884. <label for="display-moves-on-site">Display moves on site</label>
  885. </div>
  886. </div>
  887.  
  888.  
  889. <div class="card">
  890. <div class="card-body">
  891. <h4 class="card-title">Other:</h4>
  892.  
  893. <div>
  894. <input type="checkbox" id="enable-user-log" ${enableUserLog ? 'checked' : ''}>
  895. <label for="enable-user-log">Enable User Scripts Log</label>
  896. </div>
  897.  
  898.  
  899. <div>
  900. <input type="checkbox" id="use-book-moves" ${use_book_moves ? 'checked' : ''}>
  901. <label for="use-book-moves">Use book moves</label>
  902. </div>
  903.  
  904.  
  905.  
  906. <div id="reload-count-div" style="display:${node_engine_id == engineIndex ? 'none' : 'block'};">
  907. <label for="reload-count">Reload Engine every</label>
  908. <input type="number" id="reload-count" value="${reload_every}">
  909. <label for="reload-count"> moves</label>
  910. </div>
  911. </div>
  912. </div>
  913. `);
  914.  
  915.  
  916. }
  917.  
  918. function fixDepthMoveTimeInput(depthRangeElem, depthRangeNumberElem, moveTimeRangeElem, moveTimeRangeNumberElem, eloElem) {
  919. if (engineMode == DEPTH_MODE) {
  920. if (isNotCompatibleBrowser()) {
  921. depthRangeElem.style.display = "none";
  922. depthRangeNumberElem.style.display = "block";
  923. moveTimeRangeElem.style.display = "none";
  924. moveTimeRangeNumberElem.style.display = "none";
  925. } else {
  926. depthRangeElem.style.display = "block";
  927. depthRangeNumberElem.style.display = "none";
  928. moveTimeRangeElem.style.display = "none";
  929. moveTimeRangeNumberElem.style.display = "none";
  930. }
  931. } else {
  932. if (isNotCompatibleBrowser()) {
  933. depthRangeElem.style.display = "none";
  934. depthRangeNumberElem.style.display = "none";
  935. moveTimeRangeElem.style.display = "none";
  936. moveTimeRangeNumberElem.style.display = "block";
  937. } else {
  938. depthRangeElem.style.display = "none";
  939. depthRangeNumberElem.style.display = "none";
  940. moveTimeRangeElem.style.display = "block";
  941. moveTimeRangeNumberElem.style.display = "none";
  942. }
  943. }
  944.  
  945.  
  946. eloElem.innerText = getEloDescription();
  947.  
  948. }
  949.  
  950. function openGUI() {
  951. Interface.log(`Opening GUI!`);
  952.  
  953. const hide = elem => elem.classList.add('hidden');
  954. const show = elem => elem.classList.remove('hidden');
  955.  
  956. Gui.open(() => {
  957. const depthRangeElem = Gui.document.querySelector('#depth-range');
  958. const depthRangeNumberElem = Gui.document.querySelector('#depth-range-number');
  959. const moveTimeRangeElem = Gui.document.querySelector('#movetime-range');
  960. const moveTimeRangeNumberElem = Gui.document.querySelector('#movetime-range-number');
  961. const engineModeElem = Gui.document.querySelector('#select-engine-mode');
  962. const engineElem = Gui.document.querySelector('#select-engine');
  963. const engineNameDivElem = Gui.document.querySelector('#node-engine-div');
  964. const reloadEveryDivElem = Gui.document.querySelector('#reload-count-div');
  965. const nodeEngineNameElem = Gui.document.querySelector('#engine-name');
  966. const nodeEngineUrlElem = Gui.document.querySelector('#engine-url');
  967. const useLocalEngineElem = Gui.document.querySelector('#use-book-moves');
  968. const showOppositeMovesElem = Gui.document.querySelector('#show-opposite-moves');
  969. const displayMovesOnSiteElem = Gui.document.querySelector('#display-moves-on-site');
  970. const openGuiAutomaticallyAdditionalElem = Gui.document.querySelector('#display-moves-on-site-additional');
  971. const reloadEveryElem = Gui.document.querySelector('#reload-count');
  972. const enableUserLogElem = Gui.document.querySelector('#enable-user-log');
  973. const eloElem = Gui.document.querySelector('#elo');
  974. const getBestMoveElem = Gui.document.querySelector('#bestmove-btn');
  975.  
  976.  
  977. fixDepthMoveTimeInput(depthRangeElem, depthRangeNumberElem, moveTimeRangeElem, moveTimeRangeNumberElem, eloElem);
  978.  
  979. engineElem.selectedIndex = engineIndex;
  980. engineModeElem.selectedIndex = engineMode;
  981.  
  982.  
  983. // compatibility fixed
  984. if (isNotCompatibleBrowser()) {
  985. Gui.document.querySelector('#content').style.maxHeight = "500px";
  986. Gui.document.querySelector('#content').style.overflow = "scroll";
  987. Gui.document.querySelector('#chessboard').style.display = "none";
  988. Gui.document.querySelector('#orientation').style.display = "none";
  989. Gui.document.querySelector('#engine-log-container').style.maxHeight = "100px";
  990. Gui.document.querySelector('#engine-log-container').style.overflow = "scroll";
  991. Gui.document.querySelector('#userscript-log-container').style.maxHeight = "100px";
  992. Gui.document.querySelector('#userscript-log-container').style.overflow = "scroll";
  993.  
  994. Gui.document.querySelector('#button-close-gui').addEventListener('click', e => {
  995. e.preventDefault();
  996. if (closedGui == true) {
  997. closedGui = false;
  998. Gui.document.querySelector("#content").style.display = "block";
  999. }
  1000. else {
  1001. closedGui = true;
  1002. Gui.document.querySelector("#content").style.display = "none";
  1003.  
  1004. }
  1005.  
  1006.  
  1007. });
  1008. }
  1009.  
  1010. getBestMoveElem.onclick = () => {
  1011. if(isPlayerTurn){
  1012. clearBoard();
  1013. sendBestMoveRequest();
  1014. }
  1015. }
  1016.  
  1017. engineModeElem.onchange = () => {
  1018. engineMode = engineModeElem.selectedIndex;
  1019.  
  1020. fixDepthMoveTimeInput(depthRangeElem, depthRangeNumberElem, moveTimeRangeElem, moveTimeRangeNumberElem, eloElem);
  1021. }
  1022. nodeEngineNameElem.onchange = () => {
  1023. node_engine_name = nodeEngineNameElem.value;
  1024. }
  1025. nodeEngineUrlElem.onchange = () => {
  1026. node_engine_url = nodeEngineUrlElem.value;
  1027. }
  1028.  
  1029. enableUserLogElem.onchange = () => {
  1030. const isChecked = enableUserLogElem.checked;
  1031.  
  1032. if (isChecked)
  1033. enableUserLog = true;
  1034. else
  1035. enableUserLog = false;
  1036. }
  1037.  
  1038. reloadEveryElem.onchange = () => {
  1039. reload_every = reloadEveryElem.value;
  1040. }
  1041.  
  1042. engineElem.onchange = () => {
  1043. lastEngine = engineIndex;
  1044. engineIndex = engineElem.selectedIndex;
  1045.  
  1046.  
  1047.  
  1048. if (node_engine_id == engineIndex) {
  1049. reloadEveryDivElem.style.display = "none";
  1050. engineNameDivElem.style.display = "block";
  1051.  
  1052.  
  1053. }
  1054. else {
  1055. reloadEveryDivElem.style.display = "block";
  1056. engineNameDivElem.style.display = "none";
  1057. }
  1058.  
  1059.  
  1060.  
  1061.  
  1062.  
  1063. if (engineObjectURL) {
  1064. URL.revokeObjectURL(engineObjectURL);
  1065. engineObjectURL = null;
  1066. }
  1067.  
  1068.  
  1069.  
  1070.  
  1071. reloadChessEngine(true, () => {
  1072. Interface.boardUtils.removeBestMarkings();
  1073.  
  1074. removeSiteMoveMarkings();
  1075.  
  1076. Interface.boardUtils.updateBoardPower(0, 0);
  1077. });
  1078.  
  1079. }
  1080.  
  1081.  
  1082.  
  1083. depthRangeElem.onchange = () => {
  1084. changeEnginePower(depthRangeElem.value, eloElem);
  1085. };
  1086.  
  1087. depthRangeNumberElem.onchange = () => {
  1088. changeEnginePower(depthRangeNumberElem.value, eloElem);
  1089. };
  1090.  
  1091. moveTimeRangeElem.onchange = () => {
  1092. changeEnginePower(moveTimeRangeElem.value, eloElem);
  1093. };
  1094.  
  1095. moveTimeRangeNumberElem.onchange = () => {
  1096. changeEnginePower(moveTimeRangeNumberElem.value, eloElem);
  1097. };
  1098.  
  1099. showOppositeMovesElem.onchange = () => {
  1100. const isChecked = showOppositeMovesElem.checked;
  1101.  
  1102. if (isChecked) {
  1103. show_opposite_moves = true;
  1104. } else {
  1105. show_opposite_moves = false;
  1106. }
  1107. }
  1108.  
  1109. useLocalEngineElem.onchange = () => {
  1110. const isChecked = useLocalEngineElem.checked;
  1111.  
  1112. if (isChecked) {
  1113. use_book_moves = true;
  1114. } else {
  1115. use_book_moves = false;
  1116. }
  1117. }
  1118.  
  1119. displayMovesOnSiteElem.onchange = () => {
  1120. const isChecked = displayMovesOnSiteElem.checked;
  1121.  
  1122. if (isChecked) {
  1123. displayMovesOnSite = true;
  1124.  
  1125. show(openGuiAutomaticallyAdditionalElem);
  1126.  
  1127. } else {
  1128. displayMovesOnSite = false;
  1129.  
  1130. hide(openGuiAutomaticallyAdditionalElem);
  1131. }
  1132. };
  1133.  
  1134.  
  1135.  
  1136.  
  1137.  
  1138.  
  1139. window.onunload = () => {
  1140. if (Gui.window && !Gui.window.closed) {
  1141. Gui.window.close();
  1142. }
  1143. };
  1144.  
  1145. const isWindowClosed = setInterval(() => {
  1146. if (Gui.window.closed) {
  1147. clearInterval(isWindowClosed);
  1148. if (engine != null)
  1149. engine.terminate();
  1150. }
  1151. }, 1000);
  1152.  
  1153. observeNewMoves();
  1154.  
  1155. Interface.log('Initialized!');
  1156. });
  1157. }
  1158.  
  1159. function changeEnginePower(val, eloElem) {
  1160. if (engineMode == DEPTH_MODE) {
  1161. current_depth = val
  1162. } else {
  1163. current_movetime = val
  1164. }
  1165.  
  1166.  
  1167. eloElem.innerText = getEloDescription();
  1168. }
  1169.  
  1170. function reloadChessEngine(forced, callback) {
  1171. // reload only if using local engines
  1172. if (node_engine_id == engineIndex && forced == false)
  1173. callback();
  1174. else if (reload_count >= reload_every || forced == true) {
  1175. reload_count = 1;
  1176. Interface.log(`Reloading the chess engine!`);
  1177.  
  1178. if (engine)
  1179. engine.terminate();
  1180.  
  1181. loadChessEngine(callback);
  1182. }
  1183. else {
  1184. reload_count = reload_count + 1;
  1185. callback();
  1186. }
  1187. }
  1188.  
  1189.  
  1190.  
  1191. function loadChessEngine(callback) {
  1192. if (!engineObjectURL) {
  1193. if (engineIndex == 0)
  1194. engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('lozza.js')], { type: 'application/javascript' }));
  1195. else if (engineIndex == 1)
  1196. engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('stockfish.js')], { type: 'application/javascript' }));
  1197. else if (engineIndex == 2)
  1198. engineObjectURL = URL.createObjectURL(new Blob([GM_getResourceText('stockfish2.js')], { type: 'application/javascript' }));
  1199. }
  1200.  
  1201. if (engineObjectURL) {
  1202. engine = new Worker(engineObjectURL);
  1203.  
  1204. engine.onmessage = e => {
  1205. if (e.data.includes('bestmove')) {
  1206.  
  1207.  
  1208. let move = e.data.split(' ')[1];
  1209. let opponent_move = e.data.split(' ')[3];
  1210.  
  1211.  
  1212. moveResult(move.slice(0, 2), move.slice(2, 4), 0, true);
  1213.  
  1214. }
  1215.  
  1216. else if (e.data.includes('info')) {
  1217.  
  1218. const infoObj = LozzaUtils.extractInfo(e.data);
  1219. let depth = infoObj.depth || current_depth;
  1220. let move_time = infoObj.time || current_movetime;
  1221.  
  1222.  
  1223.  
  1224. if (engineMode == DEPTH_MODE) {
  1225.  
  1226. Interface.updateBestMoveProgress(`Depth: ${depth}`);
  1227.  
  1228. } else {
  1229.  
  1230. Interface.updateBestMoveProgress(`Move time: ${move_time} ms`);
  1231.  
  1232. }
  1233.  
  1234. }
  1235. Interface.engineLog(e.data);
  1236. };
  1237.  
  1238. engine.postMessage('ucinewgame');
  1239.  
  1240. Interface.log(`Loaded the chess engine!`);
  1241. }
  1242.  
  1243. callback();
  1244. }
  1245.  
  1246.  
  1247.  
  1248. async function updatePlayerColor() {
  1249. const boardOrientation = Interface.getBoardOrientation();
  1250.  
  1251. playerColor = boardOrientation;
  1252. turn = boardOrientation;
  1253.  
  1254.  
  1255. Interface.boardUtils.updateBoardOrientation(playerColor);
  1256. }
  1257.  
  1258. async function initialize() {
  1259. Interface = new InterfaceUtils();
  1260. LozzaUtils = new LozzaUtility();
  1261.  
  1262. const boardOrientation = Interface.getBoardOrientation();
  1263. turn = boardOrientation;
  1264.  
  1265.  
  1266.  
  1267. loadChessEngine(() => {
  1268.  
  1269. });
  1270.  
  1271. updatePlayerColor();
  1272.  
  1273.  
  1274. addGuiPages();
  1275. openGUI();
  1276.  
  1277. }
  1278.  
  1279. if (typeof GM_registerMenuCommand == 'function') {
  1280. GM_registerMenuCommand("Open C.A.S", e => {
  1281. if (chessBoardElem) {
  1282. initialize();
  1283. }
  1284. }, 's');
  1285. }
  1286.  
  1287. const waitForChessBoard = setInterval(() => {
  1288. const boardElem = document.querySelector('chess-board');
  1289. const firstPieceElem = document.querySelector('.piece');
  1290.  
  1291. if (boardElem && firstPieceElem && chessBoardElem != boardElem) {
  1292. chessBoardElem = boardElem;
  1293.  
  1294. if (window.location.href != 'https://www.chess.com/play') {
  1295. initialize();
  1296. }
  1297. }
  1298. }, 2000);