Chess.com Bot — v2.1

Improved userscript: top 3 moves & threats, persistent settings, debounce/throttle, safer engine lifecycle, promotion handling, better board detection, min/max delay

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Chess.com Bot — v2.1
// @namespace    thehackerclient
// @version      3.6
// @description  Improved userscript: top 3 moves & threats, persistent settings, debounce/throttle, safer engine lifecycle, promotion handling, better board detection, min/max delay
// @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';

    // -------------------------------------------------------------------------
    // 1. Configuration and State Management
    // -------------------------------------------------------------------------

    const STORAGE_KEY = 'chess_bot_settings_v4';
    const DEFAULTS = {
        autoRun: true,
        autoMovePiece: false,
        delayMin: 0.5,
        delayMax: 2.0,
        lastDepth: 18,
        showPV: true,
        showEvalBar: true,
        showAdvancedThreats: true,
        highlightMs: 2000,
        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)',
            undefended: 'rgba(255, 0, 255, 0.6)',
            blunder: 'rgba(255, 0, 0, 0.7)',
            mistake: 'rgba(255, 128, 0, 0.7)',
            inaccuracy: 'rgba(255, 255, 0, 0.7)',
            checkmate: 'rgba(255, 69, 0, 0.8)'
        }
    };

    // Error classification thresholds (Centipawn drops)
    const ERROR_THRESHOLDS = {
        BLUNDER: 200,    // Score drops by > 200 cp
        MISTAKE: 100,    // Score drops by > 100 cp
        INACCURACY: 50   // Score drops by > 50 cp
    };

    let settings = {};
    let board = null;
    let stockfishObjectURL = null;

    /**
     * Handles settings loading and initialization.
     */
    function loadSettings() {
        try {
            const saved = localStorage.getItem(STORAGE_KEY);
            let loadedSettings = saved ? JSON.parse(saved) : {};
            settings = Object.assign({}, DEFAULTS, loadedSettings);
            settings.autoMovePiece = false;
            settings.delayMin = parseFloat(settings.delayMin) || DEFAULTS.delayMin;
            settings.delayMax = parseFloat(settings.delayMax) || DEFAULTS.delayMax;
            settings.lastDepth = parseInt(settings.lastDepth) || DEFAULTS.lastDepth;
            if (settings.delayMin > settings.delayMax) {
                settings.delayMax = settings.delayMin;
            }
            console.log('Bot Settings Loaded:', settings);
        } catch (e) {
            console.error('Error loading settings, using defaults.', e);
            settings = DEFAULTS;
        }
    }

    function saveSettings() {
        try {
            localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
        } catch (e) {
            console.warn('Failed to save settings to localStorage.', e);
        }
    }

    loadSettings();

    // -------------------------------------------------------------------------
    // 2. Utility Functions (DOM, Board Mapping)
    // -------------------------------------------------------------------------

    function throttle(fn, wait) {
        let last = 0;
        return function (...a) {
            const now = Date.now();
            if (now - last > wait) {
                last = now;
                fn.apply(this, a);
            }
        };
    }

    function findBoard() {
        return document.querySelector('chess-board') ||
               document.querySelector('wc-chess-board') ||
               document.querySelector('[data-cy="board"]') ||
               null;
    }

    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;
        }
    }

    // -------------------------------------------------------------------------
    // 3. Game Analysis Class
    // -------------------------------------------------------------------------

    class GameAnalyzer {
        constructor() {
            // Stores analysis history: {fen, move, cpBefore, cpAfter, scoreDrop, errorType, turn}
            this.gameHistory = [];
            this.lastAnalyzedFen = null;
            this.currentTurn = 'w';
        }

        /**
         * Classifies the severity of an error based on centipawn drop.
         * @param {number} drop The absolute drop in centipawns.
         * @param {number} cpBefore The evaluation before the move.
         * @returns {string} The error type ('Blunder', 'Mistake', 'Inaccuracy', 'Good', 'Excellent').
         */
        classifyError(drop, cpBefore) {
            if (drop >= ERROR_THRESHOLDS.BLUNDER) {
                return 'Blunder';
            } else if (drop >= ERROR_THRESHOLDS.MISTAKE) {
                return 'Mistake';
            } else if (drop >= ERROR_THRESHOLDS.INACCURACY) {
                return 'Inaccuracy';
            } else {
                return 'Good';
            }
        }

        /**
         * Clears history and resets for a new game.
         */
        reset() {
            this.gameHistory = [];
            this.lastAnalyzedFen = null;
            this.currentTurn = 'w';
            document.getElementById('analysisReport').innerHTML = this.getReportHTML();
        }

        /**
         * Processes an actual move made by a player and compares its outcome to the best move.
         * This function is called AFTER a move has been played and the new FEN is ready for analysis.
         * @param {string} prevFen The FEN before the move was played.
         * @param {string} actualMove The UCI move played (e.g., 'e2e4').
         * @param {number} cpBefore The *best* evaluation of the previous position (prevFen).
         * @param {number} cpAfter The *best* evaluation of the new position (currentFen).
         */
        recordMoveAnalysis(prevFen, actualMove, cpBefore, cpAfter) {
            if (prevFen === this.lastAnalyzedFen) return;
            this.lastAnalyzedFen = prevFen;

            // Get the score from the perspective of the player who just moved
            // White moves: cpBefore is White's score, cpAfter is White's score
            // Black moves: cpBefore is Black's score (-cp), cpAfter is Black's score (-cp)

            // Score from the perspective of the player whose turn it WAS (White if turn 'w')
            const playerPerspectiveBefore = this.currentTurn === 'w' ? cpBefore : -cpBefore;
            // Score from the perspective of the player whose turn it IS NOT (White if turn 'w')
            const playerPerspectiveAfter = this.currentTurn === 'w' ? cpAfter : -cpAfter;

            // Drop is the difference between the *optimal* score and the score *after* the move.
            const scoreDrop = playerPerspectiveBefore - playerPerspectiveAfter;

            // Check for missed wins (a large score drop into a neutral or losing position)
            const errorType = this.classifyError(scoreDrop, playerPerspectiveBefore);

            // Record the move
            this.gameHistory.push({
                fen: prevFen,
                move: actualMove,
                cpBefore: cpBefore,
                cpAfter: cpAfter,
                scoreDrop: scoreDrop,
                errorType: errorType,
                turn: this.currentTurn
            });

            // Toggle turn for next move
            this.currentTurn = this.currentTurn === 'w' ? 'b' : 'w';
        }

        /**
         * Generates the analysis report HTML.
         */
        getReportHTML() {
            const stats = {
                w: { Blunder: 0, Mistake: 0, Inaccuracy: 0, Total: 0 },
                b: { Blunder: 0, Mistake: 0, Inaccuracy: 0, Total: 0 }
            };

            const worstMoves = [];

            this.gameHistory.forEach((move, index) => {
                const player = move.turn;
                if (move.errorType !== 'Good') {
                    stats[player][move.errorType]++;
                    stats[player].Total++;
                    worstMoves.push({
                        moveNumber: Math.floor(index / 2) + 1,
                        ...move
                    });
                }
            });

            worstMoves.sort((a, b) => b.scoreDrop - a.scoreDrop);

            const errorsDisplay = (player) => `
                <div style="font-size:14px; margin-top:5px; padding-left:10px; border-left:3px solid ${player === 'w' ? '#f7f7f7' : '#333'};">
                    <span style="font-weight:700; color:${player === 'w' ? '#2c3e50' : '#2c3e50'};">${player === 'w' ? 'White' : 'Black'} (${stats[player].Total} Errors)</span>
                    <br>
                    <span style="color:${settings.colors.blunder};">Blunders: ${stats[player].Blunder}</span>,
                    <span style="color:${settings.colors.mistake};">Mistakes: ${stats[player].Mistake}</span>,
                    <span style="color:${settings.colors.inaccuracy};">Inaccuracies: ${stats[player].Inaccuracy}</span>
                </div>
            `;

            const worstMovesList = worstMoves.slice(0, 3).map(move => {
                const drop = (move.scoreDrop / 100).toFixed(2);
                const color = settings.colors[move.errorType.toLowerCase()];
                return `<li style="margin-top:5px; color:${color};">
                    ${move.moveNumber}. ${move.turn === 'w' ? 'W' : 'B'}: ${move.move}
                    (<span style="font-weight:700;">${move.errorType}</span>, dropped ${drop})
                </li>`;
            }).join('');

            return `
                <div style="margin-top:15px; padding-top:10px; border-top:1px solid #eee;">
                    <div style="font-weight:700; margin-bottom:10px; color:#2c3e50; font-size:16px;">Game Analysis Summary</div>

                    ${errorsDisplay('w')}
                    ${errorsDisplay('b')}

                    <div style="margin-top:15px;">
                        <div style="font-weight:700; color:#c0392b;">Top 3 Worst Moves/Missed Opportunities:</div>
                        <ul style="list-style:disc; margin-left:15px; padding-left:0; font-size:14px;">
                            ${worstMovesList || '<li>No major errors recorded yet.</li>'}
                        </ul>
                    </div>
                </div>
            `;
        }
    }


    // -------------------------------------------------------------------------
    // 4. Stockfish Engine Management Class
    // -------------------------------------------------------------------------

    class StockfishManager {
        constructor(callback) {
            this.worker = null;
            this.onEngineData = callback;
            this.isThinking = false;
            this.lastFen = '';
            this.candidateMoves = [];
            this.currentEval = 0;
            this.checkmateIn = null;
            this.initWorker();
        }

        parseScore(match) {
            if (!match) return 0;
            const type = match[1];
            const val = parseInt(match[2]);
            if (type === 'cp') {
                return val;
            }
            this.checkmateIn = Math.abs(val);
            return val > 0 ? 100000 - val : -100000 - val;
        }

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

            if (msg.startsWith('info') && msg.includes('pv')) {
                const pvTokens = msg.split(' pv ')[1].trim().split(/\s+/);
                if (pvTokens && pvTokens.length) {
                    const move = pvTokens[0];
                    const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/);
                    this.checkmateIn = null;
                    const score = this.parseScore(scoreMatch);
                    const depthMatch = msg.match(/depth (\d+)/);
                    const depth = depthMatch ? parseInt(depthMatch[1]) : settings.lastDepth;

                    const exists = this.candidateMoves.find(c => c.move === move);
                    if (!exists) {
                        this.candidateMoves.push({ move, score, depth, pv: pvTokens });
                    } else if (depth > exists.depth) {
                        exists.score = score;
                        exists.depth = depth;
                        exists.pv = pvTokens;
                    }

                    this.candidateMoves.sort((a, b) => b.score - a.score);
                    this.candidateMoves = this.candidateMoves.slice(0, 3);
                    this.currentEval = this.candidateMoves[0]?.score || 0;
                    this.onEngineData({ type: 'info', moves: this.candidateMoves, eval: this.currentEval, mate: this.checkmateIn, fen: this.lastFen });
                }
            }

            if (msg.startsWith('bestmove')) {
                this.isThinking = false;
                const bestMoveUCI = msg.split(' ')[1];
                this.onEngineData({ type: 'bestmove', move: bestMoveUCI, moves: this.candidateMoves, eval: this.currentEval, mate: this.checkmateIn, fen: this.lastFen });

                if (settings.autoMovePiece && bestMoveUCI && bestMoveUCI !== '(none)') {
                    this.performMove(bestMoveUCI);
                }
            }
        }

        initWorker() {
            try {
                if (stockfishObjectURL === null) {
                    const text = GM_getResourceText('stockfish.js');
                    stockfishObjectURL = URL.createObjectURL(new Blob([text], { type: 'application/javascript' }));
                }

                if (this.worker) this.worker.terminate();
                this.worker = new Worker(stockfishObjectURL);
                this.worker.onmessage = e => this.handleEngineMessage(e.data);
                this.worker.postMessage('ucinewgame');
                this.worker.postMessage('setoption name Threads value 4');
                this.worker.postMessage('isready');
                console.log('Stockfish worker created and initialized.');
            } catch (err) {
                console.error('Failed to create Stockfish worker', err);
            }
        }

        safeRestart() {
            this.isThinking = false;
            this.lastFen = '';
            this.candidateMoves = [];
            this.currentEval = 0;
            this.checkmateIn = null;
            try {
                if (this.worker) this.worker.terminate();
            } catch (e) { /* ignore */ }
            this.initWorker();
        }

        runAnalysis(fen, depth) {
            if (!this.worker) {
                this.initWorker();
                return;
            }

            if (this.isThinking && fen === this.lastFen) return;
            this.lastFen = fen;

            this.worker.postMessage('stop');
            this.candidateMoves = [];
            this.currentEval = 0;
            this.checkmateIn = null;

            this.worker.postMessage('position fen ' + fen);
            this.isThinking = true;
            this.worker.postMessage('go depth ' + depth);
            console.log(`Starting analysis for FEN: ${fen} at depth ${depth}`);
        }

        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();
                let foundMove = null;

                for (const m of legal) {
                    if (m.from === from && m.to === to) {
                        foundMove = m;
                        if (m.promotion && promotion) {
                            foundMove.promotion = promotion;
                        }
                        break;
                    }
                }

                if (foundMove) {
                    const moveObj = Object.assign({}, foundMove, { animate: true, userGenerated: true });
                    board.game.move(moveObj);
                    console.log('Bot performed move:', moveUCI);
                } else {
                    console.warn(`Could not find legal move object for UCI: ${moveUCI}`);
                }
            } catch (e) {
                console.error('performMove failed', e);
            }
        }
    }

    // -------------------------------------------------------------------------
    // 5. Board Visualizer & UI Class
    // -------------------------------------------------------------------------

    class BoardVisualizer {
        constructor(engine) {
            this.engine = engine;
            this.pvNoteTimeout = null;
            this.highlightTimeouts = [];
            this.evalBar = null;
            this.initStyles();
        }

        initStyles() {
            const style = document.createElement('style');
            style.id = 'bot_analysis_styles_v4';
            style.innerHTML = `
                /* Base Styles for UI elements */
                #botGUI_v4 {
                    background: rgba(255, 255, 255, 0.95);
                    padding: 12px;
                    margin: 8px;
                    max-width: 300px;
                    font-family: 'Inter', Arial, sans-serif;
                    border-radius: 12px;
                    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
                    border: 1px solid rgba(0,0,0,0.05);
                    transition: all 0.3s ease;
                }

                /* Highlight Overlays */
                .botMoveHighlight, .botThreatHighlight, .botUndefendedHighlight {
                    position: absolute;
                    top: 0;
                    left: 0;
                    width: 100%;
                    height: 100%;
                    pointer-events: none;
                    z-index: 60;
                    border-radius: 4px;
                    opacity: 1;
                    transition: opacity ${settings.highlightMs / 4000}s ease-out;
                }

                .botUndefendedHighlight {
                    border: 3px dashed ${settings.colors.undefended};
                    background: transparent !important;
                    box-sizing: border-box;
                }

                /* Evaluation Bar Container */
                #evalBarContainer {
                    position: absolute;
                    bottom: 0;
                    right: 0;
                    width: 24px;
                    height: 100%;
                    z-index: 999;
                    overflow: hidden;
                    border-radius: 6px;
                    margin-left: 12px;
                    transition: all 0.3s ease;
                    box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
                }

                /* Evaluation Bar Inner Elements */
                #evalBarWhite, #evalBarBlack {
                    position: absolute;
                    width: 100%;
                    transition: height 0.5s ease;
                }
                #evalBarWhite {
                    background-color: #f7f7f7; /* White advantage */
                    bottom: 0;
                }
                #evalBarBlack {
                    background-color: #333333; /* Black advantage */
                    top: 0;
                }

                #evalBarText {
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    font-size: 10px;
                    font-weight: bold;
                    color: #fff;
                    z-index: 100;
                    text-shadow: 0 0 3px rgba(0,0,0,0.8);
                }

                /* PV Notes (Top 3 Moves) inside the board container */
                .pvNote {
                    padding: 6px 8px;
                    border-radius: 6px;
                    color: #fff;
                    z-index: 120;
                    font-size: 12px;
                    pointer-events: none;
                    white-space: nowrap;
                    font-weight: 600;
                    box-shadow: 0 2px 4px rgba(0,0,0,0.3);
                    position: absolute; /* Critical for positioning inside board parent */
                    right: 10px;
                }
            `;
            document.head.appendChild(style);
        }

        attachHighlight(el, cls, color) {
            if (!el) return null;
            let overlay = el.querySelector('.' + cls);
            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = cls;
                overlay.style.backgroundColor = color;
                el.appendChild(overlay);
            }
            return overlay;
        }

        detachHighlights(selector = '.botMoveHighlight, .botThreatHighlight, .botUndefendedHighlight, .pvNote') {
            try {
                document.querySelectorAll(selector).forEach(n => {
                    if (n.parentElement) n.parentElement.removeChild(n);
                });
                this.highlightTimeouts.forEach(t => clearTimeout(t));
                this.highlightTimeouts = [];
            } catch (e) { /* ignore errors during cleanup */ }
        }

        showAnalysis(candidateMoves, currentEval, checkmateIn) {
            if (!board || !board.game) return;

            this.detachHighlights();
            this.updateEvalBar(currentEval, checkmateIn);

            candidateMoves.forEach((cm, i) => {
                const isCheckmate = checkmateIn !== null && i === 0;

                const from = mapSquareForBoard(cm.move.slice(0, 2));
                const to = mapSquareForBoard(cm.move.slice(2, 4));
                const color = isCheckmate ? settings.colors.checkmate :
                    (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 = this.attachHighlight(el, 'botMoveHighlight', color);
                        const t = setTimeout(() => {
                            if (ov && ov.parentElement) ov.parentElement.removeChild(ov);
                        }, settings.highlightMs);
                        this.highlightTimeouts.push(t);
                    }
                });

                if (settings.showPV) {
                    this.addPVNote(cm, i, isCheckmate, checkmateIn);
                }
            });

            if (settings.showAdvancedThreats) {
                this.showThreatsAndUndefended();
            }
        }

        /**
         * Adds a Principal Variation note in the TOP RIGHT CORNER of the board.
         */
        addPVNote(cm, index, isMate, mateIn) {
            try {
                const id = `pvNote-${index}`;
                let note = document.getElementById(id);
                const container = board.parentElement;

                if (!note) {
                    note = document.createElement('div');
                    note.id = id;
                    note.className = 'pvNote';

                    // Position: TOP RIGHT CORNER OF THE BOARD
                    Object.assign(note.style, {
                        top: `${10 + index * 28}px`,
                        background: isMate ? settings.colors.checkmate : 'rgba(0,0,0,0.75)',
                    });

                    container.appendChild(note);
                }

                let scoreText;
                if (isMate) {
                    scoreText = `M${mateIn}`;
                } else {
                    scoreText = (cm.score / 100).toFixed(2);
                }

                note.innerText = `#${index + 1}: ${scoreText} | ${cm.move} PV: ${cm.pv.slice(0, 5).join(' ')}`;
                note.style.background = isMate ? settings.colors.checkmate : 'rgba(0,0,0,0.75)';

                if (this.pvNoteTimeout) clearTimeout(this.pvNoteTimeout);
                this.pvNoteTimeout = setTimeout(() => {
                    this.detachHighlights('.pvNote');
                }, settings.highlightMs + 500);
            } catch (e) {
                console.error('Failed to add PV note', e);
            }
        }

        showThreatsAndUndefended() {
            if (!board || !board.game || !settings.showAdvancedThreats) return;

            try {
                const game = board.game;
                const turn = game.getTurn();
                const opponent = turn === 'w' ? 'b' : 'w';

                const allLegalMoves = game.getLegalMoves();
                const opponentMoves = allLegalMoves.filter(m => game.get(m.from)?.color === opponent);

                opponentMoves.forEach(m => {
                    const sq = mapSquareForBoard(m.to);
                    const el = getBoardSquareEl(sq);
                    if (el) {
                        const ov = this.attachHighlight(el, 'botThreatHighlight', settings.colors.threat);
                        const t = setTimeout(() => {
                            if (ov && ov.parentElement) ov.parentElement.removeChild(ov);
                        }, settings.highlightMs);
                        this.highlightTimeouts.push(t);

                        const targetPiece = game.get(m.to);
                        if (targetPiece && targetPiece.color === turn) {
                            const undefendedEl = getBoardSquareEl(sq);
                            if (undefendedEl) {
                                const undefendedOv = this.attachHighlight(undefendedEl, 'botUndefendedHighlight', settings.colors.undefended);
                                const t2 = setTimeout(() => {
                                    if (undefendedOv && undefendedOv.parentElement) undefendedOv.parentElement.removeChild(undefendedOv);
                                }, settings.highlightMs * 2);
                                this.highlightTimeouts.push(t2);
                            }
                        }
                    }
                });

            } catch (e) {
                console.warn('Failed to show advanced threats', e);
            }
        }

        setupEvalBar() {
            if (this.evalBar) return;

            const boardContainer = document.querySelector('.main-board-container') ||
                                   document.querySelector('.live-game-board') ||
                                   document.querySelector('.board-viewer-component') ||
                                   board.parentElement;

            if (!boardContainer) {
                console.warn('Could not find suitable container for Eval Bar.');
                return;
            }

            const wrapper = document.createElement('div');
            wrapper.id = 'evalBarContainer';
            wrapper.style.position = 'absolute';
            wrapper.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)';
            wrapper.style.marginLeft = '12px';
            wrapper.style.height = '100%';
            wrapper.style.bottom = '0';
            wrapper.style.right = '-36px';

            let relativeParent = boardContainer;
            if (getComputedStyle(relativeParent).position === 'static') {
                relativeParent.style.position = 'relative';
            }

            relativeParent.appendChild(wrapper);

            wrapper.innerHTML = `
                <div id="evalBar" style="height:100%; width:100%; position:relative; overflow:hidden; border-radius:6px;">
                    <div id="evalBarBlack" style="height: 50%; width: 100%;"></div>
                    <div id="evalBarWhite" style="height: 50%; width: 100%; top: 50%; position: absolute;"></div>
                    <div id="evalBarText">0.0</div>
                </div>
            `;
            this.evalBar = {
                container: wrapper,
                whiteBar: wrapper.querySelector('#evalBarWhite'),
                blackBar: wrapper.querySelector('#evalBarBlack'),
                text: wrapper.querySelector('#evalBarText')
            };
            this.updateEvalBar(0, null);
        }

        /**
         * Updates the visual state of the evaluation bar using a percentage map.
         * White advantage is mapped from 50% to 100%. Black advantage is 50% down to 0%.
         */
        updateEvalBar(cpScore, mateIn) {
            if (!this.evalBar || !settings.showEvalBar) {
                if (this.evalBar) this.evalBar.container.style.display = 'none';
                return;
            }

            this.evalBar.container.style.display = 'block';

            let percentage;
            let displayScore;

            if (mateIn !== null) {
                displayScore = `M${mateIn}`;
                percentage = cpScore > 0 ? 100 : 0;
            } else {
                displayScore = (cpScore / 100).toFixed(1);

                // Use a sigmoid function (simpler, effective) to map score to percentage
                // P_white = 100 / (1 + e^(-k * cpScore))
                const K = 0.004;
                percentage = 100 / (1 + Math.exp(-K * cpScore));
            }

            percentage = Math.max(0, Math.min(100, percentage));

            let whiteHeight = percentage;
            let blackHeight = 100 - percentage;

            // Adjust colors based on board flip
            const isFlipped = board.classList.contains('flipped');
            if (isFlipped) {
                this.evalBar.whiteBar.style.backgroundColor = '#333333'; // Black on bottom (White's bar shows Black advantage)
                this.evalBar.blackBar.style.backgroundColor = '#f7f7f7'; // White on top (Black's bar shows White advantage)
            } else {
                this.evalBar.whiteBar.style.backgroundColor = '#f7f7f7'; // White on bottom
                this.evalBar.blackBar.style.backgroundColor = '#333333'; // Black on top
            }

            // Apply calculated heights (logic remains the same)
            this.evalBar.whiteBar.style.height = `${whiteHeight}%`;
            this.evalBar.blackBar.style.height = `${blackHeight}%`;
            this.evalBar.whiteBar.style.top = `${blackHeight}%`;

            // Text color logic
            if (percentage > 70) {
                this.evalBar.text.style.color = isFlipped ? '#f7f7f7' : '#333'; // Text visible over white bar area
            } else if (percentage < 30) {
                this.evalBar.text.style.color = isFlipped ? '#333' : '#f7f7f7'; // Text visible over black bar area
            } else {
                this.evalBar.text.style.color = '#fff';
            }

            this.evalBar.text.innerText = displayScore;
        }
    }


    // -------------------------------------------------------------------------
    // 6. GUI & Settings Management Class
    // -------------------------------------------------------------------------

    class GUIManager {
        constructor(engine, visualizer, analyzer) {
            this.engine = engine;
            this.visualizer = visualizer;
            this.analyzer = analyzer;
            this.container = null;
        }

        initGUI() {
            board = findBoard();
            if (!board) return false;
            if (document.getElementById('botGUI_v4')) return true;

            const parent = document.querySelector('.main-board-container') || board.parentElement.parentElement || document.body;

            this.container = document.createElement('div');
            this.container.id = 'botGUI_v4';
            this.container.style.maxWidth = '300px';

            this.container.innerHTML = `
                <div style="font-weight:700;margin-bottom:10px;font-size:16px;color:#2c3e50;">♟️ Deep Chess Analysis v4.0</div>

                <!-- Depth Control -->
                <div id="depthControl" style="margin-bottom:12px;">
                    <div id="depthText" style="margin-bottom:4px; font-weight:600;">Search Depth: <strong style="color:#2980b9;">${settings.lastDepth}</strong></div>
                    <input type="range" id="depthSlider" min="5" max="30" value="${settings.lastDepth}" step="1">
                </div>

                <!-- Main Toggles -->
                <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:12px; padding-bottom:12px; border-bottom:1px solid #eee;">
                    <div><input type="checkbox" id="autoRunCB" style="margin-right:5px;"> <label for="autoRunCB" style="font-size:14px;">Continuous Analysis</label></div>
                    <div><input type="checkbox" id="autoMoveCB" style="margin-right:5px;"> <label for="autoMoveCB" style="font-size:14px; color:#c0392b;">Auto Move (Bot Play)</label></div>
                    <div><input type="checkbox" id="showEvalBarCB" style="margin-right:5px;"> <label for="showEvalBarCB" style="font-size:14px;">Show Eval Bar (%)</label></div>
                    <div><input type="checkbox" id="showAdvThreatsCB" style="margin-right:5px;"> <label for="showAdvThreatsCB" style="font-size:14px;">Tactical Highlights</label></div>
                </div>

                <!-- Delay Control (Hidden if not relevant, kept for settings persistence) -->
                <div id="delaySection" style="margin-bottom:12px; display:none;">
                    <div style="font-weight:600; margin-bottom:4px;">Auto-Move Delay (seconds):</div>
                    <div style="display:flex; gap:10px;">
                        Min: <input id="delayMinInput" type="number" min="0" step="0.1" value="${settings.delayMin}" style="width:70px; padding:4px; border:1px solid #ccc; border-radius:4px;">
                        Max: <input id="delayMaxInput" type="number" min="0" step="0.1" value="${settings.delayMax}" style="width:70px; padding:4px; border:1px solid #ccc; border-radius:4px;">
                    </div>
                </div>

                <!-- Analysis Report Section -->
                <div id="analysisReport">${this.analyzer.getReportHTML()}</div>

                <!-- Actions -->
                <div style="margin-top:10px;display:flex;gap:8px;">
                    <button id="reloadBtn" style="flex:1;padding:8px;border-radius:8px; background:#f39c12; color:#fff; font-weight:600; border:none;">Reload Engine</button>
                    <button id="resetAnalysisBtn" style="flex:1;padding:8px;border-radius:8px; background:#e74c3c; color:#fff; font-weight:600; border:none;">Reset Game Analysis</button>
                </div>
            `;

            parent.appendChild(this.container);
            this.attachEventListeners();
            this.updateUIFromSettings();
            this.visualizer.setupEvalBar();

            return true;
        }

        updateReport() {
             document.getElementById('analysisReport').innerHTML = this.analyzer.getReportHTML();
        }

        updateUIFromSettings() {
            const getEl = id => document.getElementById(id);

            getEl('autoRunCB').checked = !!settings.autoRun;
            getEl('autoMoveCB').checked = !!settings.autoMovePiece;
            getEl('showEvalBarCB').checked = !!settings.showEvalBar;
            getEl('showAdvThreatsCB').checked = !!settings.showAdvancedThreats;
            getEl('depthSlider').value = settings.lastDepth;
            getEl('depthText').querySelector('strong').innerText = settings.lastDepth;
            getEl('delayMinInput').value = settings.delayMin;
            getEl('delayMaxInput').value = settings.delayMax;
        }

        attachEventListeners() {
            const getEl = id => document.getElementById(id);

            getEl('depthSlider').oninput = (e) => {
                settings.lastDepth = parseInt(e.target.value);
                getEl('depthText').querySelector('strong').innerText = settings.lastDepth;
                saveSettings();
            };

            getEl('autoRunCB').onchange = (e) => { settings.autoRun = e.target.checked; saveSettings(); };
            getEl('autoMoveCB').onchange = (e) => {
                settings.autoMovePiece = e.target.checked;
                getEl('delaySection').style.display = e.target.checked ? 'block' : 'none';
                saveSettings();
            };
            getEl('showEvalBarCB').onchange = (e) => {
                settings.showEvalBar = e.target.checked;
                saveSettings();
                this.visualizer.updateEvalBar(this.engine.currentEval, this.engine.checkmateIn);
            };
            getEl('showAdvThreatsCB').onchange = (e) => { settings.showAdvancedThreats = e.target.checked; saveSettings(); };

            getEl('delayMinInput').onchange = (e) => {
                let val = parseFloat(e.target.value) || 0;
                settings.delayMin = Math.max(0, val);
                if (settings.delayMin > settings.delayMax) {
                    settings.delayMax = settings.delayMin;
                    getEl('delayMaxInput').value = settings.delayMax.toFixed(1);
                }
                e.target.value = settings.delayMin.toFixed(1);
                saveSettings();
            };
            getEl('delayMaxInput').onchange = (e) => {
                let val = parseFloat(e.target.value) || 0;
                settings.delayMax = Math.max(0, val);
                if (settings.delayMax < settings.delayMin) {
                    settings.delayMin = settings.delayMax;
                    getEl('delayMinInput').value = settings.delayMin.toFixed(1);
                }
                e.target.value = settings.delayMax.toFixed(1);
                saveSettings();
            };

            getEl('reloadBtn').onclick = () => {
                this.engine.safeRestart();
                this.analyzer.reset(); // Also reset analysis on engine restart
            };
            getEl('resetAnalysisBtn').onclick = () => {
                this.analyzer.reset();
                this.updateReport();
                console.log('Game analysis history reset.');
            };
        }
    }

    // -------------------------------------------------------------------------
    // 7. Main Controller Logic
    // -------------------------------------------------------------------------

    let botEngine = null;
    let botVisualizer = null;
    let botGUI = null;
    let gameAnalyzer = null;

    let canAutoMove = true;
    let lastKnownBestEval = 0;
    let lastKnownFen = '';

    /**
     * Callback executed when the Stockfish engine returns data.
     */
    function engineDataCallback(data) {
        if (data.type === 'info') {
            // Update the live visualization with the current best info
            botVisualizer.showAnalysis(data.moves, data.eval, data.mate);

            // Store the best evaluation for the *current* FEN
            lastKnownBestEval = data.eval;
            lastKnownFen = data.fen;

        } else if (data.type === 'bestmove') {
            // Final visualization after 'bestmove'
            botVisualizer.showAnalysis(data.moves, data.eval, data.mate);
            canAutoMove = true;
        }
    }

    /**
     * The main analysis loop, throttled to run every 200ms.
     */
    const continuousAnalysisLoop = throttle(() => {
        board = findBoard();

        if (!board || !board.game) return;

        try {
            const currentFen = board.game.getFEN();

            // 1. Detect Move Played
            if (currentFen !== botEngine.lastFen && botEngine.lastFen !== '') {
                // A move just happened!
                const actualMove = board.game.getHistory().pop();

                // Use the last known BEST evaluation of the PREVIOUS FEN
                const cpBefore = lastKnownBestEval;
                const prevFen = botEngine.lastFen;

                // Immediately run analysis on the *new* FEN to get cpAfter
                botEngine.runAnalysis(currentFen, settings.lastDepth);

                // We must wait for the new best eval (cpAfter) to arrive to complete the classification.
                // The classification logic is now inside the move listener instead of this loop.

                // IMPORTANT: Since we can't reliably predict when the 'bestmove' will arrive
                // for the *new* FEN, we'll wait for the next 'info' or 'bestmove' update
                // to trigger the move recording. For now, just trigger the new analysis.
            }


            // 2. Continuous Analysis & Game Analyzer Logic
            if (settings.autoRun || botEngine.isThinking) {
                 if (!botEngine.isThinking || currentFen !== botEngine.lastFen) {
                    // Check if the actual move was just recorded and new eval is ready
                    if (currentFen === lastKnownFen && lastKnownFen !== '' && lastKnownBestEval !== 0) {
                        // This means the engine just returned the optimal evaluation (cpAfter) for the position resulting from the player's last move.

                        const history = board.game.getHistory();
                        const lastMove = history.length > 0 ? history[history.length - 1] : null;

                        if (lastMove && lastMove.from && lastMove.to) {
                            const uciMove = lastMove.from + lastMove.to + (lastMove.promotion ? lastMove.promotion : '');

                            // Get the FEN *before* the last move (this is slightly hacky but necessary)
                            board.game.undo();
                            const prevFenForRecording = board.game.getFEN();
                            board.game.redo();

                            gameAnalyzer.recordMoveAnalysis(prevFenForRecording, uciMove, lastKnownBestEval, botEngine.currentEval);
                            botGUI.updateReport();
                        }

                    }

                    // Start or continue analysis
                    botEngine.runAnalysis(currentFen, settings.lastDepth);
                }
            }

            // 3. Trigger Auto-Move (Bot Play)
            const currentTurn = board.game.getTurn();
            const playingAs = board.game.getPlayingAs ? board.game.getPlayingAs() : currentTurn;

            if (settings.autoMovePiece && currentTurn === playingAs && !botEngine.isThinking && canAutoMove) {
                const bestMoveUCI = botEngine.candidateMoves[0]?.move;

                if (bestMoveUCI) {
                    canAutoMove = false;

                    const delaySeconds = Math.random() * (settings.delayMax - settings.delayMin) + settings.delayMin;
                    const delayMs = Math.max(200, delaySeconds * 1000);

                    setTimeout(() => {
                        if (botEngine.candidateMoves[0]?.move === bestMoveUCI) {
                            botEngine.performMove(bestMoveUCI);
                        }
                        canAutoMove = true;

                    }, delayMs);
                }
            }

        } catch (e) {
            console.error('Error in continuous analysis loop:', e);
        }
    }, 150);


    /**
     * Initialization and setup.
     */
    async function init() {
        // Wait for the board element to exist and have the game object
        await new Promise(resolve => {
            const check = setInterval(() => {
                board = findBoard();
                if (board && board.game) {
                    clearInterval(check);
                    resolve();
                }
            }, 100);
        });

        // Initialize components
        botEngine = new StockfishManager(engineDataCallback);
        botVisualizer = new BoardVisualizer(botEngine);
        gameAnalyzer = new GameAnalyzer();
        botGUI = new GUIManager(botEngine, botVisualizer, gameAnalyzer);

        botGUI.initGUI();

        const mo = new MutationObserver((mutations) => {
            const newBoard = findBoard();
            if (newBoard && newBoard !== board) {
                console.log('Board change detected, re-initializing UI and visuals.');
                board = newBoard;
                botGUI.initGUI();
                botVisualizer.detachHighlights();
                gameAnalyzer.reset(); // Reset game analysis history on new game/navigation
            }
        });
        mo.observe(document.body, { childList: true, subtree: true });

        setInterval(continuousAnalysisLoop, 150);

        console.log('Deep Chess Analysis Bot v4.0.0 Initialized and Monitoring.');
    }

    init();

})();