Chess.com Stockfish Bot

Uses unsafeWindow to read game state & dual APIs for reliability

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
/*
 * Copyright (c) 2025 [email protected] / proprietary
 * All rights reserved.
 *
 * This code is proprietary and confidential.
 *
 * 1. USE: You are permitted to execute and use this software for personal purposes.
 * 2. MODIFICATION: You are NOT permitted to modify, merge, publish, distribute,
 *    sublicense, and/or sell copies of this software.
 * 3. DISTRIBUTION: You are NOT permitted to distribute this software or derivative
 *    works of this software.
 */
// @name         Chess.com Stockfish Bot
// @namespace    BottleOrg Scripts
// @version      2.2
// @description  Uses unsafeWindow to read game state & dual APIs for reliability
// @author       BottleOrg / Optimized by Assistant
// @match        https://www.chess.com/play/*
// @match        https://www.chess.com/game/*
// @match        https://www.chess.com/puzzles/*
// @icon         https://www.chess.com/bundles/web/images/offline-play/standardboard.1d6f9426.png
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @run-at       document-start
// @license      All Rights Reserved
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        primaryApi: "https://chess-api.com/v1",
        backupApi: "https://stockfish.online/api/s/v2.php",
        version: '4.7.0'
    };

    const STATE = {
        isCoach: false,
        isMove: false,
        isAutoMatch: false,
        isDebug: false,
        depth: 13,
        isThinking: false,
        lastFen: "",
        boardFound: false
    };

    let arrowLayer = null;

    function log(msg, style = "color: #81b64c; font-weight: bold;") {
        console.log(`%c[SF Bot] ${msg}`, style);
    }

    log(`Initializing v${CONFIG.version}...`);

    // --- 1. VISUALS ---

    function createArrowLayer() {
        if (document.getElementById('stockfish-arrows')) return;
        arrowLayer = document.createElement('div');
        arrowLayer.id = 'stockfish-arrows';
        arrowLayer.style.cssText = `
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            pointer-events: none; z-index: 9999;
        `;
        document.body.appendChild(arrowLayer);
    }

    function clearArrows() {
        if (arrowLayer) arrowLayer.innerHTML = '';
    }

    function drawArrow(fromSq, toSq) {
        createArrowLayer();
        clearArrows();
        const start = getSquareCoords(fromSq);
        const end = getSquareCoords(toSq);
        if (!start || !end) return;

        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.style.cssText = "width: 100%; height: 100%; position: absolute; left: 0; top: 0;";

        const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
        line.setAttribute("x1", start.x);
        line.setAttribute("y1", start.y);
        line.setAttribute("x2", end.x);
        line.setAttribute("y2", end.y);
        line.setAttribute("stroke", "#81b64c");
        line.setAttribute("stroke-width", "16");
        line.setAttribute("stroke-opacity", "0.7");
        line.setAttribute("stroke-linecap", "round");

        svg.appendChild(line);
        arrowLayer.appendChild(svg);
    }

    function showDebugDot(x, y, color = 'red') {
        if (!STATE.isDebug) return;
        const dot = document.createElement('div');
        dot.style.cssText = `
            position: fixed; left: ${x-5}px; top: ${y-5}px; width: 10px; height: 10px;
            background: ${color}; border-radius: 50%; z-index: 100000; pointer-events: none;
            transition: opacity 0.5s; box-shadow: 0 0 3px white;
        `;
        document.body.appendChild(dot);
        setTimeout(() => { dot.style.opacity = 0; setTimeout(() => dot.remove(), 500); }, 500);
    }

    // --- 2. GEOMETRY & ACCESS ---

    // Attempt to find the board in both standard DOM and unsafeWindow (Sandbox escape)
    function getBoard() {
        let board = document.querySelector('wc-chess-board') || document.querySelector('chess-board') || document.getElementById('board-single');

        // If we found the element but it has no 'game' property, try unsafeWindow
        if (board && !board.game && window.unsafeWindow) {
            const unsafeBoard = unsafeWindow.document.querySelector('wc-chess-board') || unsafeWindow.document.querySelector('chess-board');
            if (unsafeBoard && unsafeBoard.game) {
                return unsafeBoard;
            }
        }
        return board;
    }

    function getBoardOrientation() {
        const board = getBoard();
        if (!board) return 'white';

        if (board.game && board.game.getPlayingAs) {
            const p = board.game.getPlayingAs();
            return (p === 'b' || p === 2) ? 'black' : 'white';
        }
        if (board.classList.contains('flipped')) return 'black';

        const coords = document.querySelectorAll('.coordinates text, .coords-component text');
        for(let c of coords) {
            if(c.textContent === '1') return c.getBoundingClientRect().top < board.getBoundingClientRect().top + 100 ? 'black' : 'white';
        }
        return 'white';
    }

    function getSquareCoords(square) {
        const board = document.querySelector('wc-chess-board') || document.querySelector('chess-board'); // Use standard DOM for coords
        if (!board) return null;

        const rect = board.getBoundingClientRect();
        if (rect.width < 10) return null;

        const size = rect.width / 8;
        const file = square.charCodeAt(0) - 97;
        const rank = square.charCodeAt(1) - 49;
        const isBlack = getBoardOrientation() === 'black';

        let xIdx = isBlack ? (7 - file) : file;
        let yIdx = isBlack ? rank : (7 - rank);

        let x = rect.left + (xIdx * size) + (size / 2);
        let y = rect.top + (yIdx * size) + (size / 2);

        return { x, y };
    }

    // --- 3. CLICK EXECUTION ---

    function fireEventsAt(x, y) {
        const candidates = document.elementsFromPoint(x, y);
        let target = null;

        target = candidates.find(el => el.classList.contains('piece') || el.getAttribute('data-piece'));
        if (!target) target = candidates.find(el => el.classList.contains('hint') || el.classList.contains('square') || el.tagName.includes('CHESS'));
        if (!target) target = candidates[0];

        if (!target) return;

        showDebugDot(x, y, target.classList.contains('piece') ? 'green' : 'red');

        // Use real window for event view
        const win = target.ownerDocument.defaultView || window;

        const opts = {
            bubbles: true, cancelable: true, view: win,
            clientX: x, clientY: y, button: 0, buttons: 1
        };

        target.dispatchEvent(new PointerEvent('pointerdown', opts));
        target.dispatchEvent(new MouseEvent('mousedown', opts));
        target.dispatchEvent(new PointerEvent('pointerup', opts));
        target.dispatchEvent(new MouseEvent('mouseup', opts));
        target.dispatchEvent(new MouseEvent('click', opts));
    }

    function movePiece(from, to) {
        const start = getSquareCoords(from);
        const end = getSquareCoords(to);
        if (!start || !end) return;

        fireEventsAt(start.x, start.y);

        setTimeout(() => {
            fireEventsAt(end.x, end.y);
            setTimeout(() => {
                const promoQueens = document.querySelectorAll('.promotion-piece.wq, .promotion-piece.bq');
                if(promoQueens.length > 0) promoQueens[0].click();
            }, 150);
        }, 150 + Math.random() * 80);
    }

    // --- 4. API & ENGINE ---

    function isValidFen(fen) {
        if (!fen || typeof fen !== 'string') return false;
        const parts = fen.trim().split(/\s+/);
        return parts.length >= 6;
    }

    function fetchPrimary(fen) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "POST",
                url: CONFIG.primaryApi,
                headers: { "Content-Type": "application/json" },
                data: JSON.stringify({
                    fen: fen,
                    depth: Math.min(STATE.depth, 18),
                    maxThinkingTime: 200
                }),
                onload: (res) => {
                    try {
                        const d = JSON.parse(res.responseText);
                        if (d.move) resolve(d.move);
                        else reject("Missing move");
                    } catch (e) { reject(e); }
                },
                onerror: reject
            });
        });
    }

    function fetchBackup(fen) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `${CONFIG.backupApi}?fen=${encodeURIComponent(fen)}&depth=${STATE.depth}`,
                onload: (res) => {
                    try {
                        const d = JSON.parse(res.responseText);
                        if (d.success) resolve(d.bestmove.split(' ')[1]);
                        else reject("Backup API failed");
                    } catch (e) { reject(e); }
                },
                onerror: reject
            });
        });
    }

    async function fetchMove(fen) {
        try {
            return await fetchPrimary(fen);
        } catch (err) {
            log("Primary failed, using Backup...", "color:orange");
            return await fetchBackup(fen);
        }
    }

    function mainLoop() {
        if ((!STATE.isCoach && !STATE.isMove) || STATE.isThinking) return;

        const board = getBoard();

        if (!board) {
            if (STATE.boardFound) { log("Board lost!", "color:red"); STATE.boardFound = false; }
            return;
        }

        if (!STATE.boardFound) {
            log("Board connected.", "color:green");
            STATE.boardFound = true;
        }

        // Turn Detection
        let isMyTurn = false;

        // 1. Try API
        if (board.game && board.game.getTurn && board.game.getPlayingAs) {
            isMyTurn = board.game.getTurn() === board.game.getPlayingAs();
        }
        // 2. Try DOM Fallback (Clock)
        else {
            const bottomClock = document.querySelector('.clock-bottom');
            if (bottomClock && bottomClock.classList.contains('clock-player-turn')) isMyTurn = true;
        }

        if (!isMyTurn) {
            STATE.lastFen = "";
            if (arrowLayer) clearArrows();
            return;
        }

        // FEN Extraction
        let fen = "";
        if (board.game && board.game.getFEN) {
            fen = board.game.getFEN();
        } else {
            // Silent fail for now, will retry next loop
            return;
        }

        if (fen === STATE.lastFen || !isValidFen(fen)) return;

        STATE.isThinking = true;
        updateStatus('Thinking...');

        fetchMove(fen).then(move => {
            // Sanity check turn
            if(board.game && board.game.getFEN() !== fen) {
                STATE.isThinking = false; return;
            }

            STATE.lastFen = fen;
            STATE.isThinking = false;
            updateStatus('Ready');

            const f = move.substring(0, 2);
            const t = move.substring(2, 4);

            if (STATE.isMove) movePiece(f, t);
            if (STATE.isCoach) drawArrow(f, t);

        }).catch(err => {
            STATE.isThinking = false;
            if(err !== "Missing move") {
                updateStatus('Error');
                log("Engine Error: " + err, "color:red");
            }
        });
    }

    // --- 5. GUI ---

    function buildGUI() {
        const id = 'stockfish-gui-v47';
        if (document.getElementById(id)) return;

        const div = document.createElement('div');
        div.id = id;
        div.style.cssText = `
            position: fixed; top: 80px; right: 20px; width: 210px;
            background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 6px;
            color: #e0e0e0; font-family: 'Segoe UI', sans-serif; font-size: 12px;
            z-index: 100000; box-shadow: 0 4px 15px rgba(0,0,0,0.5);
        `;

        div.innerHTML = `
            <div style="padding:8px 12px; background:#252525; border-bottom:1px solid #3a3a3a; font-weight:600; display:flex; align-items:center; gap:10px; cursor:move;" id="${id}-drag">
                <div id="bot-status" style="width:8px; height:8px; background:#555; border-radius:50%;"></div>
                <span>SF Sandbox v${CONFIG.version}</span>
            </div>
            <div style="padding:12px;">
                <div style="display:flex; flex-direction:column; gap:8px;">
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <input type="checkbox" id="chk-coach" style="margin-right:8px; accent-color:#81b64c;"> Coach Mode (Arrows)
                    </label>
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <input type="checkbox" id="chk-move" style="margin-right:8px; accent-color:#81b64c;"> Auto Move
                    </label>
                    <label style="display:flex;align-items:center;cursor:pointer;">
                        <input type="checkbox" id="chk-match" style="margin-right:8px; accent-color:#81b64c;"> Auto New Game
                    </label>
                    <label style="display:flex;align-items:center;cursor:pointer;color:#888;">
                        <input type="checkbox" id="chk-debug" style="margin-right:8px; accent-color:#81b64c;"> Debug Clicks
                    </label>
                </div>
                <div style="margin-top:12px; padding-top:10px; border-top:1px solid #3a3a3a; display:flex; justify-content:space-between; align-items:center;">
                    <span>Depth: <b id="val-depth" style="color:#81b64c;">13</b></span>
                    <div style="display:flex; gap:4px;">
                        <button id="btn-dm" style="background:#333;color:#fff;border:none;width:24px;height:24px;border-radius:4px;cursor:pointer;font-weight:bold;">-</button>
                        <button id="btn-dp" style="background:#333;color:#fff;border:none;width:24px;height:24px;border-radius:4px;cursor:pointer;font-weight:bold;">+</button>
                    </div>
                </div>
            </div>
        `;
        document.body.appendChild(div);

        const bind = (id, key) => document.getElementById(id).onchange = e => STATE[key] = e.target.checked;
        bind('chk-coach', 'isCoach');
        bind('chk-move', 'isMove');
        bind('chk-match', 'isAutoMatch');
        bind('chk-debug', 'isDebug');

        document.getElementById('chk-coach').onchange = (e) => { STATE.isCoach = e.target.checked; if(!e.target.checked) clearArrows(); };
        document.getElementById('btn-dm').onclick = () => { STATE.depth = Math.max(1, STATE.depth-1); document.getElementById('val-depth').innerText = STATE.depth; };
        document.getElementById('btn-dp').onclick = () => { STATE.depth = Math.min(18, STATE.depth+1); document.getElementById('val-depth').innerText = STATE.depth; };

        const head = document.getElementById(`${id}-drag`);
        let isDrag = false, sX, sY, iL, iT;
        head.onmousedown = e => { isDrag = true; sX = e.clientX; sY = e.clientY; const r = div.getBoundingClientRect(); iL = r.left; iT = r.top; };
        document.onmousemove = e => { if (isDrag) { div.style.left = (iL + e.clientX - sX) + 'px'; div.style.top = (iT + e.clientY - sY) + 'px'; }};
        document.onmouseup = () => { isDrag = false; };
    }

    function updateStatus(status) {
        const el = document.getElementById('bot-status');
        if (!el) return;
        const colors = { 'Thinking...': '#f1c40f', 'Ready': '#2ecc71', 'Error': '#e74c3c' };
        el.style.backgroundColor = colors[status] || '#555';
    }

    function autoMatch() {
        if (!STATE.isAutoMatch) return;
        const btn = document.querySelector('.game-over-modal button.cc-button-primary, .game-over-controls button.ui_v5-button-primary');
        if (btn) setTimeout(() => btn.click(), 2000);
    }

    function init() {
        const check = setInterval(() => {
            if (document.body) {
                clearInterval(check);
                buildGUI();
                setInterval(mainLoop, 200);
                setInterval(autoMatch, 1000);
            }
        }, 100);
    }

    init();

})();