Chess.com Cheat Engine

Chess.com cheat engine — K-EXPERT edition

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      12.0
// @description  Chess.com cheat engine — K-EXPERT edition
// @author       kliel
// @license      MIT
// @match        https://www.chess.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      unpkg.com
// @connect      lichess.org
// @connect      explorer.lichess.ovh
// @run-at       document-idle
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    // ─── PAGE GUARD ─────────────────────────────────────────────────────────────
    const INACTIVE_PATHS = [/\/analysis/, /\/learn/, /\/puzzles/, /\/lessons/, /\/drills/, /\/courses/, /\/practice/, /\/openings/];
    const isInactivePage = () => INACTIVE_PATHS.some(p => p.test(location.pathname));
    let _paused = isInactivePage();

    (['pushState', 'replaceState']).forEach(m => {
        const orig = history[m];
        history[m] = function (...a) { const r = orig.apply(this, a); window.dispatchEvent(new Event('_chess_nav')); return r; };
    });
    window.addEventListener('popstate', () => window.dispatchEvent(new Event('_chess_nav')));
    window.addEventListener('_chess_nav', () => {
        const was = _paused;
        _paused = isInactivePage();
        if (_paused && !was) {
            try { SF.worker && SF.worker.postMessage('stop'); } catch (_) {}
            UI.clearArrows();
        }
        if (!_paused && was) {
            State.lastFen = null;
            State.boardCache = null;
            setTimeout(Loop.tick, 200);
        }
    });

    // ─── CONFIG ──────────────────────────────────────────────────────────────────
    const CFG = {
        active: false,
        autoPlay: false,
        useBook: true,
        showThreats: true,
        depth: 14,
        multiPV: 5,
        humanization: true,
        suboptimalRate: 0.35,
        correlation: 0.60,
        timeControl: 'blitz',
        // min/max = auto-play delay in ms | depth = SF search depth
        timing: {
            bullet: { min: 200,   max: 700,   depth: 8  },
            blitz:  { min: 1000,  max: 4000,  depth: 12 },
            rapid:  { min: 4000,  max: 12000, depth: 16 },
        },
    };

    // ─── STATE ───────────────────────────────────────────────────────────────────
    const State = {
        lastFen: null,
        playerColor: null,
        moveCount: 0,
        boardCache: null,
        candidates: {},
        currentEval: null,
        opponentMove: null,
        bestCount: 0,
        totalCount: 0,
        perfectStreak: 0,
        sloppyStreak: 0,
    };

    // ─── UTILS ───────────────────────────────────────────────────────────────────
    const $ = (sel, root = document) => {
        try {
            if (Array.isArray(sel)) { for (const s of sel) { const e = root.querySelector(s); if (e) return e; } return null; }
            return root.querySelector(sel);
        } catch { return null; }
    };
    const sleep = ms => new Promise(r => setTimeout(r, ms));
    const rnd = (a, b) => Math.random() * (b - a) + a;
    const gauss = (m, s) => { const u = Math.random(), v = Math.random(); return m + Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v) * s; };
    const humanMs = (min, max) => {
        // Average of 3 uniforms = triangular-bell shape, strictly between min and max
        const r = (Math.random() + Math.random() + Math.random()) / 3;
        return Math.round(min + r * (max - min));
    };
    const log = (m, t) => console.log('%c[KE] ' + m, 'color:' + ({info:'#4caf50',warn:'#ffcc00',error:'#f44336',debug:'#90caf9'})[t||'info'] + ';font-weight:bold');

    // ─── BOARD / GAME ─────────────────────────────────────────────────────────────
    const Board = {
        el: () => {
            if (State.boardCache && State.boardCache.isConnected) return State.boardCache;
            State.boardCache = $(['wc-chess-board', 'chess-board']);
            return State.boardCache;
        },
        color: () => {
            try {
                const b = Board.el();
                return (b && (b.classList.contains('flipped') || b.getAttribute('flipped') === 'true')) ? 'b' : 'w';
            } catch (e) { return 'w'; }
        },
        fen: () => {
            try {
                const b = Board.el();
                if (!b) return null;
                if (b.game && b.game.getFEN) return b.game.getFEN();
                const rk = Object.keys(b).find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternal'));
                if (rk) {
                    let cur = b[rk], d = 0;
                    while (cur && d++ < 150) {
                        if (cur.memoizedProps && cur.memoizedProps.game && cur.memoizedProps.game.fen) return cur.memoizedProps.game.fen;
                        if (typeof cur.memoizedProps === 'object' && cur.memoizedProps !== null && typeof cur.memoizedProps.fen === 'string') return cur.memoizedProps.fen;
                        cur = cur.return;
                    }
                }
                return null;
            } catch (e) { return null; }
        },
        myTurn: (fen) => fen && State.playerColor && fen.split(' ')[1] === State.playerColor,
        coords: (sq) => {
            try {
                const b = Board.el();
                if (!b) return null;
                const r = b.getBoundingClientRect();
                if (!r || !r.width) return null;
                const sqSz = r.width / 8;
                const flip = State.playerColor === 'b';
                const f = sq.charCodeAt(0) - 97;
                const rk = parseInt(sq[1]) - 1;
                return {
                    x: r.left + (flip ? 7 - f : f) * sqSz + sqSz / 2,
                    y: r.top + (flip ? rk : 7 - rk) * sqSz + sqSz / 2
                };
            } catch (e) { return null; }
        },
    };

    // ─── OPENING BOOK ─────────────────────────────────────────────────────────────
    const Book = {
        fetch: (fen) => {
            if (!CFG.useBook) return Promise.resolve(null);
            return new Promise(res => {
                const t = setTimeout(() => res(null), 2000);
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: 'https://explorer.lichess.ovh/masters?fen=' + encodeURIComponent(fen),
                    onload: r => {
                        clearTimeout(t);
                        try {
                            const d = JSON.parse(r.responseText);
                            if (!d.moves || !d.moves.length) return res(null);
                            const top = d.moves.slice(0, 3);
                            const total = top.reduce((s, m) => s + m.white + m.draw + m.black, 0);
                            let x = Math.random() * total;
                            for (const m of top) {
                                x -= m.white + m.draw + m.black;
                                if (x <= 0) return res(m.uci);
                            }
                            res(top[0].uci);
                        } catch (e) { res(null); }
                    },
                    onerror: () => { clearTimeout(t); res(null); }
                });
            });
        },
    };

    // ─── STOCKFISH ───────────────────────────────────────────────────────────────
    const SF = {
        worker: null,
        ready: false,
        _analyzing: false,
        _pendingFen: null,
        _resolveInit: null,

        init: () => new Promise(resolve => {
            if (SF.ready) return resolve(true);
            log('Loading Stockfish...');
            UI.setStatus('loading');
            GM_xmlhttpRequest({
                method: 'GET',
                url: 'https://unpkg.com/[email protected]/stockfish.js',
                onload: res => {
                    try {
                        const blob = new Blob([res.responseText], { type: 'application/javascript' });
                        SF.worker = new Worker(URL.createObjectURL(blob));
                        SF.worker.onerror = err => { log('SF worker error: ' + (err && err.message), 'error'); UI.setStatus('error'); };
                        SF.worker.onmessage = e => SF._msg(e.data);
                        SF._resolveInit = resolve;
                        SF.worker.postMessage('uci');
                    } catch (e) { log('SF init failed: ' + e, 'error'); UI.setStatus('error'); resolve(false); }
                },
                onerror: () => { log('SF download failed', 'error'); UI.setStatus('error'); resolve(false); }
            });
        }),

        _msg: (msg) => {
            if (msg === 'uciok') {
                SF.worker.postMessage('setoption name MultiPV value ' + CFG.multiPV);
                SF.worker.postMessage('isready');
                return;
            }
            if (msg === 'readyok') {
                SF.ready = true;
                log('Stockfish ready');
                UI.setStatus('ready');
                if (SF._resolveInit) { SF._resolveInit(true); SF._resolveInit = null; }
                // If we were already activated before engine was ready, fire immediately
                if (CFG.active && !_paused) { State.lastFen = null; Loop.tick(); }
                return;
            }
            if (msg.startsWith('info') && msg.includes(' score ') && SF._analyzing) {
                SF._parseInfo(msg);
                return;
            }
            if (msg.startsWith('bestmove') && SF._analyzing) {
                SF._analyzing = false;
                const parts = msg.split(' ');
                const move = parts[1];
                if (move && move !== '(none)') Engine.onBestMove(move);
                // Drain any queued position
                if (SF._pendingFen) {
                    const fen = SF._pendingFen;
                    SF._pendingFen = null;
                    SF._go(fen);
                }
            }
        },

        _parseInfo: (msg) => {
            try {
                const score = msg.match(/score (cp|mate) (-?\d+)/);
                const pv    = msg.match(/multipv (\d+)/);
                const mv    = msg.match(/ pv ([a-h][1-8][a-h][1-8]\w*)/);
                const dep   = msg.match(/depth (\d+)/);
                if (!score || !pv || !mv) return;
                const mpv  = parseInt(pv[1]);
                const type = score[1];
                let val    = parseInt(score[2]);
                if (type === 'cp') val = val / 100;
                State.candidates[mpv] = { move: mv[1], eval: { type: type, val: val }, depth: parseInt((dep && dep[1]) || 0) };
                if (mpv === 1) {
                    State.currentEval = { type: type, val: val };
                    const pvStr = msg.split(' pv ')[1];
                    if (pvStr) { const pvMoves = pvStr.split(' '); State.opponentMove = pvMoves[1] || null; }
                    UI.updateEval(type, val);
                }
            } catch (e) { /* ignore */ }
        },

        _go: (fen) => {
            if (!SF.ready || !SF.worker) return;
            SF._analyzing = true;
            State.candidates = {};
            State.currentEval = null;
            SF.worker.postMessage('stop');
            SF.worker.postMessage('position fen ' + fen);
            SF.worker.postMessage('setoption name MultiPV value ' + CFG.multiPV);
            SF.worker.postMessage('go depth ' + CFG.depth);
        },

        analyze: (fen) => {
            if (!SF.ready || !SF.worker) return;
            if (SF._analyzing) {
                SF._pendingFen = fen;
                SF.worker.postMessage('stop'); // triggers bestmove -> drains pending
            } else {
                SF._go(fen);
            }
        },

        stop: () => {
            SF._pendingFen = null;
            SF._analyzing = false;
            try { SF.worker && SF.worker.postMessage('stop'); } catch (e) { /* ignore */ }
        },
    };

    // ─── HUMANIZATION ────────────────────────────────────────────────────────────
    const Human = {
        phase: (fen) => {
            if (!fen) return 'mid';
            const mn = parseInt(fen.split(' ')[5] || 1);
            const pieces = (fen.split(' ')[0].match(/[rnbqRNBQ]/g) || []).length;
            if (mn <= 10) return 'open';
            if (pieces <= 6) return 'end';
            return 'mid';
        },

        pickMove: (bestMove) => {
            if (!CFG.humanization) return { move: bestMove, best: true };
            const best = State.candidates[1];
            if (!best) return { move: bestMove, best: true };

            // Streak guards
            if (State.perfectStreak >= 4) { const s = Human._subopt(); if (s) return s; }
            if (State.sloppyStreak >= 3) return { move: bestMove, best: true };

            // Dynamic rate
            let rate = CFG.suboptimalRate;
            if (State.totalCount >= 6) {
                const corr = State.bestCount / State.totalCount;
                if (corr > CFG.correlation + 0.08) rate += 0.15;
                else if (corr < CFG.correlation - 0.12) rate *= 0.2;
            }
            const ev = (State.currentEval && State.currentEval.val) || 0;
            if (ev > 2)  rate += 0.12;
            if (ev > 4)  rate += 0.15;
            if (ev > 6)  rate += 0.20;
            if (ev < -1) rate *= 0.3;
            rate = Math.max(0.05, Math.min(0.65, rate));

            if (Math.random() < rate) {
                const s = Human._subopt();
                if (s) return s;
            }
            return { move: bestMove, best: true };
        },

        _subopt: () => {
            const best = State.candidates[1];
            if (!best || Object.keys(State.candidates).length < 2) return null;
            const maxLoss = ({ open: 50, mid: 100, end: 60 })[Human.phase(State.lastFen)] || 80;
            const alts = [];
            for (let i = 2; i <= CFG.multiPV; i++) {
                const c = State.candidates[i];
                if (!c || !c.eval) continue;
                let loss;
                if (best.eval.type === 'mate') { loss = 999; }
                else if (c.eval.type === 'mate' && c.eval.val > 0) { loss = 0; }
                else { loss = (best.eval.val - c.eval.val) * 100; }
                if (loss >= 0 && loss <= maxLoss) alts.push({ move: c.move, loss: loss });
            }
            if (!alts.length) return null;
            const w = alts.map(a => 1 / (1 + a.loss / 25));
            const tot = w.reduce((s, x) => s + x, 0);
            let r = Math.random() * tot;
            for (let i = 0; i < alts.length; i++) {
                r -= w[i];
                if (r <= 0) return { move: alts[i].move, best: false };
            }
            return { move: alts[0].move, best: false };
        },

        track: (best) => {
            State.totalCount++;
            if (best) { State.bestCount++; State.perfectStreak++; State.sloppyStreak = 0; }
            else { State.perfectStreak = 0; State.sloppyStreak++; }
        },

        delay: () => {
            const tc = CFG.timing[CFG.timeControl] || CFG.timing.blitz;
            // Also keep depth in sync (in case delay is called before a TC-switch fires analyze)
            if (tc.depth && CFG.depth !== tc.depth) CFG.depth = tc.depth;
            return humanMs(tc.min, tc.max);
        },
    };

    // ─── ENGINE COORDINATOR ──────────────────────────────────────────────────────
    const Engine = {
        onBestMove: async (bestMove, isBook) => {
            if (_paused || !CFG.active) return;
            State.moveCount++;
            const picked = isBook ? { move: bestMove, best: true } : Human.pickMove(bestMove);
            Human.track(picked.best);
            UI.drawArrows(bestMove, picked.move);
            UI.updateMove(picked.move, picked.best);
            if (CFG.autoPlay && Board.myTurn(State.lastFen)) {
                const delay = Human.delay();
                log('Auto-play in ' + Math.round(delay) + 'ms');
                await sleep(delay);
                // After sleeping, re-read the live FEN — State.lastFen may have advanced
                const liveFen = Board.fen();
                if (!_paused && CFG.active && liveFen && Board.myTurn(liveFen)) {
                    await Mover.exec(picked.move);
                }
            }
        },
    };

    // ─── MAIN LOOP ───────────────────────────────────────────────────────────────
    const Loop = {
        _lastAnalyzedFen: null,

        tick: () => {
            if (_paused || !CFG.active) return;
            try {
                const fen = Board.fen();
                if (!fen) return;
                State.playerColor = Board.color();

                // New game detection
                const isStart = fen.startsWith('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR');
                if (isStart && State.moveCount > 2) {
                    State.moveCount = 0; State.bestCount = 0; State.totalCount = 0;
                    State.perfectStreak = 0; State.sloppyStreak = 0;
                    Loop._lastAnalyzedFen = null;
                }

                if (fen === State.lastFen) return;
                State.lastFen = fen;
                UI.clearArrows();
                UI.updateMove('...', true);

                if ($(['.game-over-modal-container', '.modal-game-over-component', '[data-cy="game-over-modal"]'])) return;

                if (Board.myTurn(fen) && fen !== Loop._lastAnalyzedFen) {
                    Loop._lastAnalyzedFen = fen;
                    if (CFG.useBook) {
                        Book.fetch(fen).then(bm => {
                            if (!CFG.active || _paused || fen !== State.lastFen) return;
                            if (bm) { log('Book: ' + bm); Engine.onBestMove(bm, true); }
                            else SF.analyze(fen);
                        });
                    } else {
                        SF.analyze(fen);
                    }
                }
            } catch (e) { log('loop error: ' + e.message, 'warn'); }
        },

        start: () => {
            const target = $(['vertical-move-list', 'wc-move-list', '.move-list-component']) || document.body;
            new MutationObserver(() => {
                if (!_paused && CFG.active) requestAnimationFrame(Loop.tick);
            }).observe(target, { childList: true, subtree: true, characterData: true });
            setInterval(() => { if (!_paused && CFG.active) Loop.tick(); }, 600);
        },
    };

    // ─── MOVE EXECUTOR ───────────────────────────────────────────────────────────
    const Mover = {
        exec: async (move) => {
            try {
                // Use live board FEN — stored State.lastFen may be stale after a long delay
                const liveFen = Board.fen();
                if (!liveFen || !Board.myTurn(liveFen)) return;
                const from = move.slice(0, 2);
                const to   = move.slice(2, 4);
                const promo = move[4] || 'q';
                await Mover.drag(from, to);
                if ((to[1] === '8' || to[1] === '1') && Mover.isPawn(from)) await Mover.promo(promo);
            } catch (e) { log('exec error: ' + e.message, 'warn'); }
        },

        isPawn: (sq) => {
            try {
                const b = Board.el();
                if (!b) return false;
                const coord = (sq.charCodeAt(0) - 96) + '' + sq[1];
                const f = b.querySelector('.piece.square-' + coord);
                if (f) return f.className.includes('wp') || f.className.includes('bp');
                return sq[1] === '2' || sq[1] === '7';
            } catch (e) { return false; }
        },

        drag: async (from, to) => {
            const p1 = Board.coords(from);
            const p2 = Board.coords(to);
            if (!p1 || !p2) return;
            const b = Board.el();
            const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };
            const pe = (t, x, y) => new PointerEvent(t, Object.assign({}, opts, { clientX: x, clientY: y }));
            const me = (t, x, y) => new MouseEvent(t, Object.assign({}, opts, { clientX: x, clientY: y }));

            const fromCoord = (p1.x + '').slice(0, 3); // not used for selector, just drag
            const src = document.elementFromPoint(p1.x, p1.y) || b;
            src.dispatchEvent(pe('pointerdown', p1.x, p1.y));
            src.dispatchEvent(me('mousedown',   p1.x, p1.y));
            await sleep(rnd(25, 65));

            const steps = 10 + Math.round(rnd(0, 6));
            const cpx = (p1.x + p2.x) / 2 + gauss(0, 18);
            const cpy = (p1.y + p2.y) / 2 + gauss(0, 18);
            for (let i = 1; i <= steps; i++) {
                const t = i / steps;
                const x = (1-t)*(1-t)*p1.x + 2*(1-t)*t*cpx + t*t*p2.x + gauss(0, 1.2);
                const y = (1-t)*(1-t)*p1.y + 2*(1-t)*t*cpy + t*t*p2.y + gauss(0, 1.2);
                document.dispatchEvent(pe('pointermove', x, y));
                document.dispatchEvent(me('mousemove',   x, y));
                if (Math.random() < 0.35) await sleep(rnd(2, 8));
            }

            const dst = document.elementFromPoint(p2.x, p2.y) || b;
            dst.dispatchEvent(pe('pointerup', p2.x, p2.y));
            dst.dispatchEvent(me('mouseup',   p2.x, p2.y));
            dst.dispatchEvent(pe('click',     p2.x, p2.y));
        },

        promo: async (piece) => {
            piece = piece || 'q';
            await sleep(200);
            const idx = { q: 0, r: 1, b: 2, n: 3 }[piece] || 0;
            const sels = [
                '.promotion-piece[data-piece="' + piece + '"]',
                '.promotion-window .promotion-piece:nth-child(' + (idx+1) + ')',
                '.promotion-piece',
            ];
            let el = null;
            for (let i = 0; i < 15 && !el; i++) {
                await sleep(80);
                for (const s of sels) { try { el = document.querySelector(s); } catch(e){} if (el) break; }
            }
            if (el) { el.click(); log('Promoted to ' + piece); }
        },
    };

    // ─── UI ──────────────────────────────────────────────────────────────────────
    const UI = {
        panel: null,
        _stealth: false,

        init: () => {
            UI._css();
            UI._build();
        },

        _css: () => {
            GM_addStyle(`
                @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;600&display=swap');

                .ke-panel {
                    position: fixed; top: 60px; left: 60px; z-index: 99999;
                    width: 272px;
                    background: #0d0d10;
                    border: 1px solid rgba(255,255,255,0.07);
                    border-radius: 12px;
                    font-family: 'Inter', sans-serif;
                    box-shadow: 0 24px 64px rgba(0,0,0,0.75), 0 0 0 1px rgba(255,255,255,0.03);
                    color: #e0e0e0;
                    touch-action: none;
                    user-select: none;
                    transition: opacity 0.2s;
                }
                .ke-panel.stealth { opacity: 0 !important; pointer-events: none; }

                /* Header */
                .ke-hdr {
                    padding: 13px 14px 11px;
                    display: flex; align-items: center; gap: 9px;
                    cursor: grab; border-bottom: 1px solid rgba(255,255,255,0.06);
                    background: linear-gradient(180deg,rgba(255,255,255,0.025) 0%,transparent 100%);
                    border-radius: 12px 12px 0 0;
                }
                .ke-hdr:active { cursor: grabbing; }
                .ke-logo {
                    font-weight: 800; font-size: 12.5px; letter-spacing: 0.1em; flex: 1;
                    background: linear-gradient(120deg,#69f0ae,#4caf50); -webkit-background-clip: text; -webkit-text-fill-color: transparent;
                }
                .ke-logo em { -webkit-text-fill-color: rgba(255,255,255,0.35); font-style: normal; font-weight: 400; }
                .ke-dot {
                    width: 6px; height: 6px; border-radius: 50%; background: #333;
                    transition: background 0.3s, box-shadow 0.3s; flex-shrink: 0;
                }
                .ke-dot.loading { background: #ffc107; animation: ke-pulse 0.7s ease-in-out infinite; }
                .ke-dot.ready   { background: #444; }
                .ke-dot.active  { background: #4caf50; box-shadow: 0 0 7px #4caf50; animation: ke-pulse 1.8s ease-in-out infinite; }
                .ke-dot.error   { background: #f44336; }
                .ke-colbtn {
                    width: 22px; height: 22px; border-radius: 5px; background: rgba(255,255,255,0.05);
                    border: none; color: #555; cursor: pointer; font-size: 13px;
                    display: flex; align-items: center; justify-content: center;
                    transition: 0.15s; flex-shrink: 0; line-height: 1;
                }
                .ke-colbtn:hover { background: rgba(255,255,255,0.1); color: #ccc; }

                /* Body collapse */
                .ke-body { overflow: hidden; }
                .ke-body.col { max-height: 0 !important; overflow: hidden; }

                /* Master button */
                .ke-master {
                    margin: 12px 12px 10px;
                    padding: 11px 13px;
                    border-radius: 8px;
                    border: 1px solid rgba(255,255,255,0.07);
                    background: #141418;
                    cursor: pointer;
                    display: flex; align-items: center; gap: 10px;
                    transition: border-color 0.2s, box-shadow 0.2s;
                    position: relative; overflow: hidden;
                }
                .ke-master-bg {
                    position: absolute; inset: 0;
                    background: linear-gradient(120deg,#2e7d32,#43a047);
                    opacity: 0; transition: opacity 0.2s; pointer-events: none;
                }
                .ke-master.on .ke-master-bg { opacity: 1; }
                .ke-master.on { border-color: #4caf50; box-shadow: 0 0 18px rgba(76,175,80,0.28); }
                .ke-master:hover { border-color: rgba(255,255,255,0.13); }
                .ke-mic {
                    width: 30px; height: 30px; border-radius: 50%;
                    background: rgba(255,255,255,0.07);
                    display: flex; align-items: center; justify-content: center;
                    font-size: 13px; position: relative; z-index: 1; flex-shrink: 0;
                }
                .ke-master.on .ke-mic { background: rgba(255,255,255,0.14); }
                .ke-mtxt { position: relative; z-index: 1; }
                .ke-mtitle { font-size: 11.5px; font-weight: 700; letter-spacing: 0.07em; color: #555; transition: color 0.2s; }
                .ke-master.on .ke-mtitle { color: #fff; }
                .ke-msub { font-size: 10px; color: #444; margin-top: 1px; transition: color 0.2s; }
                .ke-master.on .ke-msub { color: rgba(255,255,255,0.65); }

                /* Eval row */
                .ke-eval-row {
                    margin: 0 12px 10px;
                    background: #111115; border: 1px solid rgba(255,255,255,0.06);
                    border-radius: 8px; padding: 11px 13px;
                    display: flex; align-items: center; justify-content: space-between;
                }
                .ke-eval {
                    font-family: 'JetBrains Mono',monospace; font-size: 28px; font-weight: 600;
                    color: #333; transition: color 0.2s; line-height: 1;
                }
                .ke-eval.pos { color: #4caf50; }
                .ke-eval.neg { color: #f44336; }
                .ke-eval.neu { color: #e0e0e0; }
                .ke-movebox { text-align: right; }
                .ke-movelbl { font-size: 9px; color: #444; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 4px; }
                .ke-move {
                    font-family: 'JetBrains Mono',monospace; font-size: 15px; font-weight: 600;
                    color: #4caf50; background: rgba(76,175,80,0.1);
                    padding: 3px 8px; border-radius: 5px; letter-spacing: 0.05em;
                    transition: color 0.15s, background 0.15s;
                }
                .ke-move.sub { color: #ffc107; background: rgba(255,193,7,0.1); }
                .ke-move.idle { color: #333; background: transparent; }

                /* Tabs */
                .ke-tabbar {
                    display: flex; margin: 0 12px 10px;
                    background: #111115; padding: 3px; border-radius: 7px;
                    border: 1px solid rgba(255,255,255,0.05); gap: 3px;
                }
                .ke-tab {
                    flex: 1; padding: 6px 0; font-size: 9.5px; font-weight: 600;
                    letter-spacing: 0.07em; color: #444; cursor: pointer;
                    border-radius: 5px; text-align: center; transition: 0.15s;
                }
                .ke-tab:hover { color: #999; }
                .ke-tab.act { background: #1a1a20; color: #e0e0e0; box-shadow: 0 1px 4px rgba(0,0,0,0.5); }

                /* Pages */
                .ke-page { display: none; padding: 0 12px 12px; }
                .ke-page.act { display: block; }

                /* Row */
                .ke-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 9px; }
                .ke-rlbl { font-size: 11px; color: #888; }

                /* Toggle switch */
                .ke-sw {
                    width: 32px; height: 17px; border-radius: 9px;
                    background: #1e1e24; border: 1px solid rgba(255,255,255,0.07);
                    cursor: pointer; position: relative; transition: 0.18s; flex-shrink: 0;
                }
                .ke-sw.on { background: #4caf50; border-color: #4caf50; }
                .ke-sw::after {
                    content:''; position: absolute; top: 2px; left: 2px;
                    width: 11px; height: 11px; border-radius: 50%;
                    background: #fff; transition: transform 0.18s;
                    box-shadow: 0 1px 3px rgba(0,0,0,0.4);
                }
                .ke-sw.on::after { transform: translateX(15px); }

                /* TC buttons */
                .ke-tc-row { display: flex; gap: 5px; margin-bottom: 10px; }
                .ke-tc {
                    flex: 1; padding: 5px 0; font-size: 9.5px; font-weight: 600;
                    border-radius: 5px; cursor: pointer; text-align: center;
                    background: #141418; border: 1px solid rgba(255,255,255,0.06);
                    color: #444; transition: 0.15s;
                }
                .ke-tc:hover { color: #ccc; border-color: rgba(255,255,255,0.13); }
                .ke-tc.act { color: #111; font-weight: 700; border-color: transparent; }
                .ke-tc[data-tc=bullet].act { background: #ef5350; }
                .ke-tc[data-tc=blitz].act  { background: #ffc107; }
                .ke-tc[data-tc=rapid].act  { background: #4caf50; }

                /* Sliders */
                .ke-slwrap { margin-bottom: 11px; }
                .ke-slhdr { display: flex; justify-content: space-between; margin-bottom: 4px; }
                .ke-slhdr span:first-child { font-size: 10.5px; color: #777; }
                .ke-slhdr span:last-child  { font-size: 10px; color: #4caf50; font-family: 'JetBrains Mono',monospace; }
                .ke-sl {
                    -webkit-appearance: none; width: 100%; height: 3px;
                    border-radius: 2px; outline: none; cursor: pointer;
                    background: linear-gradient(90deg,#4caf50 var(--v,50%),#1e1e24 var(--v,50%));
                }
                .ke-sl::-webkit-slider-thumb {
                    -webkit-appearance: none; width: 13px; height: 13px;
                    background: #fff; border-radius: 50%; cursor: pointer;
                    box-shadow: 0 0 0 3px rgba(76,175,80,0.25), 0 2px 5px rgba(0,0,0,0.4);
                }

                /* Stats */
                .ke-stats { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-bottom: 2px; }
                .ke-stat {
                    background: #111115; border: 1px solid rgba(255,255,255,0.05);
                    border-radius: 7px; padding: 8px 10px;
                }
                .ke-stat-l { font-size: 9px; color: #444; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 3px; }
                .ke-stat-v { font-family: 'JetBrains Mono',monospace; font-size: 17px; font-weight: 600; color: #ddd; }

                /* Footer */
                .ke-footer {
                    padding: 7px 12px; border-top: 1px solid rgba(255,255,255,0.05);
                    display: flex; gap: 10px; font-size: 9.5px; color: #444;
                }
                .ke-k {
                    background: #141418; border: 1px solid rgba(255,255,255,0.09);
                    border-radius: 3px; padding: 1px 5px;
                    font-family: 'JetBrains Mono',monospace; font-size: 9px;
                }

                /* Arrow overlay */
                .ke-overlay {
                    position: fixed; top: 0; left: 0;
                    pointer-events: none; z-index: 9998;
                    transition: opacity 0.2s;
                }

                @keyframes ke-pulse { 0%,100%{opacity:1} 50%{opacity:0.35} }
            `);
        },

        _build: () => {
            if (UI.panel) return;
            const p = document.createElement('div');
            p.className = 'ke-panel';
            p.innerHTML = `
                <div class="ke-hdr">
                    <div class="ke-logo">K-EXPERT<em>.MENU</em></div>
                    <div class="ke-dot ready" id="ke-dot"></div>
                    <button class="ke-colbtn" id="ke-col">−</button>
                </div>
                <div class="ke-body" id="ke-body">

                    <div class="ke-master" id="ke-master">
                        <div class="ke-master-bg"></div>
                        <div class="ke-mic">⚡</div>
                        <div class="ke-mtxt">
                            <div class="ke-mtitle" id="ke-mtitle">CLICK TO ACTIVATE</div>
                            <div class="ke-msub"   id="ke-msub">Engine loading...</div>
                        </div>
                    </div>

                    <div class="ke-eval-row">
                        <div class="ke-eval idle" id="ke-eval">—</div>
                        <div class="ke-movebox">
                            <div class="ke-movelbl">Best Move</div>
                            <div class="ke-move idle" id="ke-move">—</div>
                        </div>
                    </div>

                    <div class="ke-tabbar">
                        <div class="ke-tab act" data-tab="play">PLAY</div>
                        <div class="ke-tab"     data-tab="tune">TUNE</div>
                        <div class="ke-tab"     data-tab="stats">STATS</div>
                    </div>

                    <div class="ke-page act" id="ke-p-play">
                        <div class="ke-row"><span class="ke-rlbl">Auto-Play</span>    <div class="ke-sw ${CFG.autoPlay?'on':''}" id="sw-auto"></div></div>
                        <div class="ke-row"><span class="ke-rlbl">Opening Book</span> <div class="ke-sw ${CFG.useBook?'on':''}"  id="sw-book"></div></div>
                        <div class="ke-row"><span class="ke-rlbl">Show Threats</span> <div class="ke-sw ${CFG.showThreats?'on':''}" id="sw-thr"></div></div>
                        <div style="font-size:9.5px;color:#444;letter-spacing:0.09em;text-transform:uppercase;margin-bottom:7px">Time Control</div>
                        <div class="ke-tc-row">
                            <div class="ke-tc ${CFG.timeControl==='bullet'?'act':''}" data-tc="bullet">🔴 Bullet</div>
                            <div class="ke-tc ${CFG.timeControl==='blitz'?'act':''}"  data-tc="blitz">🟡 Blitz</div>
                            <div class="ke-tc ${CFG.timeControl==='rapid'?'act':''}"  data-tc="rapid">🟢 Rapid</div>
                        </div>
                    </div>

                    <div class="ke-page" id="ke-p-tune">
                        <div class="ke-slwrap">
                            <div class="ke-slhdr"><span>Engine Depth</span><span id="sv-dep">${CFG.depth}</span></div>
                            <input type="range" class="ke-sl" id="sl-dep" min="6" max="20" value="${CFG.depth}">
                        </div>
                        <div class="ke-slwrap">
                            <div class="ke-slhdr"><span>Best-Move Target %</span><span id="sv-cor">${Math.round(CFG.correlation*100)}</span></div>
                            <input type="range" class="ke-sl" id="sl-cor" min="40" max="98" value="${Math.round(CFG.correlation*100)}">
                        </div>
                        <div class="ke-slwrap">
                            <div class="ke-slhdr"><span>Suboptimal Rate %</span><span id="sv-sub">${Math.round(CFG.suboptimalRate*100)}</span></div>
                            <input type="range" class="ke-sl" id="sl-sub" min="5" max="60" value="${Math.round(CFG.suboptimalRate*100)}">
                        </div>
                        <div class="ke-row"><span class="ke-rlbl">Humanization</span><div class="ke-sw ${CFG.humanization?'on':''}" id="sw-hum"></div></div>
                    </div>

                    <div class="ke-page" id="ke-p-stats">
                        <div class="ke-stats">
                            <div class="ke-stat"><div class="ke-stat-l">Moves</div>      <div class="ke-stat-v" id="st-mv">0</div></div>
                            <div class="ke-stat"><div class="ke-stat-l">Correlation</div><div class="ke-stat-v" id="st-co">—</div></div>
                            <div class="ke-stat"><div class="ke-stat-l">Best</div>        <div class="ke-stat-v" id="st-be">0</div></div>
                            <div class="ke-stat"><div class="ke-stat-l">Eval</div>        <div class="ke-stat-v" id="st-ev">—</div></div>
                        </div>
                    </div>

                    <div class="ke-footer">
                        <span><span class="ke-k">A</span> Auto</span>
                        <span><span class="ke-k">E</span> Toggle</span>
                        <span><span class="ke-k">X</span> Hide</span>
                    </div>
                </div>
            `;
            document.body.appendChild(p);
            UI.panel = p;
            UI._drag(p);
            UI._bind(p);
        },

        _drag: (el) => {
            const hdr = el.querySelector('.ke-hdr');
            let dragging = false, ox, oy, ol, ot;
            const down = (cx, cy) => { dragging = true; ox = cx; oy = cy; ol = el.offsetLeft; ot = el.offsetTop; };
            const move = (cx, cy) => { if (dragging) { el.style.left = (ol+cx-ox)+'px'; el.style.top = (ot+cy-oy)+'px'; } };
            const up   = () => { dragging = false; };
            hdr.addEventListener('mousedown', e => { if (!e.target.classList.contains('ke-colbtn')) down(e.clientX, e.clientY); });
            document.addEventListener('mousemove', e => move(e.clientX, e.clientY));
            document.addEventListener('mouseup', up);
            hdr.addEventListener('touchstart', e => { const t=e.touches[0]; if (!e.target.classList.contains('ke-colbtn')) down(t.clientX,t.clientY); }, {passive:true});
            document.addEventListener('touchmove', e => { const t=e.touches[0]; move(t.clientX,t.clientY); }, {passive:true});
            document.addEventListener('touchend', up);
        },

        _bind: (p) => {
            // Collapse
            const body = p.querySelector('#ke-body');
            const colBtn = p.querySelector('#ke-col');
            let col = false;
            const doCol = e => {
                e.preventDefault(); e.stopPropagation(); col = !col;
                body.classList.toggle('col', col); colBtn.textContent = col ? '+' : '−';
            };
            colBtn.addEventListener('click', doCol);
            colBtn.addEventListener('touchend', doCol, {passive:false});

            // Master toggle
            const masterBtn = p.querySelector('#ke-master');
            const doMaster = e => {
                e.stopPropagation();
                CFG.active = !CFG.active;
                masterBtn.classList.toggle('on', CFG.active);
                p.querySelector('#ke-mtitle').textContent = CFG.active ? 'ENGINE ACTIVE' : 'CLICK TO ACTIVATE';
                p.querySelector('#ke-msub').textContent   = CFG.active ? 'Analysing every move' : 'Engine ready';
                p.querySelector('#ke-dot').className = 'ke-dot ' + (CFG.active ? 'active' : (SF.ready ? 'ready' : 'loading'));
                if (CFG.active) {
                    // Reset so gameLoop fires immediately on current position
                    State.lastFen = null;
                    Loop._lastAnalyzedFen = null;
                    SF._pendingFen = null;
                    requestAnimationFrame(Loop.tick);
                } else {
                    SF.stop();
                    UI.clearArrows();
                    p.querySelector('#ke-eval').textContent = '—';
                    p.querySelector('#ke-eval').className = 'ke-eval idle';
                    p.querySelector('#ke-move').textContent = '—';
                    p.querySelector('#ke-move').className = 'ke-move idle';
                }
            };
            masterBtn.addEventListener('click', doMaster);
            masterBtn.addEventListener('touchend', e => { e.preventDefault(); doMaster(e); }, {passive:false});

            // Tabs
            p.querySelectorAll('.ke-tab').forEach(t => {
                t.addEventListener('click', () => {
                    p.querySelectorAll('.ke-tab').forEach(x => x.classList.remove('act'));
                    p.querySelectorAll('.ke-page').forEach(x => x.classList.remove('act'));
                    t.classList.add('act');
                    p.querySelector('#ke-p-' + t.dataset.tab).classList.add('act');
                });
            });

            // Switches
            const sw = (id, key) => {
                const el = p.querySelector(id);
                if (!el) return;
                el.addEventListener('click', function() { CFG[key] = !CFG[key]; this.classList.toggle('on', CFG[key]); });
            };
            sw('#sw-auto', 'autoPlay');
            sw('#sw-book', 'useBook');
            sw('#sw-thr',  'showThreats');
            sw('#sw-hum',  'humanization');

            // TC buttons
            p.querySelectorAll('.ke-tc').forEach(b => {
                b.addEventListener('click', () => {
                    CFG.timeControl = b.dataset.tc;
                    p.querySelectorAll('.ke-tc').forEach(x => x.classList.toggle('act', x.dataset.tc === b.dataset.tc));
                    // Apply the depth for this time control
                    const preset = CFG.timing[CFG.timeControl];
                    if (preset) {
                        CFG.depth = preset.depth;
                        const depSlider = p.querySelector('#sl-dep');
                        const depVal    = p.querySelector('#sv-dep');
                        if (depSlider) {
                            depSlider.value = CFG.depth;
                            depSlider.style.setProperty('--v', ((CFG.depth - depSlider.min) / (depSlider.max - depSlider.min) * 100).toFixed(1) + '%');
                        }
                        if (depVal) depVal.textContent = CFG.depth;
                    }
                });
            });

            // Sliders
            const sl = (id, valId, setter) => {
                const el = p.querySelector(id);
                const vl = p.querySelector(valId);
                if (!el) return;
                const upd = () => {
                    const v = parseInt(el.value);
                    setter(v);
                    if (vl) vl.textContent = v;
                    el.style.setProperty('--v', ((v - el.min) / (el.max - el.min) * 100).toFixed(1) + '%');
                };
                el.addEventListener('input', upd);
                upd();
            };
            sl('#sl-dep', '#sv-dep', v => { CFG.depth = v; });
            sl('#sl-cor', '#sv-cor', v => { CFG.correlation = v / 100; });
            sl('#sl-sub', '#sv-sub', v => { CFG.suboptimalRate = v / 100; });

            // Keyboard
            document.addEventListener('keydown', e => {
                if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
                if (e.key === 'a') { CFG.autoPlay = !CFG.autoPlay; p.querySelector('#sw-auto').classList.toggle('on', CFG.autoPlay); }
                if (e.key === 'e') doMaster(e);
                if (e.key === 'x') {
                    UI._stealth = !UI._stealth;
                    p.classList.toggle('stealth', UI._stealth);
                    document.querySelectorAll('.ke-overlay').forEach(o => o.style.opacity = UI._stealth ? '0' : '1');
                }
            });
        },

        setStatus: (s) => {
            const dot = UI.panel && UI.panel.querySelector('#ke-dot');
            if (dot) dot.className = 'ke-dot ' + s;
            const msub = UI.panel && UI.panel.querySelector('#ke-msub');
            if (msub && !CFG.active) msub.textContent = s === 'ready' ? 'Engine ready' : (s === 'error' ? 'Load failed — reload page' : 'Loading engine...');
        },

        updateEval: (type, val) => {
            if (!UI.panel) return;
            try {
                const el = UI.panel.querySelector('#ke-eval');
                if (type === 'mate') {
                    el.textContent = 'M' + Math.abs(val);
                    el.className = 'ke-eval ' + (val > 0 ? 'pos' : 'neg');
                } else {
                    el.textContent = (val > 0 ? '+' : '') + val.toFixed(2);
                    el.className = 'ke-eval ' + (val > 0.4 ? 'pos' : val < -0.4 ? 'neg' : 'neu');
                }
                const se = UI.panel.querySelector('#st-ev');
                if (se) se.textContent = type === 'mate' ? 'M'+Math.abs(val) : (val > 0 ? '+' : '') + val.toFixed(1);
            } catch(e) {}
        },

        updateMove: (move, isBest) => {
            if (!UI.panel) return;
            try {
                const el = UI.panel.querySelector('#ke-move');
                el.textContent = move || '—';
                el.className = 'ke-move' + (move === '...' || move === '—' ? ' idle' : (isBest ? '' : ' sub'));
                const sm = UI.panel.querySelector('#st-mv');
                const sc = UI.panel.querySelector('#st-co');
                const sb = UI.panel.querySelector('#st-be');
                if (sm) sm.textContent = State.totalCount;
                if (sb) sb.textContent = State.bestCount;
                if (sc) sc.textContent = State.totalCount > 0 ? Math.round(State.bestCount / State.totalCount * 100) + '%' : '—';
            } catch(e) {}
        },

        clearArrows: () => {
            try { document.querySelectorAll('.ke-overlay').forEach(e => e.remove()); } catch(e) {}
        },

        drawArrows: (bestMove, pickedMove) => {
            if (UI._stealth) return;
            UI.clearArrows();
            try {
                const b = Board.el();
                if (!b) return;
                const rect = b.getBoundingClientRect();
                if (!rect || !rect.width) return;

                const overlay = document.createElement('div');
                overlay.className = 'ke-overlay';
                overlay.style.cssText = 'width:'+rect.width+'px;height:'+rect.height+'px;left:'+rect.left+'px;top:'+rect.top+'px;';
                const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                svg.style.cssText = 'width:100%;height:100%;overflow:visible;';
                overlay.appendChild(svg);
                document.body.appendChild(overlay);

                const sqSz = rect.width / 8;
                const pos = (sq) => {
                    const flip = State.playerColor === 'b';
                    const f = sq.charCodeAt(0) - 97;
                    const r = parseInt(sq[1]) - 1;
                    return { x: (flip ? 7-f : f)*sqSz + sqSz/2, y: (flip ? r : 7-r)*sqSz + sqSz/2 };
                };

                const arrow = (mv, color, dashed) => {
                    if (!mv || mv.length < 4) return;
                    const p1 = pos(mv.slice(0,2));
                    const p2 = pos(mv.slice(2,4));
                    const dx = p2.x-p1.x, dy = p2.y-p1.y;
                    const len = Math.sqrt(dx*dx+dy*dy);
                    if (!len) return;
                    const ux = dx/len, uy = dy/len;
                    const sw = dashed ? sqSz*0.085 : sqSz*0.14;
                    const ex = p2.x - ux*sqSz*0.25, ey = p2.y - uy*sqSz*0.25;

                    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    line.setAttribute('x1', p1.x); line.setAttribute('y1', p1.y);
                    line.setAttribute('x2', ex);   line.setAttribute('y2', ey);
                    line.setAttribute('stroke', color);
                    line.setAttribute('stroke-width', sw);
                    line.setAttribute('stroke-opacity', dashed ? '0.5' : '0.82');
                    line.setAttribute('stroke-linecap', 'round');
                    if (dashed) line.setAttribute('stroke-dasharray', '7,4');
                    svg.appendChild(line);

                    const tip = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                    tip.setAttribute('cx', p2.x); tip.setAttribute('cy', p2.y);
                    tip.setAttribute('r', dashed ? sqSz*0.085 : sqSz*0.155);
                    tip.setAttribute('fill', color);
                    tip.setAttribute('opacity', dashed ? '0.5' : '0.82');
                    svg.appendChild(tip);
                };

                arrow(bestMove,  '#4caf50', false);
                if (pickedMove && pickedMove !== bestMove) arrow(pickedMove, '#ffc107', false);
                if (CFG.showThreats && State.opponentMove) arrow(State.opponentMove, '#f44336', true);
            } catch(e) { log('arrow error: ' + e.message, 'warn'); }
        },
    };

    // ─── BOOT ────────────────────────────────────────────────────────────────────
    (async () => {
        UI.init();
        if (_paused) { log('Inactive page — standby', 'warn'); return; }

        // Wait for board (max ~12s)
        let tries = 0;
        while (!Board.el() && tries++ < 30) await sleep(400);
        if (!Board.el()) { log('Board not found', 'error'); return; }

        // Pre-load engine silently so it is hot when user activates
        SF.init().then(ok => {
            if (ok) { log('Engine ready — press E or click the button'); UI.setStatus('ready'); }
        });

        Loop.start();

        // Redraw arrows if board shifts (resize, scroll, layout reflow)
        const redraw = () => {
            if (!CFG.active || _paused) return;
            const overlays = document.querySelectorAll('.ke-overlay');
            if (!overlays.length) return;
            const b = Board.el();
            if (!b) return;
            const rect = b.getBoundingClientRect();
            if (!rect || !rect.width) return;
            overlays.forEach(o => {
                o.style.left   = rect.left + 'px';
                o.style.top    = rect.top  + 'px';
                o.style.width  = rect.width + 'px';
                o.style.height = rect.height + 'px';
            });
        };
        window.addEventListener('resize', redraw, { passive: true });
        window.addEventListener('scroll', redraw, { passive: true });
    })();

})();