Chess.com Stockfish Assistant

Chess analysis tool with Stockfish integration and auto-match

目前为 2025-04-05 提交的版本。查看 最新版本

  1. // ==UserScript==
  2. // @name Chess.com Stockfish Assistant
  3. // @namespace BottleOrg Scripts
  4. // @version 1.6.6
  5. // @description Chess analysis tool with Stockfish integration and auto-match
  6. // @author [REDACTED] - Rightful owner & Contributors & Gemini 2.5 Pro, Chatgpt-4o
  7. // @match https://www.chess.com/play/*
  8. // @match https://www.chess.com/game/*
  9. // @match https://www.chess.com/puzzles/*
  10. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_xmlhttpRequest
  14. // @require https://code.jquery.com/jquery-3.6.0.min.js
  15. // @require https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.2/chess.min.js
  16. // @run-at document-start
  17. // @license 2025, qbaonguyen050@gmail.com, All Rights Reserved
  18. // @notice This script is protected under the Berne Convention for the Protection of Literary and Artistic Works, an international treaty recognized by over 180 countries. Unauthorized copying, modification, or distribution is strictly prohibited and will be enforced under applicable national copyright laws worldwide. Violations may result in legal action, including takedown requests and civil penalties.
  19. // ==/UserScript==
  20.  
  21. (function() {
  22. 'use strict';
  23.  
  24. const config = {
  25. API_URL: "https://stockfish.online/api/s/v2.php",
  26. MIN_DELAY: 0.8,
  27. MAX_DELAY: 3.0,
  28. MAX_DEPTH: 15,
  29. MIN_DEPTH: 1,
  30. RATE_LIMIT: 1200,
  31. MISTAKE_THRESHOLD: 0.15
  32. };
  33.  
  34. let state = {
  35. autoMove: false,
  36. autoRun: false,
  37. autoMatch: false,
  38. visualOnly: false,
  39. mistakePercentage: 0,
  40. lastMoveTime: 0,
  41. lastFen: null,
  42. lastTurn: null,
  43. isThinking: false,
  44. canMove: true,
  45. board: null,
  46. gameEnded: false,
  47. hasAutoMatched: false
  48. };
  49.  
  50. const utils = {
  51. getRandomDelay: () => {
  52. const base = Math.random() * (config.MAX_DELAY - config.MIN_DELAY) + config.MIN_DELAY;
  53. return base * 1000 + (Math.random() * 300 - 150);
  54. },
  55.  
  56. simulateHumanClick: (element) => {
  57. if (!element) return Promise.resolve(false);
  58. const rect = element.getBoundingClientRect();
  59. const x = rect.left + rect.width / 2 + (Math.random() * 10 - 5);
  60. const y = rect.top + rect.height / 2 + (Math.random() * 10 - 5);
  61.  
  62. return new Promise(resolve => {
  63. const eventProps = {
  64. bubbles: true,
  65. cancelable: true,
  66. clientX: x,
  67. clientY: y,
  68. button: 0,
  69. buttons: 1,
  70. view: window
  71. };
  72. const down = new MouseEvent('mousedown', eventProps);
  73. const up = new MouseEvent('mouseup', eventProps);
  74. const click = new MouseEvent('click', eventProps);
  75.  
  76. element.dispatchEvent(down);
  77. setTimeout(() => {
  78. element.dispatchEvent(up);
  79. element.dispatchEvent(click);
  80. resolve(true);
  81. }, 50 + Math.random() * 100);
  82. });
  83. },
  84.  
  85. simulateHumanMove: (fromElement, toElement) => {
  86. if (!fromElement || !toElement) return Promise.resolve(false);
  87.  
  88. const fromRect = fromElement.getBoundingClientRect();
  89. const toRect = toElement.getBoundingClientRect();
  90. const steps = 12;
  91. const time = 250 + Math.random() * 150;
  92.  
  93. const dispatchEvent = (element, type, x, y) => {
  94. return new Promise(resolve => {
  95. const event = new MouseEvent(type, {
  96. bubbles: true,
  97. cancelable: true,
  98. clientX: x,
  99. clientY: y,
  100. button: 0,
  101. buttons: type === 'mouseup' ? 0 : 1,
  102. view: window
  103. });
  104. setTimeout(() => {
  105. element.dispatchEvent(event);
  106. resolve();
  107. }, time / steps);
  108. });
  109. };
  110.  
  111. const movePath = [];
  112. for (let i = 0; i <= steps; i++) {
  113. const t = i / steps;
  114. const x = fromRect.left + (toRect.left - fromRect.left) * t + (Math.random() * 6 - 3);
  115. const y = fromRect.top + (toRect.top - fromRect.top) * t + (Math.random() * 6 - 3);
  116. movePath.push({ x, y });
  117. }
  118.  
  119. return new Promise(async resolve => {
  120. await dispatchEvent(fromElement, 'mousedown', movePath[0].x, movePath[0].y);
  121. for (let i = 1; i < movePath.length - 1; i++) {
  122. await dispatchEvent(fromElement, 'mousemove', movePath[i].x, movePath[i].y);
  123. }
  124. await dispatchEvent(toElement, 'mousemove', movePath[steps].x, movePath[steps].y);
  125. await dispatchEvent(toElement, 'mouseup', movePath[steps].x, movePath[steps].y);
  126. resolve(true);
  127. });
  128. },
  129.  
  130. getFen: () => {
  131. const board = state.board;
  132. if (!board || !board.game) return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
  133.  
  134. const chess = new Chess();
  135. const pieces = $(board).find(".piece");
  136. if (!pieces.length) return chess.fen();
  137.  
  138. const position = Array(64).fill('');
  139. pieces.each((_, el) => {
  140. const classes = el.className.split(' ');
  141. const square = classes.find(c => c.startsWith('square-'))?.replace('square-', '');
  142. const piece = classes.find(c => /^[wb][prnbqk]$/.test(c));
  143. if (square && piece) {
  144. const [file, rank] = square.split('');
  145. const idx = (8 - parseInt(rank)) * 8 + (file.charCodeAt(0) - 97);
  146. position[idx] = { wp: 'P', bp: 'p', wr: 'R', br: 'r', wn: 'N', bn: 'n',
  147. wb: 'B', bb: 'b', wq: 'Q', bq: 'q', wk: 'K', bk: 'k' }[piece];
  148. }
  149. });
  150.  
  151. const fen = position.reduce((fen, p, i) => {
  152. if (i % 8 === 0 && i > 0) fen += '/';
  153. if (!p) {
  154. let empty = 1;
  155. while (i + 1 < 64 && !position[i + 1] && (i + 1) % 8 !== 0) {
  156. empty++;
  157. i++;
  158. }
  159. return fen + empty;
  160. }
  161. return fen + p;
  162. }, '') + ` ${board.game.getTurn() || 'w'} - - 0 1`;
  163. chess.load(fen);
  164. return chess.fen();
  165. },
  166.  
  167. uciToSquare: (uci) => {
  168. const [from, to] = [uci.slice(0, 2), uci.slice(2, 4)];
  169. return {
  170. from: (8 - parseInt(from[1])) * 8 + (from[0].charCodeAt(0) - 97) + 1,
  171. to: (8 - parseInt(to[1])) * 8 + (to[0].charCodeAt(0) - 97) + 1
  172. };
  173. }
  174. };
  175.  
  176. const chessEngine = {
  177. lastDepth: 11,
  178.  
  179. fetchMove: (fen, depth) => {
  180. if (Date.now() - state.lastMoveTime < config.RATE_LIMIT || state.isThinking) {
  181. return setTimeout(() => chessEngine.fetchMove(fen, depth), config.RATE_LIMIT);
  182. }
  183.  
  184. state.isThinking = true;
  185. ui.updateStatus("Thinking...");
  186. GM_xmlhttpRequest({
  187. method: "GET",
  188. url: `${config.API_URL}?fen=${encodeURIComponent(fen)}&depth=${depth}`,
  189. onload: (res) => {
  190. try {
  191. const data = JSON.parse(res.responseText);
  192. if (data.success) {
  193. chessEngine.handleMove(data);
  194. } else {
  195. ui.updateStatus("API Failed");
  196. }
  197. } catch (e) {
  198. console.error("API Error:", e);
  199. ui.updateStatus("API Error");
  200. }
  201. state.isThinking = false;
  202. },
  203. onerror: () => {
  204. state.isThinking = false;
  205. ui.updateStatus("Network Error");
  206. }
  207. });
  208. },
  209.  
  210. handleMove: (data) => {
  211. const bestMove = data.bestmove.split(' ')[1];
  212. const altMove = data.altmove?.split(' ')[1];
  213. state.lastMoveTime = Date.now();
  214.  
  215. const shouldMakeMistake = Math.random() < state.mistakePercentage / 100;
  216. const moveToUse = (shouldMakeMistake && altMove) ? altMove : bestMove;
  217.  
  218. if (state.visualOnly) {
  219. ui.visualHighlight(moveToUse);
  220. } else if (state.autoMove) {
  221. setTimeout(() => chessEngine.executeMove(moveToUse), utils.getRandomDelay());
  222. } else {
  223. ui.highlightMove(moveToUse);
  224. }
  225. ui.updateStatus("Idle");
  226. },
  227.  
  228. executeMove: async (uci) => {
  229. const { from, to } = utils.uciToSquare(uci);
  230. const fromEl = $(state.board).find(`.square-${from} .piece`)[0];
  231. const toEl = $(state.board).find(`.square-${to}`)[0];
  232.  
  233. if (!fromEl || !toEl) {
  234. console.error("Move elements not found:", { from, to });
  235. state.isThinking = false;
  236. ui.updateStatus("Move Error");
  237. return;
  238. }
  239.  
  240. if (await utils.simulateHumanMove(fromEl, toEl)) {
  241. state.lastFen = utils.getFen();
  242. state.lastTurn = state.board.game?.getTurn();
  243. } else {
  244. console.error("Move simulation failed");
  245. state.isThinking = false;
  246. ui.updateStatus("Move Failed");
  247. }
  248. },
  249.  
  250. startNewGame: async () => {
  251. if (state.hasAutoMatched || !state.gameEnded) return;
  252.  
  253. // Handle decline button if present
  254. const declineButton = $('.cc-button-component.cc-button-secondary[aria-label="Decline"]')[0];
  255. if (declineButton) {
  256. await utils.simulateHumanClick(declineButton);
  257. await new Promise(resolve => setTimeout(resolve, utils.getRandomDelay()));
  258. }
  259.  
  260. // Look for "New <time> min" button dynamically
  261. const gameOverModal = $('.game-over-modal-content');
  262. if (gameOverModal.length) {
  263. // Find button with text matching "New" followed by time
  264. const newGameButton = gameOverModal.find('.cc-button-component').filter(function() {
  265. const text = $(this).text().trim();
  266. return text.match(/^New\s+\d+(\.\d+)?\s+min$/i);
  267. })[0];
  268. if (newGameButton) {
  269. await utils.simulateHumanClick(newGameButton);
  270. state.hasAutoMatched = true;
  271. return;
  272. }
  273. }
  274.  
  275. // Alternative location for new game button
  276. const newGameButtons = $('.game-over-buttons-component .cc-button-component').filter(function() {
  277. const text = $(this).text().trim();
  278. return text.match(/^New\s+\d+(\.\d+)?\s+min$/i) && !$(this).attr('aria-label')?.includes('Rematch');
  279. })[0];
  280. if (newGameButtons) {
  281. await utils.simulateHumanClick(newGameButtons);
  282. state.hasAutoMatched = true;
  283. return;
  284. }
  285.  
  286. // Fallback to main play button
  287. const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full')[0];
  288. if (playButton) {
  289. await utils.simulateHumanClick(playButton);
  290. state.hasAutoMatched = true;
  291. }
  292. }
  293. };
  294.  
  295. const ui = {
  296. loaded: false,
  297.  
  298. init: () => {
  299. if (ui.loaded) return;
  300. const checkBoard = () => {
  301. state.board = document.querySelector('chess-board, wc-chess-board');
  302. return state.board && state.board.game;
  303. };
  304. if (!checkBoard()) {
  305. setTimeout(ui.init, 1000);
  306. return;
  307. }
  308.  
  309. const panel = $(`<div style="position: fixed; top: 10px; right: 10px; z-index: 10000;
  310. background: #f9f9f9; padding: 10px; border: 1px solid #333; border-radius: 5px;">
  311. <p id="depthText">Depth: <strong>${chessEngine.lastDepth}</strong></p>
  312. <button id="depthMinus">-</button>
  313. <button id="depthPlus">+</button>
  314. <label><input type="checkbox" id="autoRun"> Auto Run</label><br>
  315. <label><input type="checkbox" id="autoMove"> Auto Move</label><br>
  316. <label><input type="checkbox" id="autoMatch"> Auto Match</label><br>
  317. <label><input type="checkbox" id="visualOnly"> Visual Only</label><br>
  318. <label>Mistakes %: <input type="number" id="mistakePercentage" min="0" max="100" value="0" style="width: 50px;"></label><br>
  319. <p id="statusMessage">Idle</p>
  320. <p style="font-size: 10px; color: #666;">Set Mistakes % > 15% to reduce detection risk</p>
  321. </div>`).appendTo(document.body);
  322.  
  323. $('#depthPlus').click(() => {
  324. chessEngine.lastDepth = Math.min(config.MAX_DEPTH, chessEngine.lastDepth + 1);
  325. $('#depthText').html(`Depth: <strong>${chessEngine.lastDepth}</strong>`);
  326. });
  327. $('#depthMinus').click(() => {
  328. chessEngine.lastDepth = Math.max(config.MIN_DEPTH, chessEngine.lastDepth - 1);
  329. $('#depthText').html(`Depth: <strong>${chessEngine.lastDepth}</strong>`);
  330. });
  331. $('#autoRun').change(function() { state.autoRun = this.checked; });
  332. $('#autoMove').change(function() { state.autoMove = this.checked; });
  333. $('#autoMatch').change(function() {
  334. state.autoMatch = this.checked;
  335. if (state.autoMatch && state.gameEnded) chessEngine.startNewGame();
  336. });
  337. $('#visualOnly').change(function() { state.visualOnly = this.checked; });
  338. $('#mistakePercentage').change(function() {
  339. state.mistakePercentage = Math.max(0, Math.min(100, parseInt(this.value) || 0));
  340. this.value = state.mistakePercentage;
  341. });
  342.  
  343. ui.loaded = true;
  344. },
  345.  
  346. updateStatus: (msg) => {
  347. $('#statusMessage').text(msg);
  348. },
  349.  
  350. highlightMove: (uci) => {
  351. const { from, to } = utils.uciToSquare(uci);
  352. $(state.board).find(`.square-${from}, .square-${to}`)
  353. .css('background-color', 'rgba(235, 97, 80, 0.5)')
  354. .delay(2000)
  355. .queue(function() { $(this).css('background-color', ''); $(this).dequeue(); });
  356. },
  357.  
  358. visualHighlight: (uci) => {
  359. const { to } = utils.uciToSquare(uci);
  360. $(state.board).find(`.square-${to}`)
  361. .append('<div class="visual" style="position: absolute; width: 100%; height: 100%; border: 2px solid green; opacity: 0.6;">')
  362. .find('.visual')
  363. .delay(2000)
  364. .fadeOut(300, function() { $(this).remove(); });
  365. }
  366. };
  367.  
  368. const mainLoop = setInterval(() => {
  369. if (!ui.loaded) ui.init();
  370. if (!state.board) state.board = document.querySelector('chess-board, wc-chess-board');
  371. if (!state.board?.game) return;
  372.  
  373. const fen = utils.getFen();
  374. const turn = state.board.game.getTurn();
  375. const myTurn = turn === state.board.game.getPlayingAs();
  376. const gameOver = document.querySelector('.game-over-message-component') || document.querySelector('.game-result');
  377.  
  378. if (gameOver && !state.gameEnded) {
  379. state.gameEnded = true;
  380. state.isThinking = false;
  381. if (state.autoMatch) {
  382. setTimeout(chessEngine.startNewGame, utils.getRandomDelay());
  383. }
  384. } else if (!gameOver && state.gameEnded) {
  385. state.gameEnded = false;
  386. state.hasAutoMatched = false;
  387. }
  388.  
  389. if (state.autoRun && myTurn && !state.isThinking && fen !== state.lastFen) {
  390. chessEngine.fetchMove(fen, chessEngine.lastDepth);
  391. state.lastFen = fen;
  392. }
  393. }, 500);
  394.  
  395. $(document).keydown((e) => {
  396. const depthKeys = 'qwertyuiopasdfg'.split('').reduce((obj, key, i) => {
  397. obj[key.charCodeAt(0)] = i + 1;
  398. return obj;
  399. }, {});
  400.  
  401. if (e.keyCode in depthKeys && !state.isThinking) {
  402. chessEngine.fetchMove(utils.getFen(), depthKeys[e.keyCode]);
  403. }
  404. });
  405.  
  406. setTimeout(() => {
  407. if (!ui.loaded) ui.init();
  408. }, 2000);
  409. })();