chess
// ==UserScript== /* * Copyright (c) 2026 [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 (v1.161 Smooth Drag + WASM NNUE) // @namespace BottleOrg Scripts // @version 5.1 // @description chess // @author BottleOrg // @match https://www.chess.com/* // @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 STATE = { isCoach: false, showThreats: false, isMove: false, depth: 15, humanDelay: 400, isThinking: false, lastFen: "", bestMove: null, threatMove: null, hasMovedThisTurn: false }; let arrowLayer = null; let engineWorker = null; let engineQueue =[]; let engineBusy = false; function saveState() { GM_setValue('sf_state_wasm_v1', JSON.stringify({ isCoach: STATE.isCoach, showThreats: STATE.showThreats, isMove: STATE.isMove, depth: STATE.depth, humanDelay: STATE.humanDelay })); } function loadState() { const saved = GM_getValue('sf_state_wasm_v1'); if (saved) { try { const p = JSON.parse(saved); if(p.isCoach !== undefined) STATE.isCoach = p.isCoach; if(p.showThreats !== undefined) STATE.showThreats = p.showThreats; if(p.isMove !== undefined) STATE.isMove = p.isMove; if(p.depth !== undefined) STATE.depth = p.depth; if(p.humanDelay !== undefined) STATE.humanDelay = p.humanDelay; } catch (e) { console.warn("Failed to load settings."); } } } loadState(); function initEngine() { updateStatus('Loading JS Engine...'); GM_xmlhttpRequest({ method: "GET", url: "https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.js", onload: (resJs) => { if (resJs.status !== 200) { updateStatus('Error: CDN Blocked'); return; } let jsCode = resJs.responseText; jsCode = jsCode.replace(/import\.meta\.url/g, '"https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.js"'); jsCode = jsCode.replace(/stockfish-nnue-16-single\.wasm/g, "https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.wasm"); jsCode = jsCode.replace(/stockfish-nnue-16-single\.nnue/g, "https://unpkg.com/[email protected]/src/stockfish-nnue-16-single.nnue"); const blobCode = ` ${jsCode} if (typeof Stockfish === "function") { Stockfish().then(function(engine) { engine.addMessageListener(function(msg) { postMessage(msg); }); self.onmessage = function(ev) { engine.postMessage(ev.data); }; postMessage("readyok_custom"); }).catch(function(err) { postMessage("error: " + err); }); } `; try { const blob = new Blob([blobCode], { type: "application/javascript" }); engineWorker = new Worker(URL.createObjectURL(blob)); } catch(e) { updateStatus('Error: Worker Blocked'); return; } engineWorker.onmessage = (e) => { const msg = (e.data + "").trim(); if (msg === "readyok_custom") { engineWorker.postMessage("uci"); } else if (msg === "uciok") { engineWorker.postMessage("setoption name Use NNUE value false"); engineWorker.postMessage("isready"); } else if (msg === "readyok") { updateStatus('Ready'); } else if (msg.startsWith("bestmove")) { const parts = msg.split(" "); const move = parts[1] || "0000"; if (engineQueue.length > 0) { const task = engineQueue.shift(); engineBusy = false; STATE.isThinking = engineQueue.length > 0; updateStatus(STATE.isThinking ? 'Thinking...' : 'Ready'); task.resolve({ move: move.toLowerCase(), evalStr: task.evalStr }); processQueue(); } } else if (msg.includes("score cp") || msg.includes("score mate")) { const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/); if (scoreMatch && engineQueue.length > 0) { const type = scoreMatch[1]; let val = parseInt(scoreMatch[2], 10); let evalStr = ""; const isBlack = engineQueue[0].fen.split(' ')[1] === 'b'; if (type === "mate") { let displayVal = val; if (isBlack) displayVal = -val; evalStr = (displayVal > 0 ? "+M" : "-M") + Math.abs(displayVal); } else { if (isBlack) val = -val; evalStr = (val > 0 ? "+" : "") + (val / 100).toFixed(2); } engineQueue[0].evalStr = evalStr; if (!engineQueue[0].isThreat) { document.getElementById('val-eval').innerText = evalStr; } } } }; }, onerror: () => updateStatus('Error: Network JS') }); } function fetchEngineMove(fen, isThreat = false) { return new Promise((resolve) => { const timeout = setTimeout(() => { if (engineQueue[0] && engineQueue[0].resolve === resolve) { engineQueue.shift(); engineBusy = false; resolve({ move: "0000", evalStr: "0.00" }); processQueue(); } }, 30000); engineQueue.push({ resolve: (res) => { clearTimeout(timeout); resolve(res); }, fen, evalStr: "0.00", isThreat }); processQueue(); }); } function processQueue() { if (engineBusy || engineQueue.length === 0 || !engineWorker) return; engineBusy = true; STATE.isThinking = true; updateStatus('Thinking...'); const task = engineQueue[0]; engineWorker.postMessage(`position fen ${task.fen}`); engineWorker.postMessage(`go depth ${STATE.depth}`); } function createArrowLayer() { if (!document.getElementById('stockfish-arrows')) { arrowLayer = document.createElement('div'); arrowLayer.id = 'stockfish-arrows'; arrowLayer.style.cssText = ` position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; pointer-events: none !important; z-index: 9999; `; document.body.appendChild(arrowLayer); } } function clearArrows() { if (arrowLayer) arrowLayer.innerHTML = ''; } function renderArrows() { clearArrows(); if (STATE.showThreats && STATE.threatMove) { drawArrowSvg(STATE.threatMove.from, STATE.threatMove.to, '#e74c3c', 20, 0.5); } if (STATE.isCoach && STATE.bestMove) { drawArrowSvg(STATE.bestMove.from, STATE.bestMove.to, '#81b64c', 10, 0.9); } } function drawArrowSvg(fromSq, toSq, color, width, opacity) { createArrowLayer(); 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; pointer-events: none !important;"; const defs = document.createElementNS("http://www.w3.org/2000/svg", "defs"); const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker"); const markerId = "arrowhead" + color.replace('#', '') + width; marker.setAttribute("id", markerId); marker.setAttribute("markerWidth", "4"); marker.setAttribute("markerHeight", "4"); marker.setAttribute("refX", "2"); marker.setAttribute("refY", "2"); marker.setAttribute("orient", "auto"); const polygon = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); polygon.setAttribute("points", "0 0, 4 2, 0 4"); polygon.setAttribute("fill", color); polygon.setAttribute("pointer-events", "none"); marker.appendChild(polygon); defs.appendChild(marker); svg.appendChild(defs); 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", color); line.setAttribute("stroke-width", width.toString()); line.setAttribute("stroke-opacity", opacity.toString()); line.setAttribute("stroke-linecap", "round"); line.setAttribute("marker-end", `url(#${markerId})`); line.setAttribute("pointer-events", "none"); svg.appendChild(line); arrowLayer.appendChild(svg); } function getBoard() { let board = document.querySelector('wc-chess-board') || document.querySelector('chess-board') || document.getElementById('board-single'); 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 && board.classList && board.classList.contains('flipped')) return 'black'; return 'white'; } function getSquareCoords(square) { const board = document.querySelector('wc-chess-board') || document.querySelector('chess-board'); 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); return { x: rect.left + (xIdx * size) + (size / 2), y: rect.top + (yIdx * size) + (size / 2) }; } function dispatchPointerEvent(type, x, y) { const target = document.elementFromPoint(x, y) || document.body; const isDown = type.includes('down') || type.includes('move'); const ev = new PointerEvent(type, { clientX: x, clientY: y, bubbles: true, cancelable: true, composed: true, pointerId: 1, pointerType: 'mouse', isPrimary: true, button: 0, buttons: isDown ? 1 : 0 }); target.dispatchEvent(ev); if (type !== 'pointercancel') { const mouseType = type.replace('pointer', 'mouse'); target.dispatchEvent(new MouseEvent(mouseType, { clientX: x, clientY: y, bubbles: true, cancelable: true, composed: true, button: 0, buttons: isDown ? 1 : 0 })); } } function simulateClick(x, y) { dispatchPointerEvent('pointerdown', x, y); dispatchPointerEvent('pointerup', x, y); const el = document.elementFromPoint(x, y) || document.body; el.dispatchEvent(new MouseEvent('click', { clientX: x, clientY: y, bubbles: true, cancelable: true })); } function movePiece(from, to, promoChar) { const start = getSquareCoords(from); const end = getSquareCoords(to); if (!start || !end) return; if (arrowLayer) arrowLayer.hidden = true; setTimeout(() => { dispatchPointerEvent('pointerdown', start.x, start.y); const steps = 15; const stepDelay = Math.max(5, (STATE.humanDelay * 0.5) / steps); for (let i = 1; i <= steps; i++) { setTimeout(() => { const curX = start.x + (end.x - start.x) * (i / steps); const curY = start.y + (end.y - start.y) * (i / steps); dispatchPointerEvent('pointermove', curX, curY); if (i === steps) { dispatchPointerEvent('pointerup', end.x, end.y); setTimeout(() => { simulateClick(start.x, start.y); setTimeout(() => { simulateClick(end.x, end.y); setTimeout(() => { STATE.hasMovedThisTurn = false; }, 2000); }, 20); if (arrowLayer) arrowLayer.hidden = false; if (promoChar) { setTimeout(() => { let prefix = from.charAt(1) === '7' ? 'w' : 'b'; let sels =[ `.promotion-piece.${prefix}${promoChar}`, `.promotion-piece.${promoChar}`, `.promotion-piece.wq, .promotion-piece.bq`, `.promotion-window .${promoChar}` ]; for (let sel of sels) { const els = document.querySelectorAll(sel); if (els.length > 0) { const rect = els[0].getBoundingClientRect(); const px = rect.left + rect.width / 2; const py = rect.top + rect.height / 2; simulateClick(px, py); break; } } }, 300); } }, 50); } }, i * stepDelay); } }, Math.random() * (STATE.humanDelay * 0.4)); } function normalizeFen(fen) { if (!fen || typeof fen !== 'string') return ""; let parts = fen.trim().split(/\s+/); if (parts.length < 4) return ""; while (parts.length < 6) { if (parts.length === 4) parts.push('0'); else if (parts.length === 5) parts.push('1'); } return parts.join(' '); } function getNullMoveFen(fen) { let parts = fen.split(' '); if (parts.length < 6) return null; parts[1] = parts[1] === 'w' ? 'b' : 'w'; parts[3] = '-'; if (parts[1] === 'w') parts[5] = (parseInt(parts[5], 10) + 1).toString(); return parts.join(' '); } function getFenFromDOM() { const board = getBoard(); if (!board) return null; let pieces = board.querySelectorAll('.piece'); if (!pieces.length) return null; let boardArray = Array(8).fill("").map(() => Array(8).fill("")); for (let p of pieces) { let cls = p.className; let matchColorPiece = cls.match(/(w|b)(p|n|b|r|q|k)/); let matchSquare = cls.match(/square-(\d)(\d)/); if (!matchSquare) matchSquare = cls.match(/sq-(\d)(\d)/); if (matchColorPiece && matchSquare) { let color = matchColorPiece[1]; let piece = matchColorPiece[2]; let file = parseInt(matchSquare[1], 10) - 1; let rank = 8 - parseInt(matchSquare[2], 10); if(rank >= 0 && rank < 8 && file >= 0 && file < 8) { boardArray[rank][file] = color === 'w' ? piece.toUpperCase() : piece.toLowerCase(); } } } let fen = ""; for (let r = 0; r < 8; r++) { let empty = 0; for (let f = 0; f < 8; f++) { if (boardArray[r][f] === "") { empty++; } else { if (empty > 0) { fen += empty; empty = 0; } fen += boardArray[r][f]; } } if (empty > 0) fen += empty; if (r < 7) fen += "/"; } let turn = 'w'; const bottomClock = document.querySelector('.clock-bottom'); if (bottomClock) { if (bottomClock.classList.contains('clock-player-turn') || bottomClock.classList.contains('is-active')) { turn = getBoardOrientation() === 'white' ? 'w' : 'b'; } else { turn = getBoardOrientation() === 'white' ? 'b' : 'w'; } } return fen + ` ${turn} - - 0 1`; } async function processTurnSequentially(fen, isMyTurn) { if (!engineWorker) return; try { if (isMyTurn && (STATE.isCoach || STATE.isMove)) { const res = await fetchEngineMove(fen, false); const m = res.move; if (m && m !== "0000" && m !== "(none)") { STATE.bestMove = { from: m.substring(0, 2), to: m.substring(2, 4), promo: m.length > 4 ? m.substring(4, 5) : null }; renderArrows(); if (STATE.isMove && !STATE.hasMovedThisTurn) { STATE.hasMovedThisTurn = true; movePiece(STATE.bestMove.from, STATE.bestMove.to, STATE.bestMove.promo); } } } if (STATE.showThreats || !isMyTurn) { let threatFen = isMyTurn ? getNullMoveFen(fen) : fen; if (threatFen) { const isActualEval = !isMyTurn; const res2 = await fetchEngineMove(threatFen, !isActualEval); const m = res2.move; if (m && m !== "0000" && m !== "(none)") { STATE.threatMove = { from: m.substring(0,2), to: m.substring(2,4) }; renderArrows(); } } } } catch (err) { console.error(err); } } function mainLoop() { if (STATE.isThinking || !engineWorker) return; const board = getBoard(); if (!board) return; let rawFen = (board.game && board.game.getFEN) ? board.game.getFEN() : getFenFromDOM(); let fen = normalizeFen(rawFen); if (!fen) return; let myColor = 'w'; const isPuzzle = window.location.href.includes('puzzle'); if (!isPuzzle && board.game && typeof board.game.getPlayingAs === 'function') { const p = board.game.getPlayingAs(); if (p === 1 || p === 'w' || p === 'white') myColor = 'w'; else if (p === 2 || p === 'b' || p === 'black') myColor = 'b'; } else { myColor = getBoardOrientation() === 'white' ? 'w' : 'b'; } const turnStr = fen.split(' ')[1]; let isMyTurn = (turnStr === myColor); if (fen !== STATE.lastFen) { STATE.lastFen = fen; STATE.bestMove = null; STATE.threatMove = null; STATE.hasMovedThisTurn = false; renderArrows(); processTurnSequentially(fen, isMyTurn); } else { if (isMyTurn && STATE.isMove && STATE.bestMove && !STATE.hasMovedThisTurn) { STATE.hasMovedThisTurn = true; movePiece(STATE.bestMove.from, STATE.bestMove.to, STATE.bestMove.promo); } else if (isMyTurn && STATE.isMove && !STATE.bestMove && !STATE.isThinking) { processTurnSequentially(fen, isMyTurn); } } } function buildGUI() { const id = 'stockfish-gui-v8'; if (document.getElementById(id)) return; const div = document.createElement('div'); div.id = id; div.style.cssText = ` position: fixed; top: 80px; right: 20px; width: 250px; background: #1e1e1e; border: 1px solid #3a3a3a; border-radius: 6px; color: #e0e0e0; font-family: 'Segoe UI', sans-serif; font-size: 13px; z-index: 100000; box-shadow: 0 4px 15px rgba(0,0,0,0.5); `; div.innerHTML = ` <div style="padding:10px 12px; background:#252525; border-bottom:1px solid #3a3a3a; font-weight:600; display:flex; align-items:center; gap:10px; cursor:move; border-radius: 6px 6px 0 0;" id="${id}-drag"> <div id="bot-status" style="width:10px; height:10px; background:#555; border-radius:50%; box-shadow: 0 0 5px #555; transition: 0.3s;"></div> <span>Stockfish Local NNUE</span> </div> <div style="padding:12px;"> <div style="display:flex; justify-content:space-between; margin-bottom: 12px; font-size: 14px; background: #2a2a2a; padding: 6px; border-radius: 4px;"> <span style="color:#aaa;">Evaluation:</span> <strong id="val-eval" style="color:#81b64c;">0.00</strong> </div> <div style="margin-bottom: 12px; background: #222; padding: 8px; border-radius: 4px; border: 1px solid #444;"> <div style="width:100%; padding:5px; background:#2ecc71; color:#fff; border:none; border-radius:4px; text-align:center; font-weight:bold; margin-bottom: 6px;">Local Wasm Engine Active</div> <div style="font-size:11px; color:#aaa; text-align:center;">Stockfish 16.0 (NNUE, 1-Thread)</div> </div> <div style="display:flex; flex-direction:column; gap:10px;"> <label style="display:flex;align-items:center;cursor:pointer;"> <input type="checkbox" id="chk-coach" style="margin-right:8px; width:16px; height:16px; accent-color:#81b64c;" ${STATE.isCoach ? 'checked' : ''}> Coach Mode (Green) </label> <label style="display:flex;align-items:center;cursor:pointer;"> <input type="checkbox" id="chk-threats" style="margin-right:8px; width:16px; height:16px; accent-color:#e74c3c;" ${STATE.showThreats ? 'checked' : ''}> Show Threats (Red) </label> <label style="display:flex;align-items:center;cursor:pointer;"> <input type="checkbox" id="chk-move" style="margin-right:8px; width:16px; height:16px; accent-color:#81b64c;" ${STATE.isMove ? 'checked' : ''}> Auto Move </label> </div> <div style="margin-top:14px; padding-top:12px; border-top:1px solid #3a3a3a; display:flex; flex-direction:column; gap:12px;"> <div style="display:flex; justify-content:space-between; align-items:center;"> <span>Depth Cap: <b id="val-depth" style="color:#81b64c;">${STATE.depth}</b></span> <div style="display:flex; gap:6px;"> <button id="btn-dm" style="background:#333;color:#fff;border:none;width:26px;height:26px;border-radius:4px;cursor:pointer;font-weight:bold;transition:0.2s;">+</button> <button id="btn-dp" style="background:#333;color:#fff;border:none;width:26px;height:26px;border-radius:4px;cursor:pointer;font-weight:bold;transition:0.2s;">+</button> </div> </div> <div style="display:flex; flex-direction:column; gap:6px;"> <div style="display:flex; justify-content:space-between;"> <span>Delay: <b id="val-delay" style="color:#81b64c;">${STATE.humanDelay}ms</b></span> </div> <input type="range" id="slider-delay" min="50" max="1500" step="50" value="${STATE.humanDelay}" style="width:100%; accent-color:#81b64c;"> </div> </div> </div> `; document.body.appendChild(div); const bind = (cid, key, customCb) => { const el = document.getElementById(cid); if(el) el.addEventListener('change', e => { STATE[key] = e.target.checked; saveState(); if(customCb) customCb(); }); }; bind('chk-coach', 'isCoach', renderArrows); bind('chk-threats', 'showThreats', renderArrows); bind('chk-move', 'isMove'); document.getElementById('btn-dm').onclick = () => { STATE.depth = Math.max(1, STATE.depth-1); document.getElementById('val-depth').innerText = STATE.depth; saveState(); }; document.getElementById('btn-dp').onclick = () => { STATE.depth = Math.min(20, STATE.depth+1); document.getElementById('val-depth').innerText = STATE.depth; saveState(); }; document.getElementById('slider-delay').oninput = (e) => { STATE.humanDelay = parseInt(e.target.value, 10); document.getElementById('val-delay').innerText = STATE.humanDelay + "ms"; saveState(); }; 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', 'Waiting...': '#3498db', 'Error: Network JS': '#e74c3c', 'Error: Worker Blocked': '#e74c3c', 'Error: CDN Blocked': '#e74c3c', 'Loading JS Engine...': '#9b59b6' }; el.style.backgroundColor = colors[status] || '#555'; el.style.boxShadow = `0 0 8px ${colors[status] || '#555'}`; el.title = status; } function init() { const check = setInterval(() => { if (document.body) { clearInterval(check); buildGUI(); initEngine(); setInterval(mainLoop, 200); } }, 100); } init(); })();