Uses chess-api.com (POST) with the "Pointer Event" fix for clicking.
目前為
// ==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();
})();