Greasy Fork

Greasy Fork is available in English.

Chess.com Stockfish Bot

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

当前为 2025-04-05 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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      http://greasyfork.icu/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 = 'http://greasyfork.icu/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());