Chess.com Stockfish Bot

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

目前为 2025-03-21 提交的版本。查看 最新版本

// ==UserScript==
// @name         Chess.com Stockfish Bot
// @namespace    BottleOrg Scripts
// @version      1.6.2.19
// @description  Chess.com Stockfish Bot with Enhanced Auto-Match using Stockfish Online API
// @author       [REDACTED]
// @license      Chess.com Bot/Cheat by [REDACTED]
// @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.19';
const scriptURL = 'https://greasyfork.org/en/scripts/526240-chess-com-stockfish-bot';
const stockfishAPI_URI = "https://stockfish.online/api/s/v2.php";

// Function to check if the Stockfish API domain is accessible
function checkStockfishDomainAccess(callback) {
    console.log("Checking if Stockfish API domain is accessible...");
    const testFen = "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
    const testDepth = 1;
    const testUrl = `${stockfishAPI_URI}?fen=${encodeURIComponent(testFen)}&depth=${testDepth}`;

    GM_xmlhttpRequest({
        method: "GET",
        url: testUrl,
        onload: function(response) {
            if (response.status === 200) {
                console.log("Stockfish API domain is accessible.");
                callback(true);
            } else {
                console.error("Stockfish API request failed with status:", response.status);
                callback(false);
            }
        },
        onerror: function(error) {
            console.error("Stockfish API request blocked or failed:", error);
            // Check if the error indicates the request was blocked (e.g., by an ad-blocker)
            if (error && error.statusText && error.statusText.includes("net::ERR_BLOCKED_BY_CLIENT")) {
                alert("Allow domain request to stockfish.online for the script to be able to work!");
            } else {
                alert("Failed to connect to Stockfish API. Please check your internet connection or allow requests to stockfish.online.");
            }
            callback(false);
        },
        timeout: 5000 // Set a timeout of 5 seconds for the test request
    });
}

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() {
    var myVars = document.myVars = { autoMove: false, autoRun: false, autoMatch: false, delay: 0.1, hasAutoMatched: false, gameEnded: false, stockfishAccessible: false };
    var myFunctions = document.myFunctions = {};
    var currentStockfishVersion = "Stockfish API";
    var uiElementsLoaded = false;

    // Check Stockfish API access before proceeding
    checkStockfishDomainAccess(function(isAccessible) {
        myVars.stockfishAccessible = isAccessible;
        if (!isAccessible) {
            console.error("Stockfish API is not accessible. Script functionality will be limited.");
            return; // Stop script execution if Stockfish API is not accessible
        }
        proceedWithMainLogic();
    });

    function proceedWithMainLogic() {
        var stop_b = 0, stop_w = 0, s_br = 0, s_br2 = 0, s_wr = 0, s_wr2 = 0;

        myFunctions.rescan = function() {
            console.log("Rescanning board...");
            var 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";
            }
            var 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";
            }
            var boardArray = Array(64).fill('');
            pieces.forEach(piece => {
                var classes = piece.split(' ');
                var squareClass = classes.find(c => c.startsWith('square-'));
                var pieceClass = classes.find(c => /^[wb][prnbqk]$/.test(c));
                if (squareClass && pieceClass) {
                    var squareNum = squareClass.replace('square-', '');
                    var file = parseInt(squareNum[0]) - 1;
                    var rank = parseInt(squareNum[1]) - 1;
                    var square = (7 - rank) * 8 + file;
                    if (square >= 0 && square < 64) {
                        var 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;
                    }
                }
            });
            var fen = '';
            for (var i = 0; i < 64; i++) {
                if (i % 8 === 0 && i > 0) fen += '/';
                var piece = boardArray[i];
                if (!piece) {
                    var emptyCount = 1;
                    while (i + 1 < 64 && !boardArray[i + 1] && (i + 1) % 8 !== 0) {
                        emptyCount++;
                        i++;
                    }
                    fen += emptyCount;
                } else {
                    fen += piece;
                }
            }
            var turn = $('.coordinates').children().first().text() === "1" ? 'b' : 'w';
            var 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) {
            var res1 = bestmoveUCI.substring(0, 2), 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);
            let 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) {
            if (!myVars.stockfishAccessible) {
                console.error("Stockfish API is not accessible. Cannot fetch best move.");
                isThinking = false;
                myFunctions.spinner();
                return;
            }
            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 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;
                        // Wait for the modal to appear and then attempt to click the guest button
                        setTimeout(() => {
                            attemptGuestButtonClick();
                        }, 2000); // Increased delay to ensure modal loads
                    } 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;
            }
        };

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

        myFunctions.runChessEngine = function(depth) {
            if (!myVars.stockfishAccessible) {
                console.error("Stockfish API is not accessible. Cannot run chess engine.");
                return;
            }
            depth = Math.max(MIN_DEPTH, Math.min(MAX_DEPTH, depth));
            var 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(body) {
            if (!dynamicStyles) {
                dynamicStyles = document.createElement('style');
                document.head.appendChild(dynamicStyles);
            }
            dynamicStyles.sheet.insertRule(body, dynamicStyles.length);
        }

        var loaded = false;
        myFunctions.loadEx = function() {
            try {
                console.log("Attempting to load UI...");
                board = document.querySelector('chess-board, wc-chess-board');
                var div = document.createElement('div');
                div.innerHTML = `
                    <div style="margin: 8px; padding: 10px; background: white; border: 1px solid #000; border-radius: 5px;">
                        <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: <strong>Stockfish API</strong></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>
                        <p style="color: blue; font-size: 12px; margin-top: 5px;">Script is made for guest account since Stockfish makes unhuman moves!</p>
                    </div>`;
                div.style.cssText = 'position: fixed; top: 10px; right: 10px; z-index: 10000;';
                document.body.appendChild(div);

                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);

                var spinCont = document.createElement('div');
                spinCont.id = 'overlay';
                spinCont.style.cssText = 'display: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);';
                div.appendChild(spinCont);
                var 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) {
            var endTime = Date.now() + delay;
            var timer = setInterval(() => {
                if (Date.now() >= endTime) {
                    myFunctions.autoRun();
                    canGo = true;
                    clearInterval(timer);
                }
            }, 10);
        }

        // Function to attempt clicking the guest button with retries
        function attemptGuestButtonClick(attempts = 10, delay = 1000) {
            if (attempts <= 0) {
                console.error("Failed to click 'Play as a Guest' button after all retries.");
                return;
            }

            // Check if the modal is present
            const modal = $('.authentication-modal-content');
            if (!modal.length) {
                console.log("Modal not found, attempts left:", attempts);
                setTimeout(() => attemptGuestButtonClick(attempts - 1, delay), delay);
                return;
            }

            const guestButton = $('#guest-button.authentication-intro-guest');
            if (guestButton.length && !myVars.hasAutoMatched) {
                console.log("Found 'Play as a Guest' button:", guestButton[0]);
                guestButton[0].click(); // Use jQuery's click method
                console.log("Clicked 'Play as a Guest' button.");
                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!");
                    }
                }, 1500); // Increased delay to 1500ms to ensure page transition
            } else {
                console.log("Guest button not found or already clicked, attempts left:", attempts);
                setTimeout(() => attemptGuestButtonClick(attempts - 1, delay), delay);
            }
        }

        const waitForChessBoard = setInterval(() => {
            if (!loaded) {
                myFunctions.loadEx();
            } else {
                clearInterval(waitForChessBoard);
                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;
                let minDel = parseFloat($('#timeDelayMin')[0].value) || 0.1;
                let 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);
    }
}

var isThinking = false, canGo = true, myTurn = false, board;

window.addEventListener("load", () => main());