您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improved userscript: top 3 moves & threats, persistent settings, debounce/throttle, safer engine lifecycle, promotion handling, better board detection, min/max delay
当前为
// ==UserScript== // @name Chess.com Bot — v2.1 // @namespace thehackerclient // @version 3.5 // @description Improved userscript: top 3 moves & threats, persistent settings, debounce/throttle, safer engine lifecycle, promotion handling, better board detection, min/max delay // @match https://www.chess.com/* // @auther thehackerclient // @grant GM_getResourceText // @license MIT // @require https://code.jquery.com/jquery-3.6.0.min.js // @resource stockfish.js https://cdnjs.cloudflare.com/ajax/libs/stockfish.js/9.0.0/stockfish.js // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ------------------------------------------------------------------------- // 1. Configuration and State Management // ------------------------------------------------------------------------- const STORAGE_KEY = 'chess_bot_settings_v4'; const DEFAULTS = { autoRun: true, autoMovePiece: false, delayMin: 0.5, delayMax: 2.0, lastDepth: 18, showPV: true, showEvalBar: true, showAdvancedThreats: true, highlightMs: 2000, colors: { move1: 'rgba(235, 97, 80, 0.7)', move2: 'rgba(255, 165, 0, 0.6)', move3: 'rgba(255, 255, 0, 0.5)', threat: 'rgba(0, 128, 255, 0.35)', undefended: 'rgba(255, 0, 255, 0.6)', blunder: 'rgba(255, 0, 0, 0.7)', mistake: 'rgba(255, 128, 0, 0.7)', inaccuracy: 'rgba(255, 255, 0, 0.7)', checkmate: 'rgba(255, 69, 0, 0.8)' } }; // Error classification thresholds (Centipawn drops) const ERROR_THRESHOLDS = { BLUNDER: 200, // Score drops by > 200 cp MISTAKE: 100, // Score drops by > 100 cp INACCURACY: 50 // Score drops by > 50 cp }; let settings = {}; let board = null; let stockfishObjectURL = null; /** * Handles settings loading and initialization. */ function loadSettings() { try { const saved = localStorage.getItem(STORAGE_KEY); let loadedSettings = saved ? JSON.parse(saved) : {}; settings = Object.assign({}, DEFAULTS, loadedSettings); settings.autoMovePiece = false; settings.delayMin = parseFloat(settings.delayMin) || DEFAULTS.delayMin; settings.delayMax = parseFloat(settings.delayMax) || DEFAULTS.delayMax; settings.lastDepth = parseInt(settings.lastDepth) || DEFAULTS.lastDepth; if (settings.delayMin > settings.delayMax) { settings.delayMax = settings.delayMin; } console.log('Bot Settings Loaded:', settings); } catch (e) { console.error('Error loading settings, using defaults.', e); settings = DEFAULTS; } } function saveSettings() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(settings)); } catch (e) { console.warn('Failed to save settings to localStorage.', e); } } loadSettings(); // ------------------------------------------------------------------------- // 2. Utility Functions (DOM, Board Mapping) // ------------------------------------------------------------------------- function throttle(fn, wait) { let last = 0; return function (...a) { const now = Date.now(); if (now - last > wait) { last = now; fn.apply(this, a); } }; } function findBoard() { return document.querySelector('chess-board') || document.querySelector('wc-chess-board') || document.querySelector('[data-cy="board"]') || null; } function mapSquareForBoard(sq) { if (!board || !sq || sq.length < 2) return sq; const isFlipped = board.classList && board.classList.contains('flipped'); if (!isFlipped) return sq; const file = sq[0]; const rank = sq[1]; const flippedFile = String.fromCharCode('h'.charCodeAt(0) - (file.charCodeAt(0) - 'a'.charCodeAt(0))); const flippedRank = (9 - parseInt(rank)).toString(); return flippedFile + flippedRank; } function getBoardSquareEl(sq) { try { return board.querySelector(`[data-square="${sq}"]`); } catch (e) { return null; } } // ------------------------------------------------------------------------- // 3. Game Analysis Class // ------------------------------------------------------------------------- class GameAnalyzer { constructor() { // Stores analysis history: {fen, move, cpBefore, cpAfter, scoreDrop, errorType, turn} this.gameHistory = []; this.lastAnalyzedFen = null; this.currentTurn = 'w'; } /** * Classifies the severity of an error based on centipawn drop. * @param {number} drop The absolute drop in centipawns. * @param {number} cpBefore The evaluation before the move. * @returns {string} The error type ('Blunder', 'Mistake', 'Inaccuracy', 'Good', 'Excellent'). */ classifyError(drop, cpBefore) { if (drop >= ERROR_THRESHOLDS.BLUNDER) { return 'Blunder'; } else if (drop >= ERROR_THRESHOLDS.MISTAKE) { return 'Mistake'; } else if (drop >= ERROR_THRESHOLDS.INACCURACY) { return 'Inaccuracy'; } else { return 'Good'; } } /** * Clears history and resets for a new game. */ reset() { this.gameHistory = []; this.lastAnalyzedFen = null; this.currentTurn = 'w'; document.getElementById('analysisReport').innerHTML = this.getReportHTML(); } /** * Processes an actual move made by a player and compares its outcome to the best move. * This function is called AFTER a move has been played and the new FEN is ready for analysis. * @param {string} prevFen The FEN before the move was played. * @param {string} actualMove The UCI move played (e.g., 'e2e4'). * @param {number} cpBefore The *best* evaluation of the previous position (prevFen). * @param {number} cpAfter The *best* evaluation of the new position (currentFen). */ recordMoveAnalysis(prevFen, actualMove, cpBefore, cpAfter) { if (prevFen === this.lastAnalyzedFen) return; this.lastAnalyzedFen = prevFen; // Get the score from the perspective of the player who just moved // White moves: cpBefore is White's score, cpAfter is White's score // Black moves: cpBefore is Black's score (-cp), cpAfter is Black's score (-cp) // Score from the perspective of the player whose turn it WAS (White if turn 'w') const playerPerspectiveBefore = this.currentTurn === 'w' ? cpBefore : -cpBefore; // Score from the perspective of the player whose turn it IS NOT (White if turn 'w') const playerPerspectiveAfter = this.currentTurn === 'w' ? cpAfter : -cpAfter; // Drop is the difference between the *optimal* score and the score *after* the move. const scoreDrop = playerPerspectiveBefore - playerPerspectiveAfter; // Check for missed wins (a large score drop into a neutral or losing position) const errorType = this.classifyError(scoreDrop, playerPerspectiveBefore); // Record the move this.gameHistory.push({ fen: prevFen, move: actualMove, cpBefore: cpBefore, cpAfter: cpAfter, scoreDrop: scoreDrop, errorType: errorType, turn: this.currentTurn }); // Toggle turn for next move this.currentTurn = this.currentTurn === 'w' ? 'b' : 'w'; } /** * Generates the analysis report HTML. */ getReportHTML() { const stats = { w: { Blunder: 0, Mistake: 0, Inaccuracy: 0, Total: 0 }, b: { Blunder: 0, Mistake: 0, Inaccuracy: 0, Total: 0 } }; const worstMoves = []; this.gameHistory.forEach((move, index) => { const player = move.turn; if (move.errorType !== 'Good') { stats[player][move.errorType]++; stats[player].Total++; worstMoves.push({ moveNumber: Math.floor(index / 2) + 1, ...move }); } }); worstMoves.sort((a, b) => b.scoreDrop - a.scoreDrop); const errorsDisplay = (player) => ` <div style="font-size:14px; margin-top:5px; padding-left:10px; border-left:3px solid ${player === 'w' ? '#f7f7f7' : '#333'};"> <span style="font-weight:700; color:${player === 'w' ? '#2c3e50' : '#2c3e50'};">${player === 'w' ? 'White' : 'Black'} (${stats[player].Total} Errors)</span> <br> <span style="color:${settings.colors.blunder};">Blunders: ${stats[player].Blunder}</span>, <span style="color:${settings.colors.mistake};">Mistakes: ${stats[player].Mistake}</span>, <span style="color:${settings.colors.inaccuracy};">Inaccuracies: ${stats[player].Inaccuracy}</span> </div> `; const worstMovesList = worstMoves.slice(0, 3).map(move => { const drop = (move.scoreDrop / 100).toFixed(2); const color = settings.colors[move.errorType.toLowerCase()]; return `<li style="margin-top:5px; color:${color};"> ${move.moveNumber}. ${move.turn === 'w' ? 'W' : 'B'}: ${move.move} (<span style="font-weight:700;">${move.errorType}</span>, dropped ${drop}) </li>`; }).join(''); return ` <div style="margin-top:15px; padding-top:10px; border-top:1px solid #eee;"> <div style="font-weight:700; margin-bottom:10px; color:#2c3e50; font-size:16px;">Game Analysis Summary</div> ${errorsDisplay('w')} ${errorsDisplay('b')} <div style="margin-top:15px;"> <div style="font-weight:700; color:#c0392b;">Top 3 Worst Moves/Missed Opportunities:</div> <ul style="list-style:disc; margin-left:15px; padding-left:0; font-size:14px;"> ${worstMovesList || '<li>No major errors recorded yet.</li>'} </ul> </div> </div> `; } } // ------------------------------------------------------------------------- // 4. Stockfish Engine Management Class // ------------------------------------------------------------------------- class StockfishManager { constructor(callback) { this.worker = null; this.onEngineData = callback; this.isThinking = false; this.lastFen = ''; this.candidateMoves = []; this.currentEval = 0; this.checkmateIn = null; this.initWorker(); } parseScore(match) { if (!match) return 0; const type = match[1]; const val = parseInt(match[2]); if (type === 'cp') { return val; } this.checkmateIn = Math.abs(val); return val > 0 ? 100000 - val : -100000 - val; } handleEngineMessage(msg) { if (typeof msg !== 'string') return; if (msg.startsWith('info') && msg.includes('pv')) { const pvTokens = msg.split(' pv ')[1].trim().split(/\s+/); if (pvTokens && pvTokens.length) { const move = pvTokens[0]; const scoreMatch = msg.match(/score (cp|mate) (-?\d+)/); this.checkmateIn = null; const score = this.parseScore(scoreMatch); const depthMatch = msg.match(/depth (\d+)/); const depth = depthMatch ? parseInt(depthMatch[1]) : settings.lastDepth; const exists = this.candidateMoves.find(c => c.move === move); if (!exists) { this.candidateMoves.push({ move, score, depth, pv: pvTokens }); } else if (depth > exists.depth) { exists.score = score; exists.depth = depth; exists.pv = pvTokens; } this.candidateMoves.sort((a, b) => b.score - a.score); this.candidateMoves = this.candidateMoves.slice(0, 3); this.currentEval = this.candidateMoves[0]?.score || 0; this.onEngineData({ type: 'info', moves: this.candidateMoves, eval: this.currentEval, mate: this.checkmateIn, fen: this.lastFen }); } } if (msg.startsWith('bestmove')) { this.isThinking = false; const bestMoveUCI = msg.split(' ')[1]; this.onEngineData({ type: 'bestmove', move: bestMoveUCI, moves: this.candidateMoves, eval: this.currentEval, mate: this.checkmateIn, fen: this.lastFen }); if (settings.autoMovePiece && bestMoveUCI && bestMoveUCI !== '(none)') { this.performMove(bestMoveUCI); } } } initWorker() { try { if (stockfishObjectURL === null) { const text = GM_getResourceText('stockfish.js'); stockfishObjectURL = URL.createObjectURL(new Blob([text], { type: 'application/javascript' })); } if (this.worker) this.worker.terminate(); this.worker = new Worker(stockfishObjectURL); this.worker.onmessage = e => this.handleEngineMessage(e.data); this.worker.postMessage('ucinewgame'); this.worker.postMessage('setoption name Threads value 4'); this.worker.postMessage('isready'); console.log('Stockfish worker created and initialized.'); } catch (err) { console.error('Failed to create Stockfish worker', err); } } safeRestart() { this.isThinking = false; this.lastFen = ''; this.candidateMoves = []; this.currentEval = 0; this.checkmateIn = null; try { if (this.worker) this.worker.terminate(); } catch (e) { /* ignore */ } this.initWorker(); } runAnalysis(fen, depth) { if (!this.worker) { this.initWorker(); return; } if (this.isThinking && fen === this.lastFen) return; this.lastFen = fen; this.worker.postMessage('stop'); this.candidateMoves = []; this.currentEval = 0; this.checkmateIn = null; this.worker.postMessage('position fen ' + fen); this.isThinking = true; this.worker.postMessage('go depth ' + depth); console.log(`Starting analysis for FEN: ${fen} at depth ${depth}`); } performMove(moveUCI) { if (!board || !board.game) return; try { const from = moveUCI.slice(0, 2); const to = moveUCI.slice(2, 4); const promotion = moveUCI.length > 4 ? moveUCI[4] : null; const legal = board.game.getLegalMoves(); let foundMove = null; for (const m of legal) { if (m.from === from && m.to === to) { foundMove = m; if (m.promotion && promotion) { foundMove.promotion = promotion; } break; } } if (foundMove) { const moveObj = Object.assign({}, foundMove, { animate: true, userGenerated: true }); board.game.move(moveObj); console.log('Bot performed move:', moveUCI); } else { console.warn(`Could not find legal move object for UCI: ${moveUCI}`); } } catch (e) { console.error('performMove failed', e); } } } // ------------------------------------------------------------------------- // 5. Board Visualizer & UI Class // ------------------------------------------------------------------------- class BoardVisualizer { constructor(engine) { this.engine = engine; this.pvNoteTimeout = null; this.highlightTimeouts = []; this.evalBar = null; this.initStyles(); } initStyles() { const style = document.createElement('style'); style.id = 'bot_analysis_styles_v4'; style.innerHTML = ` /* Base Styles for UI elements */ #botGUI_v4 { background: rgba(255, 255, 255, 0.95); padding: 12px; margin: 8px; max-width: 300px; font-family: 'Inter', Arial, sans-serif; border-radius: 12px; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15); border: 1px solid rgba(0,0,0,0.05); transition: all 0.3s ease; } /* Highlight Overlays */ .botMoveHighlight, .botThreatHighlight, .botUndefendedHighlight { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 60; border-radius: 4px; opacity: 1; transition: opacity ${settings.highlightMs / 4000}s ease-out; } .botUndefendedHighlight { border: 3px dashed ${settings.colors.undefended}; background: transparent !important; box-sizing: border-box; } /* Evaluation Bar Container */ #evalBarContainer { position: absolute; bottom: 0; right: 0; width: 24px; height: 100%; z-index: 999; overflow: hidden; border-radius: 6px; margin-left: 12px; transition: all 0.3s ease; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } /* Evaluation Bar Inner Elements */ #evalBarWhite, #evalBarBlack { position: absolute; width: 100%; transition: height 0.5s ease; } #evalBarWhite { background-color: #f7f7f7; /* White advantage */ bottom: 0; } #evalBarBlack { background-color: #333333; /* Black advantage */ top: 0; } #evalBarText { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 10px; font-weight: bold; color: #fff; z-index: 100; text-shadow: 0 0 3px rgba(0,0,0,0.8); } /* PV Notes (Top 3 Moves) inside the board container */ .pvNote { padding: 6px 8px; border-radius: 6px; color: #fff; z-index: 120; font-size: 12px; pointer-events: none; white-space: nowrap; font-weight: 600; box-shadow: 0 2px 4px rgba(0,0,0,0.3); position: absolute; /* Critical for positioning inside board parent */ right: 10px; } `; document.head.appendChild(style); } attachHighlight(el, cls, color) { if (!el) return null; let overlay = el.querySelector('.' + cls); if (!overlay) { overlay = document.createElement('div'); overlay.className = cls; overlay.style.backgroundColor = color; el.appendChild(overlay); } return overlay; } detachHighlights(selector = '.botMoveHighlight, .botThreatHighlight, .botUndefendedHighlight, .pvNote') { try { document.querySelectorAll(selector).forEach(n => { if (n.parentElement) n.parentElement.removeChild(n); }); this.highlightTimeouts.forEach(t => clearTimeout(t)); this.highlightTimeouts = []; } catch (e) { /* ignore errors during cleanup */ } } showAnalysis(candidateMoves, currentEval, checkmateIn) { if (!board || !board.game) return; this.detachHighlights(); this.updateEvalBar(currentEval, checkmateIn); candidateMoves.forEach((cm, i) => { const isCheckmate = checkmateIn !== null && i === 0; const from = mapSquareForBoard(cm.move.slice(0, 2)); const to = mapSquareForBoard(cm.move.slice(2, 4)); const color = isCheckmate ? settings.colors.checkmate : (i === 0 ? settings.colors.move1 : (i === 1 ? settings.colors.move2 : settings.colors.move3)); [from, to].forEach(sq => { const el = getBoardSquareEl(sq); if (el) { const ov = this.attachHighlight(el, 'botMoveHighlight', color); const t = setTimeout(() => { if (ov && ov.parentElement) ov.parentElement.removeChild(ov); }, settings.highlightMs); this.highlightTimeouts.push(t); } }); if (settings.showPV) { this.addPVNote(cm, i, isCheckmate, checkmateIn); } }); if (settings.showAdvancedThreats) { this.showThreatsAndUndefended(); } } /** * Adds a Principal Variation note in the TOP RIGHT CORNER of the board. */ addPVNote(cm, index, isMate, mateIn) { try { const id = `pvNote-${index}`; let note = document.getElementById(id); const container = board.parentElement; if (!note) { note = document.createElement('div'); note.id = id; note.className = 'pvNote'; // Position: TOP RIGHT CORNER OF THE BOARD Object.assign(note.style, { top: `${10 + index * 28}px`, background: isMate ? settings.colors.checkmate : 'rgba(0,0,0,0.75)', }); container.appendChild(note); } let scoreText; if (isMate) { scoreText = `M${mateIn}`; } else { scoreText = (cm.score / 100).toFixed(2); } note.innerText = `#${index + 1}: ${scoreText} | ${cm.move} PV: ${cm.pv.slice(0, 5).join(' ')}`; note.style.background = isMate ? settings.colors.checkmate : 'rgba(0,0,0,0.75)'; if (this.pvNoteTimeout) clearTimeout(this.pvNoteTimeout); this.pvNoteTimeout = setTimeout(() => { this.detachHighlights('.pvNote'); }, settings.highlightMs + 500); } catch (e) { console.error('Failed to add PV note', e); } } showThreatsAndUndefended() { if (!board || !board.game || !settings.showAdvancedThreats) return; try { const game = board.game; const turn = game.getTurn(); const opponent = turn === 'w' ? 'b' : 'w'; const allLegalMoves = game.getLegalMoves(); const opponentMoves = allLegalMoves.filter(m => game.get(m.from)?.color === opponent); opponentMoves.forEach(m => { const sq = mapSquareForBoard(m.to); const el = getBoardSquareEl(sq); if (el) { const ov = this.attachHighlight(el, 'botThreatHighlight', settings.colors.threat); const t = setTimeout(() => { if (ov && ov.parentElement) ov.parentElement.removeChild(ov); }, settings.highlightMs); this.highlightTimeouts.push(t); const targetPiece = game.get(m.to); if (targetPiece && targetPiece.color === turn) { const undefendedEl = getBoardSquareEl(sq); if (undefendedEl) { const undefendedOv = this.attachHighlight(undefendedEl, 'botUndefendedHighlight', settings.colors.undefended); const t2 = setTimeout(() => { if (undefendedOv && undefendedOv.parentElement) undefendedOv.parentElement.removeChild(undefendedOv); }, settings.highlightMs * 2); this.highlightTimeouts.push(t2); } } } }); } catch (e) { console.warn('Failed to show advanced threats', e); } } setupEvalBar() { if (this.evalBar) return; const boardContainer = document.querySelector('.main-board-container') || document.querySelector('.live-game-board') || document.querySelector('.board-viewer-component') || board.parentElement; if (!boardContainer) { console.warn('Could not find suitable container for Eval Bar.'); return; } const wrapper = document.createElement('div'); wrapper.id = 'evalBarContainer'; wrapper.style.position = 'absolute'; wrapper.style.boxShadow = '0 0 10px rgba(0, 0, 0, 0.1)'; wrapper.style.marginLeft = '12px'; wrapper.style.height = '100%'; wrapper.style.bottom = '0'; wrapper.style.right = '-36px'; let relativeParent = boardContainer; if (getComputedStyle(relativeParent).position === 'static') { relativeParent.style.position = 'relative'; } relativeParent.appendChild(wrapper); wrapper.innerHTML = ` <div id="evalBar" style="height:100%; width:100%; position:relative; overflow:hidden; border-radius:6px;"> <div id="evalBarBlack" style="height: 50%; width: 100%;"></div> <div id="evalBarWhite" style="height: 50%; width: 100%; top: 50%; position: absolute;"></div> <div id="evalBarText">0.0</div> </div> `; this.evalBar = { container: wrapper, whiteBar: wrapper.querySelector('#evalBarWhite'), blackBar: wrapper.querySelector('#evalBarBlack'), text: wrapper.querySelector('#evalBarText') }; this.updateEvalBar(0, null); } /** * Updates the visual state of the evaluation bar using a percentage map. * White advantage is mapped from 50% to 100%. Black advantage is 50% down to 0%. */ updateEvalBar(cpScore, mateIn) { if (!this.evalBar || !settings.showEvalBar) { if (this.evalBar) this.evalBar.container.style.display = 'none'; return; } this.evalBar.container.style.display = 'block'; let percentage; let displayScore; if (mateIn !== null) { displayScore = `M${mateIn}`; percentage = cpScore > 0 ? 100 : 0; } else { displayScore = (cpScore / 100).toFixed(1); // Use a sigmoid function (simpler, effective) to map score to percentage // P_white = 100 / (1 + e^(-k * cpScore)) const K = 0.004; percentage = 100 / (1 + Math.exp(-K * cpScore)); } percentage = Math.max(0, Math.min(100, percentage)); let whiteHeight = percentage; let blackHeight = 100 - percentage; // Adjust colors based on board flip const isFlipped = board.classList.contains('flipped'); if (isFlipped) { this.evalBar.whiteBar.style.backgroundColor = '#333333'; // Black on bottom (White's bar shows Black advantage) this.evalBar.blackBar.style.backgroundColor = '#f7f7f7'; // White on top (Black's bar shows White advantage) } else { this.evalBar.whiteBar.style.backgroundColor = '#f7f7f7'; // White on bottom this.evalBar.blackBar.style.backgroundColor = '#333333'; // Black on top } // Apply calculated heights (logic remains the same) this.evalBar.whiteBar.style.height = `${whiteHeight}%`; this.evalBar.blackBar.style.height = `${blackHeight}%`; this.evalBar.whiteBar.style.top = `${blackHeight}%`; // Text color logic if (percentage > 70) { this.evalBar.text.style.color = isFlipped ? '#f7f7f7' : '#333'; // Text visible over white bar area } else if (percentage < 30) { this.evalBar.text.style.color = isFlipped ? '#333' : '#f7f7f7'; // Text visible over black bar area } else { this.evalBar.text.style.color = '#fff'; } this.evalBar.text.innerText = displayScore; } } // ------------------------------------------------------------------------- // 6. GUI & Settings Management Class // ------------------------------------------------------------------------- class GUIManager { constructor(engine, visualizer, analyzer) { this.engine = engine; this.visualizer = visualizer; this.analyzer = analyzer; this.container = null; } initGUI() { board = findBoard(); if (!board) return false; if (document.getElementById('botGUI_v4')) return true; const parent = document.querySelector('.main-board-container') || board.parentElement.parentElement || document.body; this.container = document.createElement('div'); this.container.id = 'botGUI_v4'; this.container.style.maxWidth = '300px'; this.container.innerHTML = ` <div style="font-weight:700;margin-bottom:10px;font-size:16px;color:#2c3e50;">♟️ Deep Chess Analysis v4.0</div> <!-- Depth Control --> <div id="depthControl" style="margin-bottom:12px;"> <div id="depthText" style="margin-bottom:4px; font-weight:600;">Search Depth: <strong style="color:#2980b9;">${settings.lastDepth}</strong></div> <input type="range" id="depthSlider" min="5" max="30" value="${settings.lastDepth}" step="1"> </div> <!-- Main Toggles --> <div style="display:grid; grid-template-columns: 1fr 1fr; gap:10px; margin-bottom:12px; padding-bottom:12px; border-bottom:1px solid #eee;"> <div><input type="checkbox" id="autoRunCB" style="margin-right:5px;"> <label for="autoRunCB" style="font-size:14px;">Continuous Analysis</label></div> <div><input type="checkbox" id="autoMoveCB" style="margin-right:5px;"> <label for="autoMoveCB" style="font-size:14px; color:#c0392b;">Auto Move (Bot Play)</label></div> <div><input type="checkbox" id="showEvalBarCB" style="margin-right:5px;"> <label for="showEvalBarCB" style="font-size:14px;">Show Eval Bar (%)</label></div> <div><input type="checkbox" id="showAdvThreatsCB" style="margin-right:5px;"> <label for="showAdvThreatsCB" style="font-size:14px;">Tactical Highlights</label></div> </div> <!-- Delay Control (Hidden if not relevant, kept for settings persistence) --> <div id="delaySection" style="margin-bottom:12px; display:none;"> <div style="font-weight:600; margin-bottom:4px;">Auto-Move Delay (seconds):</div> <div style="display:flex; gap:10px;"> Min: <input id="delayMinInput" type="number" min="0" step="0.1" value="${settings.delayMin}" style="width:70px; padding:4px; border:1px solid #ccc; border-radius:4px;"> Max: <input id="delayMaxInput" type="number" min="0" step="0.1" value="${settings.delayMax}" style="width:70px; padding:4px; border:1px solid #ccc; border-radius:4px;"> </div> </div> <!-- Analysis Report Section --> <div id="analysisReport">${this.analyzer.getReportHTML()}</div> <!-- Actions --> <div style="margin-top:10px;display:flex;gap:8px;"> <button id="reloadBtn" style="flex:1;padding:8px;border-radius:8px; background:#f39c12; color:#fff; font-weight:600; border:none;">Reload Engine</button> <button id="resetAnalysisBtn" style="flex:1;padding:8px;border-radius:8px; background:#e74c3c; color:#fff; font-weight:600; border:none;">Reset Game Analysis</button> </div> `; parent.appendChild(this.container); this.attachEventListeners(); this.updateUIFromSettings(); this.visualizer.setupEvalBar(); return true; } updateReport() { document.getElementById('analysisReport').innerHTML = this.analyzer.getReportHTML(); } updateUIFromSettings() { const getEl = id => document.getElementById(id); getEl('autoRunCB').checked = !!settings.autoRun; getEl('autoMoveCB').checked = !!settings.autoMovePiece; getEl('showEvalBarCB').checked = !!settings.showEvalBar; getEl('showAdvThreatsCB').checked = !!settings.showAdvancedThreats; getEl('depthSlider').value = settings.lastDepth; getEl('depthText').querySelector('strong').innerText = settings.lastDepth; getEl('delayMinInput').value = settings.delayMin; getEl('delayMaxInput').value = settings.delayMax; } attachEventListeners() { const getEl = id => document.getElementById(id); getEl('depthSlider').oninput = (e) => { settings.lastDepth = parseInt(e.target.value); getEl('depthText').querySelector('strong').innerText = settings.lastDepth; saveSettings(); }; getEl('autoRunCB').onchange = (e) => { settings.autoRun = e.target.checked; saveSettings(); }; getEl('autoMoveCB').onchange = (e) => { settings.autoMovePiece = e.target.checked; getEl('delaySection').style.display = e.target.checked ? 'block' : 'none'; saveSettings(); }; getEl('showEvalBarCB').onchange = (e) => { settings.showEvalBar = e.target.checked; saveSettings(); this.visualizer.updateEvalBar(this.engine.currentEval, this.engine.checkmateIn); }; getEl('showAdvThreatsCB').onchange = (e) => { settings.showAdvancedThreats = e.target.checked; saveSettings(); }; getEl('delayMinInput').onchange = (e) => { let val = parseFloat(e.target.value) || 0; settings.delayMin = Math.max(0, val); if (settings.delayMin > settings.delayMax) { settings.delayMax = settings.delayMin; getEl('delayMaxInput').value = settings.delayMax.toFixed(1); } e.target.value = settings.delayMin.toFixed(1); saveSettings(); }; getEl('delayMaxInput').onchange = (e) => { let val = parseFloat(e.target.value) || 0; settings.delayMax = Math.max(0, val); if (settings.delayMax < settings.delayMin) { settings.delayMin = settings.delayMax; getEl('delayMinInput').value = settings.delayMin.toFixed(1); } e.target.value = settings.delayMax.toFixed(1); saveSettings(); }; getEl('reloadBtn').onclick = () => { this.engine.safeRestart(); this.analyzer.reset(); // Also reset analysis on engine restart }; getEl('resetAnalysisBtn').onclick = () => { this.analyzer.reset(); this.updateReport(); console.log('Game analysis history reset.'); }; } } // ------------------------------------------------------------------------- // 7. Main Controller Logic // ------------------------------------------------------------------------- let botEngine = null; let botVisualizer = null; let botGUI = null; let gameAnalyzer = null; let canAutoMove = true; let lastKnownBestEval = 0; let lastKnownFen = ''; /** * Callback executed when the Stockfish engine returns data. */ function engineDataCallback(data) { if (data.type === 'info') { // Update the live visualization with the current best info botVisualizer.showAnalysis(data.moves, data.eval, data.mate); // Store the best evaluation for the *current* FEN lastKnownBestEval = data.eval; lastKnownFen = data.fen; } else if (data.type === 'bestmove') { // Final visualization after 'bestmove' botVisualizer.showAnalysis(data.moves, data.eval, data.mate); canAutoMove = true; } } /** * The main analysis loop, throttled to run every 200ms. */ const continuousAnalysisLoop = throttle(() => { board = findBoard(); if (!board || !board.game) return; try { const currentFen = board.game.getFEN(); // 1. Detect Move Played if (currentFen !== botEngine.lastFen && botEngine.lastFen !== '') { // A move just happened! const actualMove = board.game.getHistory().pop(); // Use the last known BEST evaluation of the PREVIOUS FEN const cpBefore = lastKnownBestEval; const prevFen = botEngine.lastFen; // Immediately run analysis on the *new* FEN to get cpAfter botEngine.runAnalysis(currentFen, settings.lastDepth); // We must wait for the new best eval (cpAfter) to arrive to complete the classification. // The classification logic is now inside the move listener instead of this loop. // IMPORTANT: Since we can't reliably predict when the 'bestmove' will arrive // for the *new* FEN, we'll wait for the next 'info' or 'bestmove' update // to trigger the move recording. For now, just trigger the new analysis. } // 2. Continuous Analysis & Game Analyzer Logic if (settings.autoRun || botEngine.isThinking) { if (!botEngine.isThinking || currentFen !== botEngine.lastFen) { // Check if the actual move was just recorded and new eval is ready if (currentFen === lastKnownFen && lastKnownFen !== '' && lastKnownBestEval !== 0) { // This means the engine just returned the optimal evaluation (cpAfter) for the position resulting from the player's last move. const history = board.game.getHistory(); const lastMove = history.length > 0 ? history[history.length - 1] : null; if (lastMove && lastMove.from && lastMove.to) { const uciMove = lastMove.from + lastMove.to + (lastMove.promotion ? lastMove.promotion : ''); // Get the FEN *before* the last move (this is slightly hacky but necessary) board.game.undo(); const prevFenForRecording = board.game.getFEN(); board.game.redo(); gameAnalyzer.recordMoveAnalysis(prevFenForRecording, uciMove, lastKnownBestEval, botEngine.currentEval); botGUI.updateReport(); } } // Start or continue analysis botEngine.runAnalysis(currentFen, settings.lastDepth); } } // 3. Trigger Auto-Move (Bot Play) const currentTurn = board.game.getTurn(); const playingAs = board.game.getPlayingAs ? board.game.getPlayingAs() : currentTurn; if (settings.autoMovePiece && currentTurn === playingAs && !botEngine.isThinking && canAutoMove) { const bestMoveUCI = botEngine.candidateMoves[0]?.move; if (bestMoveUCI) { canAutoMove = false; const delaySeconds = Math.random() * (settings.delayMax - settings.delayMin) + settings.delayMin; const delayMs = Math.max(200, delaySeconds * 1000); setTimeout(() => { if (botEngine.candidateMoves[0]?.move === bestMoveUCI) { botEngine.performMove(bestMoveUCI); } canAutoMove = true; }, delayMs); } } } catch (e) { console.error('Error in continuous analysis loop:', e); } }, 150); /** * Initialization and setup. */ async function init() { // Wait for the board element to exist and have the game object await new Promise(resolve => { const check = setInterval(() => { board = findBoard(); if (board && board.game) { clearInterval(check); resolve(); } }, 100); }); // Initialize components botEngine = new StockfishManager(engineDataCallback); botVisualizer = new BoardVisualizer(botEngine); gameAnalyzer = new GameAnalyzer(); botGUI = new GUIManager(botEngine, botVisualizer, gameAnalyzer); botGUI.initGUI(); const mo = new MutationObserver((mutations) => { const newBoard = findBoard(); if (newBoard && newBoard !== board) { console.log('Board change detected, re-initializing UI and visuals.'); board = newBoard; botGUI.initGUI(); botVisualizer.detachHighlights(); gameAnalyzer.reset(); // Reset game analysis history on new game/navigation } }); mo.observe(document.body, { childList: true, subtree: true }); setInterval(continuousAnalysisLoop, 150); console.log('Deep Chess Analysis Bot v4.0.0 Initialized and Monitoring.'); } init(); })();