Chess.com Stockfish Bot

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

目前為 2025-03-21 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Chess.com Stockfish Bot
// @namespace    BottleOrg Scripts
// @version      1.6.2.17
// @description  Chess.com Stockfish Bot with Enhanced Auto-Match using Stockfish Online API
// @author       Quoc Bao Nguyen & Deepseek V3 AI & Gemini 2.0 AI & Grok 3 (xAI)
// @license      Chess.com Bot/Cheat by BottleOrg(me Quoc Bao Nguyen)
// @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() {
    var myVars = document.myVars = { autoMove: false, autoRun: false, autoMatch: false, delay: 0.1, hasAutoMatched: false, gameEnded: false };
    var myFunctions = document.myFunctions = {};
    var currentStockfishVersion = "Stockfish API";
    var uiElementsLoaded = false;
    const stockfishAPI_URI = "https://stockfish.online/api/s/v2.php";

    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) {
        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...");
        // Check for game-over "New <x> min" button in modal
        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;
        }

        // Check for game-over "New <x> min" button outside modal
        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;
        }

        // Check for guest prompt
        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;
        }

        // Fallback to sidebar "New Game" tab and "Play"
        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;
        }
    };

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

    myFunctions.runChessEngine = function(depth) {
        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: 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>`;
            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);
    }

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

            // Check for game end
            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(); // Check for rematch once on game end
                    setTimeout(() => {
                        myVars.hasAutoMatched = false; // Reset to allow new game
                        myFunctions.startNewGame(); // Attempt to start new game
                    }, 1000); // Delay to ensure modal loads
                }
            } else if (!gameOver && myVars.gameEnded) {
                myVars.gameEnded = false; // Reset when game restarts
            }

            // Check for game-over modal and auto-click "New <x> min" if Auto-Match is enabled
            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());