Chess.com Stockfish Bot

Chess.com Stockfish Bot with Enhanced Auto-Match using Stockfish Online API

// ==UserScript==
// @name         Chess.com Stockfish Bot
// @namespace    BottleOrg Scripts
// @version      1.7.0
// @description  Chess.com Stockfish Bot with Enhanced Auto-Match using Stockfish Online API
// @author       [REDACTED] - Rightful Owner([email protected]) + ChatGPT-4o Reasoning, GPT-3o mini advanced reasoning, a bit of Gemini 2.5 Pro
// @license      Chess.com Bot/Cheat by BottleOrg([email protected])
// @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.6.2.17';
const scriptURL = 'https://greasyfork.org/en/scripts/526240-chess-com-stockfish-bot';

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;

    // Rescan board to generate FEN string
    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("Starting new game...");
        const modalNewGameButton = $('.game-over-modal-content .game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])');
        if (modalNewGameButton.length) {
            modalNewGameButton[0].click();
            console.log("Clicked New <x> min button from game-over modal.");
            myVars.hasAutoMatched = true;
            return;
        }
        const newGameButton = $('.game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])');
        if (newGameButton.length) {
            newGameButton[0].click();
            console.log("Clicked New <x> min button from game-over.");
            myVars.hasAutoMatched = true;
            return;
        }
        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);
        } else {
            console.error("New Game tab not found!");
        }
    };

    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;  // Q
            case 87: myFunctions.runChessEngine(2); break;  // W
            case 69: myFunctions.runChessEngine(3); break;  // E
            case 82: myFunctions.runChessEngine(4); break;  // R
            case 84: myFunctions.runChessEngine(5); break;  // T
            case 89: myFunctions.runChessEngine(6); break;  // Y
            case 85: myFunctions.runChessEngine(7); break;  // U
            case 73: myFunctions.runChessEngine(8); break;  // I
            case 79: myFunctions.runChessEngine(9); break;  // O
            case 80: myFunctions.runChessEngine(10); break; // P
            case 65: myFunctions.runChessEngine(11); break; // A
            case 83: myFunctions.runChessEngine(12); break; // S
            case 68: myFunctions.runChessEngine(13); break; // D
            case 70: myFunctions.runChessEngine(14); break; // F
            case 71: myFunctions.runChessEngine(15); break; // G
            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);
    }

    // Add extra UI styles for a "chef's kiss" look with LED hover effects
    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;
            }
            /* Header for dragging */
            .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"],
            .chess-ui-panel input[type="text"] {
                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);
    }

    // Make only the header draggable so the panel doesn't stretch or disappear.
    function makeDraggable(panel, header) {
        let offsetX, offsetY;
        header.addEventListener('mousedown', dragMouseDown);
        function dragMouseDown(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();
            // Create panel structure with a header and a body
            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><br>
                    <label>Custom Command: <input type="text" id="customCmd" placeholder="Type here..."></label>
                </div>
            `;
            document.body.appendChild(panel);
            // Make only the header draggable.
            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 gameOver = document.querySelector('.game-over-message-component') || document.querySelector('.game-result');
            if (gameOver && !myVars.gameEnded) {
                console.log("Game ended detected (chat or result).");
                myVars.gameEnded = true;
                if (myVars.autoMatch) {
                    myFunctions.declineRematch();
                    setTimeout(() => {
                        myVars.hasAutoMatched = false;
                        myFunctions.startNewGame();
                    }, 1000);
                }
            } else if (!gameOver && myVars.gameEnded) {
                myVars.gameEnded = false;
            }

            const gameOverModal = $('.game-over-modal-content');
            if (myVars.autoMatch && gameOverModal.length && !myVars.hasAutoMatched) {
                console.log("Game over modal detected.");
                const newGameButton = gameOverModal.find('.game-over-buttons-component .cc-button-component:not([aria-label="Rematch"])');
                if (newGameButton.length) {
                    newGameButton[0].click();
                    console.log("Clicked New <x> min button from game-over modal in interval.");
                    myVars.hasAutoMatched = true;
                } else {
                    console.error("New <x> min button not found in game-over modal!");
                }
            }

            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, board;
window.addEventListener("load", () => main());