Chess.com Bot — Dynamic Time Management

Improved userscript with dynamic time management based on game phase, configurable Stockfish threads/hash, and the existing evaluation bar and move analysis.

Stan na 30-09-2025. Zobacz najnowsza wersja.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

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

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name Chess.com Bot — Dynamic Time Management
// @namespace thehackerclient
// @version 3.6 // Major update: Dynamic Time Management & Engine Options
// @description Improved userscript with dynamic time management based on game phase, configurable Stockfish threads/hash, and the existing evaluation bar and move analysis.
// @match https://www.chess.com/*
// @auther thehackerclient
// @grant GM_getResourceText
// @license MIT
// @require https://code.jquery.com/jquery-3.6.0.min.js
// @resource stockfish.js https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/10.0.2/stockfish.js
// @run-at document-start
// ==/UserScript==

(function () {
    'use strict';

    console.log('--- Chess Bot v3.1 Initializing: Stability Refactor Complete ---');

    // 🎯 Use a reliable CDN link for Stockfish Web Worker
    const STOCKFISH_URL = 'https://cdn.jsdelivr.net/gh/nmvsh/Stockfish.js@master/stockfish.js'; 

    // --------- Config & state ---------
    const STORAGE_KEY = 'chess_bot_settings_v3_1';
    const DEFAULTS = {
        autoRun: true,
        autoMovePiece: false,
        delayMin: 1.0, 
        delayMax: 3.0, 
        stockfishThreads: 4, 
        stockfishHash: 256,  
        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());

    // State variables
    let board = null;              
    let engine = { worker: null }; 
    let candidateMoves = [];       
    let isThinking = false;
    let canGo = true;
    let lastPlayedMove = null;     

    let lastPositionBestScore = { score: 0, mate: null, turn: 'w', initial: true };
    let lastMoveClassification = { type: 'N/A', cpl: 0, move: '' }; 

    // Move Classification Thresholds (in centipawns)
    const T_BEST = 0;
    const T_EXCELLENT = 5; 
    const T_GOOD = 15;     
    const T_INACCURACY = 40; 
    const T_MISTAKE = 120;   
    const T_BLUNDER = 250;   

    // Debounce and throttle helpers (using standard JS)
    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);} }; }

    // Robust board detection
    function findBoard(){ 
        const selectors = ['chess-board', 'wc-chess-board', '[data-cy="board"]', '.board'];
        for (const selector of selectors) {
            const el = document.querySelector(selector);
            // Ensure the element is visible and not part of an archive/puzzle board only
            if (el && el.offsetWidth > 100) return el;
        }
        return null; 
    }

    // --------- Persistence & Engine Setup ---------
    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){} }
    
    // Refactored engine creation for maximum reliability
    function createStockfishWorker(){
        try{
            if(engine.worker) engine.worker.terminate();
            
            // 🎯 The key change: Load the worker directly from the CDN URL
            engine.worker = new Worker(STOCKFISH_URL);
            engine.worker.onmessage = e => handleEngineMessage(e.data);
            engine.worker.onerror = e => console.error('Stockfish Worker Error:', e);

            // Initialize Stockfish options
            engine.worker.postMessage('ucinewgame');
            engine.worker.postMessage(`setoption name Threads value ${settings.stockfishThreads}`);
            engine.worker.postMessage(`setoption name Hash value ${settings.stockfishHash}`);
            
            console.log(`Stockfish worker created from CDN. Threads: ${settings.stockfishThreads}, Hash: ${settings.stockfishHash}MB. UCINewGame sent.`);
        }catch(err){ console.error('FATAL: Failed to create Stockfish worker.', err); }
    }
    
    function safeRestartEngine(){ 
        console.log('Restarting engine to apply new settings...'); 
        isThinking = false; 
        try{ if(engine.worker) engine.worker.terminate(); }catch(e){} 
        createStockfishWorker(); 
    }
    
    // --------- Evaluation and Classification Logic ---------
    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) {
            displayScore = mateScore > 0 ? `M+${mateScore}` : `M${mateScore}`;
            barHeightPercent = mateScore > 0 ? 98 : 2;
        } else {
            const clampedCp = Math.max(-800, Math.min(800, scoreCp));
            barHeightPercent = 50 + (clampedCp / 16);
            barHeightPercent = Math.max(0, Math.min(100, barHeightPercent));
            displayScore = (scoreCp / 100).toFixed(2);
            if (scoreCp >= 0) displayScore = `+${displayScore}`;
        }
        barWhiteEl.style.height = `${barHeightPercent}%`;
        scoreTextEl.innerText = displayScore;
        const whiteAdvantage = barHeightPercent;
        if (whiteAdvantage > 80) { scoreTextEl.style.color = '#fff'; scoreTextEl.style.top = '10%'; scoreTextEl.style.transform = 'translate(-50%, 0)'; } 
        else if ((100 - whiteAdvantage) > 80) { scoreTextEl.style.color = '#000'; scoreTextEl.style.top = '90%'; scoreTextEl.style.transform = 'translate(-50%, -100%)'; } 
        else { scoreTextEl.style.color = '#1a1a1a'; scoreTextEl.style.top = '50%'; scoreTextEl.style.transform = 'translate(-50%, -50%)'; }
    }
    
    function classifyMove(cplLoss) {
        if (cplLoss <= T_BEST) return 'Best Move';
        if (cplLoss <= T_EXCELLENT) return 'Excellent';
        if (cplLoss <= T_GOOD) return 'Good';
        if (cplLoss <= T_INACCURACY) return 'Inaccuracy';
        if (cplLoss <= T_MISTAKE) return 'Mistake';
        if (cplLoss <= T_BLUNDER) return 'Blunder';
        return 'Major Blunder';
    }
    
    function updateMoveClassificationDisplay() {
        const el = document.getElementById('moveClassText');
        if (!el) return;
        let classification = lastMoveClassification.type;
        let move = lastMoveClassification.move;
        el.innerHTML = `${move ? `(${move})` : ''} <strong>${classification}</strong>`;
        el.title = lastMoveClassification.cpl > 0 ? `CPL: ${lastMoveClassification.cpl.toFixed(0)}` : '';
        let color = '#333';
        switch(classification) {
            case 'Major Blunder':
            case 'Blunder': color = '#d9534f'; break; 
            case 'Mistake': color = '#f0ad4e'; break; 
            case 'Inaccuracy': color = '#f7e382'; break; 
            case 'Good': color = '#5bc0de'; break; 
            case 'Excellent': color = '#5cb85c'; break; 
            case 'Best Move': color = '#008000'; break; 
            case 'Thinking...': color = '#0d6efd'; break; 
            default: color = '#666'; break;
        }
        el.style.color = color;
    }

    // --------- Engine message parsing and classification ---------
    function parseScore(match){
        if(!match) return 0;
        const type = match[1];
        const val = parseInt(match[2]);
        if(type === 'cp') return val;
        // Mate scores are normalized to extreme centipawns
        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 scoreMatch = msg.match(/score (cp|mate) (-?\d+)/);
                const score = parseScore(scoreMatch); 
                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 move = pvTokens[0];
                const exists = candidateMoves.find(c=>c.move===move);
                if(!exists) candidateMoves.push({move, score, depth, pv: pvTokens, rawCp, rawMate});
                else if(depth>exists.depth) { Object.assign(exists, {score, depth, pv: pvTokens, rawCp, rawMate}); }
            }
        }

        if(msg.startsWith('bestmove')){
            candidateMoves.sort((a,b) => b.score - a.score);
            candidateMoves = candidateMoves.slice(0,3);
            showTopMoves();

            if(candidateMoves.length > 0) {
                const bestMove = candidateMoves[0];
                const finalCp = bestMove.rawCp !== null ? bestMove.rawCp : bestMove.score;
                const finalMate = bestMove.rawMate;
                const currentTurn = board.game.getTurn(); 

                // --- Move Quality Analysis ---
                if (!lastPositionBestScore.initial && lastPlayedMove) {
                    const scoreBefore = lastPositionBestScore.score; 
                    const turnBefore = lastPositionBestScore.turn; 
                    const movePlayed = lastPlayedMove;

                    const expectedAdvantage = turnBefore === 'w' ? scoreBefore : -scoreBefore;
                    const actualAdvantage = turnBefore === 'w' ? finalCp : -finalCp;
                    let cplLoss = expectedAdvantage - actualAdvantage;
                    const absCplLoss = Math.max(0, cplLoss); 

                    if (lastPositionBestScore.mate !== null || finalMate !== null || Math.abs(cplLoss) > 5000) {
                        // Skip CPL calculation if there was a mate or extreme score change
                        lastMoveClassification = { type: 'Analysis Complete', cpl: 0, move: movePlayed };
                    } else {
                        const classification = classifyMove(absCplLoss);
                        lastMoveClassification = { type: classification, cpl: absCplLoss, move: movePlayed };
                    }
                    console.log(`Move Analysis (${movePlayed}, ${turnBefore}): CPL ${absCplLoss.toFixed(0)}, Type: ${lastMoveClassification.type}`);
                    lastPlayedMove = null; 
                }

                // Update the last position score for the *next* analysis.
                lastPositionBestScore = {
                    score: finalCp,
                    mate: finalMate,
                    turn: currentTurn,
                    initial: false
                };

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

    // --------- Highlighting and Move Execution (Simplified for stability) ---------
    function mapSquareForBoard(sq){
        if(!board || !sq || sq.length<2) return sq;
        // Check for common 'flipped' class on chess.com boards
        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}"]`) || board.querySelector(`.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.cssText='position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:60;border-radius:3px;'; 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){} 
    }
    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.cssText = `
                    position:absolute; right:6px; top:${6 + index*28}px; 
                    padding:6px 8px; border-radius:6px; background:rgba(0,0,0,0.75); 
                    color:#fff; z-index:120; font-size:12px; font-family:Inter,Arial,sans-serif;
                `;
                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(' ')}`;
            
            // Auto-remove PV note after highlight duration
            setTimeout(()=>{ if(note && note.parentElement) note.parentElement.removeChild(note); }, settings.highlightMs + 5000);
        }catch(e){}
    }
    function showTopMoves(){
        if(!board || !board.game) return;
        detachHighlights('.botMoveHighlight');
        detachHighlights('.botThreatHighlight');
        
        // 1. Highlight Top Moves
        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); }
        });
        
        // 2. Show Threats (opponent's legal moves)
        showThreats();
    }
    function showThreats(){
        if(!board || !board.game) return;
        try{
            // Note: Chess.com's internal game object (board.game) needs to provide legal moves
            const legalMoves = board.game.getLegalMoves ? board.game.getLegalMoves() : [];
            const turn = board.game.getTurn ? board.game.getTurn() : 'w';
            const opponent = turn === 'w' ? 'b' : 'w';
            
            legalMoves.forEach(m=>{ 
                // Only highlight opponent's moves if they are capturable by the current player
                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 ? board.game.getLegalMoves() : [];
            for(const m of legal){ if(m.from===from && m.to===to){
                if(m.promotion && promotion){ m.promotion = promotion; }
                // Use Chess.com's internal move function
                board.game.move(Object.assign({}, m, {animate:false, userGenerated:true}));
                console.log(`Auto-moving piece: ${moveUCI}`);
                break;
            }}
        }catch(e){ console.error('performMove failed', e); }
    }


    // -------------------------------------------------------------------
    // 🎯 CORE LOGIC FOR ENGINE COMMANDS (runChessEngine)
    // -------------------------------------------------------------------
    function runChessEngine(type, value){
        if(!board || !engine.worker || !board.game) {
            console.warn('Engine run skipped: Missing board, worker, or game object.');
            return;
        }
        try{
            const fen = board.game.getFEN ? board.game.getFEN() : 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
            if(isThinking) engine.worker.postMessage('stop');
            
            candidateMoves = [];
            engine.worker.postMessage('position fen ' + fen);
            isThinking = true;

            const command = type === 'depth' 
                ? `go depth ${value}` 
                : `go movetime ${Math.max(100, value)}`; // Stockfish uses time limit (ms) for deeper calculation
            
            engine.worker.postMessage(command);
            console.log(`Engine command sent: ${command}`);
        }catch(e){ console.error('runChessEngine error', e); }
    }

    // -------------------------------------------------------------------
    // 🎯 DYNAMIC TIME MANAGEMENT (autoLoop)
    // -------------------------------------------------------------------
    const autoLoop = throttle(()=>{
        if(!board || !board.game) return;
        if(settings.autoRun && canGo && !isThinking && board.game.getTurn() === board.game.getPlayingAs()){
            canGo = false;
            
            const history = board.game.getMoveHistory ? board.game.getMoveHistory() : (board.game.history || []);
            const halfMoveCount = history.length;
            const moveNumber = Math.floor((halfMoveCount + 1) / 2); // 1-indexed full move

            let targetDelaySeconds;
            
            // --- Dynamic Time Management Logic ---
            const minTime = settings.delayMin; 
            const maxTime = settings.delayMax; 

            if (moveNumber <= 10) {
                // Opening (Moves 1-10): Faster, more standard time
                targetDelaySeconds = minTime * 1.0;
            } else if (moveNumber <= 30) {
                // Middlegame (Moves 11-30): Max complexity, use max time
                targetDelaySeconds = maxTime;
            } else {
                // Endgame (Moves 31+): High accuracy required, moderate time
                targetDelaySeconds = minTime + (maxTime - minTime) * 0.7; // ~70% of max
            }

            // Apply slight randomization
            const variance = 0.95 + Math.random() * 0.1; // +/- 5%
            targetDelaySeconds = Math.max(minTime * 0.5, targetDelaySeconds * variance); 

            const movetimeMs = Math.round(targetDelaySeconds * 1000);

            console.log(`[Move ${moveNumber}] Dynamic Time: ${targetDelaySeconds.toFixed(2)}s (${movetimeMs}ms)`);

            setTimeout(()=>{ 
                runChessEngine('movetime', movetimeMs); 
                canGo = true; 
            }, 100); 
        }
    }, 150);


    // --------- GUI Setup ---------
    function initGUI(){
        board = findBoard();
        if(!board) return false;
        if(document.getElementById('botGUI_v3_wrapper')) return true; 

        const wrapper = document.createElement('div');
        wrapper.id = 'botGUI_v3_wrapper';
        wrapper.style.cssText = 'display:flex; align-items:flex-start;'; 

        const container = document.createElement('div');
        container.id = 'botGUI_v3';
        container.style.cssText = `
            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);
            box-sizing: border-box; 
        `;

        container.innerHTML = `
            <div style="font-weight:700;margin-bottom:6px;font-size:16px;color:#333;border-bottom:2px solid #eee;padding-bottom:6px;">
                🤖 Chess Bot Live Analysis v3.1
            </div>
            <div id="moveClassification" style="margin-top:4px;margin-bottom:8px;font-weight:600;font-size:13px;">
                Last Move: <span id="moveClassText" style="color:#666;">N/A</span>
            </div>
            <div id="depthText" style="margin-top:10px;font-size:13px;">Manual Depth: <strong>${settings.lastDepth}</strong></div>
            <input type="range" id="depthSlider" min="1" max="30" value="${settings.lastDepth}" step="1" style="width:100%; height: 20px; margin: 4px 0;">
            
            <div style="margin-top:10px; font-weight: 600; font-size:13px;">Dynamic Engine Time (seconds)</div>
            <div style="margin-top:4px;display:flex;justify-content:space-between;align-items:center;font-size:12px;">
                <label style="color:#444;">Min Base:</label>
                <input id="delayMinInput" type="number" min="0.1" step="0.1" value="${settings.delayMin}" style="width:60px;padding:3px;border:1px solid #ccc;border-radius:3px;font-size:12px;">
                <label style="color:#444;">Max Cap:</label>
                <input id="delayMaxInput" type="number" min="0.1" step="0.1" value="${settings.delayMax}" style="width:60px;padding:3px;border:1px solid #ccc;border-radius:3px;font-size:12px;">
            </div>

            <div style="margin-top:12px; font-weight: 600; font-size:13px;">Stockfish Performance (Requires Reload)</div>
            <div style="margin-top:4px;display:flex;justify-content:space-between;align-items:center;font-size:12px;">
                <label style="color:#444;">Threads:</label>
                <input id="threadsInput" type="number" min="1" max="16" step="1" value="${settings.stockfishThreads}" style="width:60px;padding:3px;border:1px solid #ccc;border-radius:3px;font-size:12px;">
                <label style="color:#444;">Hash (MB):</label>
                <input id="hashInput" type="number" min="16" max="2048" step="32" value="${settings.stockfishHash}" style="width:60px;padding:3px;border:1px solid #ccc;border-radius:3px;font-size:12px;">
            </div>

            <div style="margin-top:12px;display:flex;flex-direction:column;gap:4px;font-size:13px;">
                <div style="cursor:pointer;"><input type="checkbox" id="autoRunCB" style="margin-right:5px;"> <label for="autoRunCB" style="cursor:pointer;">Auto Run Analysis</label></div>
                <div style="cursor:pointer;"><input type="checkbox" id="autoMoveCB" style="margin-right:5px;"> <label for="autoMoveCB" style="cursor:pointer;">Auto Move (⚠️ Caution)</label></div>
            </div>
            <div style="margin-top:15px;display:flex;gap:8px;">
                <button id="reloadBtn" style="flex:1;padding:8px 6px;border-radius:6px;border:1px solid #ccc;cursor:pointer;background-color:#f8f9fa;color:#333;font-weight:600;">Reload Engine</button>
                <button id="analyseBtn" style="flex:1;padding:8px 6px;border-radius:6px;background-color:#0d6efd;color:#fff;border:none;cursor:pointer;font-weight:600;">Analyse Now (Manual Depth)</button>
            </div>
            <div style="margin-top:8px;font-size:11px;color:#666;text-align:center;">Top 3 moves and threats are highlighted briefly.</div>
        `;

        // Evaluation Bar HTML
        const evalBarHtml = `
            <div id="evalBarWrapper" style="margin: 8px 8px 8px 8px; display: flex; flex-direction: column; align-items: center; max-height: 400px; flex-shrink: 0;">
                <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;
                ">
                    <div id="evalBarWhiteAdvantage" style="
                        background-color: #fff;
                        position: absolute;
                        bottom: 0;
                        width: 100%;
                        height: 50%;
                        transition: height 0.3s ease-out;
                    "></div>
                    <div id="evalPercent" style="
                        position: absolute;
                        top: 50%; 
                        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;
                    ">+0.00</div>
                </div>
                <div style="font-size:10px; color:#666; margin-top:2px;">W% / B%</div>
            </div>
        `;

        try{
            wrapper.appendChild(container);
            wrapper.innerHTML += evalBarHtml; 
            // Attempt to insert the wrapper near the board, usually its parent or a sibling
            board.parentElement.appendChild(wrapper);
        }catch(e){
            document.body.appendChild(wrapper);
        }

        // Attach Event Listeners
        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 = `Manual 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(); };
        
        // Time inputs
        document.getElementById('delayMinInput').onchange = e => {
            settings.delayMin = parseFloat(e.target.value) || 0.1;
            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.1;
            if(settings.delayMax < settings.delayMin) settings.delayMin = settings.delayMax;
            document.getElementById('delayMinInput').value = settings.delayMin;
            saveSettings();
        };

        // Performance inputs (trigger engine restart)
        document.getElementById('threadsInput').onchange = e => { 
            settings.stockfishThreads = parseInt(e.target.value) || 1; 
            if(settings.stockfishThreads < 1) settings.stockfishThreads = 1;
            e.target.value = settings.stockfishThreads;
            saveSettings(); 
            safeRestartEngine(); 
        };
        document.getElementById('hashInput').onchange = e => { 
            settings.stockfishHash = parseInt(e.target.value) || 128; 
            if(settings.stockfishHash < 16) settings.stockfishHash = 16;
            e.target.value = settings.stockfishHash;
            saveSettings(); 
            safeRestartEngine(); 
        };

        document.getElementById('reloadBtn').onclick = () => { safeRestartEngine(); };
        document.getElementById('analyseBtn').onclick = () => { runChessEngine('depth', settings.lastDepth); };

        updateMoveClassificationDisplay();
        console.log('GUI initialized.');

        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(){
        // Wait for the board element to exist
        await waitUntil(()=> findBoard());
        
        // Wait for the board's internal game object to be initialized
        await waitUntil(()=> (board = findBoard()) && board.game);

        createStockfishWorker();
        initGUI();

        // Observe the body for board changes (e.g., game ending or new game starting)
        const mo = new MutationObserver(debounce(()=>{ 
            board = findBoard(); 
            if(board && !document.getElementById('botGUI_v3_wrapper')) initGUI(); 
        }, 300));
        mo.observe(document.body, {childList:true, subtree:true});

        // Main analysis loop: Checks periodically if a move is needed
        setInterval(autoLoop, 150);

        let lastMoveCount = null;
        
        // Polling loop to detect moves made by the opponent or user
        setInterval(()=>{
            try{
                if(board && board.game){
                    // Use getMoveHistory or fall back to history property
                    const moveHistory = board.game.getMoveHistory ? board.game.getMoveHistory() : (board.game.history || []);
                    const moves = moveHistory.length;
                    
                    if(lastMoveCount === null) lastMoveCount = moves;
                    
                    if(moves !== lastMoveCount){
                        const lastMove = moveHistory[moves - 1];
                        // Attempt to get UCI format of the move
                        lastPlayedMove = lastMove.uci || (lastMove.from + lastMove.to + (lastMove.promotion || ''));
                        
                        lastMoveCount = moves;
                        
                        // Reset display and indicate thinking
                        updateEvaluationBar(0, null);
                        lastMoveClassification = { type: 'Thinking...', cpl: 0, move: lastPlayedMove };
                        updateMoveClassificationDisplay();

                        if(settings.autoRun) {
                            autoLoop(); // Start dynamic time analysis
                        } else {
                            // Run a very quick analysis if autoRun is disabled, just to update the score/bar
                            runChessEngine('movetime', 500); 
                        }
                    }
                }
            }catch(e){
                 console.error("Move detection interval error:", e);
            }
        }, 600);

        console.log('Improved Chess Bot v3.1 is ready and stable.');
    })();

})();