// ==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 // @downloadURL none // ==/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 = `
SF API v${CONFIG.version}
Depth: 15
`; 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(); })();