Chess.com Stockfish Bot

Uses chess-api.com (POST) with the "Pointer Event" fix for clicking.

目前為 2025-11-22 提交的版本,檢視 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
/*
 * Copyright (c) 2025 [Your Name/Organization]
 * 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.0
// @description  Uses chess-api.com (POST) with the "Pointer Event" fix for clicking.
// @author       BottleOrg / Gemini 3.0 Pro
// @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
// @run-at       document-start
// @license      All Rights Reserved
// ==/UserScript==

(function() {
    'use strict';

    const CONFIG = {
        apiUrl: "https://chess-api.com/v1", // UPDATED API
        version: '4.6.0'
    };

    const STATE = {
        isCoach: false,
        isMove: false,
        isAutoMatch: false,
        isDebug: false,
        depth: 15, // API max is usually 18 for free tier
        isThinking: false,
        lastFen: ""
    };

    let arrowLayer = null;

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

    // --- 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 ---

    function getBoard() {
        return document.querySelector('wc-chess-board') || document.querySelector('chess-board') || document.getElementById('board-single') || document.getElementById('board-layout-chessboard');
    }

    function getBoardOrientation() {
        const board = getBoard();
        if (!board) return 'white';
        
        // 1. API Check
        if (board.game && board.game.getPlayingAs) {
            const p = board.game.getPlayingAs();
            return (p === 'b' || p === 2) ? 'black' : 'white';
        }
        
        // 2. Class Check
        if (board.classList.contains('flipped')) return 'black';
        
        // 3. Coordinate Scrape
        const coords = board.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 = getBoard();
        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 TO FIX SANDBOX ERROR
        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);
            // Handle Promotion
            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 ---

    async function fetchMove(fen) {
        return new Promise((resolve, reject) => {
            // Limit depth to 18 as per chess-api.com free tier limits
            const apiDepth = Math.min(STATE.depth, 18);

            GM_xmlhttpRequest({
                method: "POST",
                url: CONFIG.apiUrl,
                headers: {
                    "Content-Type": "application/json"
                },
                data: JSON.stringify({
                    fen: fen,
                    depth: apiDepth
                }),
                onload: (res) => {
                    try {
                        const d = JSON.parse(res.responseText);
                        // API response: { "move": "e2e4", "eval": 0.3, ... }
                        if (d.move) resolve(d.move); 
                        else reject("No move found");
                    } catch (e) { reject(e); }
                },
                onerror: (err) => {
                    console.error(err);
                    reject("Network Error");
                }
            });
        });
    }

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

        const board = getBoard();
        if (!board) return;

        let isMyTurn = false;
        
        if (board.game && board.game.getTurn && board.game.getPlayingAs) {
            isMyTurn = board.game.getTurn() === board.game.getPlayingAs();
        } 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;
        }

        let fen = "";
        if (board.game && board.game.getFEN) {
            fen = board.game.getFEN();
        } else {
            return;
        }

        if (fen === STATE.lastFen) return;

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

        fetchMove(fen).then(move => {
            // Verify game state didn't change while waiting
            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;
            updateStatus('Error');
            console.error("Engine Error:", err);
        });
    }

    // --- 5. GUI ---

    function buildGUI() {
        const id = 'stockfish-gui-v46';
        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 API 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;">15</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();

})();