Chess.com Stockfish Bot

Chess.com Stockfish Bot with decent stability(will keep on fixing!)

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Chess.com Stockfish Bot
// @namespace    BottleOrg Scripts
// @version      1.7.1
// @description  Chess.com Stockfish Bot with decent stability(will keep on fixing!)
// @author       [REDACTED] - Rightful Owner([email protected]), BottleOrg([email protected]), Gemini 2.5 Pro
// @license      Chess.com Bot/Cheat by BottleOrg([email protected]) and its rightful owner!
// @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
// @grant        GM_getResourceText
// @grant        GM_registerMenuCommand
// @require      https://greasyfork.org/scripts/445697/code/index.js
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @run-at       document-start
// ==/UserScript==

const currentVersion = '1.7.1';
const scriptURL = 'https://greasyfork.org/en/scripts/542194-chess-com-stockfish-bot/code';

function checkForUpdate() {
    console.log("Checking for script updates...");
    GM_xmlhttpRequest({
        method: "GET",
        url: scriptURL,
        onload: function(response) {
            if (response.status === 200) {
                const html = response.responseText;
                const versionMatch = html.match(/@version\s+([\d.]+)/);
                if (versionMatch && versionMatch[1]) {
                    const latestVersion = versionMatch[1];
                    console.log("Latest version found:", latestVersion);
                    if (compareVersions(latestVersion, currentVersion) > 0) {
                        const message = `New Version: ${latestVersion} has been uploaded. Would you like me to take you there or continue with old version ${currentVersion}? (Not recommended for stability)`;
                        if (confirm(message)) {
                            window.location.href = scriptURL;
                        } else {
                            console.log("User chose to continue with old version.");
                        }
                    } else {
                        console.log("No newer version available.");
                    }
                } else {
                    console.error("Could not find version in Greasy Fork page.");
                }
            } else {
                console.error("Failed to fetch script page:", response.status);
            }
        },
        onerror: function(error) {
            console.error("Error checking for update:", error);
        }
    });
}

function compareVersions(v1, v2) {
    const parts1 = v1.split('.').map(Number);
    const parts2 = v2.split('.').map(Number);
    for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
        const p1 = parts1[i] || 0;
        const p2 = parts2[i] || 0;
        if (p1 > p2) return 1;
        if (p1 < p2) return -1;
    }
    return 0;
}

function main() {
    let myVars = document.myVars = {
        autoMove: false,
        autoRun: false,
        autoMatch: false,
        delay: 0.1,
        hasAutoMatched: false,
        gameEnded: false
    };
    let myFunctions = document.myFunctions = {};
    const currentStockfishVersion = "Stockfish API";
    let uiElementsLoaded = false;
    const stockfishAPI_URI = "https://stockfish.online/api/s/v2.php";
    let stop_b = 0, stop_w = 0;
    let board;

    myFunctions.rescan = function() {
        console.log("Rescanning board...");
        const boardElement = document.querySelector('chess-board, wc-chess-board');
        if (!boardElement) {
            console.warn("No board element found. Using default FEN.");
            return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
        }
        const pieces = $(boardElement).find(".piece").map(function() {
            return this.className;
        }).get();
        if (!pieces.length) {
            console.warn("No pieces found. Using default FEN.");
            return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
        }
        let boardArray = Array(64).fill('');
        pieces.forEach(piece => {
            const classes = piece.split(' ');
            const squareClass = classes.find(c => c.startsWith('square-'));
            const pieceClass = classes.find(c => /^[wb][prnbqk]$/.test(c));
            if (squareClass && pieceClass) {
                const squareNum = squareClass.replace('square-', '');
                const file = parseInt(squareNum[0]) - 1;
                const rank = parseInt(squareNum[1]) - 1;
                const square = (7 - rank) * 8 + file;
                if (square >= 0 && square < 64) {
                    const pieceChar = {
                        'wp': 'P', 'bp': 'p', 'wr': 'R', 'br': 'r',
                        'wn': 'N', 'bn': 'n', 'wb': 'B', 'bb': 'b',
                        'wq': 'Q', 'bq': 'q', 'wk': 'K', 'bk': 'k'
                    }[pieceClass];
                    boardArray[square] = pieceChar;
                }
            }
        });
        let fen = '';
        for (let i = 0; i < 64; i++) {
            if (i % 8 === 0 && i > 0) fen += '/';
            const piece = boardArray[i];
            if (!piece) {
                let emptyCount = 1;
                while (i + 1 < 64 && !boardArray[i + 1] && (i + 1) % 8 !== 0) {
                    emptyCount++;
                    i++;
                }
                fen += emptyCount;
            } else {
                fen += piece;
            }
        }
        const turn = $('.coordinates').children().first().text() === "1" ? 'b' : 'w';
        const castling = ((stop_w ? '' : 'KQ') + (stop_b ? '' : 'kq')) || '-';
        fen += ` ${turn} ${castling} - 0 1`;
        console.log("Generated FEN:", fen);
        return fen;
    };

    myFunctions.color = function(dat) {
        console.log("myFunctions.color CALLED with:", dat);
        const bestmoveUCI = dat.split(' ')[1];
        console.log("Extracted bestmove UCI:", bestmoveUCI);
        if (myVars.autoMove) myFunctions.movePiece(bestmoveUCI);
        else myFunctions.highlightMove(bestmoveUCI);
        isThinking = false;
        myFunctions.spinner();
    };

    myFunctions.highlightMove = function(bestmoveUCI) {
        const res1 = bestmoveUCI.substring(0, 2);
        const res2 = bestmoveUCI.substring(2, 4);
        $(board).prepend(`<div class="highlight square-${res2}" style="background-color: rgb(235, 97, 80); opacity: 0.71;"></div>`)
            .children(':first').delay(1800).queue(function() { $(this).remove(); });
        $(board).prepend(`<div class="highlight square-${res1}" style="background-color: rgb(235, 97, 80); opacity: 0.71;"></div>`)
            .children(':first').delay(1800).queue(function() { $(this).remove(); });
        console.log("Highlighted:", bestmoveUCI);
    };

    myFunctions.movePiece = function(bestmoveUCI) {
        console.log("movePiece CALLED with:", bestmoveUCI);
        if (!board || !board.game) {
            console.error("Board or board.game not initialized!");
            return;
        }
        const fromSquare = bestmoveUCI.substring(0, 2);
        const toSquare = bestmoveUCI.substring(2, 4);
        const legalMoves = board.game.getLegalMoves();
        console.log("Legal moves:", legalMoves);
        const foundMove = legalMoves.find(move => move.from === fromSquare && move.to === toSquare);
        if (foundMove) {
            console.log("Executing move:", foundMove);
            board.game.move({ ...foundMove, promotion: 'q', animate: true, userGenerated: true });
            console.log("Move executed:", bestmoveUCI);
        } else {
            console.warn("No legal move found for:", bestmoveUCI);
        }
    };

    myFunctions.reloadChessEngine = function() {
        console.log("Reload not needed for API.");
    };

    myFunctions.loadChessEngine = function() {
        console.log("Using Stockfish API.");
        if (uiElementsLoaded) $('#engineVersionText')[0].innerHTML = "Engine: <strong>Stockfish API</strong>";
    };

    myFunctions.fetchBestMoveFromAPI = function(fen, depth) {
        const apiURL = `${stockfishAPI_URI}?fen=${encodeURIComponent(fen)}&depth=${depth}`;
        console.log(`Fetching from: ${apiURL}`);
        GM_xmlhttpRequest({
            method: "GET",
            url: apiURL,
            onload: function(response) {
                if (response.status === 200) {
                    try {
                        const jsonResponse = JSON.parse(response.responseText);
                        if (jsonResponse.success) {
                            console.log("API Response:", jsonResponse);
                            myFunctions.color(jsonResponse.bestmove);
                        } else {
                            console.error("API failed:", jsonResponse);
                            isThinking = false;
                            myFunctions.spinner();
                        }
                    } catch (e) {
                        console.error("API parse error:", e);
                        isThinking = false;
                        myFunctions.spinner();
                    }
                } else {
                    console.error("API error:", response.status);
                    isThinking = false;
                    myFunctions.spinner();
                }
            },
            onerror: function(error) {
                console.error("API request error:", error);
                isThinking = false;
                myFunctions.spinner();
            }
        });
    };

    myFunctions.startNewGame = function() {
        console.log("Attempting to start a new game with auto-detection...");

        // Robust selector for any "New Game" button in the game over modal/screen that isn't a "Rematch" button.
        const newGameButtonSelector = '.game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])';
        const newGameButton = $(newGameButtonSelector);

        if (newGameButton.length > 0) {
            console.log("Found 'New Game' button with text:", newGameButton.text().trim());
            newGameButton[0].click();
            myVars.hasAutoMatched = true;
            return; // Exit after clicking
        }

        // Fallback for cases where the game-over screen might be different
        const guestButton = $('#guest-button.authentication-intro-guest');
        if (guestButton.length) {
            guestButton[0].click();
            console.log("Clicked Play as Guest.");
            setTimeout(() => {
                const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full');
                if (playButton.length) {
                    playButton[0].click();
                    console.log("Clicked Play button after guest prompt.");
                    myVars.hasAutoMatched = true;
                } else {
                    console.error("Play button not found after guest prompt!");
                }
            }, 500);
            return;
        }

        const newGameTab = $('[data-tab="newGame"]');
        if (newGameTab.length) {
            newGameTab[0].click();
            console.log("Clicked New Game tab.");
            setTimeout(() => {
                const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full');
                if (playButton.length) {
                    playButton[0].click();
                    console.log("Clicked Play button.");
                    myVars.hasAutoMatched = true;
                } else {
                    console.error("Play button not found!");
                }
            }, 500);
            return;
        }
        console.warn("Could not find any 'New Game' button to click.");
    };

    myFunctions.declineRematch = function() {
        const declineButton = $('.cc-button-component.cc-button-secondary[aria-label="Decline"], .cc-button-component.cc-button-secondary:contains("Decline")');
        if (declineButton.length) {
            declineButton[0].click();
            console.log("Declined rematch.");
            return true;
        } else {
            console.log("No rematch decline button found.");
            return false;
        }
    };

    let lastValue = 11, MAX_DEPTH = 15, MIN_DEPTH = 1;

    myFunctions.runChessEngine = function(depth) {
        depth = Math.max(MIN_DEPTH, Math.min(MAX_DEPTH, depth));
        const fen = myFunctions.rescan();
        console.log(`Analyzing FEN: ${fen}, Depth: ${depth}`);
        isThinking = true;
        myFunctions.spinner();
        myFunctions.fetchBestMoveFromAPI(fen, depth);
        lastValue = depth;
        updateDepthDisplay();
    };

    function updateDepthDisplay() {
        if (uiElementsLoaded && $('#depthText')[0]) {
            $('#depthText')[0].innerHTML = `Depth: <strong>${lastValue}</strong>`;
        }
    }

    myFunctions.incrementDepth = function(delta) {
        lastValue = Math.max(MIN_DEPTH, Math.min(MAX_DEPTH, lastValue + delta));
        updateDepthDisplay();
    };

    myFunctions.autoRun = function() {
        if (board && board.game && board.game.getTurn() === board.game.getPlayingAs()) {
            myFunctions.runChessEngine(lastValue);
        }
    };

    document.onkeydown = function(e) {
        switch (e.keyCode) {
            case 81: myFunctions.runChessEngine(1); break;
            case 87: myFunctions.runChessEngine(2); break;
            case 69: myFunctions.runChessEngine(3); break;
            case 82: myFunctions.runChessEngine(4); break;
            case 84: myFunctions.runChessEngine(5); break;
            case 89: myFunctions.runChessEngine(6); break;
            case 85: myFunctions.runChessEngine(7); break;
            case 73: myFunctions.runChessEngine(8); break;
            case 79: myFunctions.runChessEngine(9); break;
            case 80: myFunctions.runChessEngine(10); break;
            case 65: myFunctions.runChessEngine(11); break;
            case 83: myFunctions.runChessEngine(12); break;
            case 68: myFunctions.runChessEngine(13); break;
            case 70: myFunctions.runChessEngine(14); break;
            case 71: myFunctions.runChessEngine(15); break;
            case 187: myFunctions.incrementDepth(1); break;
            case 189: myFunctions.incrementDepth(-1); break;
        }
    };

    myFunctions.spinner = function() {
        if (uiElementsLoaded && $('#overlay')[0]) {
            $('#overlay')[0].style.display = isThinking ? 'block' : 'none';
        }
    };

    let dynamicStyles = null;
    function addAnimation(rule) {
        if (!dynamicStyles) {
            dynamicStyles = document.createElement('style');
            document.head.appendChild(dynamicStyles);
        }
        dynamicStyles.sheet.insertRule(rule, dynamicStyles.sheet.cssRules.length);
    }

    function addUIStyles() {
        const style = document.createElement('style');
        style.innerHTML = `
            .chess-ui-panel { width: 280px; background: linear-gradient(135deg, #ffe6e6, #fff5f5); border: 2px solid #ffcccc; border-radius: 12px; box-shadow: 4px 4px 16px rgba(0,0,0,0.3); animation: fadeIn 1s ease-out; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; user-select: none; }
            .chess-ui-header { background: #ffcccc; border-bottom: 1px solid #ffb3b3; border-top-left-radius: 10px; border-top-right-radius: 10px; padding: 8px; cursor: move; font-weight: bold; color: #800000; text-align: center; }
            .chess-ui-body { padding: 10px; }
            .chess-ui-panel button { background: #e91e63; border: none; color: white; padding: 8px 12px; margin: 4px 2px; border-radius: 5px; cursor: pointer; transition: box-shadow 0.3s ease, transform 0.2s ease; }
            .chess-ui-panel button:hover { box-shadow: 0 0 8px #ff69b4; transform: scale(1.05); }
            .chess-ui-panel input[type="checkbox"], .chess-ui-panel input[type="number"] { margin: 5px 0; padding: 4px; border: 1px solid #ffcccc; border-radius: 4px; }
            @keyframes fadeIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } }
        `;
        document.head.appendChild(style);
    }

    function makeDraggable(panel, header) {
        let offsetX, offsetY;
        header.addEventListener('mousedown', e => {
            e.preventDefault();
            const rect = panel.getBoundingClientRect();
            offsetX = e.clientX - rect.left;
            offsetY = e.clientY - rect.top;
            document.addEventListener('mousemove', elementDrag);
            document.addEventListener('mouseup', closeDragElement);
        });
        function elementDrag(e) {
            e.preventDefault();
            panel.style.left = (e.clientX - offsetX) + "px";
            panel.style.top = (e.clientY - offsetY) + "px";
        }
        function closeDragElement() {
            document.removeEventListener('mousemove', elementDrag);
            document.removeEventListener('mouseup', closeDragElement);
        }
    }

    let loaded = false;
    myFunctions.loadEx = function() {
        try {
            console.log("Attempting to load UI...");
            board = document.querySelector('chess-board, wc-chess-board');
            addUIStyles();
            const panel = document.createElement('div');
            panel.className = "chess-ui-panel";
            panel.style.position = 'fixed'; panel.style.top = '10px'; panel.style.right = '10px'; panel.style.zIndex = '10000';
            panel.innerHTML = `
                <div class="chess-ui-header">Stockfish Bot</div>
                <div class="chess-ui-body">
                    <p id="depthText">Depth: <strong>${lastValue}</strong></p>
                    <button id="depthMinus">-</button> <button id="depthPlus">+</button>
                    <p style="font-size: 12px;">Keys: Q-G (1-15), +/-</p>
                    <p id="engineVersionText">Engine: Stockfish API 😘</p>
                    <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>Min Delay (s): <input type="number" id="timeDelayMin" min="0.1" value="0.1" step="0.1" style="width: 60px;"></label><br>
                    <label>Max Delay (s): <input type="number" id="timeDelayMax" min="0.1" value="1" step="0.1" style="width: 60px;"></label>
                </div>`;
            document.body.appendChild(panel);
            const header = panel.querySelector('.chess-ui-header');
            makeDraggable(panel, header);

            setTimeout(() => {
                $('#depthPlus').off('click').on('click', () => myFunctions.incrementDepth(1));
                $('#depthMinus').off('click').on('click', () => myFunctions.incrementDepth(-1));
                $('#autoMatch').on('change', () => {
                    myVars.autoMatch = $('#autoMatch')[0].checked;
                    if (myVars.autoMatch && !myVars.hasAutoMatched) myFunctions.startNewGame();
                });
                console.log("Event listeners bound.");
            }, 100);

            const spinCont = document.createElement('div');
            spinCont.id = 'overlay';
            spinCont.style.cssText = 'display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);';
            panel.appendChild(spinCont);
            const spinr = document.createElement('div');
            spinr.style.cssText = "height: 64px; width: 64px; animation: rotate 0.8s infinite linear; border: 5px solid firebrick; border-right-color: transparent; border-radius: 50%;";
            spinCont.appendChild(spinr);
            addAnimation(`@keyframes rotate { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }`);

            loaded = true;
            uiElementsLoaded = true;
            console.log("UI loaded successfully.");
            myFunctions.loadChessEngine();
            checkForUpdate();
        } catch (error) {
            console.error("loadEx error:", error);
        }
    };

    function other(delay) {
        const endTime = Date.now() + delay;
        const timer = setInterval(() => {
            if (Date.now() >= endTime) {
                myFunctions.autoRun();
                canGo = true;
                clearInterval(timer);
            }
        }, 10);
    }

    const waitForChessBoard = setInterval(() => {
        if (!loaded) {
            myFunctions.loadEx();
        } else {
            if (!board) board = document.querySelector('chess-board, wc-chess-board');
            myVars.autoRun = $('#autoRun')[0].checked;
            myVars.autoMove = $('#autoMove')[0].checked;
            myVars.autoMatch = $('#autoMatch')[0].checked;
            const minDel = parseFloat($('#timeDelayMin')[0].value) || 0.1;
            const maxDel = parseFloat($('#timeDelayMax')[0].value) || 1;
            myVars.delay = Math.random() * (maxDel - minDel) + minDel;
            myFunctions.spinner();
            myTurn = board && board.game && board.game.getTurn() === board.game.getPlayingAs();
            updateDepthDisplay();

            const isGameOver = document.querySelector('.game-over-modal-content, .game-over-message-component, .game-result');

            if (isGameOver && !myVars.gameEnded) {
                console.log("Game over detected.");
                myVars.gameEnded = true;
                if (myVars.autoMatch) {
                    myFunctions.declineRematch();
                    setTimeout(() => {
                        myVars.hasAutoMatched = false;
                        myFunctions.startNewGame();
                    }, 1500); // Wait 1.5s for modal animations
                }
            } else if (!isGameOver && myVars.gameEnded) {
                console.log("New game started, resetting gameEnded flag.");
                myVars.gameEnded = false;
            }

            if (myVars.autoRun && canGo && !isThinking && myTurn) {
                canGo = false;
                other(myVars.delay * 1000);
            }
        }
    }, 500);

    setTimeout(() => {
        if (!loaded) myFunctions.loadEx();
    }, 2000);
}

let isThinking = false, canGo = true, myTurn = false;
window.addEventListener("load", () => main());