// ==UserScript==
// @name Chess.com Stockfish Assistant
// @namespace BottleOrg Scripts
// @version 1.6.6
// @description Chess analysis tool with Stockfish integration and auto-match
// @author [REDACTED] - Rightful owner & Contributors & Gemini 2.5 Pro, Chatgpt-4o
// @match https://www.chess.com/play/*
// @match https://www.chess.com/game/*
// @match https://www.chess.com/puzzles/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.2/chess.min.js
// @run-at document-start
// @license 2025, [email protected], All Rights Reserved
// @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.
// ==/UserScript==
(function() {
'use strict';
const config = {
API_URL: "https://stockfish.online/api/s/v2.php",
MIN_DELAY: 0.8,
MAX_DELAY: 3.0,
MAX_DEPTH: 15,
MIN_DEPTH: 1,
RATE_LIMIT: 1200,
MISTAKE_THRESHOLD: 0.15
};
let state = {
autoMove: false,
autoRun: false,
autoMatch: false,
visualOnly: false,
mistakePercentage: 0,
lastMoveTime: 0,
lastFen: null,
lastTurn: null,
isThinking: false,
canMove: true,
board: null,
gameEnded: false,
hasAutoMatched: false
};
const utils = {
getRandomDelay: () => {
const base = Math.random() * (config.MAX_DELAY - config.MIN_DELAY) + config.MIN_DELAY;
return base * 1000 + (Math.random() * 300 - 150);
},
simulateHumanClick: (element) => {
if (!element) return Promise.resolve(false);
const rect = element.getBoundingClientRect();
const x = rect.left + rect.width / 2 + (Math.random() * 10 - 5);
const y = rect.top + rect.height / 2 + (Math.random() * 10 - 5);
return new Promise(resolve => {
const eventProps = {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0,
buttons: 1,
view: window
};
const down = new MouseEvent('mousedown', eventProps);
const up = new MouseEvent('mouseup', eventProps);
const click = new MouseEvent('click', eventProps);
element.dispatchEvent(down);
setTimeout(() => {
element.dispatchEvent(up);
element.dispatchEvent(click);
resolve(true);
}, 50 + Math.random() * 100);
});
},
simulateHumanMove: (fromElement, toElement) => {
if (!fromElement || !toElement) return Promise.resolve(false);
const fromRect = fromElement.getBoundingClientRect();
const toRect = toElement.getBoundingClientRect();
const steps = 12;
const time = 250 + Math.random() * 150;
const dispatchEvent = (element, type, x, y) => {
return new Promise(resolve => {
const event = new MouseEvent(type, {
bubbles: true,
cancelable: true,
clientX: x,
clientY: y,
button: 0,
buttons: type === 'mouseup' ? 0 : 1,
view: window
});
setTimeout(() => {
element.dispatchEvent(event);
resolve();
}, time / steps);
});
};
const movePath = [];
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const x = fromRect.left + (toRect.left - fromRect.left) * t + (Math.random() * 6 - 3);
const y = fromRect.top + (toRect.top - fromRect.top) * t + (Math.random() * 6 - 3);
movePath.push({ x, y });
}
return new Promise(async resolve => {
await dispatchEvent(fromElement, 'mousedown', movePath[0].x, movePath[0].y);
for (let i = 1; i < movePath.length - 1; i++) {
await dispatchEvent(fromElement, 'mousemove', movePath[i].x, movePath[i].y);
}
await dispatchEvent(toElement, 'mousemove', movePath[steps].x, movePath[steps].y);
await dispatchEvent(toElement, 'mouseup', movePath[steps].x, movePath[steps].y);
resolve(true);
});
},
getFen: () => {
const board = state.board;
if (!board || !board.game) return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
const chess = new Chess();
const pieces = $(board).find(".piece");
if (!pieces.length) return chess.fen();
const position = Array(64).fill('');
pieces.each((_, el) => {
const classes = el.className.split(' ');
const square = classes.find(c => c.startsWith('square-'))?.replace('square-', '');
const piece = classes.find(c => /^[wb][prnbqk]$/.test(c));
if (square && piece) {
const [file, rank] = square.split('');
const idx = (8 - parseInt(rank)) * 8 + (file.charCodeAt(0) - 97);
position[idx] = { wp: 'P', bp: 'p', wr: 'R', br: 'r', wn: 'N', bn: 'n',
wb: 'B', bb: 'b', wq: 'Q', bq: 'q', wk: 'K', bk: 'k' }[piece];
}
});
const fen = position.reduce((fen, p, i) => {
if (i % 8 === 0 && i > 0) fen += '/';
if (!p) {
let empty = 1;
while (i + 1 < 64 && !position[i + 1] && (i + 1) % 8 !== 0) {
empty++;
i++;
}
return fen + empty;
}
return fen + p;
}, '') + ` ${board.game.getTurn() || 'w'} - - 0 1`;
chess.load(fen);
return chess.fen();
},
uciToSquare: (uci) => {
const [from, to] = [uci.slice(0, 2), uci.slice(2, 4)];
return {
from: (8 - parseInt(from[1])) * 8 + (from[0].charCodeAt(0) - 97) + 1,
to: (8 - parseInt(to[1])) * 8 + (to[0].charCodeAt(0) - 97) + 1
};
}
};
const chessEngine = {
lastDepth: 11,
fetchMove: (fen, depth) => {
if (Date.now() - state.lastMoveTime < config.RATE_LIMIT || state.isThinking) {
return setTimeout(() => chessEngine.fetchMove(fen, depth), config.RATE_LIMIT);
}
state.isThinking = true;
ui.updateStatus("Thinking...");
GM_xmlhttpRequest({
method: "GET",
url: `${config.API_URL}?fen=${encodeURIComponent(fen)}&depth=${depth}`,
onload: (res) => {
try {
const data = JSON.parse(res.responseText);
if (data.success) {
chessEngine.handleMove(data);
} else {
ui.updateStatus("API Failed");
}
} catch (e) {
console.error("API Error:", e);
ui.updateStatus("API Error");
}
state.isThinking = false;
},
onerror: () => {
state.isThinking = false;
ui.updateStatus("Network Error");
}
});
},
handleMove: (data) => {
const bestMove = data.bestmove.split(' ')[1];
const altMove = data.altmove?.split(' ')[1];
state.lastMoveTime = Date.now();
const shouldMakeMistake = Math.random() < state.mistakePercentage / 100;
const moveToUse = (shouldMakeMistake && altMove) ? altMove : bestMove;
if (state.visualOnly) {
ui.visualHighlight(moveToUse);
} else if (state.autoMove) {
setTimeout(() => chessEngine.executeMove(moveToUse), utils.getRandomDelay());
} else {
ui.highlightMove(moveToUse);
}
ui.updateStatus("Idle");
},
executeMove: async (uci) => {
const { from, to } = utils.uciToSquare(uci);
const fromEl = $(state.board).find(`.square-${from} .piece`)[0];
const toEl = $(state.board).find(`.square-${to}`)[0];
if (!fromEl || !toEl) {
console.error("Move elements not found:", { from, to });
state.isThinking = false;
ui.updateStatus("Move Error");
return;
}
if (await utils.simulateHumanMove(fromEl, toEl)) {
state.lastFen = utils.getFen();
state.lastTurn = state.board.game?.getTurn();
} else {
console.error("Move simulation failed");
state.isThinking = false;
ui.updateStatus("Move Failed");
}
},
startNewGame: async () => {
if (state.hasAutoMatched || !state.gameEnded) return;
// Handle decline button if present
const declineButton = $('.cc-button-component.cc-button-secondary[aria-label="Decline"]')[0];
if (declineButton) {
await utils.simulateHumanClick(declineButton);
await new Promise(resolve => setTimeout(resolve, utils.getRandomDelay()));
}
// Look for "New <time> min" button dynamically
const gameOverModal = $('.game-over-modal-content');
if (gameOverModal.length) {
// Find button with text matching "New" followed by time
const newGameButton = gameOverModal.find('.cc-button-component').filter(function() {
const text = $(this).text().trim();
return text.match(/^New\s+\d+(\.\d+)?\s+min$/i);
})[0];
if (newGameButton) {
await utils.simulateHumanClick(newGameButton);
state.hasAutoMatched = true;
return;
}
}
// Alternative location for new game button
const newGameButtons = $('.game-over-buttons-component .cc-button-component').filter(function() {
const text = $(this).text().trim();
return text.match(/^New\s+\d+(\.\d+)?\s+min$/i) && !$(this).attr('aria-label')?.includes('Rematch');
})[0];
if (newGameButtons) {
await utils.simulateHumanClick(newGameButtons);
state.hasAutoMatched = true;
return;
}
// Fallback to main play button
const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full')[0];
if (playButton) {
await utils.simulateHumanClick(playButton);
state.hasAutoMatched = true;
}
}
};
const ui = {
loaded: false,
init: () => {
if (ui.loaded) return;
const checkBoard = () => {
state.board = document.querySelector('chess-board, wc-chess-board');
return state.board && state.board.game;
};
if (!checkBoard()) {
setTimeout(ui.init, 1000);
return;
}
const panel = $(`<div style="position: fixed; top: 10px; right: 10px; z-index: 10000;
background: #f9f9f9; padding: 10px; border: 1px solid #333; border-radius: 5px;">
<p id="depthText">Depth: <strong>${chessEngine.lastDepth}</strong></p>
<button id="depthMinus">-</button>
<button id="depthPlus">+</button>
<label><input type="checkbox" id="autoRun"> Auto Run</label><br>
<label><input type="checkbox" id="autoMove"> Auto Move</label><br>
<label><input type="checkbox" id="autoMatch"> Auto Match</label><br>
<label><input type="checkbox" id="visualOnly"> Visual Only</label><br>
<label>Mistakes %: <input type="number" id="mistakePercentage" min="0" max="100" value="0" style="width: 50px;"></label><br>
<p id="statusMessage">Idle</p>
<p style="font-size: 10px; color: #666;">Set Mistakes % > 15% to reduce detection risk</p>
</div>`).appendTo(document.body);
$('#depthPlus').click(() => {
chessEngine.lastDepth = Math.min(config.MAX_DEPTH, chessEngine.lastDepth + 1);
$('#depthText').html(`Depth: <strong>${chessEngine.lastDepth}</strong>`);
});
$('#depthMinus').click(() => {
chessEngine.lastDepth = Math.max(config.MIN_DEPTH, chessEngine.lastDepth - 1);
$('#depthText').html(`Depth: <strong>${chessEngine.lastDepth}</strong>`);
});
$('#autoRun').change(function() { state.autoRun = this.checked; });
$('#autoMove').change(function() { state.autoMove = this.checked; });
$('#autoMatch').change(function() {
state.autoMatch = this.checked;
if (state.autoMatch && state.gameEnded) chessEngine.startNewGame();
});
$('#visualOnly').change(function() { state.visualOnly = this.checked; });
$('#mistakePercentage').change(function() {
state.mistakePercentage = Math.max(0, Math.min(100, parseInt(this.value) || 0));
this.value = state.mistakePercentage;
});
ui.loaded = true;
},
updateStatus: (msg) => {
$('#statusMessage').text(msg);
},
highlightMove: (uci) => {
const { from, to } = utils.uciToSquare(uci);
$(state.board).find(`.square-${from}, .square-${to}`)
.css('background-color', 'rgba(235, 97, 80, 0.5)')
.delay(2000)
.queue(function() { $(this).css('background-color', ''); $(this).dequeue(); });
},
visualHighlight: (uci) => {
const { to } = utils.uciToSquare(uci);
$(state.board).find(`.square-${to}`)
.append('<div class="visual" style="position: absolute; width: 100%; height: 100%; border: 2px solid green; opacity: 0.6;">')
.find('.visual')
.delay(2000)
.fadeOut(300, function() { $(this).remove(); });
}
};
const mainLoop = setInterval(() => {
if (!ui.loaded) ui.init();
if (!state.board) state.board = document.querySelector('chess-board, wc-chess-board');
if (!state.board?.game) return;
const fen = utils.getFen();
const turn = state.board.game.getTurn();
const myTurn = turn === state.board.game.getPlayingAs();
const gameOver = document.querySelector('.game-over-message-component') || document.querySelector('.game-result');
if (gameOver && !state.gameEnded) {
state.gameEnded = true;
state.isThinking = false;
if (state.autoMatch) {
setTimeout(chessEngine.startNewGame, utils.getRandomDelay());
}
} else if (!gameOver && state.gameEnded) {
state.gameEnded = false;
state.hasAutoMatched = false;
}
if (state.autoRun && myTurn && !state.isThinking && fen !== state.lastFen) {
chessEngine.fetchMove(fen, chessEngine.lastDepth);
state.lastFen = fen;
}
}, 500);
$(document).keydown((e) => {
const depthKeys = 'qwertyuiopasdfg'.split('').reduce((obj, key, i) => {
obj[key.charCodeAt(0)] = i + 1;
return obj;
}, {});
if (e.keyCode in depthKeys && !state.isThinking) {
chessEngine.fetchMove(utils.getFen(), depthKeys[e.keyCode]);
}
});
setTimeout(() => {
if (!ui.loaded) ui.init();
}, 2000);
})();