Chess.com Bot — With Evaluation Bar (Updated Stockfish) newer

Improved userscript: top 3 moves & threats, persistent settings, debounce/throttle, safer engine lifecycle, promotion handling, better board detection, min/max delay, and real-time evaluation bar. Now using updated Stockfish from a new resource.

اعتبارا من 02-10-2025. شاهد أحدث إصدار.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name Chess.com Bot — With Evaluation Bar (Updated Stockfish) newer
// @namespace thehackerclient
// @version 4.6 // Version incremented
// @description Improved userscript: top 3 moves & threats, persistent settings, debounce/throttle, safer engine lifecycle, promotion handling, better board detection, min/max delay, and real-time evaluation bar. Now using updated Stockfish from a new resource.
// @match https://www.chess.com/*
// @auther thehackerclient
// @license MIT
// @grant GM_getResourceText
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @resource stockfish.js https://cdn.jsdelivr.net/gh/niklasf/stockfish.js/stockfish.js
// @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;
    // Added rawCp and rawMate to the candidate move structure for display purposes
    let candidateMoves = [];         // [{move:'e2e4', score:120, depth:... , pv:[], rawCp: 120, rawMate: null }]
    let isThinking = false;
    let canGo = true;
    let lastFen = '';

    // === NEW STATE FOR DYNAMIC CALCULATION STATUS ===
    let currentCalcStatus = {
        depth: 0,
        seldepth: 0,
        nodes: 0,
        nps: 0,
        time: 0,
        isSearching: false
    };
    // ===============================================

    // 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 (UNCHANGED) ---------
    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 (UNCHANGED) ---------
    function createStockfishWorker(){
        try{
            // ⭐️ Updated to use the new resource URL via GM_getResourceText
            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 with new resource');
        }catch(err){ console.error('Failed to create Stockfish worker', err); }
    }

    function safeRestartEngine(){ isThinking = false; try{ if(engine.worker) engine.worker.terminate(); }catch(e){} createStockfishWorker(); }

    // --------- Evaluation Bar Logic (UNCHANGED) ---------

    /**
     * Updates the visual evaluation bar based on the best move's score.
     * @param {number} scoreCp - The centipawn score (positive for White, negative for Black).
     * @param {number | null} mateScore - The number of moves to mate (positive for White, negative for Black), or null if not mate.
     */
    function updateEvaluationBar(scoreCp, mateScore) {
        const barWhiteEl = document.getElementById('evalBarWhiteAdvantage');
        const scoreTextEl = document.getElementById('evalPercent');

        if (!barWhiteEl || !scoreTextEl) return;

        let displayScore;
        let barHeightPercent;

        if (mateScore !== null) {
            // Mate score: mateScore is the number of moves to mate. Positive for White, Negative for Black.
            displayScore = mateScore > 0 ? `M+${mateScore}` : `M${mateScore}`;
            // Map mate score to near 100% or near 0%
            barHeightPercent = mateScore > 0 ? 98 : 2;
        } else {
            // Centipawn score: scoreCp is the advantage for White.

            // For bar visualization, clamp CP value to a visible range (e.g., +/- 800 cp)
            const clampedCp = Math.max(-800, Math.min(800, scoreCp));
            // Simple linear scaling: 16 cp = 1% change (0% at -800, 50% at 0, 100% at +800)
            barHeightPercent = 50 + (clampedCp / 16);
            barHeightPercent = Math.max(0, Math.min(100, barHeightPercent));

            displayScore = (scoreCp / 100).toFixed(2);
            if (scoreCp >= 0) displayScore = `+${displayScore}`;
        }

        // Update the bar height (White's territory grows from the bottom up, pushing Black's down)
        barWhiteEl.style.height = `${barHeightPercent}%`;

        // Update the displayed score
        scoreTextEl.innerText = displayScore;

        // Adjust text color for contrast (White text on black background, Black text on white background)
        const whiteAdvantage = barHeightPercent;
        const blackAdvantage = 100 - barHeightPercent;

        // If the advantage is overwhelming for one side, move the text label to the other side for contrast
        if (whiteAdvantage > 80) { // Mostly white: text to black side
            scoreTextEl.style.color = '#fff';
            scoreTextEl.style.top = '10%';
        } else if (blackAdvantage > 80) { // Mostly black: text to white side
            scoreTextEl.style.color = '#000';
            scoreTextEl.style.top = '90%';
        } else { // Neutral: centered text
            scoreTextEl.style.color = '#1a1a1a';
            scoreTextEl.style.top = '50%';
        }
    }


    // --------- Engine message parsing (UPDATED to track status) ---------
    // This function is for calculating a score value primarily for sorting candidate moves.
    function parseScore(match){
        if(!match) return 0;
        const type = match[1];
        const val = parseInt(match[2]);
        if(type === 'cp') return val;
        // Mate score is converted to a large positive/negative value for sorting
        return val > 0 ? 100000 - val : -100000 - val;
    }

    function getInfoToken(msg, key, isInt=false) {
        const match = msg.match(new RegExp(`${key} (\\S+)`));
        if (match) return isInt ? parseInt(match[1]) : match[1];
        return null;
    }

    function handleEngineMessage(msg){
        if(typeof msg !== 'string') return;

        if(msg.startsWith('info')){
            // === UPDATE CALCULATION STATUS IN REAL-TIME ===
            currentCalcStatus.isSearching = true;
            const depth = getInfoToken(msg, 'depth', true);
            const seldepth = getInfoToken(msg, 'seldepth', true);
            const nodes = getInfoToken(msg, 'nodes', true);
            const nps = getInfoToken(msg, 'nps', true);
            const time = getInfoToken(msg, 'time', true);

            if (depth !== null) currentCalcStatus.depth = depth;
            if (seldepth !== null) currentCalcStatus.seldepth = seldepth;
            if (nodes !== null) currentCalcStatus.nodes = nodes;
            if (nps !== null) currentCalcStatus.nps = nps;
            if (time !== null) currentCalcStatus.time = time;

            throttledUpdateStatus();
            // ===============================================

            if(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); // Score for sorting

                    let rawCp = null;
                    let rawMate = null;

                    if (scoreMatch) {
                        const type = scoreMatch[1];
                        const val = parseInt(scoreMatch[2]);
                        if (type === 'cp') rawCp = val;
                        if (type === 'mate') rawMate = val;
                    }

                    const depthMatch = msg.match(/depth (\d+)/);
                    const depth = depthMatch? parseInt(depthMatch[1]) : settings.lastDepth;
                    const exists = candidateMoves.find(c=>c.move===move);

                    // Store both the sortable score and the raw scores for display
                    if(!exists) candidateMoves.push({move, score, depth, pv: pvTokens, rawCp, rawMate});
                    else if(depth>exists.depth) {
                        exists.score=score;
                        exists.depth=depth;
                        exists.pv=pvTokens;
                        exists.rawCp=rawCp;
                        exists.rawMate=rawMate;
                    }
                }
            }
        }

        if(msg.startsWith('bestmove')){
            currentCalcStatus.isSearching = false; // Stop searching status
            throttledUpdateStatus(); // Final status update

            candidateMoves.sort((a,b) => b.score - a.score);
            candidateMoves = candidateMoves.slice(0,3);
            showTopMoves();

            // --- Update Evaluation Bar with the best move's score ---
            if(candidateMoves.length > 0) {
                const bestMove = candidateMoves[0];
                // Use the raw mate score if available, otherwise use the raw CP score.
                const finalCp = bestMove.rawCp !== null ? bestMove.rawCp : bestMove.score;
                const finalMate = bestMove.rawMate;

                updateEvaluationBar(finalCp, finalMate);
            }

            const move = msg.split(' ')[1];
            if(settings.autoMovePiece && move) performMove(move);
            isThinking = false;
        }
    }

    // === NEW FUNCTION: Update the calculation status GUI elements ===
    function updateCalculationStatus() {
        const statusEl = document.getElementById('calcStatus');
        const searchStatusEl = document.getElementById('searchStatusText');

        if (!statusEl || !searchStatusEl) return;

        if (currentCalcStatus.isSearching) {
            searchStatusEl.innerText = 'Calculating...';
            searchStatusEl.style.color = '#d9534f'; // Red/Orange for calculating

            const nodesK = (currentCalcStatus.nodes / 1000).toFixed(0);
            const npsK = (currentCalcStatus.nps / 1000).toFixed(0);
            const timeS = (currentCalcStatus.time / 1000).toFixed(1);

            statusEl.innerHTML = `
                <div style="display:flex; justify-content:space-between;"><span>Depth:</span> <strong>${currentCalcStatus.depth} / ${currentCalcStatus.seldepth}</strong></div>
                <div style="display:flex; justify-content:space-between;"><span>Nodes:</span> <strong>${nodesK}k</strong></div>
                <div style="display:flex; justify-content:space-between;"><span>NPS:</span> <strong>${npsK}k</strong></div>
                <div style="display:flex; justify-content:space-between;"><span>Time:</span> <strong>${timeS}s</strong></div>
            `;
        } else {
            searchStatusEl.innerText = 'Idle / Ready';
            searchStatusEl.style.color = '#5cb85c'; // Green for ready
            statusEl.innerHTML = `
                <div style="display:flex; justify-content:space-between;"><span>Depth:</span> <strong>-</strong></div>
                <div style="display:flex; justify-content:space-between;"><span>Nodes:</span> <strong>-</strong></div>
                <div style="display:flex; justify-content:space-between;"><span>NPS:</span> <strong>-</strong></div>
                <div style="display:flex; justify-content:space-between;"><span>Time:</span> <strong>-</strong></div>
            `;
        }
    }

    // Throttle the status update to avoid excessive DOM manipulation
    const throttledUpdateStatus = throttle(updateCalculationStatus, 150);
    // ===============================================================

    // --------- Utilities (omitted for brevity, unchanged) ---------
    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 (omitted for brevity, largely unchanged) ---------
    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); }
            let scoreDisplay = cm.rawMate !== null ? (cm.rawMate > 0 ? `M+${cm.rawMate}` : `M${cm.rawMate}`) : (cm.rawCp !== null ? (cm.rawCp/100).toFixed(2) : (cm.score/100).toFixed(2));
            note.innerText = `#${index+1} ${cm.move} (${scoreDisplay}) 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); }
    }

    // --- FIX APPLIED HERE (UNCHANGED) ---
    function performMove(moveUCI){
        if(!board || !board.game) return;
        try{
            const from = moveUCI.slice(0,2);
            const to = moveUCI.slice(2,4);
            // Stockfish UCI for promotion includes the piece type, e.g., 'e7e8q'
            const promotionChar = moveUCI.length > 4 ? moveUCI[4] : null;

            // Map the promotion character to the full piece type (e.g., 'q' -> 'q')
            let promotionPiece = null;
            if (promotionChar) {
                // Ensure it's one of the valid promotion pieces
                if (['q', 'r', 'b', 'n'].includes(promotionChar.toLowerCase())) {
                    promotionPiece = promotionChar.toLowerCase();
                }
            }

            const legal = board.game.getLegalMoves();
            let moveFound = false;

            for(const m of legal){
                // Check for matching move, ignoring promotion initially
                if(m.from === from && m.to === to){
                    // Check if it's a promotion move
                    if(m.promotion){
                        // The engine move UCI included a promotion, and the legal move is a promotion
                        if(promotionPiece){
                            m.promotion = promotionPiece; // Set the promotion piece
                            console.log(`[Bot] Performing move: ${moveUCI} with promotion to ${promotionPiece}`);
                        } else {
                            // Default to Queen if the engine's UCI format was incomplete but it's a promotion square
                            m.promotion = 'q';
                            console.warn(`[Bot] Performing move: ${moveUCI}. Legal move required promotion but none was provided. Defaulting to 'q'.`);
                        }
                    }

                    // Perform the move.
                    board.game.move(Object.assign({}, m, {animate:false, userGenerated:true}));
                    moveFound = true;
                    break;
                }
            }

            if (!moveFound) {
                console.error(`[Bot] performMove failed: No legal move found for UCI ${moveUCI}.`);
            }
        }catch(e){
            console.error('[Bot] performMove error:', e);
        }
    }


    // --------- Engine runner & controls (SLIGHTLY UPDATED to reset status) ---------
    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 = [];

            // Reset calculation status when a new search starts
            currentCalcStatus = { depth: 0, seldepth: 0, nodes: 0, nps: 0, time: 0, isSearching: true };
            throttledUpdateStatus();

            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 (updated for Eval Bar AND Calculation Status) ---------
    function initGUI(){
        board = findBoard();
        if(!board) return false;
        if(document.getElementById('botGUI_v2_wrapper')) return true; // Check for the new wrapper

        // 1. Create the main wrapper for the GUI and Eval Bar
        const wrapper = document.createElement('div');
        wrapper.id = 'botGUI_v2_wrapper';
        wrapper.style.cssText = 'display:flex; align-items:flex-start;';

        // 2. Create the existing control panel container
        const container = document.createElement('div');
        container.id = 'botGUI_v2';
        // Adjusted margin to allow space for the bar on the right
        container.style = 'background:rgba(255,255,255,0.95);padding:10px;margin:8px 0 8px 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="statusSection" style="margin-bottom:10px;padding:6px;border:1px solid #ccc;border-radius:4px;font-size:13px;">
                <div style="font-weight:600;margin-bottom:4px;">Calculation Status: <span id="searchStatusText" style="font-weight:700; color:#5cb85c;">Idle / Ready</span></div>
                <div id="calcStatus">
                    <div style="display:flex; justify-content:space-between;"><span>Depth:</span> <strong>-</strong></div>
                    <div style="display:flex; justify-content:space-between;"><span>Nodes:</span> <strong>-</strong></div>
                    <div style="display:flex; justify-content:space-between;"><span>NPS:</span> <strong>-</strong></div>
                    <div style="display:flex; justify-content:space-between;"><span>Time:</span> <strong>-</strong></div>
                </div>
            </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>
        `;

        // 3. Evaluation Bar HTML (UNCHANGED)
        const evalBarHtml = `
            <div id="evalBarWrapper" style="margin: 8px 8px 8px 8px; display: flex; flex-direction: column; align-items: center; max-height: 400px;">
                <div style="font-size:12px; color:#666; font-weight:600; text-align:center;">Evaluation</div>
                <div id="evalBar" style="
                    height: 300px;
                    width: 24px;
                    border-radius: 4px;
                    overflow: hidden;
                    box-shadow: 0 2px 5px rgba(0,0,0,0.2);
                    position: relative;
                    margin-top: 4px;
                    background-color: #000; /* Black color base (below) */
                ">
                    <div id="evalBarWhiteAdvantage" style="
                        background-color: #fff;
                        position: absolute;
                        bottom: 0;
                        width: 100%;
                        height: 50%; /* Default 50% for equality */
                        transition: height 0.3s ease-out;
                    "></div>
                    <div id="evalPercent" style="
                        position: absolute;
                        top: 50%; /* Initial center position */
                        left: 50%;
                        transform: translate(-50%, -50%);
                        font-weight: 700;
                        color: #1a1a1a;
                        font-size: 11px;
                        text-shadow: 0 0 1px #fff;
                        width: 100%;
                        text-align: center;
                        z-index: 10;
                        transition: all 0.3s ease-out; /* Transition position too */
                    ">+0.00</div>
                </div>
                <div style="font-size:10px; color:#666; margin-top:2px;">W% / B%</div>
            </div>
        `;


        // 4. Append to DOM
        try{
            wrapper.appendChild(container);
            wrapper.innerHTML += evalBarHtml; // Append eval bar HTML next to the container

            // Try to append to the board's main parent element
            board.parentElement.parentElement.appendChild(wrapper);
        }catch(e){
            // Fallback: append both to body
            document.body.appendChild(wrapper);
        }

        // 5. Setup controls (UNCHANGED)
        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 (UNCHANGED) ---------
    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;
                        // Reset the bar to neutral when a new move is made and analysis hasn't started yet
                        updateEvaluationBar(0, null);
                        if(settings.autoRun) debouncedRun(settings.lastDepth);
                    }
                }
            }catch(e){}
        }, 600);

        console.log('Improved Chess Bot ready');
    })();

})();