Chess.com Cheat Engine

Chess.com cheat engine with advanced humanization

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         Chess.com Cheat Engine
// @namespace    http://tampermonkey.net/
// @version      11.0
// @description  Chess.com cheat engine with advanced humanization
// @author       rexxx
// @license      MIT
// @match        https://www.chess.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @connect      unpkg.com
// @connect      cdn.jsdelivr.net
// @connect      cdnjs.cloudflare.com
// @connect      lichess.org
// @connect      explorer.lichess.ovh
// @connect      stockfish.online
// @connect      tablebase.lichess.ovh
// @connect      tablebase.lichess.ovh
// @run-at       document-idle
// @grant        unsafeWindow
// ==/UserScript==

(function () {
    'use strict';

    const CONFIG = {
        // Engine settings - LOWER depth = less perfect play = harder to detect
        engineDepth: {
            base: 12,
            min: 8,
            max: 16,
            dynamicDepth: true,
        },
        multiPV: 5, // Get top 5 candidates for more suboptimal variety

        pollInterval: 1000,

        // --- HUMANIZATION CORE ---
        humanization: {
            enabled: true,
            // Chess.com flags engine correlation by rating bracket
            // 1000-1500: >65% is suspicious. 1500-2000: >72% is suspicious
            // Target 55-62% to look like a strong-but-human player
            targetEngineCorrelation: 0.58,

            // How often to pick 2nd/3rd/4th best move instead of best
            suboptimalMoveRate: {
                opening: 0.20,    // 20% in opening
                middlegame: 0.40, // 40% in middlegame (humans make most errors here)
                endgame: 0.28,    // 28% in endgame
            },

            // When winning big, play MORE suboptimal (humans get lazy/overconfident)
            winningDegradation: {
                enabled: true,
                tiers: [
                    { evalAbove: 1.0, extraSuboptimalRate: 0.08 },
                    { evalAbove: 2.0, extraSuboptimalRate: 0.18 },
                    { evalAbove: 3.5, extraSuboptimalRate: 0.30 },
                    { evalAbove: 5.0, extraSuboptimalRate: 0.40 },
                    { evalAbove: 8.0, extraSuboptimalRate: 0.55 },
                ],
            },

            // When losing, play sharper (humans try harder when behind)
            losingSharpness: {
                enabled: true,
                evalBelow: -0.8,
                suboptimalReduction: 0.40, // reduce suboptimal rate by 60% when losing
            },

            // Maximum centipawn loss for "suboptimal" moves
            maxAcceptableCPLoss: {
                opening: 55,      // slightly bigger inaccuracies
                middlegame: 110,  // moderate-large inaccuracies in middlegame
                endgame: 65,      // moderate in endgame
            },

            // Genuine blunders (context-dependent)
            blunder: {
                chance: 0.045,    // 4.5% base chance (real humans blunder ~5-8%)
                onlyInComplexPositions: true,
                maxCPLoss: 200,
                // Only blunder when we're safely ahead
                disableWhenEvalBetween: [-2.0, 2.0],
            },

            // Streaks - shorter perfect runs before forced slip
            streaks: {
                enabled: true,
                perfectStreakMax: 4,  // after 4 perfect moves, force a suboptimal
                sloppyStreakMax: 3,   // after 3 suboptimal, play best for a while
            },

            // Per-game personality variance (WIDER variance = harder to fingerprint)
            personalityVariance: {
                enabled: true,
                suboptimalRateJitter: 0.30,  // +/- 30% on suboptimal rates
                depthJitter: 3,              // +/- 3 depth variance per game
                timingJitter: 0.40,          // +/- 40% on timing
            },
        },

        // --- TIMING / HUMANIZER ---
        // Chess.com analyzes move time distributions. Uniform random is detectable.
        // Real humans have log-normal distribution (cluster around median, long tail)
        timing: {
            // Base delays (higher = safer)
            base: { min: 800, max: 3500 },
            // Fast recaptures / forced moves
            forced: { min: 250, max: 900 },
            // Opening book moves
            book: { min: 500, max: 2000 },
            // Complex positions
            complex: { min: 2500, max: 7000 },
            // Simple/winning positions
            simple: { min: 600, max: 1800 },
            // Occasional long think (simulates deep calculation)
            longThink: { chance: 0.10, min: 5000, max: 12000 },
            // Occasional instant move (pre-move / obvious reply)
            instantMove: { chance: 0.05, min: 150, max: 400 },
            // Fatigue: moves get slower as game progresses
            fatigue: {
                enabled: true,
                startMove: 20,
                msPerMove: 25,
                cap: 1200,
            },
        },

        // --- SESSION MANAGEMENT (anti-pattern detection) ---
        session: {
            maxGamesPerSession: 12,   // take a break after N games
            breakDurationMs: 180000,  // 3 minute break between sessions
            maxWinStreak: 6,          // after N wins, intentionally lose/draw one
            gamesPlayed: 0,
            wins: 0,
            currentWinStreak: 0,
        },

        auto: {
            enabled: false,
            autoQueue: true,
        },
        arrowOpacity: 0.8,
        showPanel: true,
        showThreats: true,
        stealthMode: false,
        useBook: true,
    };

    const SELECTORS = {
        board: ['wc-chess-board', 'chess-board'],
        chat: '.chat-scroll-area-component',
        moves: 'vertical-move-list, wc-move-list, .move-list-component',
        clocks: '.clock-component',
        gameOver: [
            '.game-over-modal-container',
            '.modal-game-over-component',
            '[data-cy="game-over-modal"]',
            '.game-over-modal-buttons',
            '.game-over-buttons-component',
            '.game-result-header'
        ],
        drawOffer: '.draw-offer-component',
        promotion: {
            dialog: '.promotion-window, .promotion-piece',
            items: '.promotion-piece'
        }
    };

    const State = {
        engineFound: false,
        isThinking: false,
        lastFen: null,
        playerColor: null,
        gameId: null,
        moveCount: 0,
        boredomLevel: 0,
        personality: null,
        ui: {
            overlay: null,
            panel: null,
            statusDot: null,
            autoIndicator: null
        },
        workers: {
            stockfish: null
        },
        cache: {
            fen: new Map(),
            board: null
        },
        // MultiPV candidate tracking
        candidates: {},       // { pvIndex: { move, eval: {type, value}, depth } }
        currentEval: null,
        currentBestMove: null,
        opponentResponse: null,
        // Humanization tracking
        human: {
            perfectStreak: 0,     // consecutive best-move plays
            sloppyStreak: 0,      // consecutive suboptimal plays
            bestMoveCount: 0,     // total best moves this game
            totalMoveCount: 0,    // total moves this game
            gamePersonality: null, // randomized per-game modifiers
            lastMoveWasBest: true,
        },
    };

    const Utils = {
        sleep: (ms) => new Promise(r => setTimeout(r, ms)),

        log: (msg, type = 'info') => {
            const colors = {
                info: '#3eff3e',
                warn: '#ffcc00',
                error: '#ff4444',
                debug: '#aaaaff'
            };
            console.log(`%c[BA] ${msg}`, `color: ${colors[type]}; font-weight: bold;`);
        },

        randomRange: (min, max) => Math.random() * (max - min) + min,

        // Gaussian random using Box-Muller transform
        gaussianRandom: (mean = 0, stdDev = 1) => {
            const u1 = Math.random();
            const u2 = Math.random();
            const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
            return mean + z * stdDev;
        },

        // Log-normal delay: clusters around median with occasional long thinks
        // This matches real human move-time distributions
        humanDelay: (min, max) => {
            const median = (min + max) / 2;
            const sigma = 0.6; // controls spread (higher = more variance)
            const logNormal = Math.exp(Utils.gaussianRandom(Math.log(median), sigma));
            // Clamp to reasonable range but allow occasional outliers
            return Math.max(min * 0.7, Math.min(max * 1.4, logNormal));
        },

        isTabActive: () => !document.hidden,

        query: (selector, root = document) => {
            if (Array.isArray(selector)) {
                for (const s of selector) {
                    const el = root.querySelector(s);
                    if (el) return el;
                }
                return null;
            }
            return root.querySelector(selector);
        }
    };

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

                .ba-overlay { pointer-events: none; z-index: 10000; position: absolute; top: 0; left: 0; transition: opacity 0.2s; }
                .ba-stealth { opacity: 0 !important; pointer-events: none !important; }

                .ba-panel {
                    position: fixed; top: 50px; left: 50px; z-index: 10001;
                    width: 300px;
                    background: rgba(10, 10, 12, 0.95);
                    color: #e0e0e0;
                    border: 1px solid #333;
                    border-left: 2px solid #4caf50;
                    border-radius: 8px;
                    font-family: 'Inter', sans-serif;
                    box-shadow: 0 10px 40px rgba(0,0,0,0.6);
                    overflow: hidden;
                    display: flex; flex-direction: column;
                }

                .ba-header {
                    padding: 12px 16px;
                    background: linear-gradient(90deg, rgba(76,175,80,0.1), transparent);
                    border-bottom: 1px solid rgba(255,255,255,0.05);
                    display: flex; justify-content: space-between; align-items: center;
                    cursor: grab; user-select: none;
                }
                .ba-logo { font-weight: 800; font-size: 14px; letter-spacing: 1px; color: #4caf50; }
                .ba-logo span { color: #fff; opacity: 0.7; font-weight: 400; }
                .ba-minimize { cursor: pointer; opacity: 0.5; transition: 0.2s; }
                .ba-minimize:hover { opacity: 1; color: #fff; }

                .ba-tabs { display: flex; background: rgba(0,0,0,0.2); }
                .ba-tab {
                    flex: 1; text-align: center; padding: 10px 0;
                    font-size: 11px; font-weight: 600; color: #666;
                    cursor: pointer; transition: 0.2s;
                    border-bottom: 2px solid transparent;
                }
                .ba-tab:hover { color: #aaa; background: rgba(255,255,255,0.02); }
                .ba-tab.active { color: #e0e0e0; border-bottom: 2px solid #4caf50; background: rgba(76,175,80,0.05); }

                .ba-content { padding: 16px; min-height: 150px; max-height: 400px; overflow-y: auto; }
                .ba-page { display: none; }
                .ba-page.active { display: block; animation: fadeIn 0.2s; }

                .ba-row { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
                .ba-label { font-size: 12px; color: #aaa; }
                .ba-value { font-family: 'JetBrains Mono', monospace; font-size: 12px; color: #4caf50; }

                .ba-slider-container { margin-bottom: 14px; }
                .ba-slider-header { display: flex; justify-content: space-between; margin-bottom: 6px; }
                .ba-slider {
                    -webkit-appearance: none; width: 100%; height: 4px;
                    background: #333; border-radius: 2px; outline: none;
                }
                .ba-slider::-webkit-slider-thumb {
                    -webkit-appearance: none; width: 12px; height: 12px;
                    background: #4caf50; border-radius: 50%; cursor: pointer;
                    box-shadow: 0 0 10px rgba(76,175,80,0.4);
                }

                .ba-checkbox {
                    width: 16px; height: 16px; border: 1px solid #444;
                    background: #111; border-radius: 3px; cursor: pointer;
                    display: flex; align-items: center; justify-content: center;
                }
                .ba-checkbox.checked { background: #4caf50; border-color: #4caf50; }
                .ba-checkbox.checked::after { content: '✓'; font-size: 10px; color: #000; font-weight: bold; }

                .ba-status-box {
                    background: rgba(255,255,255,0.03); border: 1px solid rgba(255,255,255,0.05);
                    border-radius: 6px; padding: 12px; margin-bottom: 16px; text-align: center;
                }
                .ba-eval-large { font-family: 'JetBrains Mono'; font-size: 24px; font-weight: 700; color: #fff; margin-bottom: 4px; display: block;}
                .ba-best-move-large { font-family: 'JetBrains Mono'; font-size: 14px; color: #4caf50; background: rgba(76,175,80,0.1); padding: 4px 8px; border-radius: 4px; display: inline-block; }

                .ba-footer {
                    padding: 8px 16px; font-size: 10px; color: #555;
                    border-top: 1px solid rgba(255,255,255,0.05);
                    display: flex; gap: 12px;
                }
                .ba-key { color: #888; background: #222; padding: 1px 4px; border-radius: 3px; font-family: monospace; border: 1px solid #333; }

                @keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
                .ba-arrow { stroke-linecap: round; opacity: ${CONFIG.arrowOpacity}; filter: drop-shadow(0 0 4px rgba(0,0,0,0.5)); }
            `);
        },

        createInterface: () => {
            if (State.ui.panel) return;

            const panel = document.createElement('div');
            panel.className = 'ba-panel';
            panel.innerHTML = `
                <div class="ba-header">
                    <div class="ba-logo">REXXX<span>.MENU</span></div>
                    <div class="ba-minimize">_</div>
                </div>
                <div class="ba-tabs">
                    <div class="ba-tab active" data-tab="main">MAIN</div>
                    <div class="ba-tab" data-tab="timings">TIMINGS</div>
                    <div class="ba-tab" data-tab="visuals">VISUALS</div>
                </div>
                <div class="ba-content">
                    <div id="tab-main" class="ba-page active">
                        <div class="ba-status-box">
                            <span class="ba-eval-large">0.00</span>
                            <span class="ba-best-move-large">Waiting...</span>
                        </div>

                        <div class="ba-row">
                            <span class="ba-label">Auto-Play</span>
                            <div class="ba-checkbox ${CONFIG.auto.enabled ? 'checked' : ''}" id="toggle-auto"></div>
                        </div>
                        <div class="ba-row">
                            <span class="ba-label">Opening Book</span>
                            <div class="ba-checkbox ${CONFIG.useBook ? 'checked' : ''}" id="toggle-book"></div>
                        </div>
                    </div>

                    <div id="tab-timings" class="ba-page">
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Engine Correlation %</span>
                                <span class="ba-value" id="val-corr">${Math.round(CONFIG.humanization.targetEngineCorrelation * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="50" max="95" value="${Math.round(CONFIG.humanization.targetEngineCorrelation * 100)}" id="slide-corr">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Base Min Delay (ms)</span>
                                <span class="ba-value" id="val-min">${CONFIG.timing.base.min}</span>
                            </div>
                            <input type="range" class="ba-slider" min="200" max="3000" value="${CONFIG.timing.base.min}" id="slide-min">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Base Max Delay (ms)</span>
                                <span class="ba-value" id="val-max">${CONFIG.timing.base.max}</span>
                            </div>
                            <input type="range" class="ba-slider" min="500" max="8000" value="${CONFIG.timing.base.max}" id="slide-max">
                        </div>
                        <div class="ba-slider-container">
                            <div class="ba-slider-header">
                                <span class="ba-label">Middlegame Suboptimal %</span>
                                <span class="ba-value" id="val-subopt">${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}</span>
                            </div>
                            <input type="range" class="ba-slider" min="5" max="50" value="${Math.round(CONFIG.humanization.suboptimalMoveRate.middlegame * 100)}" id="slide-subopt">
                        </div>
                    </div>
                    <div id="tab-visuals" class="ba-page">
                        <div class="ba-row">
                            <span class="ba-label">Arrow Opacity</span>
                            <div class="ba-slider" style="width: 100px;"></div>
                        </div>
                         <div class="ba-row">
                            <span class="ba-label">Show Threats</span>
                            <div class="ba-checkbox ${CONFIG.showThreats ? 'checked' : ''}" id="toggle-threats"></div>
                        </div>
                    </div>
                </div>
                <div class="ba-footer">
                    <span><span class="ba-key">A</span> Toggle Auto</span>
                    <span><span class="ba-key">X</span> Stealth</span>
                </div>
            `;

            document.body.appendChild(panel);
            State.ui.panel = panel;

            UI.makeDraggable(panel);
            UI.initListeners(panel);
        },

        initListeners: (panel) => {
            panel.querySelectorAll('.ba-tab').forEach(t => {
                t.addEventListener('click', (e) => {
                    panel.querySelectorAll('.ba-tab').forEach(x => x.classList.remove('active'));
                    panel.querySelectorAll('.ba-page').forEach(x => x.classList.remove('active'));

                    e.target.classList.add('active');
                    panel.querySelector(`#tab-${e.target.dataset.tab}`).classList.add('active');
                });
            });

            const toggle = (id, configPath, callback) => {
                panel.querySelector(`#${id}`).addEventListener('click', (e) => {
                    const keys = configPath.split('.');
                    if (keys.length === 2) CONFIG[keys[0]][keys[1]] = !CONFIG[keys[0]][keys[1]];
                    else CONFIG[configPath] = !CONFIG[configPath];

                    e.target.classList.toggle('checked');
                    if (callback) callback();
                });
            };

            toggle('toggle-auto', 'auto.enabled');
            toggle('toggle-book', 'useBook');
            toggle('toggle-threats', 'showThreats');

            const slider = (id, valId, setter) => {
                const el = panel.querySelector(`#${id}`);
                const display = panel.querySelector(`#${valId}`);
                if (!el || !display) return;
                el.addEventListener('input', (e) => {
                    const val = parseInt(e.target.value);
                    setter(val);
                    display.textContent = val;
                });
            };

            slider('slide-corr', 'val-corr', (v) => { CONFIG.humanization.targetEngineCorrelation = v / 100; });
            slider('slide-min', 'val-min', (v) => { CONFIG.timing.base.min = v; });
            slider('slide-max', 'val-max', (v) => { CONFIG.timing.base.max = v; });
            slider('slide-subopt', 'val-subopt', (v) => { CONFIG.humanization.suboptimalMoveRate.middlegame = v / 100; });
        },

        makeDraggable: (el) => {
            const header = el.querySelector('.ba-header');
            let isDragging = false;
            let startX, startY, initialLeft, initialTop;

            header.addEventListener('mousedown', (e) => {
                isDragging = true;
                startX = e.clientX;
                startY = e.clientY;
                initialLeft = el.offsetLeft;
                initialTop = el.offsetTop;
                header.style.cursor = 'grabbing';
            });

            document.addEventListener('mousemove', (e) => {
                if (!isDragging) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                el.style.left = `${initialLeft + dx}px`;
                el.style.top = `${initialTop + dy}px`;
            });

            document.addEventListener('mouseup', () => {
                isDragging = false;
                header.style.cursor = 'grab';
            });
        },

        toggleStealth: () => {
            CONFIG.stealthMode = !CONFIG.stealthMode;
            const p = State.ui.panel;
            if (p) p.style.opacity = CONFIG.stealthMode ? '0' : '1';
            document.querySelectorAll('.ba-overlay').forEach(el => {
                el.classList.toggle('ba-stealth', CONFIG.stealthMode);
            });
        },

        updatePanel: (evalData, bestMove) => {
            if (!State.ui.panel) return;

            const evalBox = State.ui.panel.querySelector('.ba-eval-large');
            const moveBox = State.ui.panel.querySelector('.ba-best-move-large');

            if (evalData) {
                evalBox.textContent = evalData.type === 'mate' ? `M${evalData.value}` : evalData.value.toFixed(2);
                evalBox.style.color = evalData.value > 0.5 ? '#4caf50' : (evalData.value < -0.5 ? '#ff5252' : '#e0e0e0');
            }
            if (bestMove) {
                moveBox.textContent = bestMove.move || '...';
            }

            State.ui.panel.querySelector('#toggle-auto').classList.toggle('checked', CONFIG.auto.enabled);
        },

        updateStatus: (color) => {
            if (!State.ui.panel) return;
            State.ui.panel.style.borderLeftColor = color;
            const logo = State.ui.panel.querySelector('.ba-logo');
            if (logo) logo.style.color = color;

            if (color === 'red') {
                const moveBox = State.ui.panel.querySelector('.ba-best-move-large');
                if (moveBox) moveBox.textContent = 'Engine Failed';
            }
        },

        clearOverlay: () => {
            document.querySelectorAll('.ba-overlay').forEach(e => e.remove());
        },

        drawMove: (move, color = '#4caf50', secondary = false) => {
            if (CONFIG.stealthMode) return;
            if (!secondary) UI.clearOverlay();

            const board = Game.getBoard();
            if (!board) return;

            const rect = board.getBoundingClientRect();
            let overlay = secondary ? document.querySelector('.ba-overlay') : null;

            if (!overlay) {
                overlay = document.createElement('div');
                overlay.className = 'ba-overlay';
                if (CONFIG.stealthMode) overlay.classList.add('ba-stealth');
                overlay.style.width = rect.width + 'px';
                overlay.style.height = rect.height + 'px';
                overlay.style.left = rect.left + window.scrollX + 'px';
                overlay.style.top = rect.top + window.scrollY + 'px';
                document.body.appendChild(overlay);
            }

            const from = move.substring(0, 2);
            const to = move.substring(2, 4);

            const file = (c) => c.charCodeAt(0) - 97;
            const rank = (c) => parseInt(c) - 1;

            const isFlipped = State.playerColor === 'b';
            const sqSize = rect.width / 8;

            const getPos = (sq) => {
                const f = file(sq[0]);
                const r = rank(sq[1]);
                return {
                    x: (isFlipped ? 7 - f : f) * sqSize + sqSize / 2,
                    y: (isFlipped ? r : 7 - r) * sqSize + sqSize / 2
                };
            };

            const p1 = getPos(from);
            const p2 = getPos(to);

            let svg = overlay.querySelector('svg');
            if (!svg) {
                svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                svg.style.width = '100%';
                svg.style.height = '100%';
                overlay.appendChild(svg);
            }

            const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
            line.setAttribute('x1', p1.x);
            line.setAttribute('y1', p1.y);
            line.setAttribute('x2', p2.x);
            line.setAttribute('y2', p2.y);
            line.setAttribute('stroke', color);
            line.setAttribute('stroke-width', sqSize * (secondary ? 0.1 : 0.18));
            line.setAttribute('class', 'ba-arrow');
            if (secondary) line.setAttribute('stroke-dasharray', '5,5');

            const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
            circle.setAttribute('cx', p2.x);
            circle.setAttribute('cy', p2.y);
            circle.setAttribute('r', sqSize * (secondary ? 0.1 : 0.18));
            circle.setAttribute('fill', color);
            circle.setAttribute('opacity', secondary ? 0.5 : 0.8);

            svg.appendChild(line);
            svg.appendChild(circle);
        }
    };

    const Game = {
        getBoard: () => {
            if (State.cache.board && State.cache.board.isConnected) return State.cache.board;
            State.cache.board = Utils.query(SELECTORS.board);
            return State.cache.board;
        },

        getElementAtSquare: (sq) => {
            const board = Game.getBoard();
            if (!board) return null;

            const piece = board.querySelector(`.piece.square-${Game.squareToCoords(sq)}`) ||
                board.querySelector(`.piece.${sq}`);

            if (piece) return piece;

            if (board.shadowRoot) {
                const shadowPiece = board.shadowRoot.querySelector(`.piece.square-${Game.squareToCoords(sq)}`);
                if (shadowPiece) return shadowPiece;
            }

            return board;
        },

        squareToCoords: (sq) => {
            const file = sq.charCodeAt(0) - 96;
            const rank = sq[1];
            return `${file}${rank}`;
        },

        detectColor: () => {
            const board = Game.getBoard();
            if (!board) return null;

            const isFlipped = board.classList.contains('flipped') || board.getAttribute('flipped') === 'true';
            return isFlipped ? 'b' : 'w';
        },

        isValidFen: (fen) => {
            if (!fen || typeof fen !== 'string') return false;
            return fen.split(' ').length >= 4;
        },

        getFen: () => {
            const board = Game.getBoard();
            if (!board) return null;

            if (board.game && board.game.getFEN) return board.game.getFEN();

            const keys = Object.keys(board);
            const reactKey = keys.find(k => k.startsWith('__reactFiber') || k.startsWith('__reactInternal'));
            if (reactKey) {
                let curr = board[reactKey];
                while (curr) {
                    if (curr.memoizedProps?.game?.fen) return curr.memoizedProps.game.fen;
                    if (typeof curr.memoizedProps?.fen === 'string') return curr.memoizedProps.fen;
                    curr = curr.return;
                }
            }

            return null;
        },

        isMyTurn: (fen) => {
            if (!fen || !State.playerColor) return false;
            const turn = fen.split(' ')[1];
            return turn === State.playerColor;
        },

        isCapture: (move) => {
            const board = Game.getBoard();
            if (!board) return false;
            const to = move.substring(2, 4);
            const coords = Game.squareToCoords(to);
            return !!board.querySelector(`.piece.square-${coords}`);
        }
    };

    const OpeningBook = {
        fetchMove: (fen) => {
            if (!CONFIG.useBook) return null;

            return new Promise((resolve) => {
                GM_xmlhttpRequest({
                    method: 'GET',
                    url: `https://explorer.lichess.ovh/masters?fen=${fen}`,
                    onload: (response) => {
                        try {
                            const data = JSON.parse(response.responseText);
                            if (data.moves && data.moves.length > 0) {
                                const topMoves = data.moves.slice(0, 3);
                                const totalGames = topMoves.reduce((sum, m) => sum + m.white + m.draw + m.black, 0);

                                let r = Math.random() * totalGames;
                                for (const move of topMoves) {
                                    const games = move.white + move.draw + move.black;
                                    if (r < games) {
                                        resolve(move.uci);
                                        return;
                                    }
                                    r -= games;
                                }
                                resolve(topMoves[0].uci);
                            } else {
                                resolve(null);
                            }
                        } catch (e) {
                            resolve(null);
                        }
                    },
                    onerror: () => resolve(null)
                });
            });
        }
    };

    const HumanStrategy = {
        // Detect game phase from FEN
        getGamePhase: (fen) => {
            if (!fen) return 'middlegame';
            const board = fen.split(' ')[0];
            const moveNum = State.moveCount;

            // Count pieces (excluding pawns and kings)
            const minorMajor = (board.match(/[rnbqRNBQ]/g) || []).length;
            const queens = (board.match(/[qQ]/g) || []).length;

            if (moveNum <= 12) return 'opening';
            if (minorMajor <= 6 || (queens === 0 && minorMajor <= 8)) return 'endgame';
            return 'middlegame';
        },

        // Count pieces for position complexity estimation
        getPositionComplexity: (fen) => {
            if (!fen) return 0.5;
            const board = fen.split(' ')[0];
            const pieces = (board.match(/[rnbqRNBQ]/g) || []).length;
            const pawns = (board.match(/[pP]/g) || []).length;

            // More pieces + more pawns = more complex
            // Also consider if eval is close (tactical complexity)
            let complexity = (pieces + pawns * 0.5) / 24; // normalized 0-1
            if (State.currentEval && Math.abs(State.currentEval.value) < 0.5) {
                complexity += 0.2; // equal positions are harder
            }
            return Math.min(1, Math.max(0, complexity));
        },

        // Randomize personality at game start for variance across games
        initGamePersonality: () => {
            const pv = CONFIG.humanization.personalityVariance;
            if (!pv.enabled) {
                State.human.gamePersonality = { suboptimalMult: 1, depthOffset: 0, timingMult: 1 };
                return;
            }
            State.human.gamePersonality = {
                suboptimalMult: 1 + (Math.random() * 2 - 1) * pv.suboptimalRateJitter,
                depthOffset: Math.round((Math.random() * 2 - 1) * pv.depthJitter),
                timingMult: 1 + (Math.random() * 2 - 1) * pv.timingJitter,
            };
            Utils.log(`Game Personality: subopt x${State.human.gamePersonality.suboptimalMult.toFixed(2)}, depth ${State.human.gamePersonality.depthOffset > 0 ? '+' : ''}${State.human.gamePersonality.depthOffset}, timing x${State.human.gamePersonality.timingMult.toFixed(2)}`, 'debug');
        },

        // Reset tracking for a new game
        resetGame: () => {
            State.human.perfectStreak = 0;
            State.human.sloppyStreak = 0;
            State.human.bestMoveCount = 0;
            State.human.totalMoveCount = 0;
            State.human.lastMoveWasBest = true;
            State.moveCount = 0;
            State.candidates = {};
            HumanStrategy.initGamePersonality();
        },

        // Calculate dynamic engine depth for this move
        getDynamicDepth: (fen) => {
            const cfg = CONFIG.engineDepth;
            if (!cfg.dynamicDepth) return cfg.base;

            const complexity = HumanStrategy.getPositionComplexity(fen);
            const phase = HumanStrategy.getGamePhase(fen);
            const personality = State.human.gamePersonality || { depthOffset: 0 };

            let depth = cfg.base + personality.depthOffset;

            // Simpler positions -> slightly lower depth (engine finds best move fast)
            // Complex positions -> higher depth
            if (complexity < 0.3) depth -= 2;
            else if (complexity > 0.7) depth += 2;

            // Endgame: slightly higher depth for precision
            if (phase === 'endgame') depth += 1;

            // Add small random jitter per move (+/- 1)
            depth += Math.round(Math.random() * 2 - 1);

            return Math.max(cfg.min, Math.min(cfg.max, depth));
        },

        // Core decision: should we play the best move or pick a suboptimal one?
        shouldPlaySuboptimal: (fen) => {
            const h = CONFIG.humanization;
            if (!h.enabled) return false;

            const phase = HumanStrategy.getGamePhase(fen);
            const personality = State.human.gamePersonality || { suboptimalMult: 1 };
            const eval_ = State.currentEval;

            // Get base suboptimal rate for this phase
            let rate = h.suboptimalMoveRate[phase] || 0.25;

            // Apply per-game personality jitter
            rate *= personality.suboptimalMult;

            // Winning degradation: play worse when winning big
            if (h.winningDegradation.enabled && eval_) {
                const adv = eval_.value; // positive = we're ahead
                for (const tier of h.winningDegradation.tiers) {
                    if (adv >= tier.evalAbove) {
                        rate += tier.extraSuboptimalRate;
                    }
                }
            }

            // Losing sharpness: play better when behind
            if (h.losingSharpness.enabled && eval_ && eval_.value <= h.losingSharpness.evalBelow) {
                rate *= h.losingSharpness.suboptimalReduction;
            }

            // Streak management: force variety in best/suboptimal patterns
            if (h.streaks.enabled) {
                if (State.human.perfectStreak >= h.streaks.perfectStreakMax) {
                    Utils.log('Streak: Forcing suboptimal after perfect run', 'debug');
                    return true; // force suboptimal after long perfect streak
                }
                if (State.human.sloppyStreak >= h.streaks.sloppyStreakMax) {
                    Utils.log('Streak: Forcing best after sloppy run', 'debug');
                    return false; // force best after sloppy streak
                }
            }

            // Engine correlation guard: if we're playing too many best moves,
            // increase suboptimal rate to keep correlation in target range
            if (State.human.totalMoveCount >= 8) {
                const currentCorrelation = State.human.bestMoveCount / State.human.totalMoveCount;
                if (currentCorrelation > h.targetEngineCorrelation + 0.05) {
                    rate += 0.15; // significantly increase suboptimal chance
                    Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% > target, boosting suboptimal`, 'debug');
                } else if (currentCorrelation < h.targetEngineCorrelation - 0.10) {
                    rate *= 0.3; // reduce suboptimal to raise correlation back up
                    Utils.log(`Correlation guard: ${(currentCorrelation * 100).toFixed(0)}% < target, reducing suboptimal`, 'debug');
                }
            }

            // Clamp rate
            rate = Math.max(0.05, Math.min(0.65, rate));

            return Math.random() < rate;
        },

        // Pick the best viable suboptimal move from candidates
        pickSuboptimalMove: (fen) => {
            const phase = HumanStrategy.getGamePhase(fen);
            const maxCPLoss = CONFIG.humanization.maxAcceptableCPLoss[phase] || 80;
            const candidates = State.candidates;
            const bestEval = candidates[1]?.eval;

            if (!bestEval || Object.keys(candidates).length < 2) {
                return candidates[1]?.move || null; // fallback to best
            }

            // Build list of acceptable alternative moves
            const alternatives = [];
            for (let i = 2; i <= CONFIG.multiPV; i++) {
                const c = candidates[i];
                if (!c || !c.eval) continue;

                let cpLoss;
                if (bestEval.type === 'mate' && c.eval.type === 'mate') {
                    // Both are mate: prefer shorter mate, but allow longer mate as "suboptimal"
                    cpLoss = Math.abs(c.eval.value - bestEval.value) * 50;
                } else if (bestEval.type === 'mate') {
                    cpLoss = 200; // losing a forced mate is big
                } else if (c.eval.type === 'mate' && c.eval.value > 0) {
                    cpLoss = 0; // alternative also gives mate - fine
                } else {
                    cpLoss = (bestEval.value - c.eval.value) * 100;
                }

                if (cpLoss <= maxCPLoss && cpLoss >= 0) {
                    alternatives.push({ move: c.move, cpLoss, pvIndex: i });
                }
            }

            if (alternatives.length === 0) {
                Utils.log('No acceptable suboptimal moves, playing best', 'debug');
                return null; // no acceptable alternative
            }

            // Weight selection: prefer smaller centipawn losses (more human-like)
            // Humans usually play the 2nd best, rarely the 4th best
            const weights = alternatives.map(a => 1 / (1 + a.cpLoss / 30));
            const totalWeight = weights.reduce((s, w) => s + w, 0);
            let r = Math.random() * totalWeight;

            for (let i = 0; i < alternatives.length; i++) {
                r -= weights[i];
                if (r <= 0) {
                    Utils.log(`Suboptimal pick: PV${alternatives[i].pvIndex} (${alternatives[i].move}, -${alternatives[i].cpLoss.toFixed(0)}cp)`, 'warn');
                    return alternatives[i].move;
                }
            }

            return alternatives[0].move;
        },

        // Check if we should blunder (very rare, context-dependent)
        shouldBlunder: (fen) => {
            const b = CONFIG.humanization.blunder;
            if (!b || b.chance <= 0) return false;

            const eval_ = State.currentEval;
            if (!eval_) return false;

            // Never blunder in close positions
            if (eval_.value >= b.disableWhenEvalBetween[0] && eval_.value <= b.disableWhenEvalBetween[1]) {
                return false;
            }

            // Only blunder in complex positions if configured
            if (b.onlyInComplexPositions) {
                const complexity = HumanStrategy.getPositionComplexity(fen);
                if (complexity < 0.4) return false;
            }

            // Never blunder when losing (would make it worse)
            if (eval_.value < 0) return false;

            return Math.random() < b.chance;
        },

        // Pick the actual blunder move (worst acceptable candidate)
        pickBlunderMove: () => {
            const candidates = State.candidates;
            const bestEval = candidates[1]?.eval;
            if (!bestEval) return null;

            const maxLoss = CONFIG.humanization.blunder.maxCPLoss;

            // Find the worst move within the maxCPLoss limit
            let worstMove = null;
            let worstLoss = 0;

            for (let i = 2; i <= CONFIG.multiPV; i++) {
                const c = candidates[i];
                if (!c || !c.eval) continue;
                const cpLoss = (bestEval.value - c.eval.value) * 100;
                if (cpLoss <= maxLoss && cpLoss > worstLoss) {
                    worstMove = c.move;
                    worstLoss = cpLoss;
                }
            }

            if (worstMove) {
                Utils.log(`BLUNDER: Playing ${worstMove} (-${worstLoss.toFixed(0)}cp)`, 'error');
            }
            return worstMove;
        },

        // Select the final move to play
        selectMove: (fen, bestMove, isBook) => {
            if (isBook || !CONFIG.humanization.enabled) {
                return { move: bestMove, reason: isBook ? 'book' : 'engine', isBest: true };
            }

            // Check for blunder first (very rare)
            if (HumanStrategy.shouldBlunder(fen)) {
                const blunderMove = HumanStrategy.pickBlunderMove();
                if (blunderMove) {
                    return { move: blunderMove, reason: 'blunder', isBest: false };
                }
            }

            // Check if we should play suboptimal
            if (HumanStrategy.shouldPlaySuboptimal(fen)) {
                const subMove = HumanStrategy.pickSuboptimalMove(fen);
                if (subMove) {
                    return { move: subMove, reason: 'suboptimal', isBest: false };
                }
            }

            return { move: bestMove, reason: 'best', isBest: true };
        },

        // Track the move we played for correlation management
        trackMove: (isBest) => {
            State.human.totalMoveCount++;
            if (isBest) {
                State.human.bestMoveCount++;
                State.human.perfectStreak++;
                State.human.sloppyStreak = 0;
            } else {
                State.human.perfectStreak = 0;
                State.human.sloppyStreak++;
            }
            State.human.lastMoveWasBest = isBest;

            const corr = State.human.totalMoveCount > 0
                ? ((State.human.bestMoveCount / State.human.totalMoveCount) * 100).toFixed(0)
                : '---';
            Utils.log(`Correlation: ${corr}% (${State.human.bestMoveCount}/${State.human.totalMoveCount})`, 'debug');
        },

        // Calculate human-like delay for this move
        calculateDelay: (fen, moveResult, isBook) => {
            const t = CONFIG.timing;
            const personality = State.human.gamePersonality || { timingMult: 1 };
            let min, max;

            if (isBook) {
                min = t.book.min;
                max = t.book.max;
            } else if (Game.isCapture(moveResult.move) && State.currentEval && Math.abs(State.currentEval.value) > 2) {
                // Obvious recapture when winning
                min = t.forced.min;
                max = t.forced.max;
                Utils.log('Timing: Reflex capture');
            } else if (Math.random() < t.instantMove.chance && State.moveCount > 5) {
                // Pre-move / instant (simulates having pre-calculated)
                min = t.instantMove.min;
                max = t.instantMove.max;
                Utils.log('Timing: Instant/pre-move');
            } else if (Math.random() < t.longThink.chance) {
                // Occasional long think
                min = t.longThink.min;
                max = t.longThink.max;
                Utils.log('Timing: Long think');
            } else {
                const complexity = HumanStrategy.getPositionComplexity(fen);
                if (complexity > 0.65) {
                    min = t.complex.min;
                    max = t.complex.max;
                } else if (complexity < 0.3 || (State.currentEval && State.currentEval.value > 3)) {
                    min = t.simple.min;
                    max = t.simple.max;
                } else {
                    min = t.base.min;
                    max = t.base.max;
                }

                // Equal eval -> think longer (humans struggle here)
                if (State.currentEval && Math.abs(State.currentEval.value) < 0.3) {
                    min += 500;
                    max += 1200;
                }

                // If we're playing a suboptimal move, add slight hesitation
                // (simulates "almost saw" the best move)
                if (moveResult.reason === 'suboptimal') {
                    min += 200;
                    max += 600;
                }
            }

            // Apply fatigue
            if (t.fatigue.enabled && State.moveCount > t.fatigue.startMove) {
                const extra = Math.min(t.fatigue.cap, (State.moveCount - t.fatigue.startMove) * t.fatigue.msPerMove);
                min += extra;
                max += extra;
            }

            // Apply per-game personality timing jitter
            min *= personality.timingMult;
            max *= personality.timingMult;

            // Use log-normal distribution instead of uniform random
            return Utils.humanDelay(min, max);
        },
    };

    const Engine = {
        init: async () => {
            if (State.workers.stockfish) return;

            Utils.log('Initializing Stockfish...');
            UI.updateStatus('orange');

            try {
                const scriptContent = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: 'https://unpkg.com/[email protected]/stockfish.js',
                        onload: (res) => resolve(res.responseText),
                        onerror: (err) => reject(err)
                    });
                });

                const blob = new Blob([scriptContent], { type: 'application/javascript' });
                State.workers.stockfish = new Worker(URL.createObjectURL(blob));

                State.workers.stockfish.onmessage = (e) => {
                    const msg = e.data;
                    if (msg === 'uciok') {
                        State.engineFound = true;
                        UI.updateStatus('#4caf50');
                        Utils.log('Stockfish Ready');
                    }
                    if (msg.startsWith('bestmove')) {
                        const move = msg.split(' ')[1];
                        Engine.handleBestMove(move);
                    }
                    if (msg.startsWith('info') && msg.includes('score')) {
                        Engine.parseScore(msg);
                    }
                };

                State.workers.stockfish.postMessage('uci');
                State.workers.stockfish.postMessage('isready');
                State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);

                HumanStrategy.initGamePersonality();

            } catch (e) {
                Utils.log('Stockfish Init Failed: ' + e, 'error');
                UI.updateStatus('red');
            }
        },

        analyze: async (fen) => {
            if (!State.workers.stockfish || !State.engineFound) return;
            State.isThinking = true;
            State.candidates = {};

            if (CONFIG.useBook) {
                const bookMove = await OpeningBook.fetchMove(fen);
                if (bookMove) {
                    Utils.log(`Book Move Found: ${bookMove}`);
                    Engine.handleBestMove(bookMove, true);
                    return;
                }
            }

            const depth = HumanStrategy.getDynamicDepth(fen);
            Utils.log(`Analyzing depth ${depth}, MultiPV ${CONFIG.multiPV}`, 'debug');

            State.workers.stockfish.postMessage('stop');
            State.workers.stockfish.postMessage(`position fen ${fen}`);
            State.workers.stockfish.postMessage(`setoption name MultiPV value ${CONFIG.multiPV}`);
            State.workers.stockfish.postMessage(`go depth ${depth}`);
        },

        parseScore: (msg) => {
            const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/);
            const pvMatch = msg.match(/multipv (\d+)/);
            const depthMatch = msg.match(/depth (\d+)/);
            const moveMatch = msg.match(/ pv (\w+)/);

            if (scoreMatch && pvMatch && moveMatch) {
                const type = scoreMatch[1];
                let value = parseInt(scoreMatch[2]);
                const mpv = parseInt(pvMatch[1]);
                const depth = parseInt(depthMatch?.[1] || 0);

                if (type === 'cp') value = value / 100;

                // Store ALL candidate moves from MultiPV
                State.candidates[mpv] = {
                    move: moveMatch[1],
                    eval: { type, value },
                    depth,
                };

                // PV1 is always the best move
                if (mpv === 1) {
                    State.currentEval = { type, value, depth };
                    State.currentBestMove = moveMatch[1];
                }

                // Extract opponent's expected response from PV1 line
                if (mpv === 1 && msg.includes(' pv ')) {
                    const pvMoves = msg.split(' pv ')[1].split(' ');
                    if (pvMoves.length > 1) {
                        State.opponentResponse = pvMoves[1];
                    }
                }
            }
        },

        handleBestMove: async (bestMove, isBook = false) => {
            State.isThinking = false;
            State.moveCount++;

            const fen = State.lastFen;
            const phase = HumanStrategy.getGamePhase(fen);

            // Use HumanStrategy to decide which move to actually play
            const moveResult = HumanStrategy.selectMove(fen, bestMove, isBook);
            const finalMove = moveResult.move;

            Utils.log(`Move #${State.moveCount} [${phase}]: ${finalMove} (${moveResult.reason})${moveResult.isBest ? '' : ' [SUBOPTIMAL]'}`);

            // Draw best move arrow (green) and chosen move if different (yellow)
            UI.drawMove(bestMove, '#4caf50');
            if (finalMove !== bestMove) {
                UI.drawMove(finalMove, '#ffcc00', true);
            }

            if (CONFIG.showThreats && State.opponentResponse) {
                UI.drawMove(State.opponentResponse, '#ff5252', true);
            }

            UI.updatePanel(State.currentEval, { move: `${finalMove}${moveResult.isBest ? '' : '*'}` });

            // Track for correlation management
            HumanStrategy.trackMove(moveResult.isBest);

            if (CONFIG.auto.enabled && Game.isMyTurn(State.lastFen)) {
                const delay = HumanStrategy.calculateDelay(fen, moveResult, isBook);
                Utils.log(`Waiting ${Math.round(delay)}ms (${moveResult.reason})...`);
                await Utils.sleep(delay);

                await Humanizer.executeMove(finalMove);
            }
        }
    };

    const Humanizer = {
        createEvent: (type, x, y, options = {}) => {
            const defaults = {
                bubbles: true,
                cancelable: true,
                view: window,
                detail: 1,
                screenX: x,
                screenY: y,
                clientX: x,
                clientY: y,
                pointerId: 1,
                pointerType: 'mouse',
                isPrimary: true,
                button: 0,
                buttons: 1,
                which: 1,
                composed: true
            };
            return new PointerEvent(type, { ...defaults, ...options });
        },

        getElementsAt: (x, y) => {
            return document.elementsFromPoint(x, y).filter(el =>
                el.tagName !== 'HTML' && el.tagName !== 'BODY' && !el.classList.contains('ba-overlay')
            );
        },

        makeGodMove: (from, to, promo) => false,

        showClick: (x, y, color = 'red') => {
            const dot = document.createElement('div');
            dot.style.cssText = `
                position: absolute;
                left: ${x}px; top: ${y}px;
                width: 12px; height: 12px;
                background: ${color}; border-radius: 50%;
                z-index: 100000; pointer-events: none;
                transform: translate(-50%, -50%);
                box-shadow: 0 0 4px white;
            `;
            document.body.appendChild(dot);
            setTimeout(() => dot.remove(), 800);
        },

        dragDrop: async (fromSq, toSq) => {
            const board = Game.getBoard();
            if (!board) return;

            const startPos = Humanizer.getCoords(fromSq);
            const endPos = Humanizer.getCoords(toSq);
            if (!startPos || !endPos) return;

            Humanizer.showClick(startPos.x, startPos.y, '#00ff00');

            const fromCoords = Game.squareToCoords(fromSq);
            const pieceEl = board.querySelector(`.piece.square-${fromCoords}`) ||
                document.elementFromPoint(startPos.x, startPos.y);

            const targetSource = pieceEl || board;

            const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };

            // Slight random offset on pickup (humans don't click exact center)
            const pickupNoise = () => Utils.gaussianRandom(0, 2);
            const sx = startPos.x + pickupNoise();
            const sy = startPos.y + pickupNoise();

            targetSource.dispatchEvent(new PointerEvent('pointerover', { ...opts, clientX: sx, clientY: sy }));
            targetSource.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: sx, clientY: sy }));
            targetSource.dispatchEvent(new MouseEvent('mousedown', { ...opts, clientX: sx, clientY: sy }));

            // Human pickup hesitation: variable delay
            await Utils.sleep(Utils.randomRange(30, 90));

            // Generate Bézier control point for curved path (humans don't drag in straight lines)
            const dx = endPos.x - startPos.x;
            const dy = endPos.y - startPos.y;
            const dist = Math.sqrt(dx * dx + dy * dy);
            const curvature = Utils.gaussianRandom(0, dist * 0.15);
            const cpX = (startPos.x + endPos.x) / 2 + curvature * (Math.random() > 0.5 ? 1 : -1);
            const cpY = (startPos.y + endPos.y) / 2 + curvature * (Math.random() > 0.5 ? 1 : -1);

            // Variable step count based on distance (more steps = smoother = more realistic)
            const steps = Math.max(8, Math.min(18, Math.round(dist / 8) + Math.round(Math.random() * 4)));

            for (let i = 1; i <= steps; i++) {
                const t = i / steps;
                // Quadratic Bézier: B(t) = (1-t)^2*P0 + 2*(1-t)*t*CP + t^2*P1
                const oneMinusT = 1 - t;
                let curX = oneMinusT * oneMinusT * startPos.x + 2 * oneMinusT * t * cpX + t * t * endPos.x;
                let curY = oneMinusT * oneMinusT * startPos.y + 2 * oneMinusT * t * cpY + t * t * endPos.y;

                // Add per-step noise (decreasing as we approach target)
                const noiseScale = Math.max(0.5, (1 - t) * 3);
                curX += Utils.gaussianRandom(0, noiseScale);
                curY += Utils.gaussianRandom(0, noiseScale);

                // Variable pressure (humans vary grip)
                const pressure = 0.4 + Math.random() * 0.3;

                document.dispatchEvent(new PointerEvent('pointermove', {
                    ...opts, clientX: curX, clientY: curY, pressure
                }));
                document.dispatchEvent(new MouseEvent('mousemove', { ...opts, clientX: curX, clientY: curY }));

                // Variable inter-step delay (humans accelerate mid-drag, slow at ends)
                const speedCurve = Math.sin(t * Math.PI); // slow-fast-slow
                const stepDelay = Math.max(2, Math.round(Utils.randomRange(4, 14) * (1.2 - speedCurve * 0.8)));
                if (Math.random() < 0.4) await Utils.sleep(stepDelay);
            }

            // Slight overshoot then correction (very human)
            if (Math.random() < 0.3) {
                const overshootX = endPos.x + Utils.gaussianRandom(0, 4);
                const overshootY = endPos.y + Utils.gaussianRandom(0, 4);
                document.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: overshootX, clientY: overshootY }));
                await Utils.sleep(Utils.randomRange(8, 20));
                document.dispatchEvent(new PointerEvent('pointermove', { ...opts, clientX: endPos.x, clientY: endPos.y }));
                await Utils.sleep(Utils.randomRange(5, 12));
            }

            const toCoords = Game.squareToCoords(toSq);
            const targetEl = board.querySelector(`.square-${toCoords}`) ||
                document.elementFromPoint(endPos.x, endPos.y);

            const dropTarget = targetEl || board;

            // Drop with slight noise
            const dropX = endPos.x + Utils.gaussianRandom(0, 1.5);
            const dropY = endPos.y + Utils.gaussianRandom(0, 1.5);

            Humanizer.showClick(endPos.x, endPos.y, 'red');

            dropTarget.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: dropX, clientY: dropY }));
            dropTarget.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: dropX, clientY: dropY }));
            dropTarget.dispatchEvent(new PointerEvent('click', { ...opts, clientX: dropX, clientY: dropY }));
        },

        clickSquare: async (sq) => { },

        executeMove: async (move) => {
            const currentFen = Game.getFen();
            if (currentFen !== State.lastFen && State.moveCount > 0) return;

            const from = move.substring(0, 2);
            const to = move.substring(2, 4);
            const promo = move.length > 4 ? move[4] : 'q';

            // Detect if this is a promotion move (pawn reaching rank 1 or 8)
            const isPromotion = (to[1] === '8' || to[1] === '1') && Humanizer.isPawnMove(from);

            if (Humanizer.makeGodMove(from, to, isPromotion ? promo : null)) {
                Utils.log('API Success?');
            }

            Utils.log(`Auto-playing (Drag): ${from} -> ${to}${isPromotion ? '=' + promo : ''}`);
            await Humanizer.dragDrop(from, to);

            if (isPromotion) {
                await Humanizer.handlePromotion(promo);
            }
        },

        isPawnMove: (fromSq) => {
            const board = Game.getBoard();
            if (!board) return false;
            const coords = Game.squareToCoords(fromSq);
            const piece = board.querySelector(`.piece.square-${coords}`);
            if (piece) {
                const classes = piece.className;
                return classes.includes('wp') || classes.includes('bp');
            }
            // Fallback: if piece on rank 2 or 7, likely a pawn
            return fromSq[1] === '2' || fromSq[1] === '7';
        },

        handlePromotion: async (promo = 'q') => {
            const pieceMap = { q: 'queen', r: 'rook', b: 'bishop', n: 'knight' };
            const pieceName = pieceMap[promo] || 'queen';

            Utils.log(`Promotion: selecting ${pieceName}`);

            // Poll for the promotion dialog to appear (up to 2 seconds)
            let promoEl = null;
            for (let i = 0; i < 20; i++) {
                await Utils.sleep(100);

                // Try multiple selector strategies for Chess.com's promotion UI
                const selectors = [
                    `.promotion-piece[data-piece="${promo}"]`,
                    `.promotion-piece.w${promo}, .promotion-piece.b${promo}`,
                    `.promotion-window .promotion-piece:first-child`,
                    `[class*="promotion"] [class*="${pieceName}"]`,
                    `[class*="promotion"] [class*="${promo}"]`,
                    `.promotion-area .promotion-piece:first-child`,
                ];

                for (const sel of selectors) {
                    promoEl = document.querySelector(sel);
                    if (promoEl) break;
                }

                // Also try: find any promotion container and click the first piece (queen is always first)
                if (!promoEl) {
                    const promoContainer = document.querySelector('.promotion-window, .promotion-area, [class*="promotion-"]');
                    if (promoContainer) {
                        const pieces = promoContainer.querySelectorAll('.promotion-piece, [class*="piece"]');
                        if (pieces.length > 0) {
                            // Queen is always the first option in Chess.com's promotion dialog
                            promoEl = promo === 'q' ? pieces[0] : pieces[{ r: 1, b: 2, n: 3 }[promo] || 0];
                        }
                    }
                }

                if (promoEl) break;
            }

            if (promoEl) {
                const rect = promoEl.getBoundingClientRect();
                const x = rect.left + rect.width / 2;
                const y = rect.top + rect.height / 2;

                const opts = { bubbles: true, composed: true, buttons: 1, pointerId: 1, isPrimary: true };
                promoEl.dispatchEvent(new PointerEvent('pointerdown', { ...opts, clientX: x, clientY: y }));
                await Utils.sleep(30);
                promoEl.dispatchEvent(new PointerEvent('pointerup', { ...opts, clientX: x, clientY: y }));
                promoEl.dispatchEvent(new MouseEvent('mouseup', { ...opts, clientX: x, clientY: y }));
                promoEl.dispatchEvent(new PointerEvent('click', { ...opts, clientX: x, clientY: y }));
                promoEl.click();
                Utils.log(`Promotion: clicked ${pieceName}`);
            } else {
                Utils.log('Promotion: dialog not found, trying direct click at queen position', 'warn');
                // Last resort: click at the expected queen position on the board
                const board = Game.getBoard();
                if (board) {
                    const boardRect = board.getBoundingClientRect();
                    const sqSize = boardRect.width / 8;
                    // Queen promotion square is at the top of the promotion popup
                    const promoX = boardRect.left + sqSize / 2;
                    const promoY = boardRect.top + sqSize / 2;
                    const opts = { bubbles: true, composed: true, buttons: 1 };
                    document.elementFromPoint(promoX, promoY)?.click();
                }
            }
        },

        getCoords: (sq) => {
            const board = Game.getBoard();
            if (!board) return null;

            const rect = board.getBoundingClientRect();
            const sqSize = rect.width / 8;
            const isFlipped = State.playerColor === 'b';

            const f = sq.charCodeAt(0) - 97;
            const r = parseInt(sq[1]) - 1;

            const x = rect.left + (isFlipped ? 7 - f : f) * sqSize + sqSize / 2;
            const y = rect.top + (isFlipped ? r : 7 - r) * sqSize + sqSize / 2;

            return { x, y };
        }
    };

    const Main = {
        _queueing: false,
        init: async () => {
            UI.injectStyles();
            UI.createInterface();

            let board = null;
            while (!board) {
                board = Game.getBoard();
                if (!board) await Utils.sleep(500);
            }

            Utils.log('Board detected. Starting Engine...');
            await Engine.init();

            Main.setupObservers();
            Main.gameLoop();

            document.addEventListener('keydown', (e) => {
                if (e.key === 'a' && !e.ctrlKey && !e.shiftKey && !e.target.matches('input')) {
                    CONFIG.auto.enabled = !CONFIG.auto.enabled;
                    Utils.log(`Auto-Play: ${CONFIG.auto.enabled}`);
                    UI.updatePanel(State.currentEval, {});
                }
                if (e.key === 'x' && !e.target.matches('input')) {
                    UI.toggleStealth();
                }
            });
        },

        checkAutoQueue: async () => {
            if (!CONFIG.auto.autoQueue || !CONFIG.auto.enabled) return;
            if (Main._queueing) return;
            Main._queueing = true;

            // Session management: take breaks to avoid pattern detection
            CONFIG.session.gamesPlayed++;
            if (CONFIG.session.gamesPlayed >= CONFIG.session.maxGamesPerSession) {
                const breakMs = CONFIG.session.breakDurationMs + Utils.randomRange(0, 60000);
                Utils.log(`Session break: ${Math.round(breakMs / 1000)}s after ${CONFIG.session.gamesPlayed} games`, 'warn');
                await Utils.sleep(breakMs);
                CONFIG.session.gamesPlayed = 0;
                CONFIG.session.currentWinStreak = 0;
            }

            const selectors = [
                'button[data-cy="game-over-modal-new-game-button"]',
                'button[data-cy="game-over-modal-rematch-button"]',
                'button[data-cy="new-game-index-main"]',
                '.game-over-buttons-button',
                '.game-over-button-component.primary',
                '.ui_v5-button-component.ui_v5-button-primary',
            ];

            for (let attempt = 0; attempt < 5; attempt++) {
                for (const sel of selectors) {
                    const btn = document.querySelector(sel);
                    if (btn && btn.offsetParent !== null) {
                        if (sel.includes('rematch') && document.querySelector(selectors[0])) continue;
                        const delay = Utils.humanDelay(2000, 6000);
                        Utils.log(`Auto-Queue: Clicking "${btn.textContent.trim()}" in ${Math.round(delay)}ms (game #${CONFIG.session.gamesPlayed})...`);
                        await Utils.sleep(delay);
                        btn.click();
                        Main._queueing = false;
                        return;
                    }
                }
                await Utils.sleep(1000);
            }
            Utils.log('Auto-Queue: No new game button found', 'warn');
            Main._queueing = false;
        },

        setupObservers: () => {
            const movesList = Utils.query(SELECTORS.moves) || document.body;

            const observer = new MutationObserver(() => {
                requestAnimationFrame(Main.gameLoop);
            });
            observer.observe(movesList, { childList: true, subtree: true, characterData: true });

            const gameOverObserver = new MutationObserver(() => {
                if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
                    Main.checkAutoQueue();
                }
            });
            gameOverObserver.observe(document.body, { childList: true, subtree: true });

            setInterval(Main.gameLoop, CONFIG.pollInterval);

            setInterval(() => {
                if (Utils.query(SELECTORS.gameOver) && CONFIG.auto.enabled && CONFIG.auto.autoQueue) {
                    Main.checkAutoQueue();
                }
            }, 3000);
        },

        gameLoop: () => {
            const fen = Game.getFen();
            if (!fen || fen === State.lastFen) return;

            State.playerColor = Game.detectColor();

            const fenParts = fen.split(' ');
            const isStartPos = fenParts[0] === 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR';
            const fenMoveNum = parseInt(fenParts[5] || '1');
            if (isStartPos || (fenMoveNum <= 1 && State.moveCount > 3)) {
                Utils.log('New game detected - resetting humanization', 'warn');
                HumanStrategy.resetGame();
                Main._queueing = false;
            }

            State.lastFen = fen;
            State.currentEval = null;
            State.candidates = {};
            UI.clearOverlay();
            UI.updatePanel(null, null);

            if (Utils.query(SELECTORS.gameOver)) {
                if (CONFIG.auto.autoQueue) {
                    setTimeout(Main.checkAutoQueue, 2000);
                }
                return;
            }

            if (Game.isMyTurn(fen)) {
                Engine.analyze(fen);
            } else {
                UI.updateStatus('#888');
            }
        }
    };

    Main.init();

})();