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.

当前为 2025-09-30 提交的版本,查看 最新版本

// ==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.');
    })();

})();