您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows Stockfish's top 3 moves & threats on Chess.com vs computer, with persistent settings, depth slider, safer engine handling.
当前为
// ==UserScript== // @name Chess.com Bot — Improved (Top 3 Moves & Threats) // @namespace thehackerclient // @version 2.1 // @description Shows Stockfish's top 3 moves & threats on Chess.com vs computer, with persistent settings, depth slider, safer engine handling. // @author thehackerclient // @match https://www.chess.com/* // @icon https://www.chess.com/bundles/web/images/favicon.ico // @grant GM_getResourceText // @require https://code.jquery.com/jquery-3.6.0.min.js // @resource stockfish.js https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/9.0.0/stockfish.js // @license MIT // @run-at document-start // ==/UserScript== (function () { 'use strict'; // --------- Config & state --------- const STORAGE_KEY = 'chess_bot_settings_v2'; const DEFAULTS = { autoRun: true, autoMovePiece: false, delay: 1, lastDepth: 18, showPV: true, colors: { move1: 'rgba(235,97,80,0.7)', move2:'rgba(255,165,0,0.6)', move3:'rgba(255,255,0,0.5)', threat:'rgba(0,128,255,0.35)' }, highlightMs: 1400 }; let settings = Object.assign({}, DEFAULTS, loadSettings()); let board = null; // DOM element for chess-board / wc-chess-board let engine = { worker: null }; // stockfish worker wrapper let stockfishObjectURL = null; let candidateMoves = []; // [{move:'e2e4', score:120, depth:... , pv:[] }] let isThinking = false; let canGo = true; let lastFen = ''; // debounce/throttle helpers function debounce(fn, wait){ let t; return function(...a){ clearTimeout(t); t = setTimeout(()=>fn.apply(this,a), wait); }; } function throttle(fn, wait){ let last=0; return function(...a){ const now=Date.now(); if(now-last>wait){ last=now; fn.apply(this,a);} }; } // safer query for board element — supports both modern and older chess.com tags function findBoard(){ return $('chess-board')[0] || $('wc-chess-board')[0] || document.querySelector('[data-cy="board"]') || null; } // --------- Persistence --------- function loadSettings(){ try{ const raw = localStorage.getItem(STORAGE_KEY); return raw? JSON.parse(raw): {}; }catch(e){ return {}; } } function saveSettings(){ try{ localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); }catch(e){} } // --------- Stockfish lifecycle --------- function createStockfishWorker(){ try{ if(stockfishObjectURL === null){ const text = GM_getResourceText('stockfish.js'); stockfishObjectURL = URL.createObjectURL(new Blob([text], {type:'application/javascript'})); } if(engine.worker) engine.worker.terminate(); engine.worker = new Worker(stockfishObjectURL); engine.worker.onmessage = e => handleEngineMessage(e.data); engine.worker.postMessage('ucinewgame'); console.log('Stockfish worker created'); }catch(err){ console.error('Failed to create Stockfish worker', err); } } function safeRestartEngine(){ isThinking = false; try{ if(engine.worker) engine.worker.terminate(); }catch(e){} createStockfishWorker(); } // --------- Engine message parsing --------- function parseScore(match){ if(!match) return 0; const type = match[1]; const val = parseInt(match[2]); if(type === 'cp') return val; // mate scores: convert to a large cp-ish value with sign return val > 0 ? 100000 - val : -100000 - val; // preserves mate signs and ordering } function handleEngineMessage(msg){ if(typeof msg !== 'string') return; // collect candidate PV info from 'info' lines if(msg.startsWith('info') && msg.includes('pv')){ const pvTokens = msg.split(' pv ')[1].trim().split(/\s+/); if(pvTokens && pvTokens.length){ const move = pvTokens[0]; const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/); const score = parseScore(scoreMatch); const depthMatch = msg.match(/depth (\d+)/); const depth = depthMatch? parseInt(depthMatch[1]) : settings.lastDepth; // avoid duplicates — update existing if higher depth/score const exists = candidateMoves.find(c=>c.move===move); if(!exists) candidateMoves.push({move, score, depth, pv: pvTokens}); else if(depth>exists.depth) { exists.score=score; exists.depth=depth; exists.pv=pvTokens; } } } if(msg.startsWith('bestmove')){ // finalize list, sort and keep top 3 candidateMoves.sort((a,b) => b.score - a.score); candidateMoves = candidateMoves.slice(0,3); showTopMoves(); const move = msg.split(' ')[1]; if(settings.autoMovePiece && move) performMove(move); isThinking = false; } } // --------- Utilities --------- function mapSquareForBoard(sq){ if(!board || !sq || sq.length<2) return sq; const isFlipped = board.classList && board.classList.contains('flipped'); if(!isFlipped) return sq; const file = sq[0]; const rank = sq[1]; const flippedFile = String.fromCharCode('h'.charCodeAt(0) - (file.charCodeAt(0)-'a'.charCodeAt(0))); const flippedRank = (9 - parseInt(rank)).toString(); return flippedFile + flippedRank; } function getBoardSquareEl(sq){ try{ return board.querySelector(`[data-square="${sq}"]`); }catch(e){ return null; } } // create or reuse overlay element on a square function attachHighlight(el, cls, color){ if(!el) return null; let overlay = el.querySelector('.' + cls); if(!overlay){ overlay = document.createElement('div'); overlay.className = cls; overlay.style.position='absolute'; overlay.style.top=0; overlay.style.left=0; overlay.style.width='100%'; overlay.style.height='100%'; overlay.style.pointerEvents='none'; overlay.style.zIndex=60; el.appendChild(overlay); } overlay.style.backgroundColor = color; return overlay; } function detachHighlights(selector){ try{ document.querySelectorAll(selector).forEach(n=>n.parentElement && n.parentElement.removeChild(n)); }catch(e){} } // --------- Highlighting & UI --------- function showTopMoves(){ if(!board || !board.game) return; detachHighlights('.botMoveHighlight'); detachHighlights('.botThreatHighlight'); candidateMoves.forEach((cm, i) => { const from = mapSquareForBoard(cm.move.slice(0,2)); const to = mapSquareForBoard(cm.move.slice(2,4)); const color = i===0? settings.colors.move1 : (i===1? settings.colors.move2 : settings.colors.move3); [from, to].forEach(sq => { const el = getBoardSquareEl(sq); if(el) { const ov = attachHighlight(el, 'botMoveHighlight', color); // remove after highlightMs setTimeout(()=>{ if(ov && ov.parentElement) ov.parentElement.removeChild(ov); }, settings.highlightMs); } }); // optionally show PV text in GUI (we add a small floating element on top-right of board) if(settings.showPV && cm.pv && cm.pv.length){ addPVNote(cm, i); } }); showThreats(); } function addPVNote(cm, index){ try{ const id = `pvNote-${index}`; let note = document.getElementById(id); if(!note){ note = document.createElement('div'); note.id=id; note.style.position='absolute'; note.style.right='6px'; note.style.top=(6 + index*28)+'px'; note.style.padding='6px 8px'; note.style.borderRadius='6px'; note.style.background='rgba(0,0,0,0.6)'; note.style.color='#fff'; note.style.zIndex=120; note.style.fontSize='12px'; board.parentElement.appendChild(note); } note.innerText = `#${index+1} ${cm.move} (${Math.round(cm.score)}) PV: ${cm.pv.slice(0,6).join(' ')}`; setTimeout(()=>{ if(note && note.parentElement) note.parentElement.removeChild(note); }, settings.highlightMs + 5000); }catch(e){} } function showThreats(){ if(!board || !board.game) return; try{ const legalMoves = board.game.getLegalMoves(); const opponent = board.game.getTurn() === 'w' ? 'b' : 'w'; legalMoves.forEach(m=>{ if(m.color===opponent){ const sq = mapSquareForBoard(m.to); const el = getBoardSquareEl(sq); if(el){ const ov = attachHighlight(el, 'botThreatHighlight', settings.colors.threat); setTimeout(()=>{ if(ov && ov.parentElement) ov.parentElement.removeChild(ov); }, settings.highlightMs); } } }); }catch(e){ console.warn('Failed to show threats', e); } } // performs move using board.game.move (handles promotion if UCI includes promotion) function performMove(moveUCI){ if(!board || !board.game) return; try{ // moveUCI might be like e7e8q for promotion; board.game expects objects from getLegalMoves const from = moveUCI.slice(0,2); const to = moveUCI.slice(2,4); const promotion = moveUCI.length>4? moveUCI[4] : null; const legal = board.game.getLegalMoves(); for(const m of legal){ if(m.from===from && m.to===to){ // if promotion required, check piece if(m.promotion && promotion){ m.promotion = promotion; } board.game.move(Object.assign({}, m, {animate:false, userGenerated:true})); break; }} }catch(e){ console.error('performMove failed', e); } } // --------- Engine runner & controls --------- function runChessEngine(depth){ if(!board || !engine.worker || !board.game) return; try{ const fen = board.game.getFEN(); // don't re-run if same fen and same depth and already thinking if(isThinking && fen === lastFen && depth===settings.lastDepth) return; lastFen = fen; candidateMoves = []; engine.worker.postMessage('position fen ' + fen); isThinking = true; engine.worker.postMessage('go depth ' + depth); }catch(e){ console.error('runChessEngine error', e); } } // debounce auto-run so rapid UI events don't spam engine const debouncedRun = debounce((d)=> runChessEngine(d), 300); // auto-run loop — throttled to avoid spamming const autoLoop = throttle(()=>{ if(!board || !board.game) return; if(settings.autoRun && canGo && !isThinking && board.game.getTurn() === board.game.getPlayingAs()){ canGo = false; setTimeout(()=>{ debouncedRun(settings.lastDepth); canGo = true; }, Math.max(200, settings.delay*1000)); } }, 200); // --------- GUI --------- function initGUI(){ board = findBoard(); if(!board) return false; if(document.getElementById('botGUI_v2')) return true; const container = document.createElement('div'); container.id = 'botGUI_v2'; container.style = 'background:rgba(255,255,255,0.95);padding:10px;margin:8px;max-width:260px;font-family:Inter,Arial,sans-serif;border-radius:8px;box-shadow:0 6px 20px rgba(0,0,0,0.08)'; container.innerHTML = ` <div style="font-weight:600;margin-bottom:6px;">Chess Bot — Improved</div> <div id="depthText">Depth: <strong>${settings.lastDepth}</strong></div> <input type="range" id="depthSlider" min="1" max="30" value="${settings.lastDepth}" step="1" style="width:100%"> <div style="margin-top:6px;"><input type="checkbox" id="autoRunCB"> <label for="autoRunCB">Auto Run</label></div> <div><input type="checkbox" id="autoMoveCB"> <label for="autoMoveCB">Auto Move</label></div> <div style="margin-top:6px;">Delay (s): <input id="delayInput" type="number" min="0" step="0.5" value="${settings.delay}" style="width:70px"></div> <div style="margin-top:8px;display:flex;gap:6px;"><button id="reloadBtn" style="flex:1;padding:6px;border-radius:6px">Reload Engine</button><button id="analyseBtn" style="flex:1;padding:6px;border-radius:6px">Analyse Now</button></div> <div style="margin-top:8px;font-size:12px;color:#666">Top 3 moves are highlighted briefly; PVs show on the board edge.</div> `; // append near board — conservative placement try{ board.parentElement.parentElement.appendChild(container); }catch(e){ document.body.appendChild(container); } // hydrate controls document.getElementById('autoRunCB').checked = !!settings.autoRun; document.getElementById('autoMoveCB').checked = !!settings.autoMovePiece; document.getElementById('depthSlider').oninput = e => { settings.lastDepth = parseInt(e.target.value); document.getElementById('depthText').innerHTML = `Depth: <strong>${settings.lastDepth}</strong>`; saveSettings(); }; document.getElementById('autoRunCB').onchange = e => { settings.autoRun = e.target.checked; saveSettings(); }; document.getElementById('autoMoveCB').onchange = e => { settings.autoMovePiece = e.target.checked; saveSettings(); }; document.getElementById('delayInput').onchange = e => { settings.delay = parseFloat(e.target.value) || 0; saveSettings(); }; document.getElementById('reloadBtn').onclick = () => { safeRestartEngine(); }; document.getElementById('analyseBtn').onclick = () => { debouncedRun(settings.lastDepth); }; return true; } // --------- Initialization & observers --------- async function waitUntil(conditionFn, interval=100){ return new Promise(resolve=>{ const t = setInterval(()=>{ try{ if(conditionFn()){ clearInterval(t); resolve(); } }catch(e){} }, interval); }); } (async function init(){ await waitUntil(()=> findBoard()); board = findBoard(); // wait for the board.game object (Chess.com's internal game API) to be present await waitUntil(()=> (board = findBoard()) && board.game); // engine createStockfishWorker(); initGUI(); // observe for board/game resets (new game or navigation) const mo = new MutationObserver(()=>{ board = findBoard(); if(board && board.game){ /* no-op but keeps reference fresh */ } }); mo.observe(document.body, {childList:true, subtree:true}); // periodic auto loop setInterval(autoLoop, 150); // also monitor user moves (if board.game emits events, use them) — fallback to polling let lastMoveCount = null; setInterval(()=>{ try{ if(board && board.game){ const moves = board.game.getMoveHistory ? board.game.getMoveHistory().length : (board.game.history? board.game.history.length:0); if(lastMoveCount === null) lastMoveCount = moves; if(moves !== lastMoveCount){ // a move occurred — trigger analysis lastMoveCount = moves; if(settings.autoRun) debouncedRun(settings.lastDepth); } } }catch(e){} }, 600); console.log('Improved Chess Bot ready'); })(); })();