// ==UserScript==
// @name Chess.com Bot — Improved (Top 3 Moves & Threats)
// @namespace thehackerclient
// @version 2.5
// @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,
delayMin: 0.5,
delayMax: 2,
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());
// 🔹 Force autoMovePiece always false at startup
settings.autoMovePiece = false;
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;
return val > 0 ? 100000 - val : -100000 - val;
}
function handleEngineMessage(msg){
if(typeof msg !== 'string') return;
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;
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')){
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; } }
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);
setTimeout(()=>{ if(ov && ov.parentElement) ov.parentElement.removeChild(ov); }, settings.highlightMs);
}
});
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); }
}
function performMove(moveUCI){
if(!board || !board.game) return;
try{
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(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();
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); }
}
const debouncedRun = debounce((d)=> runChessEngine(d), 300);
const autoLoop = throttle(()=>{
if(!board || !board.game) return;
if(settings.autoRun && canGo && !isThinking && board.game.getTurn() === board.game.getPlayingAs()){
canGo = false;
const delaySeconds = Math.random() * (settings.delayMax - settings.delayMin) + settings.delayMin;
setTimeout(()=>{ debouncedRun(settings.lastDepth); canGo = true; }, Math.max(200, delaySeconds*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:280px;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="delayMinInput" type="number" min="0" step="0.1" value="${settings.delayMin}" style="width:60px"> -
<input id="delayMaxInput" type="number" min="0" step="0.1" value="${settings.delayMax}" style="width:60px">
</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>
`;
try{ board.parentElement.parentElement.appendChild(container); }catch(e){ document.body.appendChild(container); }
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('delayMinInput').onchange = e => {
settings.delayMin = parseFloat(e.target.value) || 0;
if(settings.delayMin > settings.delayMax) settings.delayMax = settings.delayMin;
document.getElementById('delayMaxInput').value = settings.delayMax;
saveSettings();
};
document.getElementById('delayMaxInput').onchange = e => {
settings.delayMax = parseFloat(e.target.value) || 0;
if(settings.delayMax < settings.delayMin) settings.delayMin = settings.delayMax;
document.getElementById('delayMinInput').value = settings.delayMin;
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();
await waitUntil(()=> (board = findBoard()) && board.game);
createStockfishWorker();
initGUI();
const mo = new MutationObserver(()=>{ board = findBoard(); });
mo.observe(document.body, {childList:true, subtree:true});
setInterval(autoLoop, 150);
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){
lastMoveCount = moves;
if(settings.autoRun) debouncedRun(settings.lastDepth);
}
}
}catch(e){}
}, 600);
console.log('Improved Chess Bot ready');
})();
})();