Chess.com Stockfish Assistant

Chess analysis tool with Stockfish integration and auto-match

目前为 2025-04-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         Chess.com Stockfish Assistant
// @namespace    BottleOrg Scripts
// @version      1.6.6
// @description  Chess analysis tool with Stockfish integration and auto-match
// @author       [REDACTED] - Rightful owner & Contributors & Gemini 2.5 Pro, Chatgpt-4o
// @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
// @require      https://code.jquery.com/jquery-3.6.0.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/chess.js/0.10.2/chess.min.js
// @run-at       document-start
// @license 2025, [email protected], All Rights Reserved
// @notice This script is protected under the Berne Convention for the Protection of Literary and Artistic Works, an international treaty recognized by over 180 countries. Unauthorized copying, modification, or distribution is strictly prohibited and will be enforced under applicable national copyright laws worldwide. Violations may result in legal action, including takedown requests and civil penalties.
// ==/UserScript==

(function() {
    'use strict';

    const config = {
        API_URL: "https://stockfish.online/api/s/v2.php",
        MIN_DELAY: 0.8,
        MAX_DELAY: 3.0,
        MAX_DEPTH: 15,
        MIN_DEPTH: 1,
        RATE_LIMIT: 1200,
        MISTAKE_THRESHOLD: 0.15
    };

    let state = {
        autoMove: false,
        autoRun: false,
        autoMatch: false,
        visualOnly: false,
        mistakePercentage: 0,
        lastMoveTime: 0,
        lastFen: null,
        lastTurn: null,
        isThinking: false,
        canMove: true,
        board: null,
        gameEnded: false,
        hasAutoMatched: false
    };

    const utils = {
        getRandomDelay: () => {
            const base = Math.random() * (config.MAX_DELAY - config.MIN_DELAY) + config.MIN_DELAY;
            return base * 1000 + (Math.random() * 300 - 150);
        },

        simulateHumanClick: (element) => {
            if (!element) return Promise.resolve(false);
            const rect = element.getBoundingClientRect();
            const x = rect.left + rect.width / 2 + (Math.random() * 10 - 5);
            const y = rect.top + rect.height / 2 + (Math.random() * 10 - 5);

            return new Promise(resolve => {
                const eventProps = {
                    bubbles: true,
                    cancelable: true,
                    clientX: x,
                    clientY: y,
                    button: 0,
                    buttons: 1,
                    view: window
                };
                const down = new MouseEvent('mousedown', eventProps);
                const up = new MouseEvent('mouseup', eventProps);
                const click = new MouseEvent('click', eventProps);

                element.dispatchEvent(down);
                setTimeout(() => {
                    element.dispatchEvent(up);
                    element.dispatchEvent(click);
                    resolve(true);
                }, 50 + Math.random() * 100);
            });
        },

        simulateHumanMove: (fromElement, toElement) => {
            if (!fromElement || !toElement) return Promise.resolve(false);

            const fromRect = fromElement.getBoundingClientRect();
            const toRect = toElement.getBoundingClientRect();
            const steps = 12;
            const time = 250 + Math.random() * 150;

            const dispatchEvent = (element, type, x, y) => {
                return new Promise(resolve => {
                    const event = new MouseEvent(type, {
                        bubbles: true,
                        cancelable: true,
                        clientX: x,
                        clientY: y,
                        button: 0,
                        buttons: type === 'mouseup' ? 0 : 1,
                        view: window
                    });
                    setTimeout(() => {
                        element.dispatchEvent(event);
                        resolve();
                    }, time / steps);
                });
            };

            const movePath = [];
            for (let i = 0; i <= steps; i++) {
                const t = i / steps;
                const x = fromRect.left + (toRect.left - fromRect.left) * t + (Math.random() * 6 - 3);
                const y = fromRect.top + (toRect.top - fromRect.top) * t + (Math.random() * 6 - 3);
                movePath.push({ x, y });
            }

            return new Promise(async resolve => {
                await dispatchEvent(fromElement, 'mousedown', movePath[0].x, movePath[0].y);
                for (let i = 1; i < movePath.length - 1; i++) {
                    await dispatchEvent(fromElement, 'mousemove', movePath[i].x, movePath[i].y);
                }
                await dispatchEvent(toElement, 'mousemove', movePath[steps].x, movePath[steps].y);
                await dispatchEvent(toElement, 'mouseup', movePath[steps].x, movePath[steps].y);
                resolve(true);
            });
        },

        getFen: () => {
            const board = state.board;
            if (!board || !board.game) return "rnbqkbnr/pppppppp/5n2/8/8/5N2/PPPPPPPP/RNBQKBNR w KQkq - 0 1";

            const chess = new Chess();
            const pieces = $(board).find(".piece");
            if (!pieces.length) return chess.fen();

            const position = Array(64).fill('');
            pieces.each((_, el) => {
                const classes = el.className.split(' ');
                const square = classes.find(c => c.startsWith('square-'))?.replace('square-', '');
                const piece = classes.find(c => /^[wb][prnbqk]$/.test(c));
                if (square && piece) {
                    const [file, rank] = square.split('');
                    const idx = (8 - parseInt(rank)) * 8 + (file.charCodeAt(0) - 97);
                    position[idx] = { wp: 'P', bp: 'p', wr: 'R', br: 'r', wn: 'N', bn: 'n',
                        wb: 'B', bb: 'b', wq: 'Q', bq: 'q', wk: 'K', bk: 'k' }[piece];
                }
            });

            const fen = position.reduce((fen, p, i) => {
                if (i % 8 === 0 && i > 0) fen += '/';
                if (!p) {
                    let empty = 1;
                    while (i + 1 < 64 && !position[i + 1] && (i + 1) % 8 !== 0) {
                        empty++;
                        i++;
                    }
                    return fen + empty;
                }
                return fen + p;
            }, '') + ` ${board.game.getTurn() || 'w'} - - 0 1`;
            chess.load(fen);
            return chess.fen();
        },

        uciToSquare: (uci) => {
            const [from, to] = [uci.slice(0, 2), uci.slice(2, 4)];
            return {
                from: (8 - parseInt(from[1])) * 8 + (from[0].charCodeAt(0) - 97) + 1,
                to: (8 - parseInt(to[1])) * 8 + (to[0].charCodeAt(0) - 97) + 1
            };
        }
    };

    const chessEngine = {
        lastDepth: 11,

        fetchMove: (fen, depth) => {
            if (Date.now() - state.lastMoveTime < config.RATE_LIMIT || state.isThinking) {
                return setTimeout(() => chessEngine.fetchMove(fen, depth), config.RATE_LIMIT);
            }

            state.isThinking = true;
            ui.updateStatus("Thinking...");
            GM_xmlhttpRequest({
                method: "GET",
                url: `${config.API_URL}?fen=${encodeURIComponent(fen)}&depth=${depth}`,
                onload: (res) => {
                    try {
                        const data = JSON.parse(res.responseText);
                        if (data.success) {
                            chessEngine.handleMove(data);
                        } else {
                            ui.updateStatus("API Failed");
                        }
                    } catch (e) {
                        console.error("API Error:", e);
                        ui.updateStatus("API Error");
                    }
                    state.isThinking = false;
                },
                onerror: () => {
                    state.isThinking = false;
                    ui.updateStatus("Network Error");
                }
            });
        },

        handleMove: (data) => {
            const bestMove = data.bestmove.split(' ')[1];
            const altMove = data.altmove?.split(' ')[1];
            state.lastMoveTime = Date.now();

            const shouldMakeMistake = Math.random() < state.mistakePercentage / 100;
            const moveToUse = (shouldMakeMistake && altMove) ? altMove : bestMove;

            if (state.visualOnly) {
                ui.visualHighlight(moveToUse);
            } else if (state.autoMove) {
                setTimeout(() => chessEngine.executeMove(moveToUse), utils.getRandomDelay());
            } else {
                ui.highlightMove(moveToUse);
            }
            ui.updateStatus("Idle");
        },

        executeMove: async (uci) => {
            const { from, to } = utils.uciToSquare(uci);
            const fromEl = $(state.board).find(`.square-${from} .piece`)[0];
            const toEl = $(state.board).find(`.square-${to}`)[0];

            if (!fromEl || !toEl) {
                console.error("Move elements not found:", { from, to });
                state.isThinking = false;
                ui.updateStatus("Move Error");
                return;
            }

            if (await utils.simulateHumanMove(fromEl, toEl)) {
                state.lastFen = utils.getFen();
                state.lastTurn = state.board.game?.getTurn();
            } else {
                console.error("Move simulation failed");
                state.isThinking = false;
                ui.updateStatus("Move Failed");
            }
        },

        startNewGame: async () => {
            if (state.hasAutoMatched || !state.gameEnded) return;

            // Handle decline button if present
            const declineButton = $('.cc-button-component.cc-button-secondary[aria-label="Decline"]')[0];
            if (declineButton) {
                await utils.simulateHumanClick(declineButton);
                await new Promise(resolve => setTimeout(resolve, utils.getRandomDelay()));
            }

            // Look for "New <time> min" button dynamically
            const gameOverModal = $('.game-over-modal-content');
            if (gameOverModal.length) {
                // Find button with text matching "New" followed by time
                const newGameButton = gameOverModal.find('.cc-button-component').filter(function() {
                    const text = $(this).text().trim();
                    return text.match(/^New\s+\d+(\.\d+)?\s+min$/i);
                })[0];
                
                if (newGameButton) {
                    await utils.simulateHumanClick(newGameButton);
                    state.hasAutoMatched = true;
                    return;
                }
            }

            // Alternative location for new game button
            const newGameButtons = $('.game-over-buttons-component .cc-button-component').filter(function() {
                const text = $(this).text().trim();
                return text.match(/^New\s+\d+(\.\d+)?\s+min$/i) && !$(this).attr('aria-label')?.includes('Rematch');
            })[0];
            if (newGameButtons) {
                await utils.simulateHumanClick(newGameButtons);
                state.hasAutoMatched = true;
                return;
            }

            // Fallback to main play button
            const playButton = $('.cc-button-component.cc-button-primary.cc-button-xx-large.cc-button-full')[0];
            if (playButton) {
                await utils.simulateHumanClick(playButton);
                state.hasAutoMatched = true;
            }
        }
    };

    const ui = {
        loaded: false,

        init: () => {
            if (ui.loaded) return;
            const checkBoard = () => {
                state.board = document.querySelector('chess-board, wc-chess-board');
                return state.board && state.board.game;
            };
            if (!checkBoard()) {
                setTimeout(ui.init, 1000);
                return;
            }

            const panel = $(`<div style="position: fixed; top: 10px; right: 10px; z-index: 10000;
                background: #f9f9f9; padding: 10px; border: 1px solid #333; border-radius: 5px;">
                <p id="depthText">Depth: <strong>${chessEngine.lastDepth}</strong></p>
                <button id="depthMinus">-</button>
                <button id="depthPlus">+</button>
                <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><input type="checkbox" id="visualOnly"> Visual Only</label><br>
                <label>Mistakes %: <input type="number" id="mistakePercentage" min="0" max="100" value="0" style="width: 50px;"></label><br>
                <p id="statusMessage">Idle</p>
                <p style="font-size: 10px; color: #666;">Set Mistakes % > 15% to reduce detection risk</p>
            </div>`).appendTo(document.body);

            $('#depthPlus').click(() => {
                chessEngine.lastDepth = Math.min(config.MAX_DEPTH, chessEngine.lastDepth + 1);
                $('#depthText').html(`Depth: <strong>${chessEngine.lastDepth}</strong>`);
            });
            $('#depthMinus').click(() => {
                chessEngine.lastDepth = Math.max(config.MIN_DEPTH, chessEngine.lastDepth - 1);
                $('#depthText').html(`Depth: <strong>${chessEngine.lastDepth}</strong>`);
            });
            $('#autoRun').change(function() { state.autoRun = this.checked; });
            $('#autoMove').change(function() { state.autoMove = this.checked; });
            $('#autoMatch').change(function() {
                state.autoMatch = this.checked;
                if (state.autoMatch && state.gameEnded) chessEngine.startNewGame();
            });
            $('#visualOnly').change(function() { state.visualOnly = this.checked; });
            $('#mistakePercentage').change(function() {
                state.mistakePercentage = Math.max(0, Math.min(100, parseInt(this.value) || 0));
                this.value = state.mistakePercentage;
            });

            ui.loaded = true;
        },

        updateStatus: (msg) => {
            $('#statusMessage').text(msg);
        },

        highlightMove: (uci) => {
            const { from, to } = utils.uciToSquare(uci);
            $(state.board).find(`.square-${from}, .square-${to}`)
                .css('background-color', 'rgba(235, 97, 80, 0.5)')
                .delay(2000)
                .queue(function() { $(this).css('background-color', ''); $(this).dequeue(); });
        },

        visualHighlight: (uci) => {
            const { to } = utils.uciToSquare(uci);
            $(state.board).find(`.square-${to}`)
                .append('<div class="visual" style="position: absolute; width: 100%; height: 100%; border: 2px solid green; opacity: 0.6;">')
                .find('.visual')
                .delay(2000)
                .fadeOut(300, function() { $(this).remove(); });
        }
    };

    const mainLoop = setInterval(() => {
        if (!ui.loaded) ui.init();
        if (!state.board) state.board = document.querySelector('chess-board, wc-chess-board');
        if (!state.board?.game) return;

        const fen = utils.getFen();
        const turn = state.board.game.getTurn();
        const myTurn = turn === state.board.game.getPlayingAs();
        const gameOver = document.querySelector('.game-over-message-component') || document.querySelector('.game-result');

        if (gameOver && !state.gameEnded) {
            state.gameEnded = true;
            state.isThinking = false;
            if (state.autoMatch) {
                setTimeout(chessEngine.startNewGame, utils.getRandomDelay());
            }
        } else if (!gameOver && state.gameEnded) {
            state.gameEnded = false;
            state.hasAutoMatched = false;
        }

        if (state.autoRun && myTurn && !state.isThinking && fen !== state.lastFen) {
            chessEngine.fetchMove(fen, chessEngine.lastDepth);
            state.lastFen = fen;
        }
    }, 500);

    $(document).keydown((e) => {
        const depthKeys = 'qwertyuiopasdfg'.split('').reduce((obj, key, i) => {
            obj[key.charCodeAt(0)] = i + 1;
            return obj;
        }, {});

        if (e.keyCode in depthKeys && !state.isThinking) {
            chessEngine.fetchMove(utils.getFen(), depthKeys[e.keyCode]);
        }
    });

    setTimeout(() => {
        if (!ui.loaded) ui.init();
    }, 2000);
})();