Chess.com Cheat Engine

Chess.com cheat engine — K-EXPERT edition (ELO mode)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      13.0
// @description  Chess.com cheat engine — K-EXPERT edition (ELO mode)
// @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',
        // ── NEW: ELO strength mode ──
        eloMode: false,
        targetElo: 1500,
        // 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) => {
        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');

    // ─── ELO STRENGTH ENGINE ─────────────────────────────────────────────────────
    //
    //  Maps a human ELO rating (500–3200) to:
    //    • Stockfish Skill Level  (0–20)
    //    • UCI_Elo               (passed directly to SF when supported)
    //    • Search depth          (shallower = weaker / faster)
    //    • Suboptimal-move rate  (how often a non-best move is picked)
    //    • MultiPV               (fewer lines needed at low strength)
    //
    const EloStrength = {

        // ── Skill Level table ───────────────────────────────────────────────────
        //  Stockfish Skill Level 0 ≈ 1100 ELO, Level 20 = full strength.
        //  We extend below 1100 by clamping to 0 (depth / suboptimal rate handle it).
        toSkill: (elo) => {
            // Roughly linear between ~1100 (skill 0) and ~3100 (skill 20)
            const skill = Math.round((elo - 1100) / (3100 - 1100) * 20);
            return Math.max(0, Math.min(20, skill));
        },

        // ── Search depth ────────────────────────────────────────────────────────
        toDepth: (elo) => {
            if (elo <  800) return 1;
            if (elo < 1000) return 2;
            if (elo < 1200) return 4;
            if (elo < 1400) return 6;
            if (elo < 1600) return 8;
            if (elo < 1800) return 10;
            if (elo < 2000) return 12;
            if (elo < 2200) return 14;
            if (elo < 2500) return 16;
            if (elo < 2800) return 18;
            return 20;
        },

        // ── Suboptimal-move rate ─────────────────────────────────────────────────
        //  Beginners blunder a lot; masters almost never play non-best moves.
        toSuboptimalRate: (elo) => {
            if (elo <  600) return 0.90;
            if (elo <  800) return 0.75;
            if (elo < 1000) return 0.62;
            if (elo < 1200) return 0.50;
            if (elo < 1400) return 0.40;
            if (elo < 1600) return 0.30;
            if (elo < 1800) return 0.20;
            if (elo < 2000) return 0.12;
            if (elo < 2200) return 0.07;
            if (elo < 2500) return 0.04;
            return 0.02;
        },

        // ── Max centipawn loss allowed for "suboptimal" picks ────────────────────
        //  Lower ELO players routinely drop 200-300 cp; higher players never do.
        toMaxLoss: (elo) => {
            if (elo <  800) return 500;
            if (elo < 1000) return 350;
            if (elo < 1200) return 250;
            if (elo < 1400) return 180;
            if (elo < 1600) return 120;
            if (elo < 1800) return 80;
            if (elo < 2000) return 50;
            if (elo < 2200) return 30;
            return 20;
        },

        // ── Friendly label shown in the UI ──────────────────────────────────────
        label: (elo) => {
            if (elo <  800) return 'Beginner';
            if (elo < 1000) return 'Novice';
            if (elo < 1200) return 'Class D';
            if (elo < 1400) return 'Class C';
            if (elo < 1600) return 'Class B';
            if (elo < 1800) return 'Class A';
            if (elo < 2000) return 'Candidate';
            if (elo < 2200) return 'Expert';
            if (elo < 2400) return 'NM / FM';
            if (elo < 2500) return 'IM';
            if (elo < 2700) return 'GM';
            return 'Super-GM';
        },

        // ── Apply all settings to a live SF worker ───────────────────────────────
        apply: (elo) => {
            if (!SF.ready || !SF.worker) return;
            const skill = EloStrength.toSkill(elo);
            // UCI_LimitStrength + UCI_Elo: supported by modern Stockfish builds.
            // Stockfish.js v10 may ignore UCI_Elo but Skill Level will still work.
            SF.worker.postMessage('setoption name UCI_LimitStrength value true');
            SF.worker.postMessage('setoption name UCI_Elo value ' + Math.max(1320, Math.min(3190, elo)));
            SF.worker.postMessage('setoption name Skill Level value ' + skill);
            log('ELO mode: ' + elo + ' → Skill ' + skill + ', Depth ' + EloStrength.toDepth(elo));
        },

        // ── Reset SF to full strength ────────────────────────────────────────────
        reset: () => {
            if (!SF.ready || !SF.worker) return;
            SF.worker.postMessage('setoption name UCI_LimitStrength value false');
            SF.worker.postMessage('setoption name Skill Level value 20');
            log('ELO mode disabled — full strength');
        },
    };

    // ─── 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) => {
            // Skip opening book at low ELO — real beginners don't know theory
            if (!CFG.useBook) return Promise.resolve(null);
            if (CFG.eloMode && CFG.targetElo < 1200) 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');
                // Apply ELO settings right after engine is ready if mode was pre-enabled
                if (CFG.eloMode) EloStrength.apply(CFG.targetElo);
                if (SF._resolveInit) { SF._resolveInit(true); SF._resolveInit = null; }
                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);
                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);

            // ── ELO mode: send strength-limiting options before each search ──────
            if (CFG.eloMode) {
                const skill = EloStrength.toSkill(CFG.targetElo);
                const uciElo = Math.max(1320, Math.min(3190, CFG.targetElo));
                SF.worker.postMessage('setoption name UCI_LimitStrength value true');
                SF.worker.postMessage('setoption name UCI_Elo value ' + uciElo);
                SF.worker.postMessage('setoption name Skill Level value ' + skill);
                SF.worker.postMessage('go depth ' + EloStrength.toDepth(CFG.targetElo));
            } else {
                SF.worker.postMessage('setoption name UCI_LimitStrength value false');
                SF.worker.postMessage('setoption name Skill Level value 20');
                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');
            } 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 };

            // In ELO mode, use ELO-derived rate; otherwise use CFG rate with correlation adjustment
            let rate;
            if (CFG.eloMode) {
                rate = EloStrength.toSuboptimalRate(CFG.targetElo);
            } else {
                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;
            // Use ELO-aware max loss if in ELO mode
            const defaultMax = ({ open: 50, mid: 100, end: 60 })[Human.phase(State.lastFen)] || 80;
            const maxLoss = CFG.eloMode ? EloStrength.toMaxLoss(CFG.targetElo) : defaultMax;
            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;
            if (tc.depth && CFG.depth !== tc.depth && !CFG.eloMode) 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);
                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();

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

                .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.elo     { background: #9c27b0; box-shadow: 0 0 7px #9c27b0; 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; }

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

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

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

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

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

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

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

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

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

                /* ── ELO block ─────────────────────────────────────────────── */
                .ke-elo-block {
                    background: #0f0f14;
                    border: 1px solid rgba(255,255,255,0.06);
                    border-radius: 8px;
                    padding: 10px 11px 11px;
                    margin-bottom: 10px;
                    transition: border-color 0.2s;
                }
                .ke-elo-block.elo-on { border-color: rgba(156,39,176,0.5); box-shadow: 0 0 14px rgba(156,39,176,0.15); }

                .ke-elo-hdr { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
                .ke-elo-title { font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; color: #777; }
                .ke-elo-block.elo-on .ke-elo-title { color: #ce93d8; }

                .ke-sw.elo-sw.on { background: #9c27b0; border-color: #9c27b0; }

                .ke-elo-input-row { display: flex; align-items: center; gap: 7px; margin-bottom: 8px; }
                .ke-elo-input {
                    flex: 1;
                    background: #1a1a22; border: 1px solid rgba(255,255,255,0.1);
                    border-radius: 6px; color: #e0e0e0;
                    font-family: 'JetBrains Mono', monospace; font-size: 18px; font-weight: 600;
                    text-align: center; padding: 6px 8px;
                    outline: none; transition: border-color 0.15s;
                    -moz-appearance: textfield;
                }
                .ke-elo-input::-webkit-inner-spin-button,
                .ke-elo-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
                .ke-elo-input:focus { border-color: #9c27b0; }
                .ke-elo-block.elo-on .ke-elo-input { border-color: rgba(156,39,176,0.4); }

                .ke-elo-badge {
                    font-size: 9.5px; font-weight: 700; letter-spacing: 0.06em;
                    background: rgba(156,39,176,0.15); color: #ce93d8;
                    padding: 3px 7px; border-radius: 4px; white-space: nowrap;
                    min-width: 70px; text-align: center;
                }

                .ke-elo-bar-wrap {
                    position: relative; height: 4px; border-radius: 2px;
                    background: #1e1e24; overflow: hidden;
                }
                .ke-elo-bar {
                    height: 100%; border-radius: 2px;
                    background: linear-gradient(90deg,#4caf50,#ffc107,#f44336,#9c27b0);
                    background-size: 400% 100%;
                    transition: width 0.3s;
                }
                .ke-elo-ticks {
                    display: flex; justify-content: space-between;
                    margin-top: 4px; font-size: 8px; color: #333;
                }

                .ke-elo-presets { display: flex; gap: 4px; margin-top: 8px; flex-wrap: wrap; }
                .ke-elo-preset {
                    flex: 1; min-width: 0;
                    padding: 4px 0; font-size: 9px; font-weight: 600;
                    border-radius: 4px; cursor: pointer; text-align: center;
                    border: 1px solid rgba(255,255,255,0.06); color: #444;
                    background: #141418; transition: 0.15s; white-space: nowrap; overflow: hidden;
                }
                .ke-elo-preset:hover { color: #ccc; border-color: rgba(255,255,255,0.15); }
                .ke-elo-preset.act { background: rgba(156,39,176,0.25); color: #ce93d8; border-color: rgba(156,39,176,0.5); }

                /* ── 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; }

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

                .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>

                    <!-- ── PLAY TAB ── -->
                    <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>

                    <!-- ── TUNE TAB ── -->
                    <div class="ke-page" id="ke-p-tune">

                        <!-- ELO STRENGTH BLOCK -->
                        <div class="ke-elo-block" id="ke-elo-block">
                            <div class="ke-elo-hdr">
                                <span class="ke-elo-title">🎯 ELO STRENGTH MODE</span>
                                <div class="ke-sw elo-sw" id="sw-elo"></div>
                            </div>
                            <div class="ke-elo-input-row">
                                <input type="number" class="ke-elo-input" id="elo-input"
                                       min="500" max="3200" step="50" value="${CFG.targetElo}">
                                <div class="ke-elo-badge" id="elo-badge">${EloStrength.label(CFG.targetElo)}</div>
                            </div>
                            <div class="ke-elo-bar-wrap">
                                <div class="ke-elo-bar" id="elo-bar" style="width:${((CFG.targetElo-500)/2700*100).toFixed(1)}%"></div>
                            </div>
                            <div class="ke-elo-ticks"><span>500</span><span>1200</span><span>1800</span><span>2400</span><span>3200</span></div>
                            <div class="ke-elo-presets">
                                <div class="ke-elo-preset" data-elo="800">800</div>
                                <div class="ke-elo-preset" data-elo="1200">1200</div>
                                <div class="ke-elo-preset act" data-elo="1500">1500</div>
                                <div class="ke-elo-preset" data-elo="1800">1800</div>
                                <div class="ke-elo-preset" data-elo="2200">2200</div>
                                <div class="ke-elo-preset" data-elo="2700">2700</div>
                            </div>
                        </div>

                        <!-- Manual tune (hidden while ELO mode active) -->
                        <div id="ke-manual-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>
                        <div class="ke-row"><span class="ke-rlbl">Humanization</span><div class="ke-sw ${CFG.humanization?'on':''}" id="sw-hum"></div></div>
                    </div>

                    <!-- ── STATS TAB ── -->
                    <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 class="ke-stat" style="margin-top:6px">
                            <div class="ke-stat-l">Active ELO</div>
                            <div class="ke-stat-v" id="st-elo" style="font-size:13px;color:#ce93d8">—</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);
            UI._updateEloUI(CFG.targetElo, false);
        },

        // ── Update all ELO-related UI elements ──────────────────────────────────
        _updateEloUI: (elo, modeOn) => {
            const p = UI.panel;
            if (!p) return;
            const input   = p.querySelector('#elo-input');
            const badge   = p.querySelector('#elo-badge');
            const bar     = p.querySelector('#elo-bar');
            const block   = p.querySelector('#ke-elo-block');
            const manual  = p.querySelector('#ke-manual-tune');
            const stElo   = p.querySelector('#st-elo');
            const sw      = p.querySelector('#sw-elo');
            const presets = p.querySelectorAll('.ke-elo-preset');
            const dot     = p.querySelector('#ke-dot');

            if (input) input.value = elo;
            if (badge) badge.textContent = EloStrength.label(elo);
            if (bar)   bar.style.width = ((elo - 500) / 2700 * 100).toFixed(1) + '%';
            if (block) block.classList.toggle('elo-on', modeOn);
            if (sw)    sw.classList.toggle('on', modeOn);
            // Manual sliders grayed out when ELO mode is on
            if (manual) manual.style.opacity = modeOn ? '0.35' : '1';
            if (manual) manual.style.pointerEvents = modeOn ? 'none' : '';
            if (stElo) stElo.textContent = modeOn ? elo + ' (' + EloStrength.label(elo) + ')' : '—';

            // Highlight active preset
            presets.forEach(pr => pr.classList.toggle('act', parseInt(pr.dataset.elo) === elo));

            // Update status dot if engine is active
            if (dot && CFG.active) {
                dot.className = 'ke-dot ' + (modeOn ? 'elo' : 'active');
            }
        },

        _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
                    ? (CFG.eloMode ? 'ELO ' + CFG.targetElo + ' — ' + EloStrength.label(CFG.targetElo) : 'Analysing every move')
                    : 'Engine ready';
                const dot = p.querySelector('#ke-dot');
                if (dot) dot.className = 'ke-dot ' + (CFG.active ? (CFG.eloMode ? 'elo' : 'active') : (SF.ready ? 'ready' : 'loading'));
                if (CFG.active) {
                    State.lastFen = null;
                    Loop._lastAnalyzedFen = null;
                    SF._pendingFen = null;
                    if (CFG.eloMode) EloStrength.apply(CFG.targetElo);
                    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');
                });
            });

            // Basic 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');

            // ── ELO mode switch ──────────────────────────────────────────────────
            const eloSwitch = p.querySelector('#sw-elo');
            if (eloSwitch) {
                eloSwitch.addEventListener('click', () => {
                    CFG.eloMode = !CFG.eloMode;
                    UI._updateEloUI(CFG.targetElo, CFG.eloMode);
                    if (CFG.eloMode) {
                        EloStrength.apply(CFG.targetElo);
                        if (CFG.active) {
                            p.querySelector('#ke-msub').textContent = 'ELO ' + CFG.targetElo + ' — ' + EloStrength.label(CFG.targetElo);
                        }
                    } else {
                        EloStrength.reset();
                        if (CFG.active) {
                            p.querySelector('#ke-msub').textContent = 'Analysing every move';
                        }
                    }
                    // Re-analyze current position with new settings
                    if (CFG.active && State.lastFen) {
                        Loop._lastAnalyzedFen = null;
                        State.lastFen = null;
                        requestAnimationFrame(Loop.tick);
                    }
                });
            }

            // ── ELO number input ─────────────────────────────────────────────────
            const eloInput = p.querySelector('#elo-input');
            if (eloInput) {
                const applyEloInput = () => {
                    let v = parseInt(eloInput.value) || 1500;
                    v = Math.max(500, Math.min(3200, v));
                    // Round to nearest 50 for clean steps
                    v = Math.round(v / 50) * 50;
                    CFG.targetElo = v;
                    UI._updateEloUI(v, CFG.eloMode);
                    if (CFG.eloMode) {
                        EloStrength.apply(v);
                        if (CFG.active) {
                            p.querySelector('#ke-msub').textContent = 'ELO ' + v + ' — ' + EloStrength.label(v);
                            // Re-analyze with new ELO
                            Loop._lastAnalyzedFen = null;
                            State.lastFen = null;
                            requestAnimationFrame(Loop.tick);
                        }
                    }
                };
                eloInput.addEventListener('change', applyEloInput);
                eloInput.addEventListener('keydown', e => { if (e.key === 'Enter') { applyEloInput(); eloInput.blur(); } });
                // Prevent the keyboard shortcut handler from stealing focus
                eloInput.addEventListener('keydown', e => e.stopPropagation());
            }

            // ── ELO preset buttons ───────────────────────────────────────────────
            p.querySelectorAll('.ke-elo-preset').forEach(btn => {
                btn.addEventListener('click', () => {
                    const v = parseInt(btn.dataset.elo);
                    CFG.targetElo = v;
                    UI._updateEloUI(v, CFG.eloMode);
                    if (CFG.eloMode) {
                        EloStrength.apply(v);
                        if (CFG.active) {
                            p.querySelector('#ke-msub').textContent = 'ELO ' + v + ' — ' + EloStrength.label(v);
                            Loop._lastAnalyzedFen = null;
                            State.lastFen = null;
                            requestAnimationFrame(Loop.tick);
                        }
                    }
                });
            });

            // 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));
                    if (!CFG.eloMode) {
                        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;
                        }
                    }
                });
            });

            // Manual 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 shortcuts
            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 ' + (CFG.active && CFG.eloMode ? 'elo' : 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; }

        let tries = 0;
        while (!Board.el() && tries++ < 30) await sleep(400);
        if (!Board.el()) { log('Board not found', 'error'); return; }

        SF.init().then(ok => {
            if (ok) { log('Engine ready — press E or click the button'); UI.setStatus('ready'); }
        });

        Loop.start();

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

})();